I/O多路復用
指在單個線程或進程中,同時處理多個I/O操作的技術。
旨在提高程序處理多個并發I/O操作的能力,避免程序因等待某個I/O操作而被阻塞。在傳統的I/O模型中當程序進行I/O操作時(如讀取文件、接受網路數據等),如果數據還未準備好,程序會被阻塞,直到I/O操作完成,這會導致效率低下,尤其是在需要處理大量并發連接的網絡應用中。I/O復用技術的核心理念是允許一個進程或線程同時處理多個I/O操作,而不是等待某一個操作完成后再去處理其他任務。通過則何種方式,程序能夠在多個I/O之間切換,充分利用系統資源,避免每個I/O事件都創建一個新的線程或進程,從而大大提高效率。在linux中相關技術有select()、poll()、epoll(),程序會通過上述機制同時監控多個I/O事件,并在其中某個文件描述符(或I/O操作)就緒時,進行相應的處理。這樣即使有多個I/O操作正在進行,程序也可以及時響應,并繼續進行其他任務,從而達到“非阻塞”的效果。
epoll涉及到的系統調用函數
epoll_create()
用于創建一個epoll示例,返回一個文件描述符,程序通過這個文件描述符與epoll實例進行交互。他為事件提供一個內核空間的數據結構,并為之后的epoll_ctl()和epoll_wait()調用提供一個有效的上下文。
int epoll_create(int size);
size:指定內核事件表的初始大小。這個參數在現代linux系統中已沒有什么作用,通常?? 設置為1即可,因為內核會動態調整。
返回值:創建成功返回一個非負值,表示epoll實例的文件描述符,創建失敗返回-1,??? ? 并設置errno為相應的錯誤代碼。
int epoll_create1(int flags);
flags:創建時指定的額外選項
傳入0代表不指定額外選項
EPOLL_CTL_ADD:添加新的文件描述符及其事件EPOLL_CTL_MOD:修改已經注冊的文件描述符的事件EPOLL_CTL_DEL:刪除文件描述符的注冊
傳入EPOLL_CLOEXEC:設置文件描述符為執行時關閉,以確保在exec系列函數調用后自動關閉該文件的文件描述符。
epoll_ctl()
用于控制epoll實例中的事件,包括向epoll注冊、修改或刪除文件?????? ???????????????? ? 描述符的I/O事件。
int epoll_ctl(int epfd, int op,int fd, struct epoll_event* event);
epfd:由epoll_create()返回的epoll文件描述符,用于標識epoll實例。
op:操作類型,指示所執行的操作。
EPOLL_CTL_ADD:添加新的文件描述符及其事件EPOLL_CTL_MOD:修改已經注冊的文件描述符的事件EPOLL_CTL_DEL:刪除文件描述符的注冊
fd:需要注冊、修改或刪除的文件描述符
event:struct epoll_event類型的指針,表示與文件描述符關聯的事件類型,這個結構體參數的作用不僅僅是保存文件描述符,他還包含了與該文件描述符關聯的事件類型、以及其他用于標識和處理時間的數據
返回值:成功返回0,失敗返回-1,并設置errno為相應的錯誤代碼。
..................................................................................................................................................
補:epoll_event數據結構
該結構體定義了與文件描述符關聯的事件類型及其他數據
struct epoll_event {uint32_t events; //事件類型epoll_data_t data; //與文件描述符關聯的數據,通常為文件描述符或指針
};
其中:
events:表示該文件描述符的事件類型,注意這些事件類型是可以a|b的混合注冊的
data:是一個聯合體epoll_data_t,用于存儲與文件描述符相關的自定義數據,通常用于存儲文件描述符本身或替他數據
epoll_data_t可以是:int fd(文件描述符);? void* ptr(指向用戶數據的指針)等
..................................................................................................................................................
示例:
struct epoll_event ev;
ev.event = EPOLLIN; //設置為讀取事件
ev.data.fd = server_fd; //設置文件描述符
int res = epoll_create(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
epoll_wait()
用于等待已注冊的文件描述符上的事件發生。該函數會阻塞直到至少一個事件發生或者超時。等待并返回已發生的事件。
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout);
epfd:由epoll_create返回的epoll文件描述符
events:指向epoll_event結構體數組的指針,用于接收已發生的事件。
event是一個數組,用于存儲epoll_event類型的結構體,即用于接收發生的事件event。這里的事件是指,在epoll_ctl階段注冊到epoll中的事件event,并且他得已經發生,已經發生的意思是指已注冊的文件描述符的狀態滿足了你注冊的事件條件(如:在epoll_ctl階段為某個文件描述符fd注冊了EPOLLIN,當fd上有數據可讀時,例如socket接收到網絡數據了,那么這個fd上就有數據可以讀取了,這個事件就被認為已經發///或者為某個文件描述符注冊的EPOLLOUT,當fd可以用來寫數據時,比如socket的發送緩沖區有空閑空間時,那么這個fd就可以進行寫,這個事件被認為已經發生了)。
epoll_wait()的作用可以這樣理解,它的作用就是監聽在epoll_ctl注冊到epfd中的事件集合,然后將發生的事件傳入events數組中,通過遍歷這個events可以得到event,通過event的fd和events成員可以得知該fd可以進行events對應操作了(比如:struct epoll_event ev;? ev.fd = fd1;? ev.events = EPOLLOUT,那么我們可以對fd1進行寫操作了,write(fd, ....))。
maxevents:指定events數組的大小,即一次最多返回的事件數
timeout:指定等待的超時時間(單位:毫秒)。設置為-1時表示無限等待,設置為0表示??????????????? 非阻塞,其他正值表示最大等待時間。
返回值:成功則返回已就緒的事件數,即發生的事件數量,失敗則返回-1,并設置errno????????? 為相應的錯誤代碼。
示例:
struct epoll_event events[10];
int nfds = epoll_wait(epfd, events, 10, -1);
for (int i = 0; i < nfds; ++i) {if (events[i].events & EPOLLIN) {// 處理可讀事件}if (events[i].events & EPOLLOUT) {// 處理可寫事件}
}
?綜合使用簡單示例:
// 創建一個 epoll 實例,返回 epoll 文件描述符
int epfd = epoll_create(1);// 定義一個 epoll_event 結構體變量,用于描述要監聽的事件
struct epoll_event ev;// 設置事件類型為 EPOLLOUT,表示監聽 "可寫" 事件
ev.events = EPOLLOUT;// 綁定要監聽的文件描述符(socketfd)到 ev 的 data.fd 字段
ev.data.fd = socketfd;// 將指定的文件描述符(socketfd)注冊到 epoll 實例 epfd 中,監聽可寫事件
epoll_ctl(epfd, EPOLL_CTL_ADD, socketfd, &ev);// 定義一個數組,用來接收 epoll_wait 返回的就緒事件
struct epoll_event events[10];// 調用 epoll_wait,阻塞等待內核檢測 epfd 中注冊的事件
// -1 表示永遠等待,直到有事件發生
int nfds = epoll_wait(epfd, events, 10, -1);// 遍歷所有返回的就緒事件
for (int i = 0; i < nfds; ++i) {// 檢查當前事件是否包含 EPOLLOUT,可寫事件if (events[i].events & EPOLLOUT) {// 取出就緒的文件描述符int fd = events[i].data.fd;// 定義要發送的消息內容const char* message = "hello";// 使用 write 將消息寫入到對應的文件描述符ssize_t bytes_written = write(fd, message, strlen(message));// 這里沒有做錯誤檢查,實際項目中最好檢查 bytes_written 是否出錯}
}
// 創建 epoll 實例,返回 epoll 文件描述符
int epfd = epoll_create1(0);// 注意:這里通過一個 open 打開了一個文件,得到一個文件描述符,但它不一定滿足下面的 EPOLLIN 事件,
// 只有當 example 文件中有數據時,它才可以被讀,才滿足該 fd 監聽的 / 感興趣的事件 EPOLLIN,
// 它才可以在 epoll_wait 的時候被添加到 events 中。
int fd = open("example.txt", O_RDONLY); // 注意:這里應該是 O_RDONLY,不是 0_RDONLY// 定義一個 epoll_event 結構體變量
struct epoll_event ev;// 設置監聽的事件類型為 EPOLLIN(可讀事件)
ev.events = EPOLLIN;// 將文件描述符 fd 保存到 ev.data.fd 中
ev.data.fd = fd;// 調用 epoll_ctl,將 fd 注冊到 epoll 實例 epfd 中,關注可讀事件
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);// 定義一個數組,用來存放 epoll_wait 返回的就緒事件
struct epoll_event events[10];// 調用 epoll_wait,阻塞等待 epfd 中注冊的文件描述符上有事件發生
int nfds = epoll_wait(epfd, events, 10, -1);// 遍歷所有返回的就緒事件
for (int i = 0; i < nfds; ++i) {// 如果當前事件是 EPOLLIN(可讀事件)if (events[i].events & EPOLLIN) {// 在這里處理對應的可讀文件描述符}
}
epoll底層實現原理
核心組件
①紅黑樹
epoll內部維護了一顆紅黑樹,通過紅黑樹來管理(增加、刪除、修改)所有被監控的文件描述符;當調用epoll_ctl增加、刪除或修改監控事件時,會在紅黑樹中插入、移除或更新相應的節點;紅黑樹的高效查找和插入特性(時間復雜度為O(Logn))使得epoll在管理大量文件描述符時性能優越。
②就緒鏈表(雙向鏈表)
epoll使用一個就緒鏈表來保存當前已經觸發事件的文件描述符;當某個文件描述符變為就緒狀態時(例如數據可讀或可寫),其對應的事件會被添加到就緒鏈表中;這使得epoll_wait調用只需直接掃描這個鏈表,從而避免了向poll或select那樣逐個遍歷所有的文件描述符。
與內核的交互
epoll是依賴內核中的事件通知機制來工作的,通常通過文件系統(如proc文件系統等)監控文件描述符的狀態;每個文件描述符在內核中都有一個對應的事件回調函數,當事件發生時(即通過epoll_ctl添加到紅黑樹中的event對應的fd滿足注冊的事件狀態時),會觸發這個回調函數,將事件添加到就緒鏈表中,當調用epoll_wait時,內核將掃描就緒鏈表,并返回鏈表中的數據。
觸發機制
在epoll中,觸發機制決定了epoll_wait如何返回文件描述符的事件。這直接影響事件的通知方式和應用程序對文件描述符的處理策略,epoll支持兩種觸發機制:水平觸發和邊緣觸發。
①水平觸發(level triggered)
這是epoll默認的觸發方式。文件描述符只要處于就緒狀態(可讀或可寫),epoll_wait就會一直返回該事件(調用該方法返回的int值大小中有它一席,并且傳入給epoll_wait的events數組也會一直存入這個事件event);無論文件描述符的狀態是否變化,只要其仍然滿足條件(如緩沖區有數據可讀),事件都會重復觸發,直到應用程序對其處理完成。
優點:使用簡單,適合大多數場景。不容易遺漏事件,即使處理稍有延遲,也可以通過多次調用讀取剩余數據。
缺點:對于大量文件描述符,就緒事件可能被重新觸發,導致處理效率較低。
struct epoll_event ev;
ev.events = EPOLLIN; //水平觸發是默認的
②邊沿觸發(edge triggered)
事件只會在狀態變化時觸發(如從不可讀變為可讀,或從不可寫變成可寫);如果應用程序沒有在事件觸發時處理完數據,則不會再次觸發,可能導致數據遺漏。
優點:減少了重復通知,提高了系統效率,適合大規模并發場景,支持高性能的非阻塞模式。
缺點:復雜性高,必須一次性讀取或寫入盡可能多的數據,否則可能會遺漏數據。
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET; //設置為邊沿觸發
epoll工作流程
首先,用戶通過調用epoll_create創建一個epoll實例,用于管理所有需要監聽的文件描述符(每個epoll實例中都有一個獨立的evetnepoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加的事件)。接著,使用epoll_ctl向epoll內部的紅黑樹中添加、修改或刪除需要監聽的文件描述符及事件;最后,通過調用epoll_wait等待事件發生,內核會將已經觸發的事件加入就緒鏈表,并將鏈表中的就緒文件描述符返回給用戶程序,從而實現高效的事件驅動模型。
相較于select和poll,epoll為什么高效?
以下幾個差異導致epoll更高效
1.事件監聽和管理方式的不同
select、poll:每次調用select、poll,都需要將所有文件描述符表傳遞給內核,內?? 核會對這些文件描述符逐一查其狀態,這種查找是線性查找,時間復雜度是O(n), 導致性能開銷大,尤其是在文件描述符數量較多時。
epoll:使用一個紅黑樹來存儲用戶注冊的文件描述符事件,文件描述符只需通過?? epoll_ctl注冊一次;每次epoll_wait時,內核只需檢查紅黑樹上的事件,并通過 一個就緒鏈表直接返回有事件發生的文件描述符;事件分發是基于回調的機制,無 需線性掃描。
2.數據拷貝的效率
select、poll:每次調用select、poll都需要將文件描述符列表從用戶態拷貝到內核 態,再從內核態返回結果到用戶態,如果文件描述符很多,這個過程會占用大量的 cpu和內存帶寬。
epoll:采用共享內存機制,文件描述符只在epoll_ctl注冊時傳遞給內核;內核和用 戶空間之間通過共享的就緒鏈表來傳遞數據,避免每次調用時的大量拷貝。
3.支持更大的文件描述符集合
select:文件描述符數量受到系統常量FD_SETSIZE的限制(通常是1024個)。超過限 制后無法使用。
poll:支持更多的文件描述符,但依然需要遍歷整個文件描述符列表
epoll:支持的文件描述符數量只受限于系統的最大文件描述符數量,理論上可以達到?? 數十萬甚至更多。即使文件描述符數量龐大,只關注有事件發生的文件描述符,效 率依然很高。
4.觸發機制的差異
select、poll:只支持水平觸發,即只要文件描述符的狀態滿足條件,每次都會返回, 可能會導致重復處理。
epoll:支持水平觸發和邊緣觸發,邊緣觸發模式下,只有當文件描述符的狀態從未滿?? 足到滿足時,才會觸發事件,進一步減少系統調用的次數,提高性能。
5.線程安全性
epoll:是線程安全的(epoll_wait內存實現對共享的就緒事件列表有鎖機制保活,確 保線程安全),多個線程可以同時調用epoll_wait,充分利用多核CPU提高并?? 發? 能力。
select、poll:通常需要額外的同步機制來確保多線程訪問同一個文件描述符集合時的 線程安全性。