6.3. poll 和 select

2018-02-24 15:49 更新

6.3.?poll 和 select

使用非阻塞 I/O 的應(yīng)用程序常常使用 poll, select, 和 epoll 系統(tǒng)調(diào)用. poll, select 和 epoll 本質(zhì)上有相同的功能: 每個(gè)允許一個(gè)進(jìn)程來決定它是否可讀或者寫一個(gè)或多個(gè)文件而不阻塞. 這些調(diào)用也可阻塞進(jìn)程直到任何一個(gè)給定集合的文件描述符可用來讀或?qū)? 因此, 它們常常用在必須使用多輸入輸出流的應(yīng)用程序, 而不必粘連在它們?nèi)魏我粋€(gè)上. 相同的功能常常由多個(gè)函數(shù)提供, 因?yàn)?2 個(gè)是由不同的團(tuán)隊(duì)在幾乎相同時(shí)間完成的: select 在 BSD Unix 中引入, 而 poll 是 System V 的解決方案. epoll 調(diào)用[23]添加在 2.5.45, 作為使查詢函數(shù)擴(kuò)展到幾千個(gè)文件描述符的方法.

支持任何一個(gè)這些調(diào)用都需要來自設(shè)備驅(qū)動(dòng)的支持. 這個(gè)支持(對(duì)所有 3 個(gè)調(diào)用)由驅(qū)動(dòng)的 poll 方法調(diào)用. 這個(gè)方法由下列的原型:


unsigned int (*poll) (struct file *filp, poll_table *wait); 

這個(gè)驅(qū)動(dòng)方法被調(diào)用, 無論何時(shí)用戶空間程序進(jìn)行一個(gè) poll, select, 或者 epoll 系統(tǒng)調(diào)用, 涉及一個(gè)和驅(qū)動(dòng)相關(guān)的文件描述符. 這個(gè)設(shè)備方法負(fù)責(zé)這 2 步:

    1. 在一個(gè)或多個(gè)可指示查詢狀態(tài)變化的等待隊(duì)列上調(diào)用 poll_wait. 如果沒有文件描述符可用作 I/O, 內(nèi)核使這個(gè)進(jìn)程在等待隊(duì)列上等待所有的傳遞給系統(tǒng)調(diào)用的文件描述符.
    1. 返回一個(gè)位掩碼, 描述可能不必阻塞就立刻進(jìn)行的操作.

這 2 個(gè)操作常常是直接的, 并且趨向與各個(gè)驅(qū)動(dòng)看起來類似. 但是, 它們依賴只能由驅(qū)動(dòng)提供的信息, 因此, 必須由每個(gè)驅(qū)動(dòng)單獨(dú)實(shí)現(xiàn).

poll_table 結(jié)構(gòu), 給 poll 方法的第 2 個(gè)參數(shù), 在內(nèi)核中用來實(shí)現(xiàn) poll, select, 和 epoll 調(diào)用; 它在 <linux/poll.h>中聲明, 這個(gè)文件必須被驅(qū)動(dòng)源碼包含. 驅(qū)動(dòng)編寫者不必要知道所有它內(nèi)容并且必須作為一個(gè)不透明的對(duì)象使用它; 它被傳遞給驅(qū)動(dòng)方法以便驅(qū)動(dòng)可用每個(gè)能喚醒進(jìn)程的等待隊(duì)列來加載它, 并且可改變 poll 操作狀態(tài). 驅(qū)動(dòng)增加一個(gè)等待隊(duì)列到 poll_table 結(jié)構(gòu)通過調(diào)用函數(shù) poll_wait:


 void poll_wait (struct file *, wait_queue_head_t *, poll_table *); 

poll 方法的第 2 個(gè)任務(wù)是返回位掩碼, 它描述哪個(gè)操作可馬上被實(shí)現(xiàn); 這也是直接的. 例如, 如果設(shè)備有數(shù)據(jù)可用, 一個(gè)讀可能不必睡眠而完成; poll 方法應(yīng)當(dāng)指示這個(gè)時(shí)間狀態(tài). 幾個(gè)標(biāo)志(通過 <linux/poll.h> 定義)用來指示可能的操作:

POLLIN
如果設(shè)備可被不阻塞地讀, 這個(gè)位必須設(shè)置.

POLLRDNORM
這個(gè)位必須設(shè)置, 如果"正常"數(shù)據(jù)可用來讀. 一個(gè)可讀的設(shè)備返回( POLLIN|POLLRDNORM ).

POLLRDBAND
這個(gè)位指示帶外數(shù)據(jù)可用來從設(shè)備中讀取. 當(dāng)前只用在 Linux 內(nèi)核的一個(gè)地方( DECnet 代碼 )并且通常對(duì)設(shè)備驅(qū)動(dòng)不可用.

POLLPRI
高優(yōu)先級(jí)數(shù)據(jù)(帶外)可不阻塞地讀取. 這個(gè)位使 select 報(bào)告在文件上遇到一個(gè)異常情況, 因?yàn)?selct 報(bào)告帶外數(shù)據(jù)作為一個(gè)異常情況.

POLLHUP
當(dāng)讀這個(gè)設(shè)備的進(jìn)程見到文件尾, 驅(qū)動(dòng)必須設(shè)置 POLLUP(hang-up). 一個(gè)調(diào)用 select 的進(jìn)程被告知設(shè)備是可讀的, 如同 selcet 功能所規(guī)定的.

POLLERR
一個(gè)錯(cuò)誤情況已在設(shè)備上發(fā)生. 當(dāng)調(diào)用 poll, 設(shè)備被報(bào)告位可讀可寫, 因?yàn)樽x寫都返回一個(gè)錯(cuò)誤碼而不阻塞.

POLLOUT
這個(gè)位在返回值中設(shè)置, 如果設(shè)備可被寫入而不阻塞.

POLLWRNORM
這個(gè)位和 POLLOUT 有相同的含義, 并且有時(shí)它確實(shí)是相同的數(shù). 一個(gè)可寫的設(shè)備返回( POLLOUT|POLLWRNORM).

POLLWRBAND
如同 POLLRDBAND , 這個(gè)位意思是帶有零優(yōu)先級(jí)的數(shù)據(jù)可寫入設(shè)備. 只有 poll 的數(shù)據(jù)報(bào)實(shí)現(xiàn)使用這個(gè)位, 因?yàn)橐粋€(gè)數(shù)據(jù)報(bào)看傳送帶外數(shù)據(jù).

應(yīng)當(dāng)重復(fù)一下 POLLRDBAND 和 POLLWRBAND 僅僅對(duì)關(guān)聯(lián)到 socket 的文件描述符有意義: 通常設(shè)備驅(qū)動(dòng)不使用這些標(biāo)志.

poll 的描述使用了大量在實(shí)際使用中相對(duì)簡(jiǎn)單的東西. 考慮 poll 方法的 scullpipe 實(shí)現(xiàn):


static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
        struct scull_pipe *dev = filp->private_data;
        unsigned int mask = 0;

        /*
        * The buffer is circular; it is considered full
        * if "wp" is right behind "rp" and empty if the
        * two are equal. 
        */
        down(&dev->sem);
        poll_wait(filp, &dev->inq,  wait);
        poll_wait(filp, &dev->outq, wait);
        if (dev->rp != dev->wp)
                mask |= POLLIN | POLLRDNORM;  /* readable */
        if (spacefree(dev))
                mask |= POLLOUT | POLLWRNORM;  /* writable */
        up(&dev->sem);
        return mask;
}

這個(gè)代碼簡(jiǎn)單地增加了 2 個(gè) scullpipe 等待隊(duì)列到 poll_table, 接著設(shè)置正確的掩碼位, 根據(jù)數(shù)據(jù)是否可以讀或?qū)?

所示的 poll 代碼缺乏文件尾支持, 因?yàn)?scullpipe 不支持文件尾情況. 對(duì)大部分真實(shí)的設(shè)備, poll 方法應(yīng)當(dāng)返回 POLLHUP 如果沒有更多數(shù)據(jù)(或者將)可用. 如果調(diào)用者使用 select 系統(tǒng)調(diào)用, 文件被報(bào)告為可讀. 不管是使用 poll 還是 select, 應(yīng)用程序知道它能夠調(diào)用 read 而不必永遠(yuǎn)等待, 并且 read 方法返回 0 來指示文件尾.

例如, 對(duì)于 真正的FIFO, 讀者見到一個(gè)文件尾當(dāng)所有的寫者關(guān)閉文件, 而在 scullpipe 中讀者永遠(yuǎn)見不到文件尾. 這個(gè)做法不同是因?yàn)?FIFO 是用作一個(gè) 2 個(gè)進(jìn)程的通訊通道, 而 scullpipe 是一個(gè)垃圾桶, 人人都可以放數(shù)據(jù)只要至少有一個(gè)讀者. 更多地, 重新實(shí)現(xiàn)內(nèi)核中已有的東西是沒有意義的, 因此我們選擇在我們的例子里實(shí)現(xiàn)一個(gè)不同的做法.

與 FIFO 相同的方式實(shí)現(xiàn)文件尾就意味著檢查 dev->nwwriters, 在 read 和 poll 中, 并且報(bào)告文件尾(如剛剛描述過的)如果沒有進(jìn)程使設(shè)備寫打開. 不幸的是, 使用這個(gè)實(shí)現(xiàn)方法, 如果一個(gè)讀者打開 scullpipe 設(shè)備在寫者之前, 它可能見到文件尾而沒有機(jī)會(huì)來等待數(shù)據(jù). 解決這個(gè)問題的最好的方式是在 open 中實(shí)現(xiàn)阻塞, 如同真正的 FIFO 所做的; 這個(gè)任務(wù)留作讀者的一個(gè)練習(xí).

6.3.1.?與 read 和 write 的交互

poll 和 select 調(diào)用的目的是提前決定是否一個(gè) I/O 操作會(huì)阻塞. 在那個(gè)方面, 它們補(bǔ)充了 read 和 write. 更重要的是, poll 和 select , 因?yàn)樗鼈兪箲?yīng)用程序同時(shí)等待幾個(gè)數(shù)據(jù)流, 盡管我們?cè)?scull 例子里沒有采用這個(gè)特性.

一個(gè)正確的實(shí)現(xiàn)對(duì)于使應(yīng)用程序正確工作是必要的: 盡管下列的規(guī)則或多或少已經(jīng)說明過, 我們?cè)诖丝偨Y(jié)它們.

6.3.1.1.?從設(shè)備中讀數(shù)據(jù)

  • 如果在輸入緩沖中有數(shù)據(jù), read 調(diào)用應(yīng)當(dāng)立刻返回, 沒有可注意到的延遲, 即便數(shù)據(jù)少于應(yīng)用程序要求的, 并且驅(qū)動(dòng)確保其他的數(shù)據(jù)會(huì)很快到達(dá). 你可一直返回小于你被請(qǐng)求的數(shù)據(jù), 如果因?yàn)槿魏卫碛啥奖氵@樣(我們?cè)?scull 中這樣做), 如果你至少返回一個(gè)字節(jié). 在這個(gè)情況下, poll 應(yīng)當(dāng)返回 POLLIN|POLLRDNORM.

  • 如果在輸入緩沖中沒有數(shù)據(jù), 缺省地 read 必須阻塞直到有一個(gè)字節(jié). 如果 O_NONBLOCK 被置位, 另一方面, read 立刻返回 -EAGIN (盡管一些老版本 SYSTEM V 返回 0 在這個(gè)情況時(shí)). 在這些情況中, poll 必須報(bào)告這個(gè)設(shè)備是不可讀的直到至少一個(gè)字節(jié)到達(dá). 一旦在緩沖中有數(shù)據(jù), 我們就回到前面的情況.

  • 如果我們處于文件尾, read 應(yīng)當(dāng)立刻返回一個(gè) 0, 不管是否阻塞. 這種情況 poll 應(yīng)該報(bào)告 POLLHUP.

6.3.1.2.?寫入設(shè)備

  • 如果在輸出緩沖中有空間, write 應(yīng)當(dāng)不延遲返回. 它可接受小于這個(gè)調(diào)用所請(qǐng)求的數(shù)據(jù), 但是它必須至少接受一個(gè)字節(jié). 在這個(gè)情況下, poll 報(bào)告這個(gè)設(shè)備是可寫的, 通過返回 POLLOUT|POLLWRNORM.

  • 如果輸出緩沖是滿的, 缺省地 write 阻塞直到一些空間被釋放. 如果 O_NOBLOCK 被設(shè)置, write 立刻返回一個(gè) -EAGAIN(老式的 System V Unices 返回 0). 在這些情況下, poll 應(yīng)當(dāng)報(bào)告文件是不可寫的. 另一方面, 如果設(shè)備不能接受任何多余數(shù)據(jù), write 返回 -ENOSPC("設(shè)備上沒有空間"), 不管是否設(shè)置了 O_NONBLOCK.

  • 在返回之前不要調(diào)用 wait 來傳送數(shù)據(jù), 即便當(dāng) O_NONBLOCK 被清除. 這是因?yàn)樵S多應(yīng)用程序選擇來找出一個(gè) write 是否會(huì)阻塞. 如果設(shè)備報(bào)告可寫, 調(diào)用必須不阻塞. 如果使用設(shè)備的程序想保證它加入到輸出緩沖中的數(shù)據(jù)被真正傳送, 驅(qū)動(dòng)必須提供一個(gè) fsync 方法. 例如, 一個(gè)可移除的設(shè)備應(yīng)當(dāng)有一個(gè) fsnyc 入口.

盡管有一套通用的規(guī)則, 還應(yīng)當(dāng)認(rèn)識(shí)到每個(gè)設(shè)備是唯一的并且有時(shí)這些規(guī)則必須稍微彎曲一下. 例如, 面向記錄的設(shè)備(例如磁帶設(shè)備)無法執(zhí)行部分寫.

6.3.1.3.?刷新掛起的輸出

我們已經(jīng)見到 write 方法如何自己不能解決全部的輸出需要. fsync 函數(shù), 由同名的系統(tǒng)調(diào)用而調(diào)用, 填補(bǔ)了這個(gè)空缺. 這個(gè)方法原型是:


 int (*fsync) (struct file *file, struct dentry *dentry, int datasync); 

如果一些應(yīng)用程序需要被確保數(shù)據(jù)被發(fā)送到設(shè)備, fsync 方法必須被實(shí)現(xiàn)為不管 O_NONBLOCK 是否被設(shè)置. 對(duì) fsync 的調(diào)用應(yīng)當(dāng)只在設(shè)備被完全刷新時(shí)返回(即, 輸出緩沖為空), 即便這需要一些時(shí)間. datasync 參數(shù)用來區(qū)分 fsync 和 fdatasync 系統(tǒng)調(diào)用; 這樣, 它只對(duì)文件系統(tǒng)代碼有用, 驅(qū)動(dòng)可以忽略它.

fsync 方法沒有不尋常的特性. 這個(gè)調(diào)用不是時(shí)間關(guān)鍵的, 因此每個(gè)設(shè)備驅(qū)動(dòng)可根據(jù)作者的口味實(shí)現(xiàn)它. 大部分的時(shí)間, 字符驅(qū)動(dòng)只有一個(gè) NULL 指針在它們的 fops 中. 阻塞設(shè)備, 另一方面, 常常實(shí)現(xiàn)這個(gè)方法使用通用的 block_fsync, 它接著會(huì)刷新設(shè)備的所有的塊.

6.3.2.?底層的數(shù)據(jù)結(jié)構(gòu)

poll 和 select 系統(tǒng)調(diào)用的真正實(shí)現(xiàn)是相當(dāng)?shù)睾?jiǎn)單, 對(duì)那些感興趣于它如何工作的人; epoll 更加復(fù)雜一點(diǎn)但是建立在同樣的機(jī)制上. 無論何時(shí)用戶應(yīng)用程序調(diào)用 poll, select, 或者 epoll_ctl,[24] 內(nèi)核調(diào)用這個(gè)系統(tǒng)調(diào)用所引用的所有文件的 poll 方法, 傳遞相同的 poll_table 到每個(gè). poll_table 結(jié)構(gòu)只是對(duì)一個(gè)函數(shù)的封裝, 這個(gè)函數(shù)建立了實(shí)際的數(shù)據(jù)結(jié)構(gòu). 那個(gè)數(shù)據(jù)結(jié)構(gòu), 對(duì)于 poll和 select, 是一個(gè)內(nèi)存頁(yè)的鏈表, 其中包含 poll_table_entry 結(jié)構(gòu). 每個(gè) poll_table_entry 持有被傳遞給 poll_wait 的 struct file 和 wait_queue_head_t 指針, 以及一個(gè)關(guān)聯(lián)的等待隊(duì)列入口. 對(duì) poll_wait 的調(diào)用有時(shí)還添加這個(gè)進(jìn)程到給定的等待隊(duì)列. 整個(gè)的結(jié)構(gòu)必須由內(nèi)核維護(hù)以至于這個(gè)進(jìn)程可被從所有的隊(duì)列中去除, 在 poll 或者 select 返回之前.

如果被輪詢的驅(qū)動(dòng)沒有一個(gè)指示 I/O 可不阻塞地發(fā)生, poll 調(diào)用簡(jiǎn)單地睡眠直到一個(gè)它所在的等待隊(duì)列(可能許多)喚醒它.

在 poll 實(shí)現(xiàn)中有趣的是驅(qū)動(dòng)的 poll 方法可能被用一個(gè) NULL 指針作為 poll_table 參數(shù). 這個(gè)情況出現(xiàn)由于幾個(gè)理由. 如果調(diào)用 poll 的應(yīng)用程序已提供了一個(gè) 0 的超時(shí)值(指示不應(yīng)當(dāng)做等待), 沒有理由來堆積等待隊(duì)列, 并且系統(tǒng)簡(jiǎn)單地不做它. poll_table 指針還被立刻設(shè)置為 NULL 在任何被輪詢的驅(qū)動(dòng)指示可以 I/O 之后. 因?yàn)閮?nèi)核在那一點(diǎn)知道不會(huì)發(fā)生等待, 它不建立等待隊(duì)列鏈表.

當(dāng) poll 調(diào)用完成, poll_table 結(jié)構(gòu)被去分配, 并且所有的之前加入到 poll 表的等待隊(duì)列入口被從表和它們的等待隊(duì)列中移出.

我們?cè)噲D在圖poll 背后的數(shù)據(jù)結(jié)構(gòu)中展示包含在輪詢中的數(shù)據(jù)結(jié)構(gòu); 這個(gè)圖是真實(shí)數(shù)據(jù)結(jié)構(gòu)的簡(jiǎn)化地表示, 因?yàn)樗雎粤艘粋€(gè) poll 表地多頁(yè)性質(zhì)并且忽略了每個(gè) poll_table_entry 的文件指針.

圖?6.1.?poll 背后的數(shù)據(jù)結(jié)構(gòu)

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)