今天進入《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)seqlock
2.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/devices
Character 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 mtd
116 alsa
128 ptm
136 pts
180 usb
189 usb_device
204 s3c2410_serial 252 complete
253 usb_endpoint
254 rtc
Block devices:
1 ramdisk
256 rfd
7 loop
31 mtdblock
93 nftl
96 inftl
179 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_testw
process 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_testw
process 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_testw
process 822 (completion_test) awakening the readers... write code=0 [Tekkaman2440@SBC2440V4]#./completion_testr process 823 (completion_test) going to sleep awoken 823 (completion_test) read code=0
|
實驗表明:如果先讀數據,讀的程序會被阻塞(因爲驅動在wait_for_completion,等待寫的完成)。如果先寫,讀程序會比較順利的執行下去(雖然也會休眠,但馬上會被喚醒!)。其原因可以從completion的源碼中找答案。completion其實就是自旋鎖的再包裝,具體細節參見completion的源碼。