前言:
在linux系統(tǒng)中,實際上所有的 I/O 設(shè)備都被抽象為了文件這個概念,一切皆文件,磁盤、網(wǎng)絡(luò)數(shù)據(jù)、終端,甚至進程間通信工具管道 pipe 等都被當做文件對待。
在了解多路復(fù)用 select、poll、epoll 實現(xiàn)之前,我們先簡單回憶復(fù)習(xí)以下兩個概念:
一、什么是多路復(fù)用:
- 多路:多個 socket 網(wǎng)絡(luò)連接。
- 復(fù)用:復(fù)用一個線程,使用一個線程來檢查多個文件描述符(socket)的就緒狀態(tài)。
- 多路復(fù)用主要有三種技術(shù):select、poll、epoll。epoll 是最新的,也是目前最好的多路復(fù)用技術(shù)。
二、五種 I/O 模型
+ blocking I/O - 阻塞I/O
+ non-blocking I/O - 非阻塞I/O
+ signal-driven I/O - 信號驅(qū)動I/O
+ asynchronous I/O - 異步I/O
+ I/O multiplexing - I/O多路復(fù)用
1. 阻塞 I/O 模型
進程/線程在從調(diào)用 recvfrom()
開始到它返回的整段時間內(nèi)是被阻塞的,recvfrom()
成功返回后,應(yīng)用進程/線程開始處理數(shù)據(jù)報。
recvfrom()
是一個系統(tǒng)調(diào)用函數(shù),用于從一個已連接或未連接的套接字(socket)接收數(shù)據(jù)。函數(shù)原型如下:
ssize_t recvfrom(int sockfd, //套接字文件描述符
void *buf, //指向接收數(shù)據(jù)的緩沖區(qū)
size_t len, //緩沖區(qū)的大小
int flags, //可選的標志參數(shù),用于控制接收操作的行為
struct sockaddr *src_addr, //用于存儲發(fā)送方的地址信息
socklen_t *addrlen //src_addr 的長度
);
recvfrom()
在調(diào)用時會阻塞,直到有數(shù)據(jù)到達或發(fā)生錯誤。當有數(shù)據(jù)到達時,它將數(shù)據(jù)讀取到指定的緩沖區(qū)中,并填充發(fā)送方的地址信息到 src_addr
參數(shù)中。如果套接字是已連接的,src_addr
和 addrlen
參數(shù)可以設(shè)置為 NULL
。
-
主要特點
- 進程阻塞掛起不消耗CPU資源,能及時響應(yīng)每個操作。
- 實現(xiàn)難度低。
- 適用并發(fā)量小的網(wǎng)絡(luò)應(yīng)用開發(fā),不適用并發(fā)量大的應(yīng)用。因為一個請求IO會阻塞進程,所以每請求分配一個處理進程(線程)去響應(yīng),系統(tǒng)開銷大。
2. 非阻塞 I/O 模型
進程發(fā)起 I/O
系統(tǒng)調(diào)用后,如果內(nèi)核緩沖區(qū)沒有數(shù)據(jù),需要到 I/O
設(shè)備中讀取,進程返回一個錯誤而不會被阻塞;如果內(nèi)核緩沖區(qū)有數(shù)據(jù),內(nèi)核就會把數(shù)據(jù)返回進程。
-
主要特點
- 進程輪詢(重復(fù))調(diào)用,消耗CPU的資源。
- 實現(xiàn)難度低、開發(fā)應(yīng)用相對阻塞IO模式較難。
- 適用并發(fā)量較小、且不需要及時響應(yīng)的網(wǎng)絡(luò)應(yīng)用開發(fā)。
3. 信號驅(qū)動 I/O 模型
當進程發(fā)起一個 I/O
操作,會向內(nèi)核注冊一個信號處理函數(shù),然后進程返回不阻塞;當內(nèi)核數(shù)據(jù)就緒時會發(fā)送一個信號給進程,進程便在信號處理函數(shù)中調(diào)用 I/O
讀取數(shù)據(jù)。與阻塞式 I/O
或非阻塞式 I/O
模型不同,信號驅(qū)動 I/O
模型允許應(yīng)用程序在進行 I/O
操作時繼續(xù)執(zhí)行其他任務(wù),而不需要顯式地輪詢或阻塞等待 I/O
操作的完成。
-
工作流程
- 應(yīng)用程序通過調(diào)用
sigaction()
來注冊一個信號處理函數(shù)(Signal Handler)
,用于處理特定的I/O
相關(guān)信號,如SIGIO
。 - 將
I/O 文件描述符
設(shè)置為信號驅(qū)動模式,通常使用fcntl()
并設(shè)置F_SETOWN
標志,將文件描述符的擁有者設(shè)置為當前進程。這樣,當I/O
事件發(fā)生時,內(nèi)核將向該進程發(fā)送相應(yīng)的信號。 - 當
I/O
事件(如數(shù)據(jù)到達)發(fā)生時,操作系統(tǒng)將為相應(yīng)的文件描述符生成一個信號(通常是SIGIO
),并將其發(fā)送給擁有者進程。 - 擁有者進程接收到信號后,會調(diào)用事先注冊的信號處理函數(shù)進行相應(yīng)的處理。在信號處理函數(shù)中,可以執(zhí)行讀取數(shù)據(jù)、寫入數(shù)據(jù)等操作。
- 信號處理函數(shù)執(zhí)行完畢后,應(yīng)用程序可以繼續(xù)執(zhí)行其他任務(wù),而不需要顯式地等待
I/O
操作的完成。
- 應(yīng)用程序通過調(diào)用
-
主要特點
- 實現(xiàn)、開發(fā)應(yīng)用難度大。需要合理處理信號處理函數(shù)和信號同步的問題。
- 適用于需要同時處理多個
I/O
事件的情況,如網(wǎng)絡(luò)編程中的異步處理。
4. 異步 I/O 模型
當進程發(fā)起一個 I/O
操作,進程返回(不阻塞),但也不能返回結(jié)果;內(nèi)核把整個 I/O
處理完后,會通知進程結(jié)果。如果 I/O
操作成功則進程直接獲取到數(shù)據(jù)。
-
工作原理
- 應(yīng)用程序調(diào)用系統(tǒng)調(diào)用函數(shù)(如
aio_read、aio_write
等)發(fā)起異步I/O
操作。這些函數(shù)通常是系統(tǒng)提供的異步I/O
接口函數(shù)。 - 在發(fā)起異步
I/O
操作時,應(yīng)用程序還需要提供一個回調(diào)函數(shù)
,該函數(shù)將在I/O
操作完成時被調(diào)用。 - 異步
I/O
操作被提交給操作系統(tǒng)或I/O
子系統(tǒng)進行處理。操作系統(tǒng)將負責(zé)執(zhí)行實際的I/O
操作,并在操作完成后觸發(fā)相應(yīng)的事件。 - 當
I/O
操作完成時,操作系統(tǒng)將調(diào)用之前注冊的回調(diào)函數(shù),并將操作的結(jié)果傳遞給回調(diào)函數(shù)。 - 在回調(diào)函數(shù)中,應(yīng)用程序可以處理操作的結(jié)果,例如讀取已接收的數(shù)據(jù)、處理錯誤情況等。
- 應(yīng)用程序可以繼續(xù)執(zhí)行其它任務(wù),而無需等待
I/O
操作的完成。
- 應(yīng)用程序調(diào)用系統(tǒng)調(diào)用函數(shù)(如
-
主要特點
- 不阻塞,數(shù)據(jù)一步到位。
- 需要操作系統(tǒng)的底層支持,LINUX 2.5 版本內(nèi)核首現(xiàn),2.6 版本產(chǎn)品的內(nèi)核標準特性。
- 實現(xiàn)、開發(fā)應(yīng)用難度大。
- 非常適合高性能高并發(fā)應(yīng)用。
5. I/O 復(fù)用模型
大多數(shù)文件系統(tǒng)的默認 I/O
操作都是緩存 I/O
。在 Linux
的緩存 I/O
機制中,操作系統(tǒng)會將 I/O
的數(shù)據(jù)緩存在文件系統(tǒng)的頁緩存(page cache)。也就是說,數(shù)據(jù)會先被拷貝到操作系統(tǒng)內(nèi)核的緩沖區(qū)中,然后才會從操作系統(tǒng)內(nèi)核的緩存區(qū)拷貝到應(yīng)用程序的地址空間中。這種做法的缺點就是,需要在應(yīng)用程序地址空間和內(nèi)核進行多次拷貝,這些拷貝動作所帶來的CPU以及內(nèi)存開銷是非常大的。
至于為什么不能直接讓磁盤控制器把數(shù)據(jù)送到應(yīng)用程序的地址空間中呢?最簡單的一個原因就是應(yīng)用程序不能直接操作底層硬件。
總的來說,IO分兩階段:
1)數(shù)據(jù)準備階段
2)內(nèi)核空間復(fù)制回用戶進程緩沖區(qū)階段。如下圖:
-
工作原理
- 應(yīng)用程序通過將多個
I/O
流(如套接字)注冊到I/O
復(fù)用機制中,以便對這些流的狀態(tài)進行監(jiān)視。 - 當應(yīng)用程序調(diào)用
I/O
復(fù)用機制的函數(shù)(如select
、poll
或epoll
)時,它會被阻塞,直到至少一個注冊的I/O
滿足指定的條件(如可讀、可寫等)。 - 當有流滿足指定條件時,
I/O
復(fù)用機制并返回,并通知應(yīng)用程序哪些流滿足條件。 - 應(yīng)用程序可以遍歷返回的流集合,檢查每個流的狀態(tài),并進行相應(yīng)的處理。通常,應(yīng)用程序會使用條件判斷語句來確定流是可讀還是可寫,并執(zhí)行相應(yīng)的讀取或?qū)懭氩僮鳌?/li>
- 應(yīng)用程序可以繼續(xù)執(zhí)行其他任務(wù)或再次調(diào)用
I/O
復(fù)用機制的函數(shù),以便繼續(xù)監(jiān)視I/O
流的狀態(tài)變化。
- 應(yīng)用程序通過將多個
-
主要特點
- 使用單個系統(tǒng)調(diào)用來監(jiān)視多個
I/O
流的狀態(tài),而不是針對每個流進行阻塞式的等待。 - 有效減少系統(tǒng)調(diào)用次數(shù)和上下文切換開銷。
- 提高應(yīng)用程序的并發(fā)性和響應(yīng)性。
- 使用單個系統(tǒng)調(diào)用來監(jiān)視多個
三、I/O 多路復(fù)用之select、poll、epoll詳解
目前支持 I/O
多路復(fù)用的系統(tǒng)調(diào)用有 select,pselect,poll,epoll
。與多進程和多線程技術(shù)相比,I/O
多路復(fù)用技術(shù)的最大優(yōu)勢是系統(tǒng)開銷小,系統(tǒng)不必創(chuàng)建進程/線程,也不必維護這些進程/線程,從而大大減小了系統(tǒng)的開銷。
I/O
多路復(fù)用就是通過一種機制,一個進程可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應(yīng)的讀寫操作。但 select,poll,epoll
本質(zhì)上都是 同步I/O
,因為他們都需要在讀寫事件就緒后自己負責(zé)進行讀寫,也就是說這個讀寫過程是阻塞的,而 異步I/O
則無需自己負責(zé)進行讀寫,異步I/O的實現(xiàn)會負責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
select
select函數(shù)
監(jiān)視的文件描述符分3類,分別是 writefds
、readfds
和 exceptfds
。
- 當用戶線程調(diào)用
select
的時候,select
將需要監(jiān)控的readfds集合
拷貝到內(nèi)核空間(假設(shè)監(jiān)控的僅僅是socket可讀
)。 - 然后遍歷自己監(jiān)控的
skb
(SocketBuffer),挨個調(diào)用skb
的poll
邏輯以便檢查該socket
是否有可讀事件,遍歷完所有的skb
后。 - 如果沒有任何一個
socket
可讀,那么select
會調(diào)用schedule_timeout
進入schedule
循環(huán),使得線程進入睡眠。如果在timeout
時間內(nèi)某個socket
上有數(shù)據(jù)可讀了,或者等待timeout
了,則調(diào)用select
的線程會被喚醒,接下來select
就是遍歷監(jiān)控的集合,挨個收集可讀事件并返回給用戶了。
相應(yīng)的偽碼如下:
int select(
int nfds, //監(jiān)控的文件描述符集里最大文件描述符+1
fd_set *readfds, //監(jiān)控讀數(shù)據(jù)到達文件描述符集合
fd_set *writefds, //監(jiān)控寫數(shù)據(jù)到達文件描述符集合
fd_set *exceptfds, //監(jiān)控異常發(fā)生到達文件描述符集合
struct timeval *timeout //定時阻塞監(jiān)控時間,3種情況:1.NULL,永遠等下去。
// 2.設(shè)置timeval,等待固定時間
// 3.設(shè)置timeval里時間均為0,檢查描述字后立即返回,輪詢
);
//----------------select服務(wù)端偽碼---------------------
//首先一個線程不斷接受客戶端連接,并把socket文件描述符放到一個list里
while(1){
connfd = accept(listenfd);
fcntl(connfd, F_SETFL, O_NONBLOCK);
fdlist.add(connfd);
}
/*
select函數(shù)還是返回剛剛提交的list,應(yīng)用程序依然列出所有的fd,只不過操作系統(tǒng)會將準備就緒的文件描述符做上標識,
用戶層將不會再有無意義的系統(tǒng)調(diào)用開銷。
*/
struct timeval timeout;
int max = 0; //用于記錄最大的fd,在輪詢中時刻更新即可
//初始化比特位
FD_ZERO(&read_fd);
while(1){
//阻塞獲取,每次需要把fd從用戶態(tài)拷貝到內(nèi)核態(tài)
nfds = select(max + 1, &read_fd, &write_fd, NULL, &timeout);
//每次需要遍歷所有fd,判斷有無讀寫事件發(fā)生
for(int i = 0; i <= max && nfds; ++i){
//只讀已就緒的文件描述符,不用過多遍歷
if(i == listenfd){
//這里處理accept事件
FD_SET(i, &read_fd); //將客戶端socket加入到集合中
}
if(FD_ISSET(i, &read_fd)){
//這里處理read事件
}
}
}
下面是 select
工作原理的動圖:
select工作圖
通過上面的select邏輯過程分析,相信大家都意識到,select存在三個問題:
- 每次調(diào)用
select
,都需要把被監(jiān)控的fds
集合從用戶態(tài)空間拷貝到內(nèi)核態(tài)空間,高并發(fā)場景下這樣的拷貝會使得消耗的資源是很大的。 - 能監(jiān)聽端口的數(shù)量有限,單個進程所能打開的最大連接數(shù)由
FD_SETSIZE
宏定義,監(jiān)聽上限就等于fds_bits
位數(shù)組中所有元素的二進制位總數(shù),其大小是32個整數(shù)的大?。ㄔ?2位的機器上,大小就是32,同理64位機器上為64),當然我們可以對宏FD_SETSIZE
進行修改,然后重新編譯內(nèi)核,但是性能可能會受到影響,一般該數(shù)和系統(tǒng)內(nèi)存關(guān)系很大,具體數(shù)目可以cat /proc/sys/fs/file-max
察看。32位機默認1024個,64位默認2048。 - 被監(jiān)控的
fds集合
中,只要有一個有數(shù)據(jù)可讀,整個socket集合
就會被遍歷一次調(diào)用sk
的poll
函數(shù)收集可讀事件:由于僅關(guān)心是否有數(shù)據(jù)可讀這樣一個事件,數(shù)據(jù)的到來是異步的,于是,只能挨個遍歷每個socket來收集可讀事件了。
poll
poll
的實現(xiàn)和 select
非常相似,只是描述 fd
集合的方式不同。針對 select
遺留的三個問題中(問題(2)是fd限制問題,問題(1)和(3)則是性能問題),poll
使用 pollfd結(jié)構(gòu)
而不是 select
的 fd_set結(jié)構(gòu)
,這就解決了 select
的問題(2)fds集合大小限制問題。
但 poll
和 select
同樣存在一個性能缺點就是包含大量文件描述符的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核的地址空間之間,而不論這些文件描述符是否就緒,它的開銷隨著文件描述符數(shù)量的增加而線性增大。
下面是 poll
的函數(shù)原型:
int poll(struct pollfd *ufds, unsigned int nfds, int timeout);
struct pollfd{
int fd; //文件描述符
short events; //監(jiān)控的事件
short revents; //監(jiān)控事件中滿足條件返回的事件
};
//-----------------poll服務(wù)端實現(xiàn)偽碼---------------------
struct pollfd fds[POLL_LEN];
unsigned int nfds = 0;
fds[0].fd = server_sockfd;
fds[0].events = POLLIN | POLLPRI;
nfds++;
while(1){
res = poll(fds, nfds, -1);
if(fds[0].revents & (POLLIN | POLLPRI)){
//執(zhí)行accept并加入fds中,nfds++
if(--res <= 0) continue;
}
//循環(huán)之后的fds
if(fds[i].revents & (POLLIN | POLLERR)){
//讀操作或處理異常等
if(--res <= 0) continue;
}
}
poll
相比于 select
的優(yōu)點:使用了 pollfd結(jié)構(gòu)
,使得 poll
支持的 fds
集合限制遠大于 select
的1024。
由于 poll
基于鏈表存儲
,無最大連接數(shù)限制,所以有如下缺點:(1)大量描述符數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核態(tài)的地址空間之間,以及個別描述符就緒觸發(fā)整體描述符集合的遍歷的低效問題。(2)poll
隨著監(jiān)控的 socket
集合的增加性能線性下降,使得 poll
也并不適合用于大并發(fā)場景。(3)若報告了 fd
后未被處理,下次 poll
時會再次報告該 fd
。
epoll(基于Linux2.4.5)
epoll
模型將主動輪詢改為被動通知,當有事件發(fā)生時,被動接收通知。所以 epoll
模型注冊套接字后,主程序可做其它事情,當事件發(fā)生時,接收到通知后再去處理??衫斫鉃?strong>event poll,epoll
會把哪個流發(fā)生哪種 I/O
事件通知我們。所以 epoll
是事件驅(qū)動(每個事件關(guān)聯(lián) fd
),此時我們對這些流的操作都是有意義的,復(fù)雜度也降到
O
(
1
)
O(1)
O(1)。
創(chuàng)建一個 epoll
的句柄,size
表明要監(jiān)聽的 fd
數(shù)目。這個參數(shù)不同于 select()
中的第一個參數(shù),給出最大監(jiān)聽的fd+1的值。需要注意的是,當創(chuàng)建好 epoll
句柄后,它就是會占用一個 fd
值,在 linux
下如果查看 /proc/進程id/fd/
,是能夠看到這個fd的,所以在使用完 epoll
后,必須調(diào)用 close()
關(guān)閉,否則可能導(dǎo)致 fd
被耗盡。
epoll
的接口非常簡單,一共就三個函數(shù):
-
epoll_create
:創(chuàng)建一個epoll
句柄 -
epoll_ctl
:向epoll
對象中添加/修改/刪除要管理的連接 -
epoll_wait
:等待其管理的連接時的I/O
事件
epoll_create 函數(shù)
int epoll_create(int size);
- 功能:生成一個
epoll
專用的文件描述符。 - 參數(shù)
size
:表明內(nèi)核監(jiān)聽的文件描述符數(shù)目。并不是限制epoll
所能監(jiān)聽的描述符最大個數(shù),只是對內(nèi)核初始分配內(nèi)部數(shù)據(jù)結(jié)構(gòu)的一個建議。自從linux 2.6.8
后,size
參數(shù)可以填大于0的任意值。 - 返回值:成功則返回
epoll
專用的文件描述符,失敗則返回 -1。
epoll_create
的源碼實現(xiàn):
asmlinkage int sys_epoll_create(int maxfds){
int error = -EINVAL, fd;
unsigned long addr;
struct inode *inode;
struct file *file;
struct eventpoll *ep;
//eventpoll接口中不可能存儲超過MAX_FDS_IN_EVENTPOLL的fd
if (maxfds > MAX_FDS_IN_EVENTPOLL)
goto eexit_1;
?
/*
* Creates all the items needed to setup an eventpoll file. That is,
* a file structure, and inode and a free file descriptor.
*/
error = ep_getfd(&fd, &inode, &file);
if (error)
goto eexit_1;
?
/*
* 調(diào)用去初始化eventpoll file. 這和"open" file operation callback一樣,因為 inside
* ep_getfd() we did what the kernel usually does before invoking
* corresponding file "open" callback.
*/
error = open_eventpoll(inode, file);
if (error)
goto eexit_2;
?
/* "private_data" 由open_eventpoll()設(shè)置 */
ep = file->private_data;
?
/* 分配頁給event double buffer */
error = ep_do_alloc_pages(ep, EP_FDS_PAGES(maxfds + 1));
if (error)
goto eexit_2;
?
//創(chuàng)建event double buffer的一個用戶空間的映射,以避免當返回events給調(diào)用者時,內(nèi)核到用戶空間的內(nèi)存復(fù)制
down_write(¤t->mm->mmap_sem);
addr = do_mmap_pgoff(file, 0, EP_MAP_SIZE(maxfds + 1), PROT_READ,
MAP_PRIVATE, 0);
up_write(¤t->mm->mmap_sem);
error = PTR_ERR((void *) addr);
if (IS_ERR((void *) addr))
goto eexit_2;
?
return fd;
?
eexit_2:
sys_close(fd);
eexit_1:
return error;
}
epoll_ctl
每次注冊新事件到 epoll
句柄中時(在 epoll_ctl
中指定 EPOLL_CTL_ADD
),會把所有 fd
拷貝進內(nèi)核,而非在 epoll_wait
時重復(fù)拷貝。epoll
保證每個 fd
在整個過程中只會拷貝一次。
//成功則返回0,失敗則返回-1
int epoll_ctl(int epfd, //epoll專用的文件描述符,epoll_create的返回值
int op, //表示動作,用三個宏來表示:1.EPOLL_CTL_ADD:注冊新的fd到epfd中
// 2.EPOLL_CTL_MOD:修改已注冊的fd的監(jiān)聽事件
// 3.EPOLL_CTL_DEL:從epfd中刪除一個fd
int fd, //需要監(jiān)聽的文件描述符
struct epoll_event *event //內(nèi)核要監(jiān)聽的事件類型
);
epoll_wait
//成功則返回要處理的事件數(shù)目,超時返回0,失敗返回-1
int epoll_wait(int epfd, //epoll專用的文件描述符,epoll_create的返回值
struct epoll_event * events, //內(nèi)核要監(jiān)聽的事件類型
int maxevents, //事件個數(shù)
int timeout); //超時時間,為-1時,函數(shù)為阻塞
epoll
不像 select/poll
每次都把當前文件流加入 fd
對應(yīng)的設(shè)備等待隊列,而只在 epoll_ctl
時把當前文件掛一遍(這一遍必不可少),并為每個 fd
指定一個回調(diào)函數(shù)。
當設(shè)備就緒,喚醒等待隊列上的等待者時,就會調(diào)用該回調(diào)函數(shù),而回調(diào)函數(shù)會把就緒 fd
加入一個就緒鏈表。epoll_wait
實際上就是在該就緒鏈表中查看有無就緒 fd
。
函數(shù)實現(xiàn)偽代碼如下:
const int MAX_EVENT_NUMBER = 10000; //最大事件數(shù)
// 設(shè)置句柄非阻塞
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
int main(){
// 創(chuàng)建套接字
int nRet=0;
int m_listenfd = socket(PF_INET, SOCK_STREAM, 0);
if(m_listenfd<0)
{
printf("fail to socket!");
return -1;
}
//
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
address.sin_port = htons(6666);
int flag = 1;
// 設(shè)置ip可重用
setsockopt(m_listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
// 綁定端口號
int ret = bind(m_listenfd, (struct sockaddr *)&address, sizeof(address));
if(ret<0)
{
printf("fail to bind!,errno :%d",errno);
return ret;
}
// 監(jiān)聽連接fd
ret = listen(m_listenfd, 200);
if(ret<0)
{
printf("fail to listen!,errno :%d",errno);
return ret;
}
// 初始化紅黑樹和事件鏈表結(jié)構(gòu)rdlist結(jié)構(gòu)
epoll_event events[MAX_EVENT_NUMBER];
// 創(chuàng)建epoll實例
int m_epollfd = epoll_create(5);
if(m_epollfd==-1)
{
printf("fail to epoll create!");
return m_epollfd;
}
// 創(chuàng)建節(jié)點結(jié)構(gòu)體將監(jiān)聽連接句柄
epoll_event event;
event.data.fd = m_listenfd;
//設(shè)置該句柄為邊緣觸發(fā)(數(shù)據(jù)沒處理完后續(xù)不會再觸發(fā)事件,水平觸發(fā)是不管數(shù)據(jù)有沒有觸發(fā)都返回事件),
event.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
// 添加監(jiān)聽連接句柄作為初始節(jié)點進入紅黑樹結(jié)構(gòu)中,該節(jié)點后續(xù)處理連接的句柄
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, m_listenfd, &event);
//進入服務(wù)器循環(huán)
while(1)
{
int number = epoll_wait(m_epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
printf( "epoll failure");
break;
}
for (int i = 0; i < number; i++)
{
int sockfd = events[i].data.fd;
// 屬于處理新到的客戶連接
if (sockfd == m_listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength);
if (connfd < 0)
{
printf("errno is:%d accept error", errno);
return false;
}
epoll_event event;
event.data.fd = connfd;
//設(shè)置該句柄為邊緣觸發(fā)(數(shù)據(jù)沒處理完后續(xù)不會再觸發(fā)事件,水平觸發(fā)是不管數(shù)據(jù)有沒有觸發(fā)都返回事件),
event.events = EPOLLIN | EPOLLRDHUP;
// 添加監(jiān)聽連接句柄作為初始節(jié)點進入紅黑樹結(jié)構(gòu)中,該節(jié)點后續(xù)處理連接的句柄
epoll_ctl(m_epollfd, EPOLL_CTL_ADD, connfd, &event);
setnonblocking(connfd);
}
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服務(wù)器端關(guān)閉連接,
epoll_ctl(m_epollfd, EPOLL_CTL_DEL, sockfd, 0);
close(sockfd);
}
//處理客戶連接上接收到的數(shù)據(jù)
else if (events[i].events & EPOLLIN)
{
char buf[1024]={0};
read(sockfd,buf,1024);
printf("from client :%s");
// 將事件設(shè)置為寫事件返回數(shù)據(jù)給客戶端
events[i].data.fd = sockfd;
events[i].events = EPOLLOUT | EPOLLET | EPOLLONESHOT | EPOLLRDHUP;
epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
}
else if (events[i].events & EPOLLOUT)
{
std::string response = "server response \n";
write(sockfd,response.c_str(),response.length());
// 將事件設(shè)置為讀事件,繼續(xù)監(jiān)聽客戶端
events[i].data.fd = sockfd;
events[i].events = EPOLLIN | EPOLLRDHUP;
epoll_ctl(m_epollfd, EPOLL_CTL_MOD, sockfd, &events[i]);
}
//else if 可以加管道,unix套接字等等數(shù)據(jù)
}
}
}
觸發(fā)模式
EPOLL LT
和 EPOLL EF
兩種:
-
LT
,水平觸發(fā)(默認),只要該fd
還有數(shù)據(jù)可讀,每次epoll_wait
都會返回它的事件,提醒用戶程序去處理。 -
ET
,邊緣觸發(fā)(高速),無論fd
中是否還有數(shù)據(jù)都只提示一次,直到下次有數(shù)據(jù)流入前都不會提示。所以ET
模式下,read
一個fd
時,一定要把它的buffer
讀完,即讀到read
返回值小于請求值或遇到EAGAIN
錯誤(稍后重試)。
epoll
使用 事件
就緒通知方式,通過 epoll_ctl
注冊 fd
,一旦該 fd
就緒,內(nèi)核就會采用類似回調(diào)機制激活該 fd
,epoll_wait
便可收到通知。
ET 的意義
若用 LT
,系統(tǒng)中一旦有大量無需讀寫的就緒文件描述符,它們每次調(diào)用 epoll_wait
都會返回,這大大降低處理程序檢索自己關(guān)心的就緒文件描述符的效率。
而采用 ET
,當被監(jiān)控的文件描述符上有可讀寫事件發(fā)生時,epoll_wait
會通知處理程序去讀寫。若這次沒有把數(shù)據(jù)全部讀寫完(如讀寫緩沖區(qū)太小),則下次調(diào)用 epoll_wait
時,它不會通知你,即只會通知你一次,直到該文件描述符上出現(xiàn)第二次可讀寫事件才通知你。這比水平觸發(fā)效率高,系統(tǒng)不會充斥大量你不關(guān)心的就緒文件描述符。
優(yōu)點
- 無最大并發(fā)連接的限制,能打開的
fd
上限遠大于1024(1G內(nèi)存能監(jiān)聽約10萬個端口)。 - 效率提升,不是輪詢,不會隨
fd
數(shù)目增加而效率下降。只有活躍可用的fd
才會調(diào)用callback
函數(shù),即epoll
最大優(yōu)點在于它只關(guān)心“活躍”連接,而跟連接總數(shù)無關(guān)。 - 內(nèi)存拷貝,利用
mmap()
文件映射內(nèi)存加速與內(nèi)核空間的消息傳遞;即epoll使用mmap減少復(fù)制開銷。 -
epoll
通過內(nèi)核和用戶空間共享一塊內(nèi)存而實現(xiàn)。
缺點
- 在連接數(shù)少且都十分活躍情況下,
select
和poll
性能都可能比epoll
好,因為epoll
通知機制需要很多函數(shù)回調(diào)。 -
epoll
是Linux
所特有的,而select
是POSIX
所規(guī)定,一般os
均有實現(xiàn)。
四、總結(jié)
select,poll,epoll
都是 I/O
多路復(fù)用機制,即能監(jiān)視多個 fd
,一旦某 fd
就緒(讀或?qū)懢途w),能夠通知程序進行相應(yīng)讀寫操作。 但 select,poll,epoll
本質(zhì)都是同步I/O,因為他們都需在讀寫事件就緒后,自己負責(zé)進行讀寫,即該讀寫過程是阻塞的,而異步 I/O
則無需自己負責(zé)進行讀寫,異步 I/O
實現(xiàn)會負責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
select,poll
需自己主動不斷輪詢所有 fd
集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而 epoll
其實也需調(diào)用 epoll_wait
不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但它是設(shè)備就緒時,調(diào)用回調(diào)函數(shù),把就緒 fd
放入就緒鏈表,并喚醒在 epoll_wait
中進入睡眠的進程。雖然都要睡眠和交替,但 select
和 poll
在“醒著”時要遍歷整個 fd
集合,而 epoll
在“醒著”的時候只需判斷就緒鏈表是否為空,節(jié)省大量CPU時間,這就是回調(diào)機制帶來的性能提升。
select,poll
每次調(diào)用都要把 fd
集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,且要把當前文件往設(shè)備等待隊列中掛一次,而 epoll
只要一次拷貝,且把當前文件往等待隊列上掛也只掛一次(在 epoll_wait
開始,注意這里的等待隊列并不是設(shè)備等待隊列,只是一個 epoll
內(nèi)部定義的等待隊列),這也能節(jié)省不少開銷。文章來源:http://www.zghlxwxcb.cn/news/detail-695885.html
參考:文章來源地址http://www.zghlxwxcb.cn/news/detail-695885.html
- 一文搞懂select、poll和epoll區(qū)別
- IO多路復(fù)用——深入淺出理解select、poll、epoll的實現(xiàn)
到了這里,關(guān)于02-Linux-IO多路復(fù)用之select、poll和epoll詳解的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!