0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內(nèi)不再提示

一文讀懂I/O復用

jf_78858299 ? 來源:阿Q正磚 ? 作者:阿Q正磚 ? 2023-02-15 11:17 ? 次閱讀

今天給大家聊聊I/O復用,對于大部分公司面試來說,這塊肯定是必問內(nèi)容,它不僅能側(cè)面反映面試這對基礎掌握的是否扎實,還能反映出求職者的知識廣度。

1 從阻塞 I/O 到 I/O 多路復用

阻塞 I/O,是指進程發(fā)起調(diào)用后,會被掛起(阻塞),直到收到數(shù)據(jù)再返回。如果調(diào)用一直不返回,進程就會一直被掛起。因此,當使用阻塞 I/O 時,需要使用多線程來處理多個文件描述符。

多線程切換有一定的開銷,因此引入非阻塞 I/O。非阻塞 I/O 不會將進程掛起,調(diào)用時會立即返回成功或錯誤,因此可以在一個線程輪詢多個文件描述符是否就緒。

但是非阻塞 I/O 的缺點是:每次發(fā)起系統(tǒng)調(diào)用,只能檢查一個文件描述符是否就緒。當文件描述符很多時,系統(tǒng)調(diào)用的成本很高。

因此引入了 I/O 多路復用,可以 通過一次系統(tǒng)調(diào)用,檢查多個文件描述符的狀態(tài) 。這是 I/O 多路復用的主要優(yōu)點,相比于非阻塞 I/O,在文件描述符較多的場景下,避免了頻繁的用戶態(tài)和內(nèi)核態(tài)的切換,減少了系統(tǒng)調(diào)用的開銷。

I/O 多路復用相當于將「遍歷所有文件描述符、通過非阻塞 I/O 查看其是否就緒」的過程從用戶線程移到了內(nèi)核中,由內(nèi)核來負責輪詢。

進程可以通過 select、poll、epoll 發(fā)起 I/O 多路復用的系統(tǒng)調(diào)用,這些系統(tǒng)調(diào)用都是同步阻塞的: 如果傳入的多個文件描述符中,有描述符就緒,則返回就緒的描述符;否則如果所有文件描述符都未就緒,就阻塞調(diào)用進程,直到某個描述符就緒,或者阻塞時長超過設置的 timeout 后,再返回 。I/O 多路復用內(nèi)部使用非阻塞 I/O 檢查每個描述符的就緒狀態(tài)。

如果 timeout參數(shù)設為 NULL,會無限阻塞直到某個描述符就緒;如果timeout參數(shù)設為 0,會立即返回,不阻塞。

I/O 多路復用引入了一些額外的操作和開銷,性能更差。但是好處是用戶可以在一個線程內(nèi)同時處理多個 I/O 請求。如果不采用 I/O 多路復用,則必須通過多線程的方式,每個線程處理一個 I/O 請求。后者線程切換也是有一定的開銷的。

2 為什么 I/O 多路復用內(nèi)部需要使用非阻塞 I/O?

I/O 多路復用內(nèi)部會遍歷集合中的每個文件描述符,判斷其是否就緒:

for fd in read_set if (readable(fd)) // 判斷fd是否就緒 count++; FDSET(fd, &res_rset) // 將fd添加到就緒隊列中 break;return count;

這里的 readable(fd) 就是一個非阻塞 I/O 調(diào)用。試想,如果這里使用阻塞 I/O,那么fd未就緒時,select會阻塞在這個文件描述符上,無法檢查下個文件描述符。

注意:這里說的是 I/O 多路復用的內(nèi)部實現(xiàn),而不是說,使用 I/O 多路復用就必須使用非阻塞 I/O。

3 select

函數(shù)簽名與參數(shù)

int select(int nfds,            fd_set *restrict readfds,            fd_set *restrict writefds,            fd_set *restrict errorfds.            struct timeval *restrict timeout);

readfds、writefds、errorfds 是三個文件描述符集合。select 會遍歷每個集合的前 nfds個描述符,分別找到可以讀取、可以寫入、發(fā)生錯誤的描述符,統(tǒng)稱為“就緒”的描述符。然后用找到的子集替換參數(shù)中的對應集合,返回所有就緒描述符的總數(shù)。

timeout 參數(shù)表示調(diào)用 select 時的阻塞時長。如果所有文件描述符都未就緒,就阻塞調(diào)用進程,直到某個描述符就緒,或者阻塞超過設置的 timeout 后,返回。如果 timeout 參數(shù)設為 NULL,會無限阻塞直到某個描述符就緒;如果 timeout 參數(shù)設為 0,會立即返回,不阻塞。

3.1 什么是文件描述符 fd

文件描述符(file descriptor)是一個非負整數(shù),從 0 開始。進程使用文件描述符來標識一個打開的文件。

系統(tǒng)為每一個進程維護了一個文件描述符表,表示該進程打開文件的記錄表,而 文件描述符實際上就是這張表的索引 。當進程打開(open)或者新建(create)文件時,內(nèi)核會在該進程的文件列表中新增一個表項,同時返回一個文件描述符 —— 也就是新增表項的下標。

一般來說,每個進程最多可以打開 64 個文件,fd ∈ 0~63。在不同系統(tǒng)上,最多允許打開的文件個數(shù)不同,Linux 2.4.22 強制規(guī)定最多不能超過 1,048,576。

每個進程默認都有 3 個文件描述符:0 (stdin)、1 (stdout)、2 (stderr)。

3.2 socket 與 fd 的關系

socket 是 Unix 中的術語。socket 可以用于同一臺主機的不同進程間的通信,也可以用于不同主機間的通信。一個 socket 包含地址、類型和通信協(xié)議等信息,通過 **socket() **函數(shù)創(chuàng)建:

int socket(int domain, int type, int protocol)

返回的就是這個 socket 對應的文件描述符 fd。操作系統(tǒng)將 socket 映射到進程的一個文件描述符上,進程就可以通過讀寫這個文件描述符來和遠程主機通信。

可以這樣理解:socket 是進程間通信規(guī)則的高層抽象,而 fd 提供的是底層的具體實現(xiàn)。socket 與 fd 是一一對應的。通過 socket 通信,實際上就是通過文件描述符 fd 讀寫文件。這也符合 Unix“一切皆文件”的哲學。

3.3 fd_set 文件描述符集合

參數(shù)中的 **fd_set **類型表示文件描述符的集合。

由于文件描述符 fd 是一個從 0 開始的無符號整數(shù),所以可以使用 fd_set二進制每一位來表示一個文件描述符。某一位為 1,表示對應的文件描述符已就緒。比如比如設 fd_set 長度為 1 字節(jié),則一個 fd_set 變量最大可以表示 8 個文件描述符。當 **select **返回 **fd_set = 00010011 **時,表示文件描述符 **1、2、5 **已經(jīng)就緒。

3.4 select 使用示例

下圖的代碼說明:

(1)先聲明一個 fd_set 類型的變量 readFDs

(2)調(diào)用 FD_ZERO,將 readFDs 所有位 置 0

(3)調(diào)用 FD_SET,將 readFDs 感興趣的位置 1,表示要監(jiān)聽這幾個文件描述符

(4)將 readFDs 傳給 select,調(diào)用 select

(5)select會將 readFDs 中就緒的位置 1,未就緒的位置 0,返回就緒的文件描述符的數(shù)量

(6)當 select 返回后,調(diào)用 FD_ISSET 檢測給定位是否為 1,表示對應文件描述符是否就緒

比如進程想監(jiān)聽 1、2、5 這三個文件描述符,就將 readFDs 設置為 00010011,然后調(diào)用 select

如果 fd=1fd=2 就緒,而 fd=5 未就緒,select 會將 readFDs 設置為 00000011 并返回 2。

如果每個文件描述符都未就緒,select 會阻塞 timeout 時長,再返回。這期間,如果 readFDs 監(jiān)聽的某個文件描述符上發(fā)生可讀事件,則 select 會將對應位置 1,并立即返回。

圖片

**3.5 **select 的缺點

  1. 性能開銷大
    1. 調(diào)用 select 時會陷入內(nèi)核,這時需要將參數(shù)中的 fd_set 從用戶空間拷貝到內(nèi)核空間
    2. 內(nèi)核需要遍歷傳遞進來的所有 fd_set 的每一位,不管它們是否就緒
  2. 同時能夠監(jiān)聽的文件描述符數(shù)量太少。受限于 sizeof(fd_set) 的大小,在編譯內(nèi)核時就確定了且無法更改。一般是 1024,不同的操作系統(tǒng)不相同。

4 poll

poll 和 select 幾乎沒有區(qū)別。poll 在用戶態(tài)通過數(shù)組方式傳遞文件描述符,在內(nèi)核會轉(zhuǎn)為鏈表方式 存儲 ,沒有最大數(shù)量的限制 。

poll 的函數(shù)簽名如下:

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其中 fds 是一個 pollfd 結構體類型的數(shù)組,調(diào)用 poll() 時必須通過 nfds 指出數(shù)組 fds 的大小,即文件描述符的數(shù)量。

從性能開銷上看,poll 和 select 的差別不大。

5 epoll

epoll 是對 select 和 poll 的改進,避免了“性能開銷大”和“文件描述符數(shù)量少”兩個缺點。

簡而言之,epoll 有以下幾個特點:

  • 使用紅黑樹存儲文件描述符集合
  • 使用隊列存儲就緒的文件描述符
  • 每個文件描述符只需在添加時傳入一次;通過事件更改文件描述符狀態(tài)

select、poll 模型都只使用一個函數(shù),而 epoll 模型使用三個函數(shù):epoll_create、epoll_ctlepoll_wait。

5.1 epoll_create

int epoll_create(int size);

epoll_create 會創(chuàng)建一個 epoll 實例,同時返回一個引用該實例的文件描述符。

返回的文件描述符僅僅指向?qū)?epoll 實例,并不表示真實的磁盤文件節(jié)點。其他 APIepoll_ctlepoll_wait 會使用這個文件描述符來操作相應的 epoll 實例。

當創(chuàng)建好 epoll 句柄后,它會占用一個 fd 值,在 linux 下查看 /proc/進程id/fd/,就能夠看到這個 fd。所以在使用完 epoll 后,必須調(diào)用 close(epfd) 關閉對應的文件描述符,否則可能導致 fd 被耗盡。當指向同一個 epoll 實例的所有文件描述符都被關閉后,操作系統(tǒng)會銷毀這個 epoll 實例。

epoll 實例內(nèi)部存儲:

  • 監(jiān)聽列表:所有要監(jiān)聽的文件描述符,使用紅黑樹
  • 就緒列表:所有就緒的文件描述符,使用鏈表

5.2 epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 會監(jiān)聽文件描述符 fd 上發(fā)生的 event 事件。

參數(shù)說明:

  • epfdepoll_create 返回的文件描述符,指向一個 epoll 實例
  • fd 表示要監(jiān)聽的目標文件描述符
  • event 表示要監(jiān)聽的事件(可讀、可寫、發(fā)送錯誤…)
  • op 表示要對 fd 執(zhí)行的操作,有以下幾種:
    • EPOLL_CTL_ADD:為 fd 添加一個監(jiān)聽事件 event
    • EPOLL_CTL_MOD:Change the event event associated with the target file descriptor fd(event 是一個結構體變量,這相當于變量 event 本身沒變,但是更改了其內(nèi)部字段的值)
    • EPOLL_CTL_DEL:刪除 fd 的所有監(jiān)聽事件,這種情況下 event 參數(shù)沒用

返回值 0 或 -1,表示上述操作成功與否。

epoll_ctl 會將文件描述符 fd 添加到 epoll 實例的監(jiān)聽列表里,同時為 fd 設置一個回調(diào)函數(shù),并監(jiān)聽事件 event。當 fd 上發(fā)生相應事件時,會調(diào)用回調(diào)函數(shù),將 fd 添加到 epoll 實例的就緒隊列上。

5.3 epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);

這是 epoll 模型的主要函數(shù),功能相當于 select。

參數(shù)說明:

  • epfdepoll_create 返回的文件描述符,指向一個 epoll 實例
  • events 是一個數(shù)組,保存就緒狀態(tài)的文件描述符,其空間由調(diào)用者負責申請
  • maxevents 指定 events 的大小
  • timeout 類似于 select 中的 timeout。如果沒有文件描述符就緒,即就緒隊列為空,則 epoll_wait 會阻塞 timeout 毫秒。如果 timeout 設為 -1,則 epoll_wait 會一直阻塞,直到有文件描述符就緒;如果 timeout 設為 0,則 epoll_wait 會立即返回

返回值表示 events 中存儲的就緒描述符個數(shù),最大不超過 maxevents。

5.4 epoll 的優(yōu)點

一開始說,epoll 是對 select 和 poll 的改進,避免了“性能開銷大”和“文件描述符數(shù)量少”兩個缺點。

對于“文件描述符數(shù)量少”,select 使用整型數(shù)組存儲文件描述符集合,而 epoll 使用紅黑樹存儲,數(shù)量較大。

對于“性能開銷大”,epoll_ctl 中為每個文件描述符指定了回調(diào)函數(shù),并在就緒時將其加入到就緒列表,因此 epoll 不需要像 select 那樣遍歷檢測每個文件描述符,只需要判斷就緒列表是否為空即可。這樣,在沒有描述符就緒時,epoll 能更早地讓出系統(tǒng)資源。

相當于時間復雜度從 O(n) 降為 O(1)

此外,每次調(diào)用 select 時都需要向內(nèi)核拷貝所有要監(jiān)聽的描述符集合,而 epoll 對于每個描述符,只需要在 epoll_ctl 傳遞一次,之后 epoll_wait 不需要再次傳遞。這也大大提高了效率。

5.5 水平觸發(fā)、邊緣觸發(fā)

select 只支持水平觸發(fā),epoll 支持水平觸發(fā)和邊緣觸發(fā)。

水平觸發(fā) (LT,Level Trigger):當文件描述符就緒時,會觸發(fā)通知,如果用戶程序沒有一次性把數(shù)據(jù)讀/寫完,下次還會發(fā)出可讀/可寫信號進行通知。

邊緣觸發(fā) (ET,Edge Trigger):僅當描述符從未就緒變?yōu)榫途w時,通知一次,之后不會再通知。

區(qū)別:邊緣觸發(fā)效率更高, 減少了事件被重復觸發(fā)的次數(shù) ,函數(shù)不會返回大量用戶程序可能不需要的文件描述符。

水平觸發(fā)、邊緣觸發(fā)的名稱來源:數(shù)字電路當中的電位水平,高低電平切換瞬間的觸發(fā)動作叫邊緣觸發(fā),而處于高電平的觸發(fā)動作叫做水平觸發(fā)。

5.6 為什么邊緣觸發(fā)必須使用非阻塞 I/O?

關于這個問題的解答,強烈建議閱讀這篇文章。下面是一些關鍵摘要:

  • 每次通過 read 系統(tǒng)調(diào)用讀取數(shù)據(jù)時,最多只能讀取緩沖區(qū)大小的字節(jié)數(shù);如果某個文件描述符一次性收到的數(shù)據(jù)超過了緩沖區(qū)的大小,那么需要對其 read 多次才能全部讀取完畢
  • select 可以使用阻塞 I/O 。通過 select 獲取到所有可讀的文件描述符后,遍歷每個文件描述符,read 一次數(shù)據(jù)(見上文 select 示例)
    • 這些文件描述符都是可讀的,因此即使 read 是阻塞 I/O,也一定可以讀到數(shù)據(jù),不會一直阻塞下去
    • select 采用水平觸發(fā)模式,因此如果第一次 read 沒有讀取完全部數(shù)據(jù),那么下次調(diào)用 select 時依然會返回這個文件描述符,可以再次 read
    • select 也可以使用非阻塞 I/O 。當遍歷某個可讀文件描述符時,使用 for 循環(huán)調(diào)用 read 多次 ,直到讀取完所有數(shù)據(jù)為止(返回 EWOULDBLOCK)。這樣做會多一次 read 調(diào)用,但可以減少調(diào)用 select 的次數(shù)
  • epoll 的邊緣觸發(fā)模式下,只會在文件描述符的可讀/可寫狀態(tài)發(fā)生切換時,才會收到操作系統(tǒng)的通知
    • 因此,如果使用 epoll邊緣觸發(fā)模式 ,在收到通知時,**必須使用非阻塞 I/O,并且必須循環(huán)調(diào)用 ** readwrite 多次,直到返回 EWOULDBLOCK 為止 ,然后再調(diào)用 epoll_wait 等待操作系統(tǒng)的下一次通知
    • 如果沒有一次性讀/寫完所有數(shù)據(jù),那么在操作系統(tǒng)看來這個文件描述符的狀態(tài)沒有發(fā)生改變,將不會再發(fā)起通知,調(diào)用 epoll_wait 會使得該文件描述符一直等待下去,服務端也會一直等待客戶端的響應,業(yè)務流程無法走完
    • 這樣做的好處是每次調(diào)用 epoll_wait 都是有效的——保證數(shù)據(jù)全部讀寫完畢了,等待下次通知。在水平觸發(fā)模式下,如果調(diào)用 epoll_wait 時數(shù)據(jù)沒有讀/寫完畢,會直接返回,再次通知。因此邊緣觸發(fā)能顯著減少事件被觸發(fā)的次數(shù)
    • 為什么 epoll邊緣觸發(fā)模式不能使用阻塞 I/O ?很顯然,邊緣觸發(fā)模式需要循環(huán)讀/寫一個文件描述符的所有數(shù)據(jù)。如果使用阻塞 I/O,那么一定會在最后一次調(diào)用(沒有數(shù)據(jù)可讀/寫)時阻塞,導致無法正常結束

6 三者對比

  • select:調(diào)用開銷大(需要復制集合);集合大小有限制;需要遍歷整個集合找到就緒的描述符
  • poll:poll 采用數(shù)組的方式存儲文件描述符,沒有最大存儲數(shù)量的限制,其他方面和 select 沒有區(qū)別
  • epoll:調(diào)用開銷?。ú恍枰獜椭疲?;集合大小無限制;采用回調(diào)機制,不需要遍歷整個集合

select、poll 都是在用戶態(tài)維護文件描述符集合,因此每次需要將完整集合傳給內(nèi)核;epoll 由操作系統(tǒng)在內(nèi)核中維護文件描述符集合,因此只需要在創(chuàng)建的時候傳入文件描述符。

此外 select 只支持水平觸發(fā),epoll 支持邊緣觸發(fā)。

7 適用場景

當連接數(shù)較多并且有很多的不活躍連接時,epoll 的效率比其它兩者高很多。當連接數(shù)較少并且都十分活躍的情況下,由于 epoll 需要很多回調(diào),因此性能可能低于其它兩者。

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 編程
    +關注

    關注

    88

    文章

    3545

    瀏覽量

    93501
  • i/o
    i/o
    +關注

    關注

    0

    文章

    33

    瀏覽量

    4560
收藏 人收藏

    評論

    相關推薦

    讀懂i/o端口地址譯碼

    I/O端口是接口電路中能被CPU直接訪問的寄存器。訪問端口就是訪問接口電路中的寄存器。個接口電路(外設)通常擁有不止個端口,如命令口、狀態(tài)口、數(shù)據(jù)口等。端口地址編碼形式有統(tǒng)
    的頭像 發(fā)表于 11-16 09:40 ?1.4w次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b><b class='flag-5'>i</b>/<b class='flag-5'>o</b>端口地址譯碼

    讀懂接口模塊的組合應用有哪些?

    讀懂接口模塊的組合應用有哪些?
    發(fā)表于 05-17 07:15

    讀懂如何去優(yōu)化AC耦合電容?

    讀懂如何去優(yōu)化AC耦合電容?
    發(fā)表于 06-08 07:04

    讀懂什么是NEC協(xié)議

    讀懂什么是NEC協(xié)議?
    發(fā)表于 10-15 09:22

    讀懂中斷方式和輪詢操作有什么區(qū)別嗎

    讀懂中斷方式和輪詢操作有什么區(qū)別嗎?
    發(fā)表于 12-10 06:00

    使用引腳作為普通的I/O定要進行引腳的功能復用

    ##學習筆記.相關表格1.PB3,PB4,PA13,PA14,PA15引腳可根據(jù)上表復用成普通IO口。在mcu復位的時候這幾個引腳被作為jtag的功能。當我們要使用這些引腳作為普通的I/O
    發(fā)表于 03-01 07:03

    數(shù)字I/O介紹

    數(shù)字I/O腳有專用和復用。數(shù)字I/O腳的功能通過9個16位控制寄存器來控制??刂萍拇嫫鞣譃閮深悾海?)I
    發(fā)表于 09-16 12:20 ?19次下載

    Java I/O 的相關方法分析

    asynchronous I/O。 Java 是種跨平臺語言,為了支持異步 I/O,誕生了 NIO,Java1.4 引入的 NIO1.0
    發(fā)表于 09-27 13:18 ?0次下載
    Java <b class='flag-5'>I</b>/<b class='flag-5'>O</b> 的相關方法分析

    Linux I/O多路復用

    /O,非阻塞I/O,I/O多路復用,信號驅(qū)動I/
    發(fā)表于 04-02 14:31 ?294次閱讀

    Linux中如何使用信號驅(qū)動式I/O?

    大圖 I/O 復用 (select、poll、epoll): 通過 I/O 復用函數(shù)向內(nèi)核注冊
    的頭像 發(fā)表于 03-12 14:47 ?2364次閱讀
    Linux中如何使用信號驅(qū)動式<b class='flag-5'>I</b>/<b class='flag-5'>O</b>?

    關于STM32通用和復用I/O

    關于STM32通用和復用I/O,概述? STM32F10x系列具有豐富的端口可供使用包括26、37、51、80、112個多功能雙向5V兼容的快速
    發(fā)表于 12-03 09:51 ?9次下載
    關于STM32通用和<b class='flag-5'>復用</b><b class='flag-5'>I</b>/<b class='flag-5'>O</b>口

    讀懂MCU的特點、功能及如何編寫

    讀懂MCU的特點、功能及如何編寫
    發(fā)表于 12-05 09:51 ?24次下載
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>MCU的特點、功能及如何編寫

    關于STM32的 I/O 復用功能

    今天給大家分享兩點內(nèi)容: 是,為什么我們要先開啟STM32外設時鐘;二是,關于STM32的 I/O 復用功能及什么時候開啟AFIO時鐘。
    的頭像 發(fā)表于 10-20 14:19 ?3483次閱讀

    讀懂,什么是BLE?

    讀懂,什么是BLE?
    的頭像 發(fā)表于 11-27 17:11 ?2022次閱讀
    <b class='flag-5'>一</b><b class='flag-5'>文</b><b class='flag-5'>讀懂</b>,什么是BLE?

    讀懂車規(guī)級AEC-Q認證

    讀懂車規(guī)級AEC-Q認證
    的頭像 發(fā)表于 12-04 16:45 ?844次閱讀