詳解C/C++中的select、poll和epoll
1. select
1.1 概述
select函數(shù)是UNIX和Linux中常用的多路復用IO機制,它允許程序同時監(jiān)控多個文件描述符(可以是套接字socket,也可以是普通文件)的讀、寫和異常事件。它使進程能夠告訴內(nèi)核等待多個事件中的任何一個發(fā)生,并只在有一個或多個事件發(fā)生或經(jīng)歷一段指定時間后才喚醒它。這樣做的優(yōu)點是,不需要應用程序自行檢測和處理每個客戶端連接的狀態(tài),可以節(jié)省大量的系統(tǒng)資源,提高應用程序的效率。
1.2 函數(shù)詳解
首先,我們需要包含一些必要的頭文件以使用select函數(shù)和相關的數(shù)據(jù)結構:
#include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h>
接下來是select函數(shù)的原型:
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout);
select函數(shù)接收以下參數(shù):
- maxfdpl: 是需要檢查的所有文件描述符中的最大值加1,因為這會告訴內(nèi)核檢查的文件描述符的數(shù)量。
- readset: 是一個文件描述符集合,用于檢查是否有可讀的數(shù)據(jù)。這是輸入-輸出參數(shù),select會改變其值。
- writeset: 是一個文件描述符集合,用于檢查是否可以寫入數(shù)據(jù)。這也是輸入-輸出參數(shù),select會改變其值。
- exceptset: 是一個文件描述符集合,用于檢查是否有異常情況發(fā)生(如帶外數(shù)據(jù)到達)。這同樣是輸入-輸出參數(shù),select會改變其值。
- timeout: 是一個timeval結構,用于指定select的阻塞等待時間。如果設定時間為NULL,select將會一直等待;如果設定時間為特定值,select將會等待指定時間;如果設定時間為0,select將立即返回,這稱為輪詢。
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
關于文件描述符的操作,以下四個函數(shù)是用于處理fd_set類型數(shù)據(jù)的:
- FD_ZERO(fd_set *set): 這個函數(shù)用于清除一個fd_set的所有位,即初始化一個fd_set。
- FD_SET(int fd, fd_set *set): 這個函數(shù)用于將特定的文件描述符fd加入到fd_set中。
- FD_CLR(int fd, fd_set *set): 這個函數(shù)用于將特定的文件描述符fd從fd_set中移除。
- FD_ISSET(int fd, fd_set *set): 這個函數(shù)用于檢查特定的文件描述符fd是否在fd_set中,如果在,函數(shù)返回非零值,否則返回0。
使用這些函數(shù),我們可以方便地對文件描述符集合進行操作,以便于使用select函數(shù)進行IO操作的復用。
1.3 例子
下面給出一個使用select創(chuàng)建一個可以同時處理多個客戶端的連接的服務器:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/time.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <arpa/inet.h> #define MAX_CLIENTS 5 #define BUFFER_SIZE 1024 int main(int argc , char *argv[]) { int listener, newsockfd, portno, clilen; char buffer[BUFFER_SIZE]; fd_set master; // 主文件描述符列表 fd_set read_fds; // 用select()的臨時文件描述符列表 struct sockaddr_in serv_addr, cli_addr; int FD_MAX; // 最大文件描述符號 listener = socket(AF_INET, SOCK_STREAM, 0); memset(&serv_addr, '0', sizeof(serv_addr)); portno = 5000; serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(portno); bind(listener, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); listen(listener, 10); FD_ZERO(&master); // 清除主監(jiān)聽輸入端口 FD_ZERO(&read_fds); // 清除臨時集合 // 添加監(jiān)聽器到主集合 FD_SET(listener, &master); // 追蹤最大的文件描述符 FD_MAX = listener; while(1) { read_fds = master; // 拷貝它 if(select(FD_MAX+1, &read_fds, NULL, NULL, NULL) == -1) { perror("Select error!"); exit(1); } for(int i = 0; i <= FD_MAX; i++) { if(FD_ISSET(i, &read_fds)) { if(i == listener) { // handle new connections clilen = sizeof(cli_addr); newsockfd = accept(listener, (struct sockaddr*)&cli_addr, &clilen); if(newsockfd == -1) { perror("accept error"); } else { FD_SET(newsockfd, &master); // 添加到主集合 if(newsockfd > FD_MAX) { FD_MAX = newsockfd; // 持續(xù)追蹤最大的文件描述符 } printf("selectserver: new connection from %s on socket %d\n",inet_ntoa(cli_addr.sin_addr), newsockfd); } } else { // 處理來自客戶端的數(shù)據(jù) if((recv(i, buffer, sizeof(buffer), 0)) <= 0) { // got error or connection closed by client close(i); FD_CLR(i, &master); // 從主集合中移除 } else { // 我們得到了一些數(shù)據(jù)! for(int j = 0; j <= FD_MAX; j++) { // 發(fā)送數(shù)據(jù)到所有連接 if(FD_ISSET(j, &master)) { if(j != listener && j != i) { send(j, buffer, strlen(buffer), 0); } } } } } } } } return 0; }
上面這個例子會創(chuàng)建一個在端口5000監(jiān)聽的服務器。使用select,我們可以在單個線程中同時處理多個客戶端的連接。這就是使用select的優(yōu)點:我們可以同時處理多個連接,而不需要為每個連接創(chuàng)建一個單獨的線程或進程。
1.4 總結
- 優(yōu)點:
- 跨平臺性:select是遵循POSIX標準的,所以在多種平臺上都可以使用,具有較好的跨平臺性。
- 精確的超時等待時間:select可以設置超時時間,對時間的精確度可以達到微秒級別。
- 缺點:
- 文件描述符上限:select所能監(jiān)聽的文件描述符的數(shù)量是有限的,取決于_FD_SETSIZE的值,默認為1024。如果需要處理的并發(fā)連接數(shù)過多,select可能無法滿足需求。
- 性能下降:select在內(nèi)核中通過輪詢所有文件描述符的方式來檢查其狀態(tài),當監(jiān)控的文件描述符數(shù)量增多時,性能會下降。
- 使用復雜:select在返回時,只會告訴用戶哪些描述符集合是就緒的,但并不會直接告訴用戶哪一個具體的文件描述符就緒,用戶需要自己去遍歷這些集合,操作比較復雜。
- 多次數(shù)據(jù)拷貝:每次調(diào)用select都需要將文件描述符集合從用戶空間拷貝到內(nèi)核空間,這增加了額外的開銷。
- 重復操作:每次select返回后,所有未就緒的文件描述符都會被移除,因此每次使用都需要重新向集合中添加描述符。
- 注意事項:
- 一般情況下,我們無法通過改變進程打開的文件描述符個數(shù)來改變select能夠監(jiān)聽的文件描述符個數(shù),這個數(shù)量受限于_FD_SETSIZE。
- 當套接字上發(fā)生錯誤時,select會將其標記為既可讀又可寫。
- 接收和發(fā)送低水位標記的目的在于,讓應用程序可以控制在多少數(shù)據(jù)可讀或有多少空間可寫時喚醒select。例如,當有64字節(jié)的數(shù)據(jù)可讀時,select才會被喚醒。這樣可以避免頻繁打斷應用程序來處理IO操作,提高程序的效率。
2. poll
1.1 概述
poll函數(shù)提供了類似于select的功能,允許進程向內(nèi)核指示等待多個事件中的任何一個發(fā)生,它只在有一個或多個事件發(fā)生或經(jīng)歷一段指定時間后才喚醒進程。不過,與select相比,poll在處理流設備時能夠提供更豐富的信息。它能有效地管理多個輸入/輸出源,并且在特定事件發(fā)生時進行響應,這使得對多任務并發(fā)處理的支持更為高效。
1.2 函數(shù)詳解
poll函數(shù)的聲明如下:
#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout);
這里是poll函數(shù)的參數(shù)列表:
- fds:一個指向一個pollfd結構體數(shù)組的指針。這個數(shù)組中的每一個成員都代表一個特定的文件描述符以及對它感興趣的事件和發(fā)生的事件。
- nfds:fds數(shù)組的成員數(shù)量。
- timeout:調(diào)用應該等待的最大毫秒數(shù),以阻塞的方式等待文件描述符變?yōu)榫途w。如果這個值是-1,poll將會無限期的阻塞。如果這個值是0,poll將立即返回。
pollfd
結構體的定義如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 要監(jiān)控的事件 */ short revents; /* 實際發(fā)生的事件 */ };
- fd:需要被poll監(jiān)控的文件描述符。
- events:一組位標志,表示對應的文件描述符上我們感興趣的事件。比如:POLLIN(有數(shù)據(jù)可讀),POLLOUT(寫數(shù)據(jù)不會阻塞),POLLERR(錯誤事件),POLLHUP(掛起事件)等。
- revents:一組位標志,表示在對應的文件描述符上實際發(fā)生了哪些事件。這是poll調(diào)用返回后由內(nèi)核填充的。
當調(diào)用poll函數(shù)時,內(nèi)核會檢查每個pollfd
結構體中列出的文件描述符,看看是否有任何指定的事件發(fā)生。如果有,內(nèi)核將會在revents
字段中設置相應的位,以指示哪些事件已經(jīng)發(fā)生。然后poll函數(shù)返回,應用程序可以檢查每個pollfd
結構體的revents
字段來確定每個文件描述符上發(fā)生了哪些事件。
1.3 例子
在下面這個示例中,我們會創(chuàng)建兩個管道(pipe),然后使用 poll 來等待這兩個管道中的任何一個變得可讀。
#include <stdio.h> #include <unistd.h> #include <poll.h> #define TIMEOUT 5 int main (void) { struct pollfd fds[2]; int ret; // 創(chuàng)建兩個管道 int pipefd[2]; pipe(pipefd); // watch stdin (fd 0) for input fds[0].fd = 0; fds[0].events = POLLIN; // watch pipe for input fds[1].fd = pipefd[0]; fds[1].events = POLLIN; ret = poll(fds, 2, TIMEOUT * 1000); if (ret == -1) { perror ("poll"); return 1; } if (!ret) { printf ("%d seconds elapsed.\n", TIMEOUT); return 0; } if (fds[0].revents & POLLIN) printf ("stdin is readable\n"); if (fds[1].revents & POLLIN) printf ("pipe is readable\n"); return 0; }
在上述示例中,我們使用 poll 來同時監(jiān)聽標準輸入和管道的輸入。如果在5秒鐘內(nèi),標準輸入或管道有任何數(shù)據(jù)可讀,那么 poll
就會返回,并通過檢查 revents
標志來通知我們哪一個文件描述符已經(jīng)就緒。如果在5秒內(nèi)沒有任何數(shù)據(jù)可讀,那么 poll 也會返回,此時我們可以打印一個超時信息。這就是使用 poll 的優(yōu)點:我們可以同時處理多個輸入源,而不需要為每個輸入源創(chuàng)建一個單獨的線程或進程。
1.4 總結
- 1.優(yōu)點:
- 無最大文件描述符限制:不像select有FD_SETSIZE限制,poll沒有這個限制,所以可以處理更多的文件描述符。
- 調(diào)用方式簡單:poll使用一個pollfd的數(shù)組作為參數(shù),而不是像select一樣使用一組文件描述符集合。這使得設置和處理poll的調(diào)用更加簡單。
- 無需重新設置文件描述符:每次調(diào)用select后,由于select函數(shù)會改變文件描述符集合,所以需要重新設置。而在poll中,不需要這么做,因為poll并不會更改pollfd的數(shù)組。
- 提供了更多的事件類型:除了常規(guī)的讀、寫和異常條件,poll還支持帶外數(shù)據(jù)、優(yōu)先數(shù)據(jù)等更多的事件類型。
- 2.缺點:
- 仍需要遍歷所有文件描述符:雖然poll沒有最大文件描述符限制,但返回后仍需要遍歷整個文件描述符列表來找出哪些文件描述符已就緒,當文件描述符數(shù)量較大時,這也可能會影響性能。
- 不支持文件描述符的優(yōu)先級:poll函數(shù)處理所有的文件描述符是平等的,不像某些其他的I/O多路復用技術可以設置優(yōu)先級。
- 缺乏廣泛的跨平臺支持:雖然poll在許多系統(tǒng)上都可用,但它的實現(xiàn)并不總是完全可靠,特別是在一些老的系統(tǒng)上。
- 3.注意事項:
- 移除文件描述符:如果你不再關心某個文件描述符,可以將對應的pollfd結構中的fd設置為-1,這樣poll將不再監(jiān)視這個文件描述符,并且在返回時將會將revents設置為0。"事件-結果"模式:與select使用"值-結果"參數(shù)傳遞方式不同,pollfd包含了要監(jiān)視的events和實際發(fā)生的revents,這樣應用程序可以在同一地方查看它關心的事件,并確定哪些已經(jīng)發(fā)生。
- 處理大量文件描述符:雖然poll沒有文件描述符數(shù)量的限制,但是如果監(jiān)視的文件描述符過多,可能會導致性能下降,因為無論是否有事件發(fā)生,poll返回后都需要遍歷整個文件描述符列表。
- 在poll返回后處理結果:像select一樣,當poll返回后,你需要遍歷所有的pollfd來找出哪些文件描述符已經(jīng)就緒。這是因為poll只告訴你有多少文件描述符就緒,但并不告訴你具體是哪些。
3. epoll
1.1 概述
在許多并發(fā)連接中只有少數(shù)活路的場景下,epoll是Linux下I/O多路復用接口select/poll的增強版本,能有效提升系統(tǒng)CPU的使用率。區(qū)別于select和poll每次等待事件之前都需要重新設置監(jiān)視的文件描述符集,epoll能復用文件描述符集來傳遞結果,減少了重復的準備工作。
獲取事件時,epoll無需像select和poll一樣遍歷整個被偵聽的描述符集,只需遍歷被內(nèi)核IO事件異步喚醒并加入到就緒隊列的描述符集即可。這使得處理大量文件描述符時,只有實際產(chǎn)生活動的文件描述符才需要被處理,從而大大提升了效率。
當前,在大規(guī)模并發(fā)網(wǎng)絡程序中,epoll已經(jīng)成為首選模型。除了提供select/poll的IO事件電平觸發(fā)(Level Triggered)模式,epoll還額外提供了邊沿觸發(fā)(Edge Triggered)模式,這使得用戶空間程序可以緩存IO狀態(tài),減少epoll_wait/epoll_pwait的調(diào)用,從而進一步提升了程序的運行效率。
1.2 函數(shù)詳解
epoll
是 Linux 中的 I/O 多路復用接口,常用的API有 epoll_create
、epoll_ctl
和 epoll_wait
。以下是這些API的詳細介紹:
1.epoll_create
:創(chuàng)建一個epoll的句柄。
#include <sys/epoll.h> int epoll_create(int size);
參數(shù):size
參數(shù)現(xiàn)在并不起作用,但是必須大于0。
返回值:如果成功,返回一個非負的文件描述符。失敗時,返回-1。
2.epoll_ctl
:控制某個epoll文件描述符上的事件,可以注冊、修改、刪除。
#include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數(shù):
epfd
:epoll_create函數(shù)返回的文件描述符。op
:要進行的操作。EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(刪除)。fd
:關聯(lián)的文件描述符。event
:指向epoll_event的指針。
??????struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; ?
events
成員是一組標記,系統(tǒng)所感興趣的事件和可能發(fā)生的返回事件,如EPOLLIN(可讀)、EPOLLOUT(可寫)、EPOLLET(設置為邊緣觸發(fā)模式)、EPOLLONESHOT(一次性的)、EPOLLRDHUP(對端斷開連接或者關閉寫操作的一種表示)等。data
成員用于存儲用戶數(shù)據(jù),可以是一個指針,也可以是一個整型的標識符。
返回值:成功時,返回0。失敗時,返回-1。
3.epoll_wait
:等待epoll上的I/O事件。
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
參數(shù):
epfd
:epoll_create函數(shù)返回的文件描述符。events
:用來從內(nèi)核得到事件的集合。maxevents
:告訴內(nèi)核這個events的大小,不能大于創(chuàng)建epoll_create時的size。timeout
:等待I/O事件發(fā)生的超時值(單位:毫秒)。0表示立即返回,-1表示一直等待。
返回值:成功時,返回需要處理的事件數(shù)目。如返回0表示已經(jīng)超時。失敗時,返回-1。
1.3 例子
以下是一個使用epoll的示例,其中包含了epoll_create
、epoll_ctl
和epoll_wait
等函數(shù)的使用,主要展示了epoll的IO多路復用和邊緣觸發(fā)(ET)模式特性:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/epoll.h> #include <fcntl.h> #define MAX_EVENTS 10 void set_nonblock(int fd) { int flags = fcntl(fd, F_GETFL, 0); flags |= O_NONBLOCK; fcntl(fd, F_SETFL, flags); } int main() { struct epoll_event ev, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; /* Code to set up listening socket, 'listen_sock', (socket(), bind(), listen()) omitted */ epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } ev.events = EPOLLIN | EPOLLET; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } set_nonblock(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } } close(epollfd); return 0; }
在上述代碼中,首先創(chuàng)建了一個epoll對象,然后將監(jiān)聽套接字添加到epoll事件集合中,并注冊了EPOLLIN
和EPOLLET
事件,EPOLLIN
代表對應的文件描述符可讀,EPOLLET
代表以邊緣觸發(fā)模式對事件進行處理。
在無限循環(huán)中,調(diào)用epoll_wait
來等待I/O事件的發(fā)生,當新的連接進來時,使用accept
接受新的連接,然后將新的連接設為非阻塞模式,并添加到epoll事件集合中。當連接上有數(shù)據(jù)可讀時,調(diào)用do_use_fd
函數(shù)進行處理。
此代碼展示了epoll可以動態(tài)地添加、修改和刪除關注的文件描述符,也展示了邊緣觸發(fā)模式的使用,這些都是epoll的主要特點。
1.4 總結
1.優(yōu)點:
- 沒有最大并發(fā)連接的限制,能打開的FD的上限遠大于1024(一般來說是系統(tǒng)的最大文件句柄數(shù))。
- 效率提升,不會隨著FD數(shù)目的增加而線性下降。不管有多少FD,都只有活躍的FD會調(diào)用callback(在epoll_wait中返回),所以在活動連接較少的情況下,使用epoll不僅在存儲空間上有優(yōu)勢,且效率上也有優(yōu)勢。
- 提供了更多的觸發(fā)模式選擇,除了水平觸發(fā)模式外,還支持邊緣觸發(fā)模式,使得用戶空間程序有可能緩存IO狀態(tài),減少epoll_wait的調(diào)用次數(shù),提高應用程序效率。
2.缺點:
- 只能在Linux系統(tǒng)上運行,不具備跨平臺性。
- 對于文件描述符較少的情況,使用select/poll的性能并不差。
- 使用邊緣觸發(fā)模式時,需要更細心地處理各種情況,以防止消息遺漏。
3.注意事項:
- 在使用epoll的ET(邊緣觸發(fā))模式時,需要在讀、寫操作時都檢查EAGAIN錯誤,確保所有可用數(shù)據(jù)或所有可寫的空間都被使用。
- 在使用ET模式時,如果一次讀取的數(shù)據(jù)沒有達到socket的接收緩沖區(qū)的大小,那么下一次epoll_wait可能不會返回該socket的讀事件。
4. 三者的區(qū)別
選擇方式 | select | poll | epoll |
---|---|---|---|
操作方式 | 遍歷 | 遍歷 | 回調(diào) |
底層實現(xiàn) | 數(shù)組 | 鏈表 | 紅黑樹 |
IO效率 | 每次調(diào)用都進行線性遍歷,時間復雜度為O(n) | 每次調(diào)用都進行線性遍歷,時間復雜度為O(n) | 事件通知方式,每當fd就緒,系統(tǒng)注冊的回調(diào)函數(shù)就會被調(diào)用,將就緒fd放到readyList里面,時間復雜度O(1) |
最大連接數(shù) | 1024(x86)或2048(x64) | 無上限 | 無上限 |
fd拷貝 | 每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 每次調(diào)用poll,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài) | 調(diào)用epoll_ctl時拷貝進內(nèi)核并保存,之后每次epoll_wait不拷貝 |
到此這篇關于C/C++中的select、poll和epoll的文章就介紹到這了,更多相關C++ select、poll和epoll內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
三種獲取網(wǎng)頁源碼的方法(使用MFC/Socket實現(xiàn))
Windows下比較簡單的獲取網(wǎng)頁源碼的方法:使用MFC、使用MFC、Socket實現(xiàn)2013-12-12