Linux驅動開發(三)——併發控制

併發的定義是:多個執行單元同時、並行的執行。併發會導致競態:併發的執行單元對共享資源的訪問。以下幾種情況會發生競態:

  1. 對稱多處理器(SMP)的多個 CPU。
  2. 單個 CPU 內部的多個進程。
  3. 中斷與進程之間。

解決競態的途徑是保證對共享資源的互斥訪問:一個執行單元訪問共享資源的時候,其他的執行單元被禁止訪問。我們常用的互斥機制有:
中斷屏蔽原子操作自旋鎖信號量

一、中斷屏蔽

因爲 Linux 的異步 IO、進程調度等很多操作都是通過中斷來進行的。所以,最簡單的避免競態的方法就是在進入臨界區之前屏蔽系統的中斷。但是同樣,因爲中斷有這樣重要的作用,長時間的屏蔽中斷是很危險的。另外,中斷屏蔽只能解決上述三種競態情況中的後兩種,對於第一種競態是無法解決的。所以中斷屏蔽通常和自旋鎖搭配使用。
中斷屏蔽的使用方法是:
C
local_irq_disable() //屏蔽中斷
...
critical section /*臨界區*/
...
local_irq_enable()

中斷屏蔽通常是和自旋鎖聯合使用的。

二、原子操作

有時候,共享的資源可能正好是一個整數值或者是位操作。內核提供了一種原子的整數類型(位類型)。相應的操作如下:

  • 整形原子操作
    C
    void atomic_set(atomic_t *v, int i); //設置原子變量 v 的值爲 i
    atomic_t v = ATOMIC_INIT(0); //初始化原子變量 v 的值爲 0
    atomic_read(atomic_t *v); //讀取原子變量 v 的值
    void atomic_add(int i, atomic_t *v); //原子變量 v 的值加 i
    void atomic_sub(int i, atomic_t *v); //原子變量 v 的值減 i
    void atomic_inc(atomic_t *v); //原子變量 v 的值自加1
    void atomic_dec(atomic_t *v); //原子變量 v 的值自減1
    int atomic_inc_and_test(atomic_t *v); //原子變量 v 的值自加1,並測試是否等於0
    int atomic_dec_and_test(atomic_t *v); //原子變量 v 的值自減1,並測試是否等於0
    int atomic_sub_and_test(int i, atomic_t *v); //原子變量 v 的值減 i,並測試是否等於0
    int atomic_add_and_return(int i, atomic_t *v); //原子變量 v 的值加 i,並返回值
    int atomic_sub_and_return(int i, atomic_t *v); //原子變量 v 的值減 i,並返回值
    int atomic_inc_and_return(atomic_t *v); //原子變量 v 的值自加1,並返回值
    int atomic_dec_and_return(atomic_t *v); //原子變量 v 的值自減1,並返回值

    atomic_t 的數據只能通過上述的函數進行訪問。

  • 位原子操作
    C
    void set_bit(nr, void *addr); //設置addr地址的第nr位爲1
    void clear_bit(nr, void *addr); //設置addr地址的第nr位爲0
    void change_bit(nr, void *addr); //反置addr地址的第nr位
    test_bit(nr, void *addr); //返回addr地址的第nr位
    int test_and_set_bit(nr, void *addr); //返回addr地址的第nr位並置該位爲1
    int test_and_clear_bit(nr, void *addr); //返回addr地址的第nr位並置該位爲0
    int test_and_change_bit(nr, void *addr);//返回addr地址的第nr位並反置該位

三、自旋鎖

自旋鎖是一個互斥設備,他只能有兩個值:鎖定和解鎖。爲了獲取一個自旋鎖,程序先執行一個原子操作,測試相關的位,如果鎖可用,則鎖定,代碼進入臨界區;如果鎖不可用,代碼則進入循環測試直到該鎖可用。

  1. 自旋鎖的初始化:
    可以使用兩種方法進行自旋鎖的初始化:
    編譯時使用:spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
    運行時使用:void spin_lock_init(spinlock_t *lock);

  2. 獲取鎖
    獲取鎖使用下面的函數:
    C
    void spin_lock(spinlock_t *lock);

    需要注意的是,自旋鎖的等待是不可中斷的,一旦調用了該函數,在獲取鎖之前將一直處於自旋狀態。

    如果不想阻塞等待可以使用非阻塞版本的獲取鎖:
    C
    void spin_trylock(spinlock_t *lock);

    該函數在成功獲取鎖的情況下返回非零值,在未獲取鎖的情況下返回0

  3. 釋放鎖
    釋放鎖的函數如下:
    C
    void spin_unlock(spinlock_t *lock);

    這個函數一般與 spin_lockspin_trylock 搭配使用。

  4. 自旋鎖和中斷
    自旋鎖可以保證臨界區不受當前CPU和其他CPU的搶佔進程打擾,即能解決前面提到的競態中的前兩種,但是依舊可能受到中斷的影響,所以自旋鎖有下列衍生:
    C
    void spin_lock_irq(spinlock_t *lock); //在獲取自旋鎖之前禁止中斷
    void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); //在獲取鎖之前屏蔽中斷,並將相應的中斷狀態保存在 flags 中
    void spin_lock_bh(spinlock_t *lock); //在獲取鎖之前屏蔽軟件中斷,但保持硬件中斷

    當然還有與上面幾個函數一一對應的 unlock 函數,不再詳細描述。

  5. 讀寫自旋鎖
    讀寫自旋鎖是對自旋鎖的擴展,它允許多個讀操作併發執行,但是只能有一個寫單元。相應的函數如下:
    C
    rwlock_t my_rwlock = RW_LOCK_UNLOCKED; //靜態初始化讀寫自旋鎖
    rwlock_init(rwlock_t *my_rwlock); //動態初始化讀寫自旋鎖
    void read_lock(rwlock_t *my_rwlock); //獲取讀鎖
    void read_unlock(rwlock_t *my_rwlock); //釋放讀鎖
    void write_lock(rwlock_t *my_rwlock); //獲取寫鎖
    void write_unlock(rwlock_t *my_rwlock); //釋放寫鎖

    讀寫自旋鎖也有相應的中斷衍生版本。

  6. 順序鎖
    對於資源較小,頻繁被讀取但是很少寫入的資源,可以使用順序鎖。順序鎖的讀執行單元不會被寫執行單元阻塞。
    順序鎖的初始化類似於自旋鎖:
    C
    seqlock_t my_seqlock = SEQLOCK_UNLOCKED; //靜態初始化讀寫自旋鎖
    seqlock_t_init(seqlock_t *my_seqlock); //動態初始化讀寫自旋鎖

    對於寫單元來說,相應的獲取鎖和釋放鎖的機制和自旋鎖一致,不再詳說,詳細說下讀單元的執行。
    C
    unsigned int seq;
    do {
    seq = read_seqbegin(&my_seqlock);
    /*相應的操作*/
    } while (read_seqretry(&the_lock, seq))

    read_seqbegin會返回當前順序鎖的順序號,read_seqretry 會檢查當前的順序號是否改變。
    通常不能使用順序鎖來保護數據結構中含有指針的數據。

  7. 讀-拷貝-更新
    RCU(read-copy-update,讀-拷貝-更新)是基於原理命名的,在這裏不再詳細介紹

四、信號量

信號量和自旋鎖類似,只有得到信號量的進程才能執行臨界區代碼。但是與自旋鎖不同的是,當獲取不到信號量的時候,進程不會原地打轉而是會進入休眠等待狀態。

  1. 信號量的定義和初始化
    有三種方法可以初始化一個信號量:
    C
    void sema_init(struct semaphore *sem, int val); //初始化信號量 sem,並將 sem 的值設爲 val
    void init_MUTEX(struct semaphore *sem); //初始化信號量 sem 爲0
    void init_MUTEX_LOCKED(struct semaphore *sem); //初始化信號量 sem 爲1
    DECLARE_MUTEX(name); //聲明並初始化一個名爲 name 的信號量爲0
    DECLARE_MUTEX_LOCKED(name); //聲明並初始化一個名爲 name 的信號量爲1

    對於含有 LOCKED 的初始化方法,信號量的初始狀態就是鎖定的。

  2. 信號量的獲取
    信號量的獲取使用下列方式:
    C
    void down(struct semaphore *sem); //會導致睡眠,不能被信號打斷
    int down_interruptible(struct semaphore *sem); //會導致睡眠,並能被信號打斷
    int down_trylock(struct semaphore *sem); //不導致睡眠

    使用 down_interruptible 函數被信號打斷和使用 down_trylock 函數未獲取信號量,函數會返回非零值。否則返回0。

  3. 信號量的釋放
    C
    void up(struct semaphore *sem);

    該函數會釋放信號量,喚醒等待者。

  4. 完成量
    completion (完成量) 用於一個執行單元等待另一個執行單元執行完成某事。相應的使用方法如下:
    C
    struct completion my_completion;
    init_completion(&my_completion); //定義並初始化 completion
    /*
    DECLARE_COMPLETION(my_completion); //另一種創建 completion 的方法
    */
    void wait_for_completion(struct completion *c); //等待 completion 被喚醒
    ...
    void complete(struct completion *c); //喚醒一個等待 c 的執行單元
    void complete_all(struct completion *c); //喚醒所有等待 c 的執行單元
    INIT_COMPLETION(struct completion my_completion); //用於重新初始化一個信號量
    void completion_and_exit(struct completion *c, long retval); //

    對於 completion_and_exit 的用法還有不清楚的地方,等驗證完成後再來補充。

  5. 讀寫信號量
    讀寫信號量類似與讀寫自旋鎖,使用方法如下:
    C
    struct rw_semaphore my_rws; //定義讀寫信號量
    void init_rwsem(struct rw_semaphore *sem); //初始化讀寫信號量
    void down_read(struct rw_semaphore *sem);
    void down_read_trylock(struct rw_semaphore *sem); //讀信號量獲取
    void up_read(struct rw_semaphore *sem); //讀信號量釋放
    void down_write(struct rw_semaphore *sem);
    void down_write_trylock(struct rw_semaphore *sem); //寫信號量獲取
    void up_write(struct rw_semaphore *sem); //寫信號量釋放

五、互斥體

互斥體簡單實現了互斥的功能:
C
struct mutex my_mutex;
mutex_init(&my_mutex); //初始化互斥體
void inline __sched mutex_lock(struct mutex *lock);
int __sched mutex_lock_interruptible(struct mutex *lock);
int __sched mutex_trylock(struct mutext *lock); //獲取互斥體
void __sched mutex_unlock(struct mutext *lock); //釋放互斥體

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章