上一講我們講到了 I/O 多路複用技術,並以 select 爲核心,展示了 I/O 多路複用技術的能力。select 方法是多個 UNIX 平臺支持的非常常見的 I/O 多路複用技術,但是它有一個缺點,那就是所支持的文件描述符的個數是有限的。在 Linux 系統中,select 的默認最大值爲 1024。
poll 函數
和 select 相比,poll 和內核交互的數據結構有所變化,另外,也突破了文件描述符的個數限制。
int poll(struct pollfd *fds, unsigned long nfds, int timeout);
返回值:若有就緒描述符則爲其數目,若超時則爲0,若出錯則爲-1
struct pollfd {
int fd; /* file descriptor */
short events; /* events to look for */
short revents; /* events returned */
};
這個結構體由三個部分組成,首先是描述符 fd,然後是描述符上待檢測的事件類型 events,注意這裏的 events 可以表示多個不同的事件,具體的實現可以通過使用二進制掩碼位操作來完成,例如,POLLIN 和 POLLOUT 可以表示讀和寫事件。
#define POLLIN 0x0001 /* any readable data available */
#define POLLPRI 0x0002 /* OOB/Urgent readable data */
#define POLLOUT 0x0004 /* file descriptor is writeable */
和 select 不同的地方在於,poll 每次檢測之後的結果不會修改原來的傳入值,而是將結果保留在 revents 字段中,這樣就不需要每次檢測完都得重置待檢測的描述字和感興趣的事件。我們可以把 revents 理解成“returned events”。
events 類型的事件可以分爲兩大類。第一類是可讀事件,有以下幾種:
#define POLLIN 0x0001 /* any readable data available */
#define POLLPRI 0x0002 /* OOB/Urgent readable data */
#define POLLRDNORM 0x0040 /* non-OOB/URG data available */
#define POLLRDBAND 0x0080 /* OOB/Urgent readable data */
一般我們在程序裏面有 POLLIN 即可。套接字可讀事件和 select 的 readset 基本一致,是系統內核通知應用程序有數據可以讀,通過 read 函數執行操作不會被阻塞。
第二類是可寫事件,有以下幾種:
#define POLLOUT 0x0004 /* file descriptor is writeable */
#define POLLWRNORM POLLOUT /* no write type differentiation */
#define POLLWRBAND 0x0100 /* OOB/Urgent data can be written */
一般我們在程序裏面統一使用 POLLOUT。套接字可寫事件和 select 的 writeset 基本一致,是系統內核通知套接字緩衝區已準備好,通過 write 函數執行寫操作不會被阻塞。
以上兩大類的事件都可以在“returned events”得到複用。還有另一大類事件,沒有辦法通過 poll 向系統內核遞交檢測請求,只能通過“returned events”來加以檢測,這類事件是各種錯誤事件。
#define POLLERR 0x0008 /* 一些錯誤發送 */
#define POLLHUP 0x0010 /* 描述符掛起*/
#define POLLNVAL 0x0020 /* 請求的事件無效*/
我們再回過頭看一下 poll 函數的原型。參數 nfds 描述的是數組 fds 的大小,簡單說,就是向 poll 申請的事件檢測的個數。
最後一個參數 timeout,描述了 poll 的行爲。如果是一個 <0 的數,表示在有事件發生之前永遠等待;如果是 0,表示不阻塞進程,立即返回;如果是一個 >0 的數,表示 poll 調用方等待指定的毫秒數後返回。
關於返回值,當有錯誤發生時,poll 函數的返回值爲 -1;如果在指定的時間到達之前沒有任何事件發生,則返回 0,否則就返回檢測到的事件個數,也就是“returned events”中非 0 的描述符個數。
poll 函數有一點非常好,如果我們不想對某個 pollfd 結構進行事件檢測,可以把它對應的 pollfd 結構的 fd 成員設置成一個負值。這樣,poll 函數將忽略這樣的 events 事件,檢測完成以後,所對應的“returned events”的成員值也將設置爲 0。
我們發現 poll 函數和 select 不一樣的地方就是,在 select 裏面,文件描述符的個數已經隨着 fd_set 的實現而固定,沒有辦法對此進行配置;而在 poll 函數裏,我們可以控制 pollfd 結構的數組大小,這意味着我們可以突破原來 select 函數最大描述符的限制,在這種情況下,應用程序調用者需要分配 pollfd 數組並通知 poll 函數該數組的大小。
基於 poll 的服務器程序
#define INIT_SIZE 128
int main()
{
int listenfd, connfd;
int ready_number;
ssize_t n;
char buf[MAXLINE];
struct servaddr_in cli_addr;
listenfd = socket(PF_INET, SOCK_STREAM, 0);
bzero(&serv_addr, sizeof(serv_addr));
bzero(&cli_addr, sizeof(cli_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(7878);
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
bind(listenfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr));
listen(listenfd, SOMAXCONN);
/*初始化pollfd數組,這個數組的第一個元素是listenfd,其餘的用來記錄將要連接的connfd
數組的大小固定爲 INIT_SIZE,這在實際的生產環境肯定是需要改進的。*/
struct pollfd event_set[INIT_SIZE];
/*將監聽套接字 listen_fd 和對應的 POLLRDNORM 事件加入到 event_set 裏,表示我們期望系統內核
檢測監聽套接字上的連接建立完成事件*/
event_set[0].fd = listenfd;
event_set[0].events = POLLRDNORM;
// 用-1表示這個數組位置還沒有被佔用
int i;
for (i = 1; i < INIT_SIZE; i++) {
event_set[i].fd = -1;
}
for (;;) {
//timeout 設置爲 -1,表示在 I/O 事件發生之前 poll 調用一直阻塞。
if ((ready_number = poll(event_set, INIT_SIZE, -1)) < 0) {
printf("poll failed \n");
continue;
}
if (event_set[0].revents & POLLRDNORM) {
socklen_t client_len = sizeof(client_addr);
connfd = accept(listenfd, (struct sockaddr *) &cli_addr,
&client_len);
//找到一個可以記錄該連接套接字的位置
for (i = 1; i < INIT_SIZE; i++) {
if (event_set[i].fd < 0) {
/*把連接描述字 connect_fd 也加入到 event_set 裏,而且說明了我們感興趣的事件類型爲 POLLRDNORM,也就是套集字上有數據可以讀*/
event_set[i].fd = connfd;
event_set[i].events = POLLRDNORM; //POLLIN包括了OOB等帶外數據的檢測,POLLRDNORM則不包括這部分。
break;
}
}
if (i == INIT_SIZE) {
printf("can not hold so many clients\n");
}
/*如果處理完監聽套接字之後,就已經完成了這次 I/O 複用所要處理的事情,那麼我們就可以跳過後面的處理,再次進入 poll 調用。*/
if (--ready_number <= 0)
continue;
}
/*查看 event_set 裏面其他的事件,也就是已連接套接字的可讀事件。這是通過遍歷 event_set 數組來完成的。*/
for (i = 1; i < INIT_SIZE; i++) {
int socket_fd;
if ((socket_fd = event_set[i].fd) < 0)
continue;
//通過檢測 revents 的事件類型是 POLLRDNORM 或者 POLLERR,我們可以進行讀操作。
if (event_set[i].revents & (POLLRDNORM | POLLERR)) {
if ((n = read(socket_fd, buf, MAXLINE)) > 0) {
if (write(socket_fd, buf, n) < 0) {
printf("write error\n");
}
} else if (n == 0 || errno == ECONNRESET) {
//如果讀到 EOF 或者是連接重置,則關閉這個連接,並且把 event_set 對應的 pollfd 重置
close(socket_fd);
event_set[i].fd = -1;
} else {
printf("read error\n");
}
if (--ready_number <= 0)
break;
}
}
}
}
總之,poll 是另一種在各種 UNIX 系統上被廣泛支持的 I/O 多路複用技術,雖然名聲沒有 select 那麼響,能力一點不比 select 差,而且因爲可以突破 select 文件描述符的個數限制,在高併發的場景下尤其佔優勢。
溫故而知新 !