Linux裝置驅動程式學習(3)-並發和競態

來源:互聯網
上載者:User
 今天進入《Linux裝置驅動程式(第3版)》第五章並發和競態的學習。對並發的管理是作業系統編程中核心的問題之一。 並發產生競態,競態導致共用資料的非法訪問。因為競態是一種極端低可能性的事件,因此程式員往往會忽視競態。但是在電腦世界中,百萬分之一的事件可能沒幾秒就會發生,而其結果是災難性的。 一、並發及其管理

競態通常是作為對資源的共用訪問結果而產生的。在設計自己的驅動程式時,第一個要記住的規則是:只要可能,就應該避免資源的共用。若沒有並發訪問,就不會有競態。這種思想的最明顯的應用是避免使用全域變數。但是,資源的共用是不可避免的 ,如硬體資源本質上就是共用、指標傳遞等等。資源共用的硬性規則:(1)在單個執行線程之外共用硬體或軟體資源的任何時候,因為另外一個線程可能產生對該資源的不一致觀察,因此必須顯示地管理對該資源的訪問。--訪問管理的常見技術成為“鎖定”或者“互斥”:確保一次只有一個執行線程可操作共用資源。(2)當核心代碼建立了一個可能和其他核心部分共用的對象時,該對象必須在還有其他組件引用自己時保持存在(並正確工作)。對象尚不能正確工作時,不能將其對核心可用。 二、訊號量和互斥體一個訊號量(semaphore: 旗語,號誌)本質上是一個整數值,它和一對函數聯合使用,這一對函數通常稱為P和V。希望進入臨屆區的進程將在相關訊號量上調用P;如果訊號量的值大於零,則該值會減小一,而進程可以繼續。相反,如果訊號量的值為零(或更小),進程必須等待知道其他人釋放該訊號。對訊號量的解鎖通過調用V完成;該函數增加訊號量的值,並在必要時喚醒等待的進程。當訊號量用於互斥時(即避免多個進程同是在一個臨界區運行),訊號量的值應初始化為1。這種訊號量在任何給定時刻只能由單個進程或線程擁有。在這種使用模式下,一個訊號量有事也稱為一個“互斥體(mutex)”,它是互斥(mutual exclusion)的簡稱。Linux核心中幾乎所有的訊號量均用於互斥。使用訊號量,核心代碼必須包含<asm/semaphore.h> 。以下是訊號量初始化的方法:
/*初始化函數*/void sema_init(struct semaphore *sem, int val);
由於訊號量通常被用於互斥模式。所以以下是核心提供的一組輔助函數和宏:
/*方法一、聲明+初始化宏*/DECLARE_MUTEX(name);DECLARE_MUTEX_LOCKED(name);/*方法二、初始化函數*/void init_MUTEX(struct semaphore *sem);void init_MUTEX_LOCKED(struct semaphore *sem);/*帶有“_LOCKED”的是將訊號量初始化為0,即鎖定,允許任何線程訪問時必須先解鎖。沒帶的為1。*/
P函數為:
void down(struct semaphore *sem); /*不推薦使用,會建立不可殺進程*/int down_interruptible(struct semaphore *sem);/*推薦使用,使用down_interruptible需要格外小心,若操作被中斷,該函數會返回非零值,而調用這不會擁有該訊號量。對down_interruptible的正確使用需要始終檢查傳回值,並做出相應的響應。*/int down_trylock(struct semaphore *sem);/*帶有“_trylock”的永不休眠,若訊號量在調用是不可獲得,會返回非零值。*/
V函數為:
void up(struct semaphore *sem);/*任何拿到訊號量的線程都必須通過一次(只有一次)對up的調用而釋放該訊號量。在出錯時,要特別小心;若在擁有一個訊號量時發生錯誤,必須在將錯誤狀態返回前釋放訊號量。*/
在scull中使用訊號量其實在之前的實驗中已經用到了訊號量的代碼,在這裡提一下應該注意的地方:在初始化scull_dev的地方:
/* Initialize each device. */for (i = 0; i < scull_nr_devs; i++) {        scull_devices[i].quantum = scull_quantum;        scull_devices[i].qset = scull_qset;        init_MUTEX(&scull_devices[i].sem);/* 注意順序:先初始化好互斥訊號量 ,再使scull_devices可用。*/        scull_setup_cdev(&scull_devices[i], i);}
而且要確保在不擁有訊號量的時候不會訪問scull_dev結構體。 讀取者/寫入者訊號量唯讀任務可並行完成它們的工作,而不需要等待其他讀取者退出臨界區。Linux核心提供了讀取者/寫入者訊號量“rwsem”,使用是必須包括<linux/rwsem.h> 。初始化:
void init_rwsem(struct rw_semaphore *sem);
唯讀介面:
void down_read(struct rw_semaphore *sem);int down_read_trylock(struct rw_semaphore *sem);void up_read(struct rw_semaphore *sem);
寫入介面:
void down_write(struct rw_semaphore *sem);int down_write_trylock(struct rw_semaphore *sem);void up_write(struct rw_semaphore *sem);void downgrade_write(struct rw_semaphore *sem);/*該函數用於把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫訊號量期間,任何讀者或寫者都將無法訪問該讀寫訊號量保護的共用資源,對於那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了並發性,提高了效率。*/

一個 rwsem 允許一個寫者或無限多個讀者來擁有該訊號量. 寫者有優先權; 當某個寫者試圖進入臨界區, 就不會允許讀者進入直到寫者完成了它的工作. 如果有大量的寫者競爭該訊號量,則這個實現可能導致讀者“餓死”,即可能會長期拒絕讀者訪問。因此, rwsem 最好用在很少請求寫的時候, 並且寫者只佔用短時間.

completion

completion是一種輕量級的機制,它允許一個線程告訴另一個線程某個工作已經完成。代碼必須包含<linux/completion.h>。使用的代碼如下:
DECLARE_COMPLETION(my_completion);/* 建立completion(聲明+初始化) *//////////////////////////////////////////////////////////struct completion my_completion;/* 動態聲明completion 結構體*/static inline void init_completion(&my_completion);/*動態初始化completion*////////////////////////////////////////////////////////void wait_for_completion(struct completion *c);/* 等待completion */void complete(struct completion *c);/*喚醒一個等待completion的線程*/void complete_all(struct completion *c);/*喚醒所有等待completion的線程*//*如果未使用completion_all,completion可重複使用;否則必須使用以下函數重新初始化completion*/INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/
completion的典型應用是模組退出時的核心線程終止。在這種遠行中,某些驅動程式的內部工作有一個核心線程在while(1)迴圈中完成。當核心準備清楚該模組時,exit函數會告訴該線程退出並等待completion。為此核心包含了用於這種線程的一個特殊函數:
void complete_and_exit(struct completion *c, long retval);
三、自旋鎖其實上面介紹的幾種訊號量和互斥機制,其底層源碼對於自身結構體的某些變數的維護也用到了現在我們講到的自旋鎖,但是絕不是自旋鎖的再封裝。自旋鎖是一個互斥裝置,他只能會兩個值:“鎖定”和“解鎖”。它通常實現為某個整數之中的單個位。

“測試並設定”的操作必須以原子方式完成。

任何時候,只要核心代碼擁有自旋鎖,在相關CPU上的搶佔就會被禁止。適用於自旋鎖的核心規則:

(1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。為了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。

(2)擁有自旋鎖的時間越短越好。自旋鎖原語所需包含的檔案是<linux/spinlock.h> ,以下是自旋鎖的核心API: 
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;/* 編譯時間初始化spinlock*/void spin_lock_init(spinlock_t *lock);/* 運行時初始化spinlock*//* 所有spinlock等待本質上是不可中斷的,一旦調用spin_lock,在獲得鎖之前一直處於自旋狀態*/void spin_lock(spinlock_t *lock);/* 獲得spinlock*/void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);/* 獲得spinlock,禁止本地cpu中斷,儲存中斷標誌於flags*/void spin_lock_irq(spinlock_t *lock);/* 獲得spinlock,禁止本地cpu中斷*/void spin_lock_bh(spinlock_t *lock)/* 獲得spinlock,禁止軟體中斷,保持硬體中斷開啟*//* 以下是對應的鎖釋放函數*/void spin_unlock(spinlock_t *lock);void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);void spin_unlock_irq(spinlock_t *lock);void spin_unlock_bh(spinlock_t *lock);/* 以下非阻塞自旋鎖函數,成功獲得,返回非零值;否則返回零*/int spin_trylock(spinlock_t *lock);int spin_trylock_bh(spinlock_t *lock);/*新核心的<linux/spinlock.h>包含了更多函數*/

讀取者/寫入者自旋鎖:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;/* 編譯時間初始化*/rwlock_t my_rwlock;rwlock_init(&my_rwlock); /* 運行時初始化*/void read_lock(rwlock_t *lock);void read_lock_irqsave(rwlock_t *lock, unsigned long flags);void read_lock_irq(rwlock_t *lock);void read_lock_bh(rwlock_t *lock);void read_unlock(rwlock_t *lock);void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);void read_unlock_irq(rwlock_t *lock);void read_unlock_bh(rwlock_t *lock);/* 新核心已經有了read_trylock*/void write_lock(rwlock_t *lock);void write_lock_irqsave(rwlock_t *lock, unsigned long flags);void write_lock_irq(rwlock_t *lock);void write_lock_bh(rwlock_t *lock);int write_trylock(rwlock_t *lock);void write_unlock(rwlock_t *lock);void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);void write_unlock_irq(rwlock_t *lock);void write_unlock_bh(rwlock_t *lock);/*新核心的<linux/spinlock.h>包含了更多函數*/
鎖陷阱

鎖定模式必須在一開始就安排好,否則其後的改進將會非常困難。

不明確規則:如果某個獲得鎖的函數要調用其他同樣試圖擷取這個鎖的函數,代碼就會鎖死。(不允許鎖的擁有者第二次獲得同個鎖。)為了鎖的正確工作,不得不編寫一些函數,這些函數假定調用這已經獲得了相關的鎖。

鎖的順序規則:再必須擷取多個鎖時,應始終以相同順序擷取。

若必須獲得一個局部鎖和一個屬於核心更中心位置的鎖,應先獲得局部鎖。

若我們擁有訊號量和自旋鎖的組合,必須先獲得訊號量。

不得再擁有自旋鎖時調用down。(可導致休眠)

盡量避免需要多個鎖的情況。

細顆粒度和粗顆粒度的對比:應該在最初使用粗顆粒度的鎖,除非有真正的原因相信競爭會導致問題。 四、鎖之外的辦法   (1)免鎖演算法經常用於免鎖的生產者/消費者任務的資料結構之一是迴圈緩衝區。它在裝置驅動程式中相當普遍,如以前移植的網路卡驅動程式。核心裡有一個通用的迴圈緩衝區的實現在 <linux/kfifo.h> 。 (2)原子變數完整的鎖機制對一個簡單的整數來講顯得浪費。核心提供了一種原子的整數類型,稱為atomic_t,定義在<asm/atomic.h>。原子變數操作是非常快的, 因為它們在任何可能時編譯成一條單個機器指令。以下是其介面函數:
void atomic_set(atomic_t *v, int i); /*設定原子變數 v 為整數值 i.*/atomic_t v = ATOMIC_INIT(0); /*編譯時間使用宏定義 ATOMIC_INIT 初始化原子值.*/int atomic_read(atomic_t *v); /*返回 v 的當前值.*/void atomic_add(int i, atomic_t *v);/*由 v 指向的原子變數加 i. 傳回值是 void*/void atomic_sub(int i, atomic_t *v); /*從 *v 減去 i.*/void atomic_inc(atomic_t *v);void atomic_dec(atomic_t *v); /*遞增或遞減一個原子變數.*/int atomic_inc_and_test(atomic_t *v);int atomic_dec_and_test(atomic_t *v);int atomic_sub_and_test(int i, atomic_t *v);/*進行一個特定的操作並且測試結果; 如果, 在操作後, 原子值是 0, 那麼傳回值是真; 否則, 它是假. 注意沒有 atomic_add_and_test.*/int atomic_add_negative(int i, atomic_t *v);/*加整數變數 i 到 v. 如果結果是負值傳回值是真, 否則為假.*/int atomic_add_return(int i, atomic_t *v);int atomic_sub_return(int i, atomic_t *v);int atomic_inc_return(atomic_t *v);int atomic_dec_return(atomic_t *v);/*像 atomic_add 和其類似函數, 除了它們返回原子變數的新值給調用者.*/
atomic_t 資料項目必須通過這些函數存取。 如果你傳遞一個原子項給一個期望一個整數參數的函數, 你會得到一個編譯錯誤。需要多個 atomic_t 變數的操作仍然需要某種其他種類的加鎖。 (3)位操作核心提供了一套函數來原子地修改或測試單個位。原子位操作非常快, 因為它們使用單個機器指令來進行操作, 而在任何時候低層平台做的時候不用禁止中斷. 函數是體系依賴的並且在 <asm/bitops.h> 中聲明. 以下函數中的資料是體系依賴的. nr 參數(描述要操作哪個位)在ARM體系中定義為unsigned int:
void set_bit(nr, void *addr); /*設定第 nr 位在 addr 指向的資料項目中。*/void clear_bit(nr, void *addr); /*清除指定位在 addr 處的無符號長型資料.*/void change_bit(nr, void *addr);/*翻轉nr位.*/test_bit(nr, void *addr); /*這個函數是唯一一個不需要是原子的位操作; 它簡單地返回這個位的當前值.*//*以下原子操作如同前面列出的, 除了它們還返回這個位以前的值.*/int test_and_set_bit(nr, void *addr);int test_and_clear_bit(nr, void *addr);int test_and_change_bit(nr, void *addr);

以下是一個使用範例:

/* try to set lock */while (test_and_set_bit(nr, addr) != 0)    wait_for_a_while();/* do your work *//* release lock, and check. */if (test_and_clear_bit(nr, addr) == 0)    something_went_wrong(); /* already released: error */
(4)seqlock2.6核心包含了一對新機制打算來提供快速地, 無鎖地存取一個共用資源。 seqlock要保護的資源小, 簡單, 並且常常被存取, 並且很少寫存取但是必須要快。seqlock 通常不能用在保護包含指標的資料結構。seqlock 定義在 <linux/seqlock.h> 。
/*兩種初始化方法*/seqlock_t lock1 = SEQLOCK_UNLOCKED;seqlock_t lock2;seqlock_init(&lock2);
這個類型的鎖常常用在保護某種簡單計算,讀存取通過在進入臨界區入口擷取一個(無符號的)整數序列來工作. 在退出時, 那個序列值與當前值比較; 如果不匹配, 讀存取必須重試.讀者代碼形式: 
unsigned int seq;do {    seq = read_seqbegin(&the_lock);/* Do what you need to do */} while read_seqretry(&the_lock, seq);
如果你的 seqlock 可能從一個中斷處理裡存取, 你應當使用 IRQ 安全的版本來代替:
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);
寫者必須擷取一個獨佔鎖定來進入由一個 seqlock 保護的臨界區,寫鎖由一個自旋鎖實現, 調用:
void write_seqlock(seqlock_t *lock);void write_sequnlock(seqlock_t *lock);
因為自旋鎖用來控制寫存取, 所有通常的變體都可用:
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);void write_seqlock_irq(seqlock_t *lock);void write_seqlock_bh(seqlock_t *lock);void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);void write_sequnlock_irq(seqlock_t *lock);void write_sequnlock_bh(seqlock_t *lock);
還有一個 write_tryseqlock 在它能夠獲得鎖時返回非零.

  (5)讀取-複製-更新讀取-拷貝-更新(RCU) 是一個進階的互斥方法, 在合適的情況下能夠有高效率. 它在驅動中的使用很少。 五、開發板實驗

在我的SBC2440V4開發板上
completion 實驗,因為別的實驗都要在並髮狀態下才可以實驗,所以本章的我只做了completion的實驗。我將《Linux裝置驅動程式(第3版)》提供的源碼做了修改,將原來的2.4核心的模組介面改成了2.6的介面,並編寫了測試程式。實驗源碼如下:模組程式連結:complete模組

模組測試程式連結:測試程式
[Tekkaman2440@SBC2440V4]#cd /lib/modules/[Tekkaman2440@SBC2440V4]#insmod complete.ko[Tekkaman2440@SBC2440V4]#echo 8 > /proc/sys/kernel/printk[Tekkaman2440@SBC2440V4]#cat /proc/devicesCharacter devices:  1 mem  2 pty  3 ttyp  4 /dev/vc/0  4 tty  4 ttyS  5 /dev/tty  5 /dev/console  5 /dev/ptmx  7 vcs 10 misc 13 input 14 sound 81 video4linux 89 i2c 90 mtd116 alsa128 ptm136 pts180 usb189 usb_device204 s3c2410_serial252 complete253 usb_endpoint254 rtcBlock devices:  1 ramdisk256 rfd  7 loop 31 mtdblock 93 nftl 96 inftl179 mmc[Tekkaman2440@SBC2440V4]#mknod -m 666 /dev/complete c 252 0[Tekkaman2440@SBC2440V4]#cd /tmp/[Tekkaman2440@SBC2440V4]#./completion_testr&[Tekkaman2440@SBC2440V4]#process 814 (completion_test) going to sleep[Tekkaman2440@SBC2440V4]#./completion_testr&[Tekkaman2440@SBC2440V4]#process 815 (completion_test) going to sleep[Tekkaman2440@SBC2440V4]#ps  PID Uid VSZ Stat Command    1 root 1744 S init    2 root SW< [kthreadd]    3 root SWN [ksoftirqd/0]    4 root SW< [watchdog/0]    5 root SW< [events/0]    6 root SW< [khelper]   59 root SW< [kblockd/0]   60 root SW< [ksuspend_usbd]   63 root SW< [khubd]   65 root SW< [kseriod]   77 root SW [pdflush]   78 root SW [pdflush]   79 root SW< [kswapd0]   80 root SW< [aio/0]  707 root SW< [mtdblockd]  708 root SW< [nftld]  709 root SW< [inftld]  710 root SW< [rfdd]  742 root SW< [kpsmoused]  751 root SW< [kmmcd]  769 root SW< [rpciod/0]  778 root 1752 S -sh  779 root 1744 S init  781 root 1744 S init  782 root 1744 S init  783 root 1744 S init  814 root 1336 D ./completion_testr  815 root 1336 D ./completion_testr  816 root 1744 R ps[Tekkaman2440@SBC2440V4]#./completion_testwprocess 817 (completion_test) awakening the readers...awoken 814 (completion_test)write code=0[Tekkaman2440@SBC2440V4]#read code=0[Tekkaman2440@SBC2440V4]#ps  PID Uid VSZ Stat Command    1 root 1744 S init    2 root SW< [kthreadd]    3 root SWN [ksoftirqd/0]    4 root SW< [watchdog/0]    5 root SW< [events/0]    6 root SW< [khelper]   59 root SW< [kblockd/0]   60 root SW< [ksuspend_usbd]   63 root SW< [khubd]   65 root SW< [kseriod]   77 root SW [pdflush]   78 root SW [pdflush]   79 root SW< [kswapd0]   80 root SW< [aio/0]  707 root SW< [mtdblockd]  708 root SW< [nftld]  709 root SW< [inftld]  710 root SW< [rfdd]  742 root SW< [kpsmoused]  751 root SW< [kmmcd]  769 root SW< [rpciod/0]  778 root 1752 S -sh  779 root 1744 S init  781 root 1744 S init  782 root 1744 S init  783 root 1744 S init  815 root 1336 D ./completion_testr  818 root 1744 R ps[1] - Done ./completion_testr[Tekkaman2440@SBC2440V4]#./completion_testwprocess 819 (completion_test) awakening the readers...awoken 815 (completion_test)write code=0[Tekkaman2440@SBC2440V4]#read code=0[Tekkaman2440@SBC2440V4]#ps  PID Uid VSZ Stat Command    1 root 1744 S init    2 root SW< [kthreadd]    3 root SWN [ksoftirqd/0]    4 root SW< [watchdog/0]    5 root SW< [events/0]    6 root SW< [khelper]   59 root SW< [kblockd/0]   60 root SW< [ksuspend_usbd]   63 root SW< [khubd]   65 root SW< [kseriod]   77 root SW [pdflush]   78 root SW [pdflush]   79 root SW< [kswapd0]   80 root SW< [aio/0]  707 root SW< [mtdblockd]  708 root SW< [nftld]  709 root SW< [inftld]  710 root SW< [rfdd]  742 root SW< [kpsmoused]  751 root SW< [kmmcd]  769 root SW< [rpciod/0]  778 root 1752 S -sh  779 root 1744 S init  781 root 1744 S init  782 root 1744 S init  783 root 1744 S init  820 root 1744 R ps[2] + Done ./completion_testr[Tekkaman2440@SBC2440V4]#ps  PID Uid VSZ Stat Command    1 root 1744 S init    2 root SW< [kthreadd]    3 root SWN [ksoftirqd/0]    4 root SW< [watchdog/0]    5 root SW< [events/0]    6 root SW< [khelper]   59 root SW< [kblockd/0]   60 root SW< [ksuspend_usbd]   63 root SW< [khubd]   65 root SW< [kseriod]   77 root SW [pdflush]   78 root SW [pdflush]   79 root SW< [kswapd0]   80 root SW< [aio/0]  707 root SW< [mtdblockd]  708 root SW< [nftld]  709 root SW< [inftld]  710 root SW< [rfdd]  742 root SW< [kpsmoused]  751 root SW< [kmmcd]  769 root SW< [rpciod/0]  778 root 1752 S -sh  779 root 1744 S init  781 root 1744 S init  782 root 1744 S init  783 root 1744 S init  821 root 1744 R ps[Tekkaman2440@SBC2440V4]#./completion_testwprocess 822 (completion_test) awakening the readers...write code=0[Tekkaman2440@SBC2440V4]#./completion_testrprocess 823 (completion_test) going to sleepawoken 823 (completion_test)read code=0         
實驗表明:如果先讀資料,讀的程式會被阻塞(因為驅動在wait_for_completion,等待寫的完成)。如果先寫,讀程式會比較順利的執行下去(雖然也會休眠,但馬上會被喚醒!)。其原因可以從completion的源碼中找答案。completion其實就是自旋鎖的再封裝,具體細節參見completion的源碼。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.