I/O複用(I/O multiplexing): select, pselect, poll, ppoll, epoll
阿新 • • 發佈:2019-01-02
I/O複用:select, pselect, poll, epoll.
- 注意:本文主要介紹的是epoll相關知識,無法確保正確
1. 相關問題:
- 1.1 什麼是I/O複用?
- 1.2 四個I/O複用方法相關知識點?
- 1.3 四個I/O複用方法的比較?
- 1.4 epoll有哪些觸發模式?有何區別?
- 1.5 select 什麼情況下返回?
- 1.6 如果select返回可讀,結果只讀到0位元組,什麼情況?
- 1.7 兩個epoll等待同一個檔案描述符會發生什麼?[事件發生時會同時返回給兩個epoll例項]
- 1.8 如果epoll 把自己epoll_create()返回的描述符放入自己檔案描述符集裡面,會有發生什麼情況?
- 1.9 如何設計大規模的併發模型?
[參見man epoll手冊後面的9個問題]
2.拓展問題:
- 2.1 什麼是執行緒安全?
3. 解答
3.1 什麼是I/O複用?
- I/O複用(I/O multiplexing): 單個執行緒通過記錄跟蹤每一個I/O流的狀態來同時管理多個I/O流.
3.2 四個I/O複用方法相關知識點?
- poll 和 select的工作機制是:核心遍歷所有監聽中的檔案描述符,返回”準備好”的檔案描述符的個數.
3.2.1 select:
1). 原型:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
2). 說明:
- 永遠等待: timeout == NULL;
- 不等待: timeout->tv_sec == 0 && timeout->tv_usec == 0;
- 等待指定時間: timeout->tv_sec != 0 || timeout->tv_usec != 0;
- 宣告描述符集以後,必須用FD_ZERO將描述符集置0!
- 當第2,3,4個引數都為NULL時,select 只作為定時器.
- 返回:-1: 出錯;0: 沒有描述符準備好;>0: 準備好的描述符的個數.
- 描述符阻塞與否不影響select是否阻塞.
- select關注的最大描述符數是:FD_SETSIZE, 一般為1024.(對於一般程式來說太大了)
3.2.2 pselect:
1). 原型:
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
和pselect 的區別在於:
timespec 結構是s+ns(秒+納秒)級別,且為const修飾的.(select 是s+ms)
pselect 可使用可選訊號遮蔽字.
3.2.3 poll和ppoll:
1). 原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts, const sigset_t *sigmask);
2). 說明:
- struct pollfd結構:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events,interest */
short revents; /* returned events ,occurred */
};
- timeout == -1:永遠等待
- timeout == 0: 不等待
- timeout > 0: 等待timeout**毫秒**!
- 結構中的events是感興趣的事件;revents是發生(返回)的事件.
- poll關注的描述符數為nfds(第二個引數),一般為unsigned long 型.
3.2.4 epoll:
- 由於select和poll的侷限性,linux 2.6 核心引入了event poll(epoll)機制.
- epoll的工作原理是:建立一個epoll上下文->新增/刪除檔案描述符到epoll上下文(描述符集)->事件等待,記錄發生事件的檔案描述符.
1). 可以通過epoll_create()[不贊成使用]和epoll_create1()建立一個epoll上下文[開啟一個epoll檔案描述符].
- 原型:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
引數說明:
- 從linux 2.6.8後,size就被忽略了(但必須大於0)
- 當flags==0時,epoll_create1()功能和epoll_create()功能一樣;
flags==EPOLL_CLOEXEC時,新檔案描述符中會設定close-on-exec (FD_CLOEXEC)標誌.(見man 2 open)
返回:
- 返回值是一個檔案描述符,但是此檔案描述符和真實檔案沒有關係.當不用的時候,應當close().
- 當返回-1時,代表出錯,errno被設定:
EINVAL: 無效的flags;[size不是正數]
EMFILE: 達到使用者能開啟最大檔案數;
ENFILE: 達到系統能開啟的最大檔案數;
ENOMEM: 記憶體不足.
2). 可以通過epoll_ctl()新增或刪除檔案描述符到epoll上下文.
- 原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
說明:
引數op的值:
EPOLL_CTL_ADD: 指定fd新增到epfd關聯的epoll上下文中,event定義事件; EPOLL_CTL_DEL: 刪除; EPOLL_CTL_MOD: 修改指定fd的event(監聽行為).
struct epoll_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 */ };
events 是一個位集(bit set), 可用的值有:
EPOLLIN: 可讀. EPOLLOUT: 可寫. EPOLLPRI: 高優先順序資料可讀. EPOLLERR: 錯誤條件發生在關聯的檔案描述符中.(epoll_wait總是等待這個事件,不需要把它設定在events中.) EPOLLHUP: 結束通話(hangup)發生.(epoll_wait總是等待這個事件,不需要把它設定在events中.) EPOLLET: 指定檔案描述符設定為邊緣觸發(預設動作是水平觸發). 需要用EPOLL_CTL_MOD呼叫epoll_ctl()重新設定事件才能再監聽. EPOLLRDHUP (since Linux 2.6.17): (Stream socket)關閉連線或半關閉寫連線. (This flag is especially useful for writing simple code to detect peer shutdown when using Edge Triggered monitoring.)
返回值: 成功0,失敗-1,errno唄設定:
EBADF: epfd或fd不是有效的檔案描述符. EEXIST: op是 EPOLL_CTL_ADD,但fd已經註冊過了. EINVAL: epfd不是一個epoll檔案描述符或fd和epfd相同或op所請求的操作不被支援. ENOENT: op是EPOLL_CTL_MOD或EPOLL_CTL_DEL,但fd還沒有註冊. ENOMEM: 記憶體不足. ENOSPC: 達到最大監聽數目.[?,百度一下] EPERM: 目標fd不支援epoll.
3).等待一個I/O事件發生.
原型:
#include <sys/epoll.h> int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
說明:
- epoll_wait()等待/收集監聽事件中已經發生的事件.
- events: 分配好記憶體的結構陣列.
- maxevents: 使用者指定的events結構陣列的大小(events的最大數目).[這個引數不是很理解.]
- timeout: -1未定義; 0立即返回,>0指定毫秒.
返回: 0超時,>0發生事件數目,-1錯誤,errno被設定:
EBADF: epfd不是有效檔案描述符 EFAULT: 程序對events指向的記憶體沒有寫許可權. EINTR: 呼叫在事件發生或超時前被訊號中斷. EINVAL: epfd不是一個epoll檔案描述符,或maxevents<=0.
epoll_wait()和epoll_pwait()的關係:
ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);
等於
sigset_t origmask; sigprocmask(SIG_SETMASK, &sigmask, &origmask); ready = epoll_wait(epfd, &events, maxevents, timeout); sigprocmask(SIG_SETMASK, &origmask, NULL);
當sigmask==NULL的時候,兩個函式相等.
4). 邊緣觸發和水平觸發[摘自man epoll]
- 用於讀管道的檔案描述符rfd在epoll例項中註冊了.
- writer在寫端寫2kB資料到管道.
- 此時呼叫epoll_wait()會返回rfd,作為”準備好”讀的檔案描述符.
- reader通過rfd從管道讀取1KB資料.
- 然後再呼叫一次epoll_wait(). //邊緣觸發和水平觸發的區別在這裡體現.
- 當在步驟1中註冊使用水平觸發(EPOLLLT)時,步驟5會和步驟3一樣返回rfd,因為此時管道中還有資料.
- 當使用邊緣觸發(EPOLLET)時,步驟5將可能掛起,儘管有效的資料還在輸入緩衝區中,同時,資料傳送端(寫端)可能還在等待一個反饋.發生這種情況的原因是:邊緣觸發只在檔案描述符狀態發生改變的時候才遞交事件.所以,在步驟5中呼叫者可能會不再等待已經在輸入緩衝區中的資料.
- 在上面的例子中,rfd上的事件發生後,步驟2寫資料,事件在步驟3銷燬(但輸入緩衝還有資料).因此如果步驟4讀資料但沒有全部讀完,那麼步驟5呼叫epoll_wait()可能未定義地阻塞.
- 一個程式如果用了EPOLLET標誌的話,應該使用非阻塞檔案描述符來避免讀/寫阻塞把處理多個檔案描述符的任務餓死.
- 使用邊緣觸發的epoll時,建議:
- 使用非阻塞檔案描述符
- 只在read()或write()返回EAGAIN後才等待一個事件(epoll_wait()).
3.3 四個I/O複用方法的比較?
3.3.1 select的問題:
- select 會修改傳入的引數陣列,對於一個需要呼叫很多次的函式,是非常不友好的。
- select 遍歷陣列,看哪個準備好,陣列越大,所需時間越長.
- 描述符上(I/O stream)出現了資料(準備好可讀可寫異常),select 僅僅返回準備好的描述符個數,並不會告訴你是哪個描述符.
- select 只能監視1024個描述符.
- select 不是執行緒安全的
- 核心 / 使用者空間記憶體拷貝問題,select需要複製大量的控制代碼資料結構,產生巨大的開銷
3.3.2 poll:
- poll的個數限制為unsigned long.
- poll 不修改引數陣列.
- poll 仍然不是執行緒安全的.
3.3.3 epoll:
- epoll把以上問題都解決了並且加入了一些新特性.(特性不知..)
3.4 epoll有哪些觸發模式?有何區別?
- 見3.2.4節關於epoll的知識點說明.
3.5 select 什麼情況下返回?
- 偵聽到檔案描述符可讀/可寫/異常時.
3.6 如果select返回可讀,結果只讀到0位元組, 為什麼?
- 讀到了檔案尾.[EOF]
- 如果在一個檔案描述符上碰到檔案尾端,則select會認為該描述符可讀.然後呼叫read()返回0,這是UNIX系統指示到檔案尾端的方法.[摘自<<unix環境高階程式設計(第3版)>>p407]
3.7 兩個epoll等待同一個檔案描述符會發生什麼?
- 事件發生時會同時返回給兩個epoll例項
3.8 如果epoll 把自己epoll_create()返回的描述符放入自己檔案描述符集裡面,會有發生什麼情況?
- epoll_ctl會失敗(EINVAL),但可以把自己的檔案描述符放到別的epoll描述符集裡面.
3. 9 如何設計大規模的併發模型?
- ...
4. 拓展問題
4.1 什麼是執行緒安全?
- 多執行緒訪問同一段程式碼,不會產生不確定的結果,就是執行緒安全的。
5. 參考資料
5.1 知乎答案
5.2 <<Unix 環境高階程式設計(第3版)>>
5.3 <<Linux系統程式設計>>