1. 關於競態
如果有兩個進程同時打開設備進行寫數據操作,在進程A寫數據時它將新申請一快設備內存,並在設備dev數據鏈表追加一個新的量子,使指針指向這個新的設備內存塊,而在進程B寫操作也有同樣操作,這樣如果不做任何驅動修改,因為設備被兩個進程同時打開,兩個進程擁有同一個設備數據鏈表的信息,就會產生修改同一個數據,很明顯最先操作的進程建立的數據會被後面的進程覆蓋,這就是產生了競態。
不過,不太積極的說法是,在單處理器上這種情況不會發生,因為在內核運行的代碼是非搶占性的,就是說在同一時間處理器只能處理一個代碼,但是多處理器系統就可能發生了。
LINUX提供了競態的解決辦法:
1)信號量semaphore, 用於互斥
#include
很簡單,就是在需要避免競態的數據塊中定義一個標記,當有進程在使用時把標記設置成0,表示信號已經被占用了不能再用,所有的進程如果要訪問該數據塊,必須先檢查該標記,如果為0,表示有進程正在占用,就必須等待。
因此在scull0的數據結構Scull_Dev中就有一個semaphore的標記。
但是信號量我們是由內核處理的,因為我們希望當進程要訪問信號量時,如果信號量被使用,進程就應該交給內核,進入等待,而不是由我們自己循環的檢查和等待信號量。
ok,既然又要交給內核管理,那麼必須初始化信號量。
sema_init(&scull_devices.sem, 1); //;注冊一個信號量,初始化為1,表示可用。
當需要獲取信號量時,調用down_interruptable(&sem), 釋放信號量up(&sem), up並將喚醒正在等待信號量的進程。
if (down_interruptable(&dev->sem)) return -ERESTARTSYS; //;如果失敗,直接返回,不能調用up(&sem)//;data operations...
up(&dev->sem);
注意的是,要小心信號量的使用,可見,如果有個進程持有信號量,而當它釋放信號量失敗的話,其他進程就會一直阻塞。
另外因為使用信號量會導致進程睡眠,所以在中斷處理中不能適用信號量。
2)鎖
可以看到,使用信號量,如果有一個進程持有了信號量,另一個進程就會進入睡眠等待。而很多情況並不需要進程進入等待睡眠,例如中斷處理中不允許進入睡眠,或者一些情況下只是簡單測試公共數據是否被其它進程占用,如果被占用,就重新測試直到可以使用,這裡就只需要利用自旋鎖(spinlock)。當然使用自旋鎖時處理器被占用,所以自旋鎖適用持有數據時間比較短的情況,而且絕對不能在持有鎖時進入睡眠。
#include
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 或者spin_lock_init(&my_lock); 申明/創建一個鎖
spin_lock(spinlock_t *my_lock); 獲得給定的鎖,如果鎖被占用,就自旋直到鎖可用,當spin_lock返回時調用函數即持有了該鎖,直到釋放spin_unlock(spinlock_t *my_lock); 釋放鎖
2 關於阻塞和非阻塞
2.1 關於阻塞
對read調用存在一個問題,就是當設備無數據可讀時,解決的方法有兩種,一是不阻塞直接讀失敗跳出。 二就是阻塞讀操作,進程進入睡眠,等待有數據時喚醒。
這裡探討一下阻塞型IO,處理睡眠和喚醒。
睡眠就是當一個進程需要等待一個事件時,應該暫時掛起,讓出CPU,等事件到達後再喚醒執行。
處理睡眠的一種方法是把進程加入等待隊列:
1)首先需要申明和初始化一個等待隊列項.
#include
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
如果是申明一個靜態全局的等待隊列,可以不用上面兩個定義,直接使用
DECLARE_WAIT_QUEUE_HEAD(my_queue); //靜態申明將在編譯時自動被初始化
2)使用已初始化的等待隊列項
在需要加入內核等待隊列時,調用 interruptible_sleep_on(&my_queue); 或者sleep_on(&my_queue)
在需要喚醒時,調用wake_up_interruptible(&my_queue); 或者wake_up(&my_queue)
3)interruptible_sleep_on()的缺陷
a.引起的競態:
要了解interruptible_sleep_on()等這些sleep_on函數可能引起的競態,就需要多interruptible_sleep_on()的實現有個認識。
等待隊列其實是一個隊列鏈表,鏈表中的數據是類型wait_queue_t. 簡化了的interruptible_sleep_on()內部大概是這樣:
#include
wait_queue_t wait; //;定義一個等待隊列
init_wait_queue_entry(&wait, current); //;初始化
current->state = TASK_INTERRUPTILBE; //;設置為休眠狀態,將要進入睡眠
add_wait_queue(&my_queue, &wait); //;把我們定義的等待隊列項加入到這個等待隊列中
schedule(); //;真正進入睡眠
remove_wait_queue(&my_queue, &wait); //;事件到達,schedule()返回
競態就發生在current->state = TASK_INTERRUPTIBLE和schedule()之間,在一些情況下,當驅動准備進入睡眠,即已經設置了current->state時,可能剛好有數據到達,這個時候wake_up是不會喚醒這個還沒有真正進入睡眠的進程,這樣就可能造成該進程因為沒有響應喚醒一直處於睡眠,這樣就產生這個競態,這個競態也是很容易發生的。解決辦法就是不使用interruptible_sleep_on(),而是直接使用它的內部實現。
例如:
#include
wait_queue_t wait; //;定義一個等待隊列
init_wait_queue_entry(&wait, current); //;初始化
add_wait_queue(&my_queue, &wait); //;把我們定義的等待隊列項加入到這個等待隊列中
while(1){
current->state = TASK_INTERRUPTILBE; //;設置為休眠狀態,將要進入睡眠
if (short_head != short_tail) break; //;測試是否有數據到達,如果有,跳出
schedule(); //;真正進入睡眠
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait); //;事件到達,schedule()返回
事實上,可以不用我們做這些復雜的事情,內核定義了一個宏
wait_event_interruptible(wq, condition); 或者wait_event(wq, condition) condition就是測試的條件