自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

但是,I/O多路復(fù)用中是如何判斷文件“可讀”/“可寫(xiě)”的?

存儲(chǔ) 存儲(chǔ)架構(gòu)
在進(jìn)行網(wǎng)絡(luò)編程或處理其他類(lèi)型的 I/O 操作時(shí),一個(gè)常見(jiàn)的挑戰(zhàn)是如何高效地管理多個(gè)并發(fā)的 I/O 通道。如果為每個(gè)連接或文件都創(chuàng)建一個(gè)單獨(dú)的線(xiàn)程或進(jìn)程來(lái)阻塞等待 I/O,當(dāng)連接數(shù)非常多時(shí),系統(tǒng)資源的開(kāi)銷(xiāo)(如內(nèi)存、上下文切換成本)會(huì)變得非常巨大。

在學(xué)習(xí)I/O多路復(fù)用時(shí),經(jīng)常會(huì)得到如下描述:

...,在其中任何一個(gè)或多個(gè)描述符 準(zhǔn)備好進(jìn)行 I/O 操作(可讀、可寫(xiě)或異常)時(shí)獲得通知 。

那么,操作系統(tǒng)內(nèi)核到底是如何判斷某個(gè)文件描述符“可讀”/“可寫(xiě)”呢?在達(dá)到相關(guān)狀態(tài)后,是如何“立即”通知到應(yīng)用程序的呢?本文在探究這個(gè)問(wèn)題。

I/O 多路復(fù)用與文件描述符狀態(tài)檢測(cè)

在進(jìn)行網(wǎng)絡(luò)編程或處理其他類(lèi)型的 I/O 操作時(shí),一個(gè)常見(jiàn)的挑戰(zhàn)是如何高效地管理多個(gè)并發(fā)的 I/O 通道。如果為每個(gè)連接或文件都創(chuàng)建一個(gè)單獨(dú)的線(xiàn)程或進(jìn)程來(lái)阻塞等待 I/O,當(dāng)連接數(shù)非常多時(shí),系統(tǒng)資源的開(kāi)銷(xiāo)(如內(nèi)存、上下文切換成本)會(huì)變得非常巨大。

I/O 多路復(fù)用 (I/O Multiplexing) 技術(shù)應(yīng)運(yùn)而生,它允許單個(gè)進(jìn)程或線(xiàn)程監(jiān)視多個(gè) 文件描述符 (file descriptor),并在其中任何一個(gè)或多個(gè)變得“就緒”(例如,可讀或可寫(xiě))時(shí)得到通知,從而可以在單個(gè)執(zhí)行流中處理多個(gè) I/O 事件。Linux 提供了幾種經(jīng)典的 I/O 多路復(fù)用系統(tǒng)調(diào)用,主要是 select、poll 和 epoll。

要理解這些系統(tǒng)調(diào)用如何工作,關(guān)鍵在于理解 Linux 內(nèi)核是如何跟蹤和通知文件描述符狀態(tài)變化的。這涉及到內(nèi)核中的文件系統(tǒng)抽象、網(wǎng)絡(luò)協(xié)議棧以及一種核心機(jī)制: 等待隊(duì)列 (wait queue) 。

文件描述符與內(nèi)核結(jié)構(gòu)

在 Linux 中,“一切皆文件”是一個(gè)核心設(shè)計(jì)哲學(xué)。無(wú)論是磁盤(pán)文件、管道、終端還是網(wǎng)絡(luò)套接字 (socket),在用戶(hù)空間看來(lái),它們都通過(guò)一個(gè)非負(fù)整數(shù)來(lái)標(biāo)識(shí),即文件描述符。

當(dāng)應(yīng)用程序通過(guò) socket() 系統(tǒng)調(diào)用創(chuàng)建一個(gè)套接字時(shí),內(nèi)核會(huì)執(zhí)行以下關(guān)鍵步驟:

  1. 在內(nèi)核空間創(chuàng)建表示該套接字的核心數(shù)據(jù)結(jié)構(gòu),通常是 struct socket。這個(gè)結(jié)構(gòu)包含了套接字的狀態(tài)、類(lèi)型、協(xié)議族、收發(fā)緩沖區(qū)、指向協(xié)議層處理函數(shù)的指針等信息。
  2. 創(chuàng)建一個(gè) struct file 結(jié)構(gòu)。這是內(nèi)核中代表一個(gè)打開(kāi)文件的通用結(jié)構(gòu),它包含訪(fǎng)問(wèn)模式、當(dāng)前偏移量等,并且有一個(gè)重要的成員 f_op,指向一個(gè) file_operations 結(jié)構(gòu)。
  3. file_operations 結(jié)構(gòu)包含了一系列函數(shù)指針,定義了可以對(duì)這類(lèi)文件執(zhí)行的操作,如 read、write、poll、release 等。對(duì)于套接字,struct file 會(huì)通過(guò)其私有數(shù)據(jù)指針 (private_data) 關(guān)聯(lián)到對(duì)應(yīng)的 struct socket,并且其 f_op 會(huì)指向一套適用于套接字的文件操作函數(shù)集。
  4. 內(nèi)核在當(dāng)前進(jìn)程的文件描述符表中找到一個(gè)空閑位置,將該位置指向新創(chuàng)建的 struct file 結(jié)構(gòu),并將該位置的索引(即文件描述符)返回給用戶(hù)空間。

因此,后續(xù)所有對(duì)該文件描述符的操作(如 read, write, bind, listen, accept, select, poll, epoll_ctl 等),都會(huì)通過(guò)系統(tǒng)調(diào)用進(jìn)入內(nèi)核,內(nèi)核根據(jù)文件描述符找到對(duì)應(yīng)的 struct file,再通過(guò) f_op 調(diào)用相應(yīng)的內(nèi)核函數(shù)來(lái)執(zhí)行。

等待隊(duì)列:事件通知的核心

操作系統(tǒng)需要一種機(jī)制,讓某個(gè)進(jìn)程在等待特定事件(例如,數(shù)據(jù)到達(dá)套接字、套接字發(fā)送緩沖區(qū)有可用空間)發(fā)生時(shí)能夠暫停執(zhí)行(睡眠),并在事件發(fā)生后被喚醒。這就是 等待隊(duì)列 (wait queue) 機(jī)制 (wait_queue_head_t 在 Linux 內(nèi)核中)。

struct list_head {
 struct list_head *next, *prev;
};

struct wait_queue_head {
 spinlock_t  lock;
 struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;

struct wait_queue_entry {
 unsigned int  flags;
 void   *private;
 wait_queue_func_t func;
 struct list_head entry;
};
typedef struct wait_queue_entry wait_queue_entry_t;

等待隊(duì)列 (wait_queue_head_t) 是 Linux 內(nèi)核中的一個(gè)數(shù)據(jù)結(jié)構(gòu),用于管理一組等待特定事件(如數(shù)據(jù)到達(dá))的進(jìn)程。它本質(zhì)上是一個(gè)鏈表,鏈表中的每個(gè)節(jié)點(diǎn) (wait_queue_entry_t) 代表一個(gè)等待該事件的進(jìn)程。

  • wait_queue_head_t:包含一個(gè)鎖(保護(hù)隊(duì)列)和一個(gè)指向等待條目鏈表的指針。
  • wait_queue_entry_t:包含進(jìn)程的標(biāo)識(shí)(如任務(wù)結(jié)構(gòu)體 task_struct)和指向下一個(gè)條目的指針。

等待隊(duì)列允許進(jìn)程在事件未發(fā)生時(shí)暫停執(zhí)行,并在事件發(fā)生時(shí)被喚醒,是阻塞式 I/O 和多路復(fù)用的基礎(chǔ)。

內(nèi)核中幾乎所有可能導(dǎo)致阻塞等待的資源(如套接字的接收緩沖區(qū)、發(fā)送緩沖區(qū)、管道、鎖等)都會(huì)關(guān)聯(lián)一個(gè)或多個(gè)等待隊(duì)列。

數(shù)據(jù)結(jié)構(gòu)中的嵌入

每個(gè)資源在內(nèi)核中都有對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)。例如,對(duì)于網(wǎng)絡(luò)套接字,內(nèi)核維護(hù)一個(gè) struct socket 結(jié)構(gòu),其中嵌入了與接收緩沖區(qū)和發(fā)送緩沖區(qū)相關(guān)的等待隊(duì)列。通常,每個(gè)套接字會(huì)有兩個(gè)獨(dú)立的 wait_queue_head_t:

  • 一個(gè)用于接收數(shù)據(jù)(等待接收緩沖區(qū)有數(shù)據(jù))。
  • 一個(gè)用于發(fā)送數(shù)據(jù)(等待發(fā)送緩沖區(qū)有空間)。

這些等待隊(duì)列是 struct socket 或相關(guān)結(jié)構(gòu)(如 struct sock)的成員變量,直接與資源綁定。

當(dāng)進(jìn)程對(duì)資源執(zhí)行操作(例如通過(guò) read() 讀取套接字?jǐn)?shù)據(jù))時(shí),如果資源不可用(接收緩沖區(qū)為空),內(nèi)核會(huì):

  1. 內(nèi)核創(chuàng)建一個(gè) wait_queue_entry_t,關(guān)聯(lián)到當(dāng)前進(jìn)程的 task_struct。
  2. 將這個(gè)條目加入與接收緩沖區(qū)相關(guān)的等待隊(duì)列。
  3. 將進(jìn)程狀態(tài)設(shè)置為睡眠(TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE),然后調(diào)用調(diào)度器 schedule(),讓出 CPU,運(yùn)行其他進(jìn)程。

對(duì)于套接字,struct sock(TCP/IP 協(xié)議棧中的核心結(jié)構(gòu))包含字段如 sk_sleep,它指向一個(gè)等待隊(duì)列。當(dāng)接收緩沖區(qū)為空時(shí),進(jìn)程會(huì)被加入這個(gè)隊(duì)列;當(dāng)數(shù)據(jù)到達(dá)時(shí),協(xié)議棧會(huì)操作這個(gè)隊(duì)列來(lái)喚醒進(jìn)程。

喚醒過(guò)程是如何實(shí)現(xiàn)的?

當(dāng)網(wǎng)絡(luò)協(xié)議棧(如 TCP/IP)收到數(shù)據(jù)并將其放入套接字的接收緩沖區(qū)時(shí):

  1. 事件檢測(cè): 協(xié)議棧代碼檢測(cè)到接收緩沖區(qū)從空變?yōu)榉强铡?/li>
  2. 檢查等待隊(duì)列: 協(xié)議棧訪(fǎng)問(wèn)與該緩沖區(qū)關(guān)聯(lián)的 wait_queue_head_t,檢查是否有進(jìn)程在等待。
  3. 調(diào)用 wake_up(): 如果隊(duì)列不為空,協(xié)議棧調(diào)用內(nèi)核函數(shù) wake_up()(或其變體,如 wake_up_interruptible())。
  4. wake_up() 的工作:
  • 遍歷等待隊(duì)列中的每個(gè) wait_queue_entry_t。
  • 將對(duì)應(yīng)的進(jìn)程狀態(tài)從睡眠改為 TASK_RUNNING。
  • 將這些進(jìn)程加入 CPU 的運(yùn)行隊(duì)列,等待調(diào)度器重新調(diào)度它們。
進(jìn)程 A 調(diào)用 read(sockfd) -> 接收緩沖區(qū)為空
  -> 加入等待隊(duì)列 -> 進(jìn)程睡眠
數(shù)據(jù)到達(dá) -> 協(xié)議棧放入緩沖區(qū) -> 調(diào)用 wake_up()
  -> 進(jìn)程 A 被喚醒 -> 重新調(diào)度 -> read() 返回?cái)?shù)據(jù)

可讀性

當(dāng)一個(gè)套接字接收到數(shù)據(jù)時(shí),網(wǎng)絡(luò)協(xié)議棧(如 TCP/IP 棧)處理完數(shù)據(jù)包后,會(huì)將數(shù)據(jù)放入該套接字的接收緩沖區(qū)。如果此時(shí)有進(jìn)程正在等待該套接字變?yōu)榭勺x(即接收緩沖區(qū)中有數(shù)據(jù)),協(xié)議棧代碼會(huì) 喚醒 (wake up) 在該套接字接收緩沖區(qū)關(guān)聯(lián)的等待隊(duì)列上睡眠的所有進(jìn)程。

可寫(xiě)性

當(dāng)應(yīng)用程序通過(guò) write() 或 send() 發(fā)送數(shù)據(jù)時(shí),數(shù)據(jù)首先被復(fù)制到套接字的發(fā)送緩沖區(qū)。網(wǎng)絡(luò)協(xié)議棧隨后從緩沖區(qū)取出數(shù)據(jù)并發(fā)送到網(wǎng)絡(luò)。當(dāng)數(shù)據(jù)成功發(fā)送出去,或者發(fā)送緩沖區(qū)中的空間被釋放到某個(gè)閾值以上時(shí),協(xié)議棧代碼會(huì) 喚醒 在該套接字發(fā)送緩沖區(qū)關(guān)聯(lián)的等待隊(duì)列上睡眠的所有進(jìn)程,通知它們現(xiàn)在可以寫(xiě)入更多數(shù)據(jù)了。

假設(shè)發(fā)送緩沖區(qū)大小為 8KB,高水位標(biāo)記為 6KB:

  • 寫(xiě)入 8KB 數(shù)據(jù) -> 緩沖區(qū)滿(mǎn) -> 進(jìn)程睡眠。
  • 協(xié)議棧發(fā)送 3KB 數(shù)據(jù) -> 剩余 5KB(低于高水位) -> 喚醒進(jìn)程 -> 可寫(xiě)。

這個(gè)“生產(chǎn)者-消費(fèi)者”模型(網(wǎng)絡(luò)棧是數(shù)據(jù)的生產(chǎn)者/消費(fèi)者,應(yīng)用程序是數(shù)據(jù)的消費(fèi)者/生產(chǎn)者)通過(guò)等待隊(duì)列和喚醒機(jī)制實(shí)現(xiàn),是理解 I/O 事件通知的基礎(chǔ)。

select 和 poll 的工作原理

select 和 poll 是較早的 I/O 多路復(fù)用接口。它們的工作方式類(lèi)似:

  1. 用戶(hù)調(diào)用 :應(yīng)用程序準(zhǔn)備好要監(jiān)視的文件描述符集合(select 使用 fd_set,poll 使用 struct pollfd 數(shù)組),并指定關(guān)心的事件類(lèi)型(可讀、可寫(xiě)、異常),然后調(diào)用 select 或 poll 系統(tǒng)調(diào)用。
  2. 內(nèi)核操作 :

某個(gè)被監(jiān)視的文件描述符相關(guān)的等待隊(duì)列被喚醒(例如,因?yàn)閿?shù)據(jù)到達(dá)或緩沖區(qū)變空)。

超時(shí)時(shí)間到達(dá)。

收到一個(gè)信號(hào)。

  • 檢查當(dāng)前狀態(tài) :立即檢查該文件描述符的當(dāng)前狀態(tài)是否滿(mǎn)足用戶(hù)請(qǐng)求的事件(例如,接收緩沖區(qū)是否非空?發(fā)送緩沖區(qū)是否有足夠空間?是否有錯(cuò)誤?)。如果滿(mǎn)足,就標(biāo)記該文件描述符為就緒。
  • 注冊(cè)等待 :如果當(dāng)前狀態(tài)不滿(mǎn)足,并且調(diào)用者準(zhǔn)備阻塞等待,則該 poll 方法會(huì)將當(dāng)前進(jìn)程添加到與所關(guān)心事件相關(guān)的 等待隊(duì)列 上。這是通過(guò)內(nèi)核函數(shù) poll_wait() 實(shí)現(xiàn)的,它并不直接使進(jìn)程睡眠,只是建立一個(gè)關(guān)聯(lián):如果未來(lái)該等待隊(duì)列被喚醒,當(dāng)前正在執(zhí)行 select/poll 的進(jìn)程也應(yīng)該被喚醒。
  • 內(nèi)核接收到文件描述符列表和關(guān)心的事件。
  • 內(nèi)核遍歷應(yīng)用程序提供的 每一個(gè) 文件描述符。
  • 對(duì)于每個(gè)文件描述符,內(nèi)核找到對(duì)應(yīng)的 struct file,然后調(diào)用其 file_operations 結(jié)構(gòu)中的 poll 方法(例如,對(duì)于套接字,最終會(huì)調(diào)用到類(lèi)似 sock_poll 的函數(shù))。
  • 該 poll 方法執(zhí)行兩個(gè)關(guān)鍵任務(wù):
  • 遍歷完所有文件描述符后,如果發(fā)現(xiàn)至少有一個(gè)文件描述符是就緒的,select/poll 就將就緒信息返回給應(yīng)用程序。
  • 如果沒(méi)有文件描述符就緒,并且設(shè)置了超時(shí)時(shí)間,則進(jìn)程會(huì) 睡眠 (阻塞),直到以下任一情況發(fā)生:
  • 當(dāng)進(jìn)程被喚醒后(如果是因等待隊(duì)列事件喚醒),內(nèi)核并 不知道 是哪個(gè)具體的文件描述符導(dǎo)致了喚醒。因此,內(nèi)核需要 重新遍歷一遍 所有被監(jiān)視的文件描述符,再次調(diào)用它們的 poll 方法檢查狀態(tài),找出哪些現(xiàn)在是就緒的,然后將結(jié)果返回給用戶(hù)。

解釋一下 poll_wait() :它建立了一種關(guān)聯(lián):當(dāng)?shù)却?duì)列被喚醒時(shí),當(dāng)前執(zhí)行 select 或 poll 的進(jìn)程也會(huì)被喚醒。

  • 在 select 或 poll 的內(nèi)核實(shí)現(xiàn)中,對(duì)于每個(gè)文件描述符,內(nèi)核調(diào)用其 file_operations 中的 poll 方法(例如 sock_poll)。
  • 在 poll 方法中,如果事件尚未就緒(例如接收緩沖區(qū)為空),會(huì)調(diào)用 poll_wait(file, wait_queue_head_t, poll_table),poll_wait() 中創(chuàng)建一個(gè) wait_queue_entry_t,關(guān)聯(lián)到當(dāng)前進(jìn)程:

file:文件描述符對(duì)應(yīng)的 struct file。

wait_queue_head_t:與事件(如接收緩沖區(qū))關(guān)聯(lián)的等待隊(duì)列。

poll_table:select/poll 傳入的臨時(shí)結(jié)構(gòu),用于收集等待隊(duì)列。

  • poll_wait() 只是注冊(cè)關(guān)聯(lián),不會(huì)直接調(diào)用 schedule() 使進(jìn)程睡眠。睡眠是在 select/poll 遍歷所有文件描述符后統(tǒng)一處理的。
select(fd_set) -> 內(nèi)核遍歷 fd
  -> fd1: poll() -> poll_wait(接收隊(duì)列) -> 注冊(cè)進(jìn)程
  -> fd2: poll() -> poll_wait(發(fā)送隊(duì)列) -> 注冊(cè)進(jìn)程
無(wú)就緒 fd -> 進(jìn)程睡眠
數(shù)據(jù)到達(dá) fd1 -> wake_up(接收隊(duì)列) -> 進(jìn)程喚醒 -> select 返回

select 和 poll 的主要缺點(diǎn)

  • 效率問(wèn)題 :每次調(diào)用都需要將整個(gè)文件描述符集合從用戶(hù)空間拷貝到內(nèi)核空間。更重要的是,內(nèi)核需要線(xiàn)性遍歷所有被監(jiān)視的文件描述符來(lái)檢查狀態(tài)和注冊(cè)等待,喚醒后還需要再次遍歷來(lái)確定哪些就緒。當(dāng)監(jiān)視的文件描述符數(shù)量 N 很大時(shí),這個(gè) O(N) 的開(kāi)銷(xiāo)變得非常顯著。
  • select 有最大文件描述符數(shù)量的限制(通常由 FD_SETSIZE 定義)。poll 沒(méi)有這個(gè)限制,但仍有上述效率問(wèn)題。

epoll:更高效的事件通知

epoll 是 Linux 對(duì) select 和 poll 的重大改進(jìn),旨在解決大規(guī)模并發(fā)連接下的性能瓶頸。它采用了一種不同的、基于 回調(diào) (callback) 的事件驅(qū)動(dòng)機(jī)制:

  1. **epoll_create() / epoll_create1()**:創(chuàng)建一個(gè) epoll 實(shí)例。這會(huì)在內(nèi)核中創(chuàng)建一個(gè)特殊的數(shù)據(jù)結(jié)構(gòu),用于維護(hù)兩個(gè)列表:

監(jiān)視列表 (Interest List) :通常使用高效的數(shù)據(jù)結(jié)構(gòu)(如紅黑樹(shù)或哈希表)存儲(chǔ)所有用戶(hù)通過(guò) epoll_ctl 添加的、需要監(jiān)視的文件描述符及其關(guān)心的事件。

就緒列表 (Ready List) :一個(gè)鏈表,存儲(chǔ)那些已經(jīng)被內(nèi)核檢測(cè)到發(fā)生就緒事件、但尚未被 epoll_wait 報(bào)告給用戶(hù)的文件描述符。

這個(gè) epoll 實(shí)例本身也由一個(gè)文件描述符表示。

2.epoll_ctl() :用于向 epoll 實(shí)例的監(jiān)視列表添加 (EPOLL_CTL_ADD)、修改 (EPOLL_CTL_MOD) 或刪除 (EPOLL_CTL_DEL) 文件描述符。

  • 關(guān)鍵操作:當(dāng)使用 EPOLL_CTL_ADD 添加一個(gè)文件描述符 fd 時(shí),內(nèi)核不僅將其加入 epoll 實(shí)例的監(jiān)視列表,更重要的是,它會(huì)在與 fd 相關(guān)的 等待隊(duì)列 上注冊(cè)一個(gè) 回調(diào)函數(shù)。
  • 這個(gè)回調(diào)函數(shù)非常特殊:當(dāng) fd 對(duì)應(yīng)的資源(如套接字緩沖區(qū))狀態(tài)改變,導(dǎo)致其關(guān)聯(lián)的等待隊(duì)列被喚醒時(shí),這個(gè)注冊(cè)的回調(diào)函數(shù)會(huì)被執(zhí)行。
  • 回調(diào)函數(shù)的任務(wù)是:檢查 fd 的當(dāng)前狀態(tài)是否匹配 epoll 實(shí)例對(duì)其關(guān)心的事件。如果匹配,就將這個(gè) fd 添加到 epoll 實(shí)例的 就緒列表 中。如果此時(shí)有進(jìn)程正在 epoll_wait 中睡眠等待該 epoll 實(shí)例,則喚醒該進(jìn)程。
+-----------------+      epoll_ctl(ADD fd)      +----------------------------+
| epoll instance  | <-------------------------- | Application Process        |
| - Interest List |                             +----------------------------+
| - Ready List    |                                          |
| - Wait Queue    |      Registers Callback                  | system call
+-----------------+---------------------------> +----------------------------+
                                                | Kernel                     |
                                                | +------------------------+ |
                                                | | struct file (for fd)   | |
                                                | | - f_op                 | |
                                                | | - private_data (socket)| |
                                                | +---------+--------------+ |
                                                |           |                |
                                                |           v                |
                                                | +---------+-------------+  |
                                                | | Wait Queue (e.g., rx) |  |
                                                | | - epoll callback entry|  |
                                                | +-----------------------+  |
                                                +----------------------------+

3.**epoll_wait()**:等待 epoll 實(shí)例監(jiān)視的文件描述符上發(fā)生事件。

  • 調(diào)用 epoll_wait 時(shí),內(nèi)核首先檢查 epoll 實(shí)例的 就緒列表。
  • 如果就緒列表 非空,內(nèi)核直接將就緒列表中的文件描述符信息拷貝到用戶(hù)空間提供的緩沖區(qū),并立即返回就緒的文件描述符數(shù)量。
  • 如果就緒列表 為空,進(jìn)程將 睡眠,等待在 epoll 實(shí)例自身的等待隊(duì)列上。
  • 當(dāng)某個(gè)被監(jiān)視的文件描述符 fd 發(fā)生事件(如數(shù)據(jù)到達(dá)),其關(guān)聯(lián)的等待隊(duì)列被喚醒,觸發(fā)之前注冊(cè)的 epoll 回調(diào)。
  • 回調(diào)函數(shù)將 fd 加入 epoll 實(shí)例的就緒列表,并喚醒在 epoll_wait 中等待該實(shí)例的進(jìn)程。
  • epoll_wait 被喚醒后,發(fā)現(xiàn)就緒列表非空,于是收集就緒信息并返回給用戶(hù)。

epoll 的優(yōu)勢(shì)

  • 高效 :epoll_wait 的復(fù)雜度通常是 O(1),因?yàn)樗恍枰獧z查就緒列表,而不需要像 select/poll 那樣遍歷所有監(jiān)視的文件描述符。文件描述符狀態(tài)的檢查和就緒列表的填充是由事件發(fā)生時(shí)的回調(diào)機(jī)制異步完成的。
  • 回調(diào)機(jī)制 :避免了 select/poll 在每次調(diào)用和喚醒時(shí)都需要重復(fù)遍歷所有文件描述符的問(wèn)題。文件描述符和 epoll 實(shí)例的關(guān)聯(lián)(包括回調(diào)注冊(cè))只需要在 epoll_ctl 時(shí)建立一次。
  • 邊緣觸發(fā) (Edge Triggered, ET) 與水平觸發(fā) (Level Triggered, LT) :epoll 支持這兩種模式。LT 模式(默認(rèn))行為類(lèi)似于 poll,只要條件滿(mǎn)足(如緩沖區(qū)非空),epoll_wait 就會(huì)一直報(bào)告就緒。ET 模式下,只有當(dāng)狀態(tài)從未就緒變?yōu)榫途w時(shí),epoll_wait 才會(huì)報(bào)告一次,之后即使條件仍然滿(mǎn)足也不會(huì)再報(bào)告,直到應(yīng)用程序處理了該事件(例如,讀取了所有數(shù)據(jù)使得緩沖區(qū)變空,然后又有新數(shù)據(jù)到達(dá))。ET 模式通常能提供更高的性能,但編程也更復(fù)雜,需要確保每次事件通知后都將數(shù)據(jù)處理完畢。

LT 模式(默認(rèn))

#include <sys/epoll.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int epfd = epoll_create1(0);
    int sockfd = /* 假設(shè)已創(chuàng)建并綁定監(jiān)聽(tīng)的套接字 */;

    struct epoll_event event;
    event.events = EPOLLIN; // LT 模式,默認(rèn)
    event.data.fd = sockfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

    struct epoll_event events[10];
    while (1) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == sockfd) {
                char buf[1024];
                ssize_t n = read(sockfd, buf, sizeof(buf));
                if (n > 0) {
                    printf("Read %zd bytes\n", n);
                    // 可只讀部分?jǐn)?shù)據(jù),下次仍會(huì)觸發(fā)
                } else if (n == 0) {
                    printf("Connection closed\n");
                }
            }
        }
    }
}

ET 模式

#include <sys/epoll.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>

int main() {
    int epfd = epoll_create1(0);
    int sockfd = /* 假設(shè)已創(chuàng)建并綁定監(jiān)聽(tīng)的套接字 */;

    struct epoll_event event;
    event.events = EPOLLIN | EPOLLET; // ET 模式
    event.data.fd = sockfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);

    struct epoll_event events[10];
    while (1) {
        int nfds = epoll_wait(epfd, events, 10, -1);
        for (int i = 0; i < nfds; i++) {
            if (events[i].data.fd == sockfd) {
                while (1) { // 必須一次性讀完
                    char buf[1024];
                    ssize_t n = read(sockfd, buf, sizeof(buf));
                    if (n > 0) {
                        printf("Read %zd bytes\n", n);
                    } else if (n == 0) {
                        printf("Connection closed\n");
                        break;
                    } else {
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            printf("Buffer drained\n");
                            break;
                        }
                    }
                }
            }
        }
    }
}

連接套接字(socket, bind, listen, accept)與就緒狀態(tài)

現(xiàn)在我們將這些概念與服務(wù)器套接字的工作流程聯(lián)系起來(lái):

  • socket() : 創(chuàng)建一個(gè)套接字文件描述符 sockfd。此時(shí)它通常既不可讀也不可寫(xiě)。
  • bind() : 將 sockfd 綁定到一個(gè)本地地址和端口。這本身通常不改變其可讀寫(xiě)狀態(tài)。
  • listen() : 將 sockfd 標(biāo)記為監(jiān)聽(tīng)套接字,并創(chuàng)建兩個(gè)隊(duì)列(SYN 隊(duì)列和 Accept 隊(duì)列)。此時(shí) sockfd 仍不可直接讀寫(xiě)數(shù)據(jù)。

何時(shí)監(jiān)聽(tīng)套接字 sockfd 變?yōu)椤翱勺x”?

當(dāng)一個(gè)客戶(hù)端連接請(qǐng)求完成 TCP 三次握手后,內(nèi)核會(huì)創(chuàng)建一個(gè)代表這個(gè)新連接的 已完成連接 (established connection),并將其放入與監(jiān)聽(tīng)套接字 sockfd 關(guān)聯(lián)的 Accept 隊(duì)列 中。此時(shí),對(duì)于 select/poll/epoll 來(lái)說(shuō),監(jiān)聽(tīng)套接字 sockfd 就被認(rèn)為是 可讀 的。調(diào)用 accept(sockfd, ...) 將會(huì)從 Accept 隊(duì)列中取出一個(gè)已完成連接,并返回一個(gè) 新的 文件描述符 connfd,這個(gè) connfd 才代表了與客戶(hù)端的實(shí)際通信通道。如果 Accept 隊(duì)列為空,則監(jiān)聽(tīng)套接字 sockfd 不可讀。

  • accept(): 從監(jiān)聽(tīng)套接字的 Accept 隊(duì)列中取出一個(gè)已完成的連接,返回一個(gè)新的已連接套接字 connfd。

何時(shí)已連接套接字 connfd 變?yōu)椤翱勺x”?

當(dāng)內(nèi)核的網(wǎng)絡(luò)協(xié)議棧收到屬于 connfd 這個(gè)連接的數(shù)據(jù),并將數(shù)據(jù)放入其 接收緩沖區(qū) 后,connfd 就變?yōu)榭勺x。此時(shí),網(wǎng)絡(luò)棧會(huì)喚醒在該套接字接收緩沖區(qū)等待隊(duì)列上的進(jìn)程(包括通過(guò) epoll 注冊(cè)的回調(diào))。

何時(shí)已連接套接字 connfd 變?yōu)椤翱蓪?xiě)”?

當(dāng) connfd 的 發(fā)送緩沖區(qū) 有足夠的可用空間來(lái)容納更多待發(fā)送的數(shù)據(jù)時(shí),connfd 就變?yōu)榭蓪?xiě)。當(dāng)內(nèi)核成功將發(fā)送緩沖區(qū)中的數(shù)據(jù)發(fā)送到網(wǎng)絡(luò),釋放了空間后,會(huì)喚醒在該套接字發(fā)送緩沖區(qū)等待隊(duì)列上的進(jìn)程(包括 epoll 回調(diào))。初始狀態(tài)下,新創(chuàng)建的 connfd 通常是可寫(xiě)的。

異常狀態(tài) :通常指帶外數(shù)據(jù)到達(dá),或者發(fā)生某些錯(cuò)誤(如連接被對(duì)方重置 RST)。

總結(jié)

Linux 內(nèi)核通過(guò)為每個(gè)可能阻塞的 I/O 資源(如套接字緩沖區(qū))維護(hù) 等待隊(duì)列 來(lái)跟蹤哪些進(jìn)程在等待事件。當(dāng)事件發(fā)生時(shí)(數(shù)據(jù)到達(dá)、緩沖區(qū)變空),內(nèi)核代碼(如網(wǎng)絡(luò)協(xié)議棧)會(huì) 喚醒 相應(yīng)等待隊(duì)列上的進(jìn)程。

  • select 和 poll 在每次調(diào)用時(shí),都需要遍歷所有被監(jiān)視的文件描述符,檢查它們的當(dāng)前狀態(tài),并將進(jìn)程注冊(cè)到相關(guān)的等待隊(duì)列上。喚醒后還需要再次遍歷以確定哪些就緒。
  • epoll 通過(guò) epoll_ctl 預(yù)先在文件描述符的等待隊(duì)列上注冊(cè) 回調(diào)函數(shù) 。當(dāng)事件發(fā)生并喚醒等待隊(duì)列時(shí),回調(diào)函數(shù)被觸發(fā),它負(fù)責(zé)將就緒的文件描述符添加到 epoll 實(shí)例的 就緒列表 中,并喚醒等待在 epoll_wait 上的進(jìn)程。epoll_wait 只需檢查這個(gè)就緒列表即可,大大提高了效率。

理解等待隊(duì)列和喚醒機(jī)制,以及 epoll 基于回調(diào)的事件驅(qū)動(dòng)模型,是掌握 Linux 下高性能網(wǎng)絡(luò)編程和 I/O 多路復(fù)用技術(shù)的關(guān)鍵。

總結(jié)

  • 可讀

監(jiān)聽(tīng)套接字:Accept 隊(duì)列非空(有新連接)。

已連接套接字:接收緩沖區(qū)有數(shù)據(jù)。

內(nèi)核通過(guò)網(wǎng)絡(luò)協(xié)議棧監(jiān)控緩沖區(qū)狀態(tài)。

  • 可寫(xiě)

發(fā)送緩沖區(qū)有足夠空間(低于高水位)。

協(xié)議棧監(jiān)控發(fā)送進(jìn)度并更新空間。

如何“立即”通知應(yīng)用程序?

  • 等待隊(duì)列機(jī)制: 資源狀態(tài)變化時(shí),協(xié)議棧調(diào)用 wake_up() 喚醒等待隊(duì)列上的進(jìn)程。
  • select/poll: 通過(guò) poll_wait() 注冊(cè)等待,事件發(fā)生時(shí)喚醒并重新檢查狀態(tài)。
  • epoll: 通過(guò)回調(diào)函數(shù)異步將就緒文件描述符加入就緒列表,epoll_wait 直接返回。
責(zé)任編輯:武曉燕 來(lái)源: Pipeliu
相關(guān)推薦

2023-05-08 00:06:45

Go語(yǔ)言機(jī)制

2021-03-17 16:53:51

IO多路

2021-02-10 08:09:48

Netty網(wǎng)絡(luò)多路復(fù)用

2009-06-29 18:09:12

多路復(fù)用Oracle

2020-10-13 07:51:03

五種IO模型

2021-03-24 08:03:38

NettyJava NIO網(wǎng)絡(luò)技術(shù)

2024-12-30 00:00:05

2021-06-09 19:25:13

IODubbo

2023-11-08 09:22:14

I/ORedis阻塞

2019-12-23 14:53:26

IO復(fù)用

2011-12-08 10:51:25

JavaNIO

2023-01-09 10:04:47

IO多路復(fù)用模型

2023-08-07 08:52:03

Java多路復(fù)用機(jī)制

2023-12-06 07:16:31

Go語(yǔ)言語(yǔ)句

2021-05-31 06:50:47

SelectPoll系統(tǒng)

2020-10-14 09:11:44

IO 多路復(fù)用實(shí)現(xiàn)機(jī)

2022-09-12 06:33:15

Select多路復(fù)用

2022-08-26 00:21:44

IO模型線(xiàn)程

2024-08-08 14:57:32

2024-05-15 16:41:57

進(jìn)程IO文件
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)