一.select函數
select 的調用注意事項
在使用 select 函數時,需要注意以下幾個關鍵點:
1. 參數的修改與拷貝
? ?readfds 等參數是結果參數 :
? ?select 函數會直接修改傳入的 fd_set(如 readfds、writefds 和 exceptfds)。
? ?為了保留原始監聽集合,通常會定義一個備份集合(如 allread_fdset),并將它的拷貝傳遞給 select。
?? ?示例:
?? ?fd_set allread_fdset, readfds;
?? ?FD_ZERO(&allread_fdset);
?? ?FD_SET(fd1, &allread_fdset);
?? ?FD_SET(fd2, &allread_fdset);
?? ?readfds = allread_fdset; // 拷貝到臨時集合
?? ?select(..., &readfds, ...);
2. 計算 nfds
?? ?nfds 是最大文件描述符值 + 1 :
?? ?在新增監聽句柄時,更新 nfds 較為簡單。
?? ?在減少監聽句柄時,更新 nfds 較為復雜:
?? ?如果需要精確計算,可以通過遍歷或維護一個最大堆等數據結構來找到第二大的文件描述符。
?? ?或者,可以選擇忽略 nfds 的更新,但可能導致性能下降。
?? ?
?? ?
3. 超時參數 timeout
?? ?timeout 的含義 :
?? ?如果為 NULL,表示阻塞等待,直到有事件發生。
?? ?如果指向的時間為 0,表示非阻塞模式。
?? ?如果指定超時時間,則 select 會在超時后返回。
?? ?注意:Linux 實現中,select 返回時會修改 timeout 為剩余時間 :
?? ?如果需要重復使用 timeout,需要重新初始化。
?? ?
4. 返回值的處理
?? ?返回值的意義 :
?? ?-1:表示錯誤。
?? ?0:表示超時時間到,沒有事件發生。
?? ?正數:表示監聽到的事件總數(包括可讀、可寫和異常事件)。
?? ?優化事件處理 :
?? ?可以利用返回值避免不必要的檢查。例如,如果返回值為 1,并且已經在可讀集合中處理了一個事件,則無需再檢查可寫和異常集合。
select 的缺點
?? ?盡管 select 是一種經典的 I/O 多路復用機制,但它存在以下顯著缺點:
?? ?1. 文件描述符數量限制
?? ??? ?FD_SETSIZE 的限制 :
?? ??? ?每個 fd_set 最多只能監聽 FD_SETSIZE 個文件描述符(在 Linux 上通常是 1024)。
?? ??? ?這一限制使得 select 不適合高并發場景。
?? ??? ?
?? ?2. 遍歷效率低
?? ??? ?需要逐一檢查文件描述符 :
?? ??? ?返回的 fd_set 是一個位圖,應用程序需要對所有監聽的文件描述符逐一調用 FD_ISSET 來判斷是否就緒。
?? ??? ?示例:
?? ??? ?for (int i = 0; i < nfds; i++) {
?? ??? ??? ?if (FD_ISSET(i, &readfds)) {
?? ??? ??? ??? ?// 處理可讀事件
?? ??? ??? ?}
?? ??? ?}
3. nfds 的效率問題
?? ?select 的實現方式 :
?? ?select 內部會遍歷從 0 到 nfds-1 的所有文件描述符,判斷每個描述符是否是關心的,并檢查是否有事件發生。
?? ?即使只監聽少數幾個文件描述符(如 0 和 1000),select 仍然需要遍歷 1001 個描述符,導致效率低下。
總結
優點
? ? 簡單易用,跨平臺支持廣泛。
缺點
?? ?文件描述符數量受限 :最多只能監聽 FD_SETSIZE 個文件描述符。
?? ?遍歷效率低 ? ? ? ? :需要逐一檢查文件描述符,增加了開銷。
?? ?nfds 的問題 ? ? ? ?:即使監聽的文件描述符稀疏分布,select 仍需遍歷所有小于 nfds 的描述符。
這些缺點促使了更高效的 I/O 多路復用機制(如 poll 和 epoll)的出現,尤其是在高并發場景下,epoll 成為了更優的選擇。
【1】管道select
【2】tcp服務器select
二.poll函數
poll?
? ?針對select 做了改進?
? ?底層實現 --- 用的是數組?
? ?poll --- 鏈表?
? ?poll 引入了事件機制?
? ?
? ?1. 遍歷?
? ?2. poll 需要在 用戶空間 和 內核空間 來回拷貝?
epoll?
? ?三種多路IO操作中最高效
? ?
?
三.epoll函數
3. epoll
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
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 */
};
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll 解決了select和poll的幾個性能上的缺陷:
① 不限制監聽的描述符個數(poll也是),只受進程打開描述符總數的限制;
② 監聽性能不隨著監聽描述 符數的增加而增加,是O(1) 的,
? 不再是輪詢描述符來探測事件,而是由描述符主動上報事件; //事件機制的?
③ 使用共享內存的方式,不在用戶和內核之間反復傳遞監聽的描述 符信息;
④ 返回參數中就是觸發事件的列表,不用再遍歷輸入事件表查詢各個事件是否被觸發
------------------------------------------
epoll顯著提高性能的前提是:
監聽大量描述符,
并且每次觸發事件的描述符文件非常少。
epoll的另外區別是:
①epoll創建了描述符,記得close;
②支持水平觸發和邊沿觸發。
epoll使用注意事項:
//epoll_create
① int epoll_create(int size); ? ?//創建epoll文件描述符
參數size并不是限制了epoll所能監聽的描述符最大個數,
只是對內核初始分配內部數據結構的一個建議。
返回是epoll描述符。-1表示創建失敗。
//epoll_ctl
② int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ? ?//epoll文件描述符的控制接口
功能:
? ? ?epoll_ctl控制對指定描述符fd執行op操作,event是與fd關聯的監聽事件。
參數:
? ?@epfd --- epoll對象?
? ?@op?
?? ??? ?op操作有三種:
?? ??? ?
?? ??? ?添加EPOLL_CTL_ADD,
?? ??? ?
?? ??? ?刪除EPOLL_CTL_DEL,
?? ??? ?
?? ??? ?修改EPOLL_CTL_MOD。
?? ??? ?
?? ?分別添加、刪除和修改對fd的監聽事件。
?? ?重復添加fd會怎樣(event相同或不相同):
?? ?添加失敗(errno:17, File exists)
?? ?刪除和修改不存在的fd會怎樣:
?? ?刪除或修改失敗(errno:9,Bad file descriptor)
? @fd -- 關心的fd?
??
??
event是與監聽的fd相關聯的事件信息,event->events描述了要監聽的事件類型,有以下類型:
//事件類型:
EPOLLIN ? ? ? ?可讀
EPOLLOUT ? ? ? 可寫
EPOLLRDHUP ? ? 套接口對端close或shutdown寫,在ET模式下比較有用
EPOLLPRI ? ? ? 緊急數據可讀
EPOLLERR ? ? ? 異常條件
EPOLLHUP ? ? ? 掛起,EPOLLERR和EPOLLHUP始終由epoll_wait監聽,不需要用戶設置
EPOLLET ? ? ? ?邊沿觸發模式,在描述符狀態跳變時才上報監聽事件。(監聽默認都是LT模式)(ET+非阻塞模式)
EPOLLONESHOT ? 只一次有效,設置oneshot標記,描述符在觸發一次事件之后自動失效(fd還被監聽),
? ? ? ? ? ? ? ?不會再上報任何事件,直到使用EPOLL_CTL_MOD重新激活,
? ? ? ? ? ? ? ?設置新的監聽事件為止(可不可以和之前的事件一樣?)。
event->data是個共用體,可以存放和fd綁定的描述符信息,
比如就存放描述符本身fd,或者一個結構體信息,包括fd,ip,port等等。
在epoll_wait返回時,只會返回一個event列表,需要從列表元素中獲取fd等信息。
返回值:
? ? ? ? 返回0表示控制成功,
?? ??? ?返回-1表示失敗。
③ int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//等待epfd上的io事件,最多返回maxevents個事件
timeout = -1 的行為是block;
timeout = ?0 是立即返回
④ epoll監聽ET事件時,fd必須是非阻塞套接口。
比如監聽可讀事件,當ET上報可讀后,需要一直讀fd直到遇到EAGAIN錯誤為止,以免遺留數據在緩沖區中。
如果fd是阻塞的,則會讀到阻塞了。
EAGAIN錯誤對于非阻塞套接口來說不是錯誤,只是說沒有數據可讀或者沒有空間可寫。
EWOULDBLOCK就是EAGAIN,值都是11。
selset/poll/epoll的LT模式監聽的fd可以是阻塞模式的。
⑤ 多路復用監聽io事件時,如果對某個套接口監聽可寫事件,總是會返回可寫而事實上可能沒有數據要寫。
處理方法:
①只有在有數據要寫時才把要寫的套接口加入 監聽列表中,數據全部寫完之后從監聽列表中刪除它;
②在有數據寫時,首先嘗試直接寫,當直接寫沒有把數據全部寫入發送緩沖區時再把這個套接口加入可寫事件 監聽列表。
(這種方式效率較高,需要套接口是非阻塞的,前一種方式可以是阻塞的嗎?)
可以是阻塞的。