1. 函數(shù)說明
1.1 select
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_ZERO(fd_set *set); //清空集合
void FD_CLR(int fd, fd_set *set); //將一個給定的文件描述符從集合中刪除
void FD_SET(int fd, fd_set *set); //將一個給定的文件描述符加入集合之中
int FD_ISSET(int fd, fd_set *set); //檢查集合中指定的文件描述符是否可以讀寫
struct timeval {
long tv_sec; /* seconds 秒*/
long tv_usec; /* microseconds 微妙*/
};
該函數(shù)準(zhǔn)許進(jìn)程指示內(nèi)核等待多個事件中的任何一個發(fā)生,并在有一個或多個事件發(fā)生或經(jīng)歷一段指定的時間后才被喚醒。
- 第一個參數(shù) nfds 指定待測試的描述符個數(shù),其值為最大的文件描述符加1,描述符0、1、2...nfds-1均將被測試(因為文件描述符是從0開始的)。
- 中間的三個參數(shù) readset、writeset、exceptset 指定我們要讓內(nèi)核測試讀、寫、異常條件的描述符。如果對某一個的條件不感興趣,就可以把它設(shè)為 NULL。struct fd_set 可以理解為一個集合(實際上是一long類型的數(shù)組),這個集合中存放的是文件描述符,可通過上面那四個函數(shù)進(jìn)行設(shè)置。
- timeout 告知內(nèi)核等待所指定描述符中的任何一個就緒最多等待時長。沒有等到就緒就會超時。該參數(shù)控制三種可能:
① 傳 NULL 時,一直阻塞直到有感興趣的描述符上有IO事件就緒。
② timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)都是0,檢查描述符后立即返回,不阻塞。
③ timeval結(jié)構(gòu)中指定的秒數(shù)和微秒數(shù)指定了時長,在指定時長超時之前有 IO事件就緒返回,或者超時返回。
1.2 poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; /* file descriptor 文件描述符 */
short events; /* requested events 等待的事件 */
short revents; /* returned events 實際發(fā)生了的事件 */
};
每一個 struct pollfd 結(jié)構(gòu)體指定了一個被監(jiān)視的文件描述符,可以傳遞多個結(jié)構(gòu)體,讓 poll() 監(jiān)視多個文件描述符。每個結(jié)構(gòu)體的 events 域是監(jiān)視該文件描述符的事件掩碼,由用戶來設(shè)置這個域。revents 域是文件描述符的操作結(jié)果事件掩碼,內(nèi)核在調(diào)用返回時設(shè)置這個域。events 域中請求的任何事件都可能在 revents 域中返回。
合法的事件如下:
常量 | 說明 |
---|---|
POLLIN | 普通或優(yōu)先級帶數(shù)據(jù)可讀 |
POLLRDNORM | 普通數(shù)據(jù)可讀 |
POLLRDBAND | 優(yōu)先級帶數(shù)據(jù)可讀 |
POLLPRI | 高優(yōu)先級數(shù)據(jù)可讀 |
POLLOUT | 普通數(shù)據(jù)可寫 |
POLLWRNORM | 普通數(shù)據(jù)可寫 |
POLLWRBAND | 優(yōu)先級帶數(shù)據(jù)可寫 |
POLLERR | 發(fā)生錯誤 |
POLLHUP | 發(fā)生掛起 |
POLLNVAL | 描述字不是一個打開的文件 |
POLLIN | POLLPRI 等價于 select() 的讀事件
POLLOUT 等價于 select() 的寫事件
① timeout 參數(shù)指定等待的毫秒數(shù),在超時之前有 IO 事件就緒或者超時,poll 都會返回
② timeout 為負(fù)數(shù)值,表示一直阻塞直到一個指定事件發(fā)生,poll 才返回。
③ timeout 為0,poll 調(diào)用立即返回并列出準(zhǔn)備好I/O的文件描述符,但并不等待其它的事件。
返回值和錯誤代碼
成功時:poll() 返回結(jié)構(gòu)體中 revents 域不為0的文件描述符個數(shù)。
超時后:如果在超時前沒有任何事件發(fā)生,poll()返回0。
失敗時:poll() 返回-1,并設(shè)置errno為下列值之一
EFAULT 指針指向的地址超出進(jìn)程的地址空間。
EINTR 請求的事件之前產(chǎn)生一個信號,調(diào)用可以重新發(fā)起。
EINVAL 參數(shù)超出 PLIMIT_NOFILE 值。
ENOMEM 可用內(nèi)存不足,無法完成請求。
1.3 epoll
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
(1)創(chuàng)建 epoll 實例
現(xiàn)在一般使用 epoll_create1(EPOLL_CLOEXEC),原因:
- 在linux 內(nèi)核版本大于2.6.8 后,epoll_create(int size) 這個 size 參數(shù)就被棄用了,但是傳入的值必須大于0。最初實現(xiàn)版本時, size參數(shù)的作用告訴內(nèi)核需要使用多少個文件描述符。內(nèi)核會使用 size 的大小去申請對應(yīng)的內(nèi)存。現(xiàn)在這個size參數(shù)不再使用了,內(nèi)核會動態(tài)的申請需要的內(nèi)存。
- 使用 epoll_create1() 的優(yōu)點是它允許你指定標(biāo)志,指定 EPOLL_CLOEXEC 標(biāo)記在執(zhí)行另一個進(jìn)程時文件描述符會自動關(guān)閉。
(2)epoll 的事件注冊
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
先注冊要監(jiān)聽的事件類型。
第一個參數(shù)是 epoll_create1 的返回值
第二個參數(shù)表示動作,用三個宏來表示:
EPOLL_CTL_ADD:注冊新的 fd 到 epfd 中
EPOLL_CTL_MOD:修改已經(jīng)注冊的 fd 的監(jiān)聽事件
EPOLL_CTL_DEL :從 epfd 中刪除一個 fd
第三個參數(shù)是需要監(jiān)聽的 fd
第四個參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事,struct epoll_event 結(jié)構(gòu)見上面代碼
events 可以是以下幾個宏的集合:
常量 | 說明 |
---|---|
EPOLLIN | 表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉) |
EPOLLOUT | 表示對應(yīng)的文件描述符可以寫 |
EPOLLPRI | 表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來) |
EPOLLERR | 表示對應(yīng)的文件描述符發(fā)生錯誤 |
EPOLLHUP | 表示對應(yīng)的文件描述符被掛斷 |
EPOLLET | 將 EPOLL 設(shè)為邊緣觸發(fā) (Edge Triggered) 模式,這是相對于水平觸發(fā) (Level Triggered) 來說的,LT是缺省的工作方式 |
EPOLLONESHOT | 只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后,如果還需要繼續(xù)監(jiān)聽這個socket的話,需要再次把這個 socket 加入到 EPOLL 隊列里 |
LT (Level Triggered) 水平觸發(fā):默認(rèn)的模式。內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的 fd 進(jìn)行 IO 操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你。
ET (Edge Triggered) 邊緣觸發(fā):“高速”模式。內(nèi)核只會提示一次,直到下次再有數(shù)據(jù)流入之前都不會再提示了,無論 fd 中是否還有數(shù)據(jù)可讀。在ET模式下,read一個fd的時候一定要把數(shù)據(jù)讀完。
(3)等待事件的產(chǎn)生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待事件的產(chǎn)生,類似于 select() 調(diào)用。
參數(shù) events 用來從內(nèi)核得到事件的集合
參數(shù) maxevents 告之內(nèi)核這個 events 有多大
參數(shù) timeout 是超時時間(毫秒,0會立即返回,-1一直阻塞直到有IO事件就緒)
該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時。
2. 三者之間的關(guān)聯(lián)和區(qū)別
關(guān)聯(lián)
select,poll,epoll 都是 I/O 多路復(fù)用的機制。I/O多路復(fù)用就通過一種機制,可以監(jiān)視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作。
select,poll,epoll 本質(zhì)上都是同步 I/O,因為他們都需要在讀寫事件就緒后自己負(fù)責(zé)進(jìn)行讀寫,也就是說這個讀寫過程是阻塞的。而異步 I/O 則無需自己負(fù)責(zé)進(jìn)行讀寫,異步 I/O 的實現(xiàn)會負(fù)責(zé)把數(shù)據(jù)從內(nèi)核拷貝到用戶空間。
區(qū)別
關(guān)于最大連接數(shù)先理清楚2個概念:
⑴ 一個進(jìn)程能打開的最大文件描述符,使用 ulimit -n 或者 cat /proc/進(jìn)程號/limits |grep "Max open file"
? 使用 ulimit -n xxx 修改(只針對當(dāng)前session有效),
? 通過 setrlimit 系統(tǒng)調(diào)用修改(只對當(dāng)前進(jìn)程有效),
? 修改 /etc/security/limits.conf 在該文件中添加以下兩行:
* soft nofile 100000
* hard nofile 100000
這個值是可以修改的,但是最大不要超過系統(tǒng)所能打開的最大數(shù),超過了也沒有什么意義。
⑵ 一個系統(tǒng)所能打開的最大數(shù)也是有限的,跟內(nèi)存大小有關(guān),可以通過cat /proc/sys/fs/file-max 查看。
- 注意:對于服務(wù)器程序來說并不是“理論”最大只能打開65535個 socket 連接。65535 只是linux系統(tǒng)的最大可用socket端口,比如在一臺服務(wù)器(這里指主機而非服務(wù)程序)上“理論”上最多只能發(fā)起65535個客戶端連接(實際要小于)這是對的,因為每發(fā)起一個連接都需要一個端口。服務(wù)器程序只需監(jiān)聽幾個端口就夠了,它的上限是系統(tǒng)所能打開的最大文件描述符的數(shù)量。不要搞混了這兩個概念。
> 支持一個進(jìn)程所能打開的最大連接數(shù)
- select 最大連接數(shù)有一定限制的,由 FD_SETSIZE 值決定的,默認(rèn)值是2048。可以對進(jìn)行修改,需要重新編譯內(nèi)核,但是性能可能會受到影響。
- poll 最大連接數(shù)上限是系統(tǒng)能最大可以打開文件的數(shù)目,一般來說這個數(shù)目受限于系統(tǒng)內(nèi)存,1GB內(nèi)存的機器上大約是10萬左右,具體數(shù)目可以 cat /proc/sys/fs/file-max。
- epoll 與 poll 一樣,最大連接數(shù)上限是系統(tǒng)能最大可以打開文件的數(shù)目。
> 連接數(shù)劇增后帶來的 IO 效率問題
- select 每次調(diào)用都對所有的連接進(jìn)行線性遍歷,所以隨著連接數(shù)的增加會造成遍歷速度的線性下降的性能問題。
- poll 與 select 有相同的問題
- epoll 是事件驅(qū)動的,內(nèi)核中的實現(xiàn)是根據(jù)每個連接 fd 上的 callback 函數(shù)來實現(xiàn)的,只有活躍的 socket 才會主動調(diào)用 callback,所以在活躍 socket 較少的情況下,使用 epoll 沒有前面兩者的線性下降的性能問題,但是所有 socket 都很活躍的情況下,可能也會有性能問題。
epoll 如何實現(xiàn)只處理活躍連接
epoll實現(xiàn)了eventpoll數(shù)據(jù)結(jié)構(gòu)
數(shù)據(jù)結(jié)構(gòu)中rdlist將活躍連接存儲在鏈表中,當(dāng)網(wǎng)卡發(fā)送報文時,增加節(jié)點,當(dāng)讀取一個事件后,鏈表刪除節(jié)點,需要得到活躍連接就只需要遍歷鏈表
數(shù)據(jù)結(jié)構(gòu)中rdr使用紅黑樹(自平衡二叉樹)將事件存儲,例如:當(dāng)有讀事件時,就新增節(jié)點,事件復(fù)雜度為logN
一般來說編寫高并發(fā)服務(wù)器程序都會首先 epoll 因為這種環(huán)境下其性能最好,但是在連接數(shù)少并且連接都十分活躍的情況下,select 和 poll 的性能可能比 epoll 好,畢竟 epoll 的通知機制需要很多函數(shù)回調(diào)。