linux裝置驅動學習(6) 高階字元驅動學習--阻塞型I/0
提出問題:若驅動程式無法立即滿足請求,該如何響應? 比如:當資料不可用時呼叫read,或是在緩衝區已滿時,呼叫write
解決問題:驅動程式應該(預設)該阻塞程序,將其置入休眠狀態直到請求可繼續。
休眠:
當一個程序被置入休眠時,它會被標記為一種特殊狀態並從排程器執行佇列中移走,直到某些情況下修改了這個狀態,才能執行該程序。
安全進入休眠兩原則:
1.永遠不要在原子上下文中進入休眠。(原子上下文:在執行多個步驟時,不能有任何的併發訪問。這意味著,驅動程式不能再擁有自旋鎖,seqlock,或是RCU鎖時,休眠)
2.對喚醒之後的狀態不能做任何假定,因此必須檢查以確保我們等待的條件真正為真
臨界區 vs 原子上下文
原子上下本:一般說來,具體指在中斷,軟中斷,或是擁有自旋鎖的時候。
臨界區:每次只允許一個程序進入臨界區,進入後不允許其它程序訪問。
other question:
要休眠程序,必須有一個前提:有人能喚醒程序,而起這個人必須知道在哪兒能喚醒程序,這裡,就引入了“等待佇列”這個概念。
等待佇列:就是一個程序連結串列(我的理解:是一個休眠程序連結串列),其中包含了等待某個特定事件的所有程序。
等待佇列頭:wait_queue_head_t,定義在<linux/wait.h>
定義方法:靜態 DECLARE_QUEUE_HEAD(name)
動態 wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
簡單休眠
linux最簡單的休眠方式是wait_event(queue,condition)及其變種,在實現休眠的同時,它也檢查程序等待的條件。四種wait_event形式如下:
wait_event
wait_event_interruptible(queue,condition);/*推薦,返回非零值意味著休眠被中斷,且驅動應返回-ERESTARTSYS*/
wait_event_timeout(queue,condition,timeout);
wait_event_interruptible_timeout(queue,conditon,timeout);/*有限的時間的休眠,若超時,則不管條件為何值返回0*/
喚醒休眠程序的函式:wake_up
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head *queue);
慣例:用wake_up喚醒wait_event,用wake_up_interruptible喚醒wait_event_interruptible |
休眠與喚醒 例項分析:
本例實現效果為:任何從該裝置上讀取的程序均被置於休眠。只要某個程序向給裝置寫入,所有休眠的程序就會被喚醒。
static DECLARE_WAIT_QUEUE_HEAD(wq); static int flag =0; ssize_t sleepy_read(struct file *filp,char __user *buf,size_t count,loff_t *pos) {
} ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos) {
} |
阻塞與非阻塞類操作
小知識點:
作業系統中睡眠、阻塞、掛起的區別形象解釋 | |||
|
全功能的 read 和 write 方法涉及到程序可以決定是進行非阻塞 I/O還是阻塞 I/O操作。明確的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 標誌來指示(定義再<linux/fcntl.h> ,被<linux/fs.h>自動包含)。瀏覽原始碼,會發現O_NONBLOCK 的另一個名字:O_NDELAY ,這是為了相容 System V 程式碼。O_NONBLOCK 標誌預設地被清除,因為等待資料的程序的正常行為只是睡眠.
其實不一定只有read 和 write 方法有阻塞操作,open也可以有阻塞操作。
1.如果指定了O_NONBLOCK標誌,read和write的行為就會有所不同。如果在資料沒有就緒時呼叫read或是在緩衝區沒有空間時呼叫write,則該呼叫簡單的返回-EAGAIN。
2.非阻塞型操作會立即返回,使得應用程式可以查詢資料。在處理非阻塞型檔案時,應用程式呼叫stdio函式必須非常小心,因為很容易就把一個非阻塞返回誤認為EOF,所以必須始終檢查errno。
3.有些驅動程式還為O_NONBLOCK實現了特殊的語義。例如,在磁帶還沒有插入時開啟一個磁帶裝置通常會阻塞,如果磁帶驅動程式使用O_NONBLOCK開啟的,則不管磁帶在不在,open都會立即成功返回。
4.只有read,write,open檔案操作受非阻塞標誌的影響。
read負責管理阻塞型和非阻塞型輸入,如下所示:
static ssize_t scull_p_read (struct file *filp,char __user *buf,size_t count,loff_t *f_pos) {
} |
程式碼分析:
while迴圈在擁有裝置訊號量時測試緩衝區。如果其中有資料,則可以立即將資料返回給使用者而不需要休眠,這樣,整個迴圈體就被跳過了。相反,如果緩衝區為空,則必須休眠。但在休眠之前必須釋放裝置訊號量,因為如果在擁有該訊號量時休眠,任何寫入者都沒有機會來喚醒。在釋放訊號量之後,快速檢測使用者請求的是否是非阻塞I/O,如果是,則返回,否則呼叫wait_event_interruptible。
高階休眠:
程序休眠步驟:
1.分配並初始化一個wait_queue_t結構,然後將其加入到對應的等待佇列
2.設定程序的狀態,將其標記為休眠在 <linux/sched.h> 中定義有幾個任務狀態:TASK_RUNNING意思是程序可執行。有 2 個狀態指示一個程序是在睡眠:TASK_INTERRUPTIBLE 和 TASK_UNTINTERRUPTIBLE。
2.6 核心的驅動程式碼通常不需要直接操作程序狀態。但如果需要這樣做使用的程式碼是:
void set_current_state(int new_state); |
在老的程式碼中, 你常常見到如此的東西:current->state = TASK_INTERRUPTIBLE; 但是象這樣直接改變 current 是不推薦的,當資料結構改變時這樣的程式碼將會失效。通過改變 current 狀態,只改變了排程器對待程序的方式,但程序還未讓出處理器。
3.最後一步:釋放處理器。但之前我們必須首先檢查休眠等待的條件。如果不做這個檢查,可能會引入競態:如果在忙於上面的這個過程時有其他的執行緒剛剛試圖喚醒你,你可能錯過喚醒且長時間休眠。因此典型的程式碼下:
|
如果程式碼只是從 schedule 返回,則程序處於TASK_RUNNING 狀態。 如果不需睡眠而跳過對 schedule 的呼叫,必須將任務狀態重置為 TASK_RUNNING,還必要從等待佇列中去除這個程序,否則它可能被多次喚醒
手工休眠:
1.建立並初始化一個等待佇列入口。
方法1: DEFINE_WAIT(my_wait); 方法2: wait_queue_t my_wait; init_wait(&my_wait); |
2.將我們的等待佇列入口新增到佇列中,並設定程序的狀態。
void prepare_to_wait(wait_queue_head_t *queue, wait_queue_t *wait, int state); queue和wait分別是等待佇列頭和進城入口,state是程序的新狀態,它應該是TASK_INTERRUPTIBLE(可中斷休眠)或者TASK_UNINTERRUPTIBLE(不可中斷休眠) |
3.在呼叫prepaer_to_wait之後,程序即可呼叫schedule,當然在這之前,應確保仍有必有等待。一旦schedule返回,就到了清理時間了。
void finesh_wait(wait_queue_head_t *queue,wait_queue_t *wait); |
獨佔等待
當一個程序呼叫 wake_up 在等待佇列上,所有的在這個佇列上等待的程序被置為可執行的。 這在許多情況下是正確的做法。但有時,可能只有一個被喚醒的程序將成功獲得需要的資源,而其餘的將再次休眠。這時如果等待佇列中的程序數目大,這可能嚴重降低系統性能。為此,核心開發者增加了一個“獨佔等待”選項。它與一個正常的睡眠有 2 個重要的不同:
1.當等待佇列入口設定了 WQ_FLAG_EXCLUSEVE 標誌,它被新增到等待佇列的尾部;否則,新增到頭部。
2.當 wake_up 被在一個等待佇列上呼叫, 它在喚醒第一個有 WQ_FLAG_EXCLUSIVE 標誌的程序後停止喚醒.但核心仍然每次喚醒所有的非獨佔等待。
採用獨佔等待要滿足 2 個條件:
(1)希望對資源進行有效競爭;
(2)當資源可用時,喚醒一個程序就足夠來完全消耗資源。
使一個程序進入獨佔等待,可呼叫:
|
注意:無法使用 wait_event 和它的變體來進行獨佔等待.
喚醒的相關函式
很少會需要呼叫wake_up_interruptible 之外的喚醒函式,但為完整起見,這裡是整個集合:
wake_up_interruptible_nr(wait_queue_head_t *queue, int nr); /*這些函式類似 wake_up, 除了它們能夠喚醒多達 nr 個獨佔等待者, 而不只是一個. 注意傳遞 0 被解釋為請求所有的互斥等待者都被喚醒*/ wake_up_all(wait_queue_head_t*queue);
wake_up_interruptible_sync(wait_queue_head_t
*queue);/*一個被喚醒的程序可能搶佔當前程序, 並且在 wake_up 返回之前被排程到處理器。 但是, 如果你需要不要被排程出處理器時,可以使用 wake_up_interruptible 的"同步"變體. 這個函式最常用在呼叫者首先要完成剩下的少量工作,且不希望被排程出處理器時。*/ |
poll and select
當應用程式需要進行對多檔案讀寫時,若某個檔案沒有準備好,則系統會處於讀寫阻塞的狀態下,並影響了其它檔案的讀寫。為了避免這種情況的發生,則必須使用多輸入輸出流又不想阻塞在他們任何一個上的應用程式常將非阻塞的I/O和poll(system V),select(BSD Unix),epoll(linux2.5.45開始)系統呼叫配合使用。當poll函式返回時,會給出一個檔案是否可讀寫的標誌,應用程式根據不同的標識讀寫相應的檔案,實現非阻塞的讀寫。這些系統呼叫功能相同:允許程序來決定它是否可讀或寫一個或多個檔案而不阻塞。這些呼叫也可阻塞程序直到任何一個給定集合的檔案描述符可用來讀或寫。這些呼叫都需要來自裝置驅動中的poll方法的支援。poll返回不同的標識,告訴主程序檔案是否可以讀寫,其原型:
<linux/poll.h> unsigned int (*poll) (struct file *filp,poll_table *wait); |
實現這個裝置方法分兩步:
1.在一個或多個可指示查詢狀態變化的等待佇列上呼叫poll_wait,如果沒有檔案描述符可用來執行I/O,核心使這個程序在等待佇列上等待所有的傳遞給系統呼叫的檔案描述符,驅動通過呼叫函式poll_wait增加一個佇列到poll_wait結構,原型:
void poll_wait(struct file *,wait_queue_head_t *,poll_table*); |
2.返回一個位掩碼:描述可能不必阻塞就立刻進行的操作,幾個標誌(通過<linux/poll.h>定義)用來指示可能的操作:
標誌 |
含義 |
POLLIN |
如果裝置無阻塞的讀,就返回該值 |
POLLRDNORM |
通常的資料已經準備好,可以讀了,就返回該值。通常的做法是會返回(POLLLIN|POLLRDNORA) |
POLLRDBAND |
如果可以從裝置讀出帶外資料,就返回該值,它只可在linux核心的某些網路程式碼中使用,通常不用在裝置驅動程式中 |
POLLPRI |
如果可以無阻塞的讀取高優先順序(帶外)資料,就返回該值,返回該值會導致select報告檔案發生異常,以為select八帶外資料當作異常處理 |
POLLHUP |
當讀裝置的程序到達檔案尾時,驅動程式必須返回該值,依照select的功能描述,呼叫select的程序被告知程序時可讀的。 |
POLLERR |
如果裝置發生錯誤,就返回該值。 |
POLLOUT |
如果裝置可以無阻塞地些,就返回該值 |
POLLWRNORM |
裝置已經準備好,可以寫了,就返回該值。通常地做法是(POLLOUT|POLLNORM) |
POLLWRBAND |
於POLLRDBAND類似 |
使用舉例:
|
與read與write的互動
正確實現poll呼叫的規則:
從裝置讀取資料:
(1)如果在輸入緩衝中有資料,read 呼叫應當立刻返回,即便資料少於應用程式要求的,並確保其他的資料會很快到達。 如果方便,可一直返回小於請求的資料,但至少返回一個位元組。在這個情況下,poll 應當返回 POLLIN|POLLRDNORM。
(2)如果在輸入緩衝中無資料,read預設必須阻塞直到有一個位元組。若O_NONBLOCK 被置位,read 立刻返回 -EAGIN 。在這個情況下,poll 必須報告這個裝置是不可讀(清零POLLIN|POLLRDNORM)的直到至少一個位元組到達。
(3)若處於檔案尾,不管是否阻塞,read 應當立刻返回0,且poll 應該返回POLLHUP。
向裝置寫資料
(1)若輸出緩衝有空間,write 應立即返回。它可接受小於呼叫所請求的資料,但至少必須接受一個位元組。在這個情況下,poll應返回 POLLOUT|POLLWRNORM。
(2)若輸出緩衝是滿的,write預設阻塞直到一些空間被釋放。若 O_NOBLOCK 被設定,write 立刻返回一個 -EAGAIN。在這些情況下, poll 應當報告檔案是不可寫的(清零POLLOUT|POLLWRNORM). 若裝置不能接受任何多餘資料, 不管是否設定了 O_NONBLOCK,write 應返回 -ENOSPC("裝置上沒有空間")。
(3)永遠不要讓write在返回前等待資料的傳輸結束,即使O_NONBLOCK 被清除。若程式想保證它加入到輸出緩衝中的資料被真正傳送, 驅動必須提供一個 fsync 方法。
永遠不要讓write呼叫在返回前等待資料傳輸的結束,即使O_NONBLOCK標誌被清除。這是因為很多應用程式使用select來檢查write是否會阻塞。如果報告裝置可以寫入,呼叫就不能被阻塞。如果使用裝置的程式需要保證輸出緩衝區中的資料確實已經被傳送出去,驅動程式就必須提供一個fsync方法。
重新整理待處理輸出
若一些應用程式需要確保資料被髮送到裝置,就必須實現fsync 方法。對 fsync 的呼叫只在裝置被完全重新整理時(即輸出緩衝為空)才返回,不管 O_NONBLOCK 是否被設定,即便這需要一些時間。其原型是:
|
底層的資料結構
|
非同步通知
通過使用非同步通知,應用程式可以在資料可用時收到一個訊號,而無需不停地輪詢。
啟用步驟:
(1)它們指定一個程序作為檔案的擁有者:使用 fcntl 系統呼叫發出 F_SETOWN 命令,這個擁有者程序的 ID 被儲存在 filp->f_owner。目的:讓核心知道訊號到達時該通知哪個程序。
(2)使用 fcntl 系統呼叫,通過 F_SETFL 命令設定 FASYNC 標誌。
核心操作過程
1.F_SETOWN被呼叫時filp->f_owner被賦值。
2. 當 F_SETFL 被執行來開啟 FASYNC, 驅動的 fasync 方法被呼叫.這個標誌在檔案被開啟時預設地被清除。
3. 當資料到達時,所有的註冊非同步通知的程序都會被髮送一個 SIGIO 訊號。
Linux 提供的通用方法是基於一個數據結構和兩個函式,定義在<linux/fs.h>。
資料結構:
|
驅動呼叫的兩個函式的原型:
|
當一個開啟的檔案的FASYNC標誌被修改時,呼叫fasync_helper 來從相關的程序列表中新增或去除檔案。除了最後一個引數, 其他所有引數都時被提供給 fasync 方法的相同引數並被直接傳遞。 當資料到達時,kill_fasync 被用來通知相關的程序,它的引數是被傳遞的訊號(常常是 SIGIO)和 band(幾乎都是 POLL_IN)。
這是 scullpipe 實現 fasync 方法的:
|
當資料到達, 下面的語句必須被執行來通知非同步讀者. 因為對 sucllpipe 讀者的新資料通過一個發出 write 的程序被產生, 這個語句出現在 scullpipe 的 write 方法中:
|
當檔案被關閉時必須呼叫fasync 方法,來從活動的非同步讀取程序列表中刪除該檔案。儘管這個呼叫僅當 filp->f_flags 被設定為 FASYNC 時才需要,但不管什麼情況,呼叫這個函式不會有問題,並且是普遍的實現方法。 以下是 scullpipe 的 release 方法的一部分:
|
非同步通知使用的資料結構和 struct wait_queue 幾乎相同,因為他們都涉及等待事件。區別非同步通知用 struct file 替代 struct task_struct. 佇列中的 file 用獲取 f_owner, 一邊給程序傳送訊號。