?? ch06s02.html
字號:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>6.2. 阻塞 I/O-Linux設(shè)備驅(qū)動第三版(中文版)-開發(fā)頻道-華星在線</title>
<meta name="description" content="驅(qū)動開發(fā)-開發(fā)頻道-華星在線" />
<meta name="keywords" content="Linux設(shè)備驅(qū)動,中文版,第三版,ldd,linux device driver,驅(qū)動開發(fā),電子版,程序設(shè)計,軟件開發(fā),開發(fā)頻道" />
<meta name="author" content="華星在線 www.21cstar.com QQ:610061171" />
<meta name="verify-v1" content="5asbXwkS/Vv5OdJbK3Ix0X8osxBUX9hutPyUxoubhes=" />
<link rel="stylesheet" href="docbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.69.0">
<link rel="start" href="index.html" title="Linux 設(shè)備驅(qū)動 Edition 3">
<link rel="up" href="ch06.html" title="第 6 章 高級字符驅(qū)動操作">
<link rel="prev" href="ch06.html" title="第 6 章 高級字符驅(qū)動操作">
<link rel="next" href="ch06s03.html" title="6.3. poll 和 select">
</head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
<div class="navheader">
<table width="100%" summary="Navigation header">
<tr><th colspan="3" align="center">6.2. 阻塞 I/O</th></tr>
<tr>
<td width="20%" align="left">
<a accesskey="p" href="ch06.html">上一頁</a> </td>
<th width="60%" align="center">第 6 章 高級字符驅(qū)動操作</th>
<td width="20%" align="right"> <a accesskey="n" href="ch06s03.html">下一頁</a>
</td>
</tr>
</table>
<hr>
</div>
<div class="sect1" lang="zh-cn">
<div class="titlepage"><div><div><h2 class="title" style="clear: both">
<a name="BlockingIO.sect1"></a>6.2. 阻塞 I/O</h2></div></div></div>
<p>回顧第 3 章, 我們看到如何實現(xiàn) read 和 write 方法. 在此, 但是, 我們跳過了一個重要的問題:一個驅(qū)動當它無法立刻滿足請求應(yīng)當如何響應(yīng)? 一個對 read 的調(diào)用可能當沒有數(shù)據(jù)時到來, 而以后會期待更多的數(shù)據(jù). 或者一個進程可能試圖寫, 但是你的設(shè)備沒有準備好接受數(shù)據(jù), 因為你的輸出緩沖滿了. 調(diào)用進程往往不關(guān)心這種問題; 程序員只希望調(diào)用 read 或 write 并且使調(diào)用返回, 在必要的工作已完成后. 這樣, 在這樣的情形中, 你的驅(qū)動應(yīng)當(缺省地)阻塞進程, 使它進入睡眠直到請求可繼續(xù). </p>
<p>本節(jié)展示如何使一個進程睡眠并且之后再次喚醒它. 如常, 但是, 我們必須首先解釋幾個概念.</p>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="IntroductiontoSleeping.sect2"></a>6.2.1. 睡眠的介紹</h3></div></div></div>
<p>對于一個進程"睡眠"意味著什么? 當一個進程被置為睡眠, 它被標識為處于一個特殊的狀態(tài)并且從調(diào)度器的運行隊列中去除. 直到發(fā)生某些事情改變了那個狀態(tài), 這個進程將不被在任何 CPU 上調(diào)度, 并且, 因此, 將不會運行. 一個睡著的進程已被擱置到系統(tǒng)的一邊, 等待以后發(fā)生事件.</p>
<p>對于一個 Linux 驅(qū)動使一個進程睡眠是一個容易做的事情. 但是, 有幾個規(guī)則必須記住以安全的方式編碼睡眠.</p>
<p>這些規(guī)則的第一個是: 當你運行在原子上下文時不能睡眠. 我們在第 5 章介紹過原子操作; 一個原子上下文只是一個狀態(tài), 這里多個步驟必須在沒有任何類型的并發(fā)存取的情況下進行. 這意味著, 對于睡眠, 是你的驅(qū)動在持有一個自旋鎖, seqlock, 或者 RCU 鎖時不能睡眠. 如果你已關(guān)閉中斷你也不能睡眠. 在持有一個旗標時睡眠是合法的, 但是你應(yīng)當仔細查看這樣做的任何代碼. 如果代碼在持有一個旗標時睡眠, 任何其他的等待這個旗標的線程也睡眠. 因此發(fā)生在持有旗標時的任何睡眠應(yīng)當短暫, 并且你應(yīng)當說服自己, 由于持有這個旗標, 你不能阻塞這個將最終喚醒你的進程.</p>
<p>另一件要記住的事情是, 當你醒來, 你從不知道你的進程離開 CPU 多長時間或者同時已經(jīng)發(fā)生了什么改變. 你也常常不知道是否另一個進程已經(jīng)睡眠等待同一個事件; 那個進程可能在你之前醒來并且獲取了你在等待的資源. 結(jié)果是你不能關(guān)于你醒后的系統(tǒng)狀態(tài)做任何的假設(shè), 并且你必須檢查來確保你在等待的條件是, 確實, 真的.</p>
<p>一個另外的相關(guān)的點, 當然, 是你的進程不能睡眠除非確信其他人, 在某處的, 將喚醒它. 做喚醒工作的代碼必須也能夠找到你的進程來做它的工作. 確保一個喚醒發(fā)生, 是深入考慮你的代碼和對于每次睡眠, 確切知道什么系列的事件將結(jié)束那次睡眠. 使你的進程可能被找到, 真正地, 通過一個稱為等待隊列的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的. 一個等待隊列就是它聽起來的樣子:一個進程列表, 都等待一個特定的事件.</p>
<p>在 Linux 中, 一個等待隊列由一個"等待隊列頭"來管理, 一個 wait_queue_head_t 類型的結(jié)構(gòu), 定義在<linux/wait.h>中. 一個等待隊列頭可被定義和初始化, 使用:</p>
<pre class="programlisting">
DECLARE_WAIT_QUEUE_HEAD(name);
</pre>
<p>或者動態(tài)地, 如下:</p>
<pre class="programlisting">
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
</pre>
<p>我們將很快返回到等待隊列結(jié)構(gòu), 但是我們知道了足夠多的來首先看看睡眠和喚醒.</p>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="SimpleSleeping.sect2"></a>6.2.2. 簡單睡眠</h3></div></div></div>
<p>當一個進程睡眠, 它這樣做以期望某些條件在以后會成真. 如我們之前注意到的, 任何睡眠的進程必須在它再次醒來時檢查來確保它在等待的條件真正為真. Linux 內(nèi)核中睡眠的最簡單方式是一個宏定義, 稱為 wait_event(有幾個變體); 它結(jié)合了處理睡眠的細節(jié)和進程在等待的條件的檢查. wait_event 的形式是:</p>
<pre class="programlisting">
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
</pre>
<p>在所有上面的形式中, queue 是要用的等待隊列頭. 注意它是"通過值"傳遞的. 條件是一個被這個宏在睡眠前后所求值的任意的布爾表達式; 直到條件求值為真值, 進程繼續(xù)睡眠. 注意條件可能被任意次地求值, 因此它不應(yīng)當有任何邊界效應(yīng).</p>
<p>如果你使用 wait_event, 你的進程被置為不可中斷地睡眠, 如同我們之前已經(jīng)提到的, 它常常不是你所要的. 首選的選擇是 wait_event_interruptible, 它可能被信號中斷. 這個版本返回一個你應(yīng)當檢查的整數(shù)值; 一個非零值意味著你的睡眠被某些信號打斷, 并且你的驅(qū)動可能應(yīng)當返回 -ERESTARTSYS. 最后的版本(wait_event_timeout 和 wait_event_interruptible_timeout)等待一段有限的時間; 在這個時間期間(以嘀噠數(shù)表達的, 我們將在第 7 章討論)超時后, 這個宏返回一個 0 值而不管條件是如何求值的.</p>
<p>圖片的另一半, 當然, 是喚醒. 一些其他的執(zhí)行線程(一個不同的進程, 或者一個中斷處理, 也許)必須為你進行喚醒, 因為你的進程, 當然, 是在睡眠. 基本的喚醒睡眠進程的函數(shù)稱為 wake_up. 它有幾個形式(但是我們現(xiàn)在只看其中 2 個):</p>
<pre class="programlisting">
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
</pre>
<p>wake_up 喚醒所有的在給定隊列上等待的進程(盡管這個情形比那個要復(fù)雜一些, 如同我們之后將見到的). 其他的形式(wake_up_interruptible)限制它自己到處理一個可中斷的睡眠. 通常, 這 2 個是不用區(qū)分的(如果你使用可中斷的睡眠); 實際上, 慣例是使用 wake_up 如果你在使用 wait_event , wake_up_interruptible 如果你在使用 wait_event_interruptible.</p>
<p>我們現(xiàn)在知道足夠多來看一個簡單的睡眠和喚醒的例子. 在這個例子代碼中, 你可找到一個稱為 sleepy 的模塊. 它實現(xiàn)一個有簡單行為的設(shè)備:任何試圖從這個設(shè)備讀取的進程都被置為睡眠. 無論何時一個進程寫這個設(shè)備, 所有的睡眠進程被喚醒. 這個行為由下面的 read 和 write 方法實現(xiàn):</p>
<pre class="programlisting">
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)
{
printk(KERN_DEBUG "process %i (%s) going to sleep\n",
current->pid, current->comm);
wait_event_interruptible(wq, flag != 0);
flag = 0;
printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
return 0; /* EOF */
}
ssize_t sleepy_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) awakening the readers...\n",
current->pid, current->comm);
flag = 1;
wake_up_interruptible(&wq);
return count; /* succeed, to avoid retrial */
}
</pre>
<p>注意這個例子里 flag 變量的使用. 因為 wait_event_interruptible 檢查一個必須變?yōu)檎娴臈l件, 我們使用 flag 來創(chuàng)建那個條件.</p>
<p>有趣的是考慮當 sleepy_write 被調(diào)用時如果有 2 個進程在等待會發(fā)生什么. 因為 sleepy_read 重置 flag 為 0 一旦它醒來, 你可能認為醒來的第 2 個進程會立刻回到睡眠. 在一個單處理器系統(tǒng), 這幾乎一直是發(fā)生的事情. 但是重要的是要理解為什么你不能依賴這個行為. wake_up_interruptible 調(diào)用將使 2 個睡眠進程醒來. 完全可能它們都注意到 flag 是非零, 在另一個有機會重置它之前. 對于這個小模塊, 這個競爭條件是不重要的. 在一個真實的驅(qū)動中, 這種競爭可能導(dǎo)致少見的難于查找的崩潰. 如果正確的操作要求只能有一個進程看到這個非零值, 它將必須以原子的方式被測試. 我們將見到一個真正的驅(qū)動如何處理這樣的情況. 但首先我們必須開始另一個主題.</p>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="BlockingandNonblockingOperations.sect2"></a>6.2.3. 阻塞和非阻塞操作 </h3></div></div></div>
<p>在我們看全功能的 read 和 write 方法的實現(xiàn)之前, 我們觸及的最后一點是決定何時使進程睡眠. 有時實現(xiàn)正確的 unix 語義要求一個操作不阻塞, 即便它不能完全地進行下去.</p>
<p>有時還有調(diào)用進程通知你他不想阻塞, 不管它的 I/O 是否繼續(xù). 明確的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 標志來指示. 這個標志定義于 <linux/fcntl.h>, 被 <linux/fs.h>自動包含. 這個標志得名自"打開-非阻塞", 因為它可在打開時指定(并且起初只能在那里指定). 如果你瀏覽源碼, 你會發(fā)現(xiàn)一些對一個 O_NDELAY 標志的引用; 這是一個替代 O_NONBLOCK 的名子, 為兼容 System V 代碼而被接受的. 這個標志缺省地被清除, 因為一個等待數(shù)據(jù)的進程的正常行為僅僅是睡眠. 在一個阻塞操作的情況下, 這是缺省地, 下列的行為應(yīng)當實現(xiàn)來符合標準語法:</p>
<div class="itemizedlist"><ul type="disc">
<li><p>如果一個進程調(diào)用 read 但是沒有數(shù)據(jù)可用(尚未), 這個進程必須阻塞. 這個進程在有數(shù)據(jù)達到時被立刻喚醒, 并且那個數(shù)據(jù)被返回給調(diào)用者, 即便小于在給方法的 count 參數(shù)中請求的數(shù)量.</p></li>
<li><p>如果一個進程調(diào)用 write 并且在緩沖中沒有空間, 這個進程必須阻塞, 并且它必須在一個與用作 read 的不同的等待隊列中. 當一些數(shù)據(jù)被寫入硬件設(shè)備, 并且在輸出緩沖中的空間變空閑, 這個進程被喚醒并且寫調(diào)用成功, 盡管數(shù)據(jù)可能只被部分寫入如果在緩沖只沒有空間給被請求的 count 字節(jié).</p></li>
</ul></div>
<p>這 2 句都假定有輸入和輸出緩沖; 實際上, 幾乎每個設(shè)備驅(qū)動都有. 要求有輸入緩沖是為了避免丟失到達的數(shù)據(jù), 當無人在讀時. 相反, 數(shù)據(jù)在寫時不能丟失, 因為如果系統(tǒng)調(diào)用不能接收數(shù)據(jù)字節(jié), 它們保留在用戶空間緩沖. 即便如此, 輸出緩沖幾乎一直有用, 對于從硬件擠出更多的性能.</p>
<p>在驅(qū)動中實現(xiàn)輸出緩沖所獲得的性能來自減少了上下文切換和用戶級/內(nèi)核級切換的次數(shù). 沒有一個輸出緩沖(假定一個慢速設(shè)備), 每次系統(tǒng)調(diào)用接收這樣一個或幾個字符, 并且當一個進程在 write 中睡眠, 另一個進程運行(那是一次上下文切換). 當?shù)谝粋€進程被喚醒, 它恢復(fù)(另一次上下文切換), 寫返回(內(nèi)核/用戶轉(zhuǎn)換), 并且這個進程重新發(fā)出系統(tǒng)調(diào)用來寫入更多的數(shù)據(jù)(用戶/內(nèi)核轉(zhuǎn)換); 這個調(diào)用阻塞并且循環(huán)繼續(xù). 增加一個輸出緩沖可允許驅(qū)動在每個寫調(diào)用中接收大的數(shù)據(jù)塊, 性能上有相應(yīng)的提高. 如果這個緩沖足夠大, 寫調(diào)用在第一次嘗試就成功 -- 被緩沖的數(shù)據(jù)之后將被推到設(shè)備 -- 不必控制需要返回用戶空間來第二次或者第三次寫調(diào)用. 選擇一個合適的值給輸出緩沖顯然是設(shè)備特定的.</p>
<p>我們不使用一個輸入緩沖在 scull中, 因為數(shù)據(jù)當發(fā)出 read 時已經(jīng)可用. 類似的, 不用輸出緩沖, 因為數(shù)據(jù)被簡單地拷貝到和設(shè)備關(guān)聯(lián)的內(nèi)存區(qū). 本質(zhì)上, 這個設(shè)備是一個緩沖, 因此額外緩沖的實現(xiàn)可能是多余的. 我們將在第 10 章見到緩沖的使用.</p>
<p>如果指定 O_NONBLOCK, read 和 write 的行為是不同的. 在這個情況下, 這個調(diào)用簡單地返回 -EAGAIN(("try it agin")如果一個進程當沒有數(shù)據(jù)可用時調(diào)用 read , 或者如果當緩沖中沒有空間時它調(diào)用 write .</p>
<p>如你可能期望的, 非阻塞操作立刻返回, 允許這個應(yīng)用程序輪詢數(shù)據(jù). 應(yīng)用程序當使用 stdio 函數(shù)處理非阻塞文件中, 必須小心, 因為它們?nèi)菀赘沐e一個的非阻塞返回為 EOF. 它們始終必須檢查 errno.</p>
<p>自然地, O_NONBLOCK 也在 open 方法中有意義. 這個發(fā)生在當這個調(diào)用真正阻塞長時間時; 例如, 當打開(為讀存取)一個 沒有寫者的(尚無)FIFO, 或者存取一個磁盤文件使用一個懸掛鎖. 常常地, 打開一個設(shè)備或者成功或者失敗, 沒有必要等待外部的事件. 有時, 但是, 打開這個設(shè)備需要一個長的初始化, 并且你可能選擇在你的 open 方法中支持 O_NONBLOCK , 通過立刻返回 -EAGAIN,如果這個標志被設(shè)置. 在開始這個設(shè)備的初始化進程之后. 這個驅(qū)動可能還實現(xiàn)一個阻塞 open 來支持存取策略, 通過類似于文件鎖的方式. 我們將見到這樣一個實現(xiàn)在"阻塞 open 作為對 EBUSY 的替代"一節(jié), 在本章后面.</p>
<p>一些驅(qū)動可能還實現(xiàn)特別的語義給 O_NONBLOCK; 例如, 一個磁帶設(shè)備的 open 常常阻塞直到插入一個磁帶. 如果這個磁帶驅(qū)動器使用 O_NONBLOCK 打開, 這個 open 立刻成功, 不管是否介質(zhì)在或不在.</p>
<p>只有 read, write, 和 open 文件操作受到非阻塞標志影響.</p>
</div>
<div class="sect2" lang="zh-cn">
<div class="titlepage"><div><div><h3 class="title">
<a name="ABlockingIOExample.sect2"></a>6.2.4. 一個阻塞 I/O 的例子</h3></div></div></div>
<p>最后, 我們看一個實現(xiàn)了阻塞 I/O 的真實驅(qū)動方法的例子. 這個例子來自 scullpipe 驅(qū)動; 它是 scull 的一個特殊形式, 實現(xiàn)了一個象管道的設(shè)備.</p>
<p>在驅(qū)動中, 一個阻塞在讀調(diào)用上的進程被喚醒, 當數(shù)據(jù)到達時; 常常地硬件發(fā)出一個中斷來指示這樣一個事件, 并且驅(qū)動喚醒等待的進程作為處理這個中斷的一部分. scullpipe 驅(qū)動不同, 以至于它可運行而不需要任何特殊的硬件或者一個中斷處理. 我們選擇來使用另一個進程來產(chǎn)生數(shù)據(jù)并喚醒讀進程; 類似地, 讀進程被用來喚醒正在等待緩沖空間可用的寫者進程.</p>
<p>這個設(shè)備驅(qū)動使用一個設(shè)備結(jié)構(gòu), 它包含 2 個等待隊列和一個緩沖. 緩沖大小是以常用的方法可配置的(在編譯時間, 加載時間, 或者運行時間).</p>
<pre class="programlisting">
struct scull_pipe
{
wait_queue_head_t inq, outq; /* read and write queues */
char *buffer, *end; /* begin of buf, end of buf */
int buffersize; /* used in pointer arithmetic */
char *rp, *wp; /* where to read, where to write */
int nreaders, nwriters; /* number of openings for r/w */
struct fasync_struct *async_queue; /* asynchronous readers */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
</pre>
<p>讀實現(xiàn)既管理阻塞也管理非阻塞輸入, 看來如此:</p>
<pre class="programlisting">
static ssize_t scull_p_read (struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
struct scull_pipe *dev = filp->private_data;
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
while (dev->rp == dev->wp)
?? 快捷鍵說明
復(fù)制代碼
Ctrl + C
搜索代碼
Ctrl + F
全屏模式
F11
切換主題
Ctrl + Shift + D
顯示快捷鍵
?
增大字號
Ctrl + =
減小字號
Ctrl + -