上一篇我們說了關於select的相關信息,我們可以看到select是有弊端的,所以爲了解決select的弊端,UNIX又在後期提出了poll。
select的弊端這裏就不多說了,上一篇博客有提及。
poll
poll和select類似,不過在一些方面改善了select的弊端。它也是在指定的時間進行輪詢文件描述符,查看是否有就緒時間發生。
和上次一樣,我們先來看一下poll系統調用。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds是一個pollfd的結構體數組。
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
這就是這個結構體數組每個元素。fd用來記錄對應的文件描述符,events用來表示poll所監聽的事件,這個由用戶來設置。revents用來表示返回的事件。revents是通過內核來進行操作修改。
這裏提供了一些合法事件。
事件 | 說明 |
---|---|
POLLIN | 普通或優先級帶數據可讀 |
POLLRDNORM | 普通數據可讀 |
POLLRDBAND | 優先級帶數據可讀 |
POLLPRI | 高優先級數據可讀 |
POLLOUT | 普通數據可寫 |
POLLWRNORM | 普通數據可寫 |
POLLWRBAND | 優先級帶數據可寫 |
POLLERR | 發生錯誤 |
POLLHUP | 發生掛起 |
POLLNVAL | 描述字不是一個打開的文件 |
後面的三個參數在events無意義,只能作爲返回結果存儲在revents。
另外,這裏需要說的,這些參數如何設置給events,這些宏相當於每一個佔用一個比特位,我們可以去想一下位圖,所以,如果我們要進行設置兩個事件,就使用|
操作,另外,當我們去查看事件是否發生的時候,這個時候我們可以使用revents&
事件,如果事件發生了,那麼結果大於1。這就是一個簡單的位運算的,相信你仔細想想就能夠理解。
第二個參數nfds,用來監視的文件描述符的數目。
第三個參數是timeout,用來設置超時時間。
參數 | 說明 |
---|---|
-1 | poll將永遠阻塞,等待知道某個時間發生 |
0 | 立即返回 |
大於0的值 | 設置正常時間值 |
返回值
poll返回revents不爲0的文件描述符的個數。
失敗返回-1
總結
poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,然後查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
poll使用了events和revents分流的特點,這樣可以使得對關心事件只進行註冊一次。
poll基於鏈表進行存儲,沒有最大連接數的限制,只取決於內存大小。
poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
poll的實現機制與select類似,其對應內核中的sys_poll,只不過poll向內核傳遞pollfd數組,然後對pollfd中的每個描述符進行poll,相比處理fdset來說,poll效率更高。poll返回後,需要對pollfd中的每個元素檢查其revents值,來得指事件是否發生。
poll的缺點
1、大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣是不是有意義。
2、poll依然需要進行輪詢,所消耗的時間太多。
3、水平觸發,效率低
示例程序
聊天室程序:
#define _GNU_SOURCE 1
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<fcntl.h>
#include<errno.h>
#include<string.h>
#include<poll.h>
#define LIMIT_FD 65535
#define LIMIT_USER 5
#define BUF_SIZE 1024
//客戶數據:包含客戶的socket地址,待寫到客戶端的數據的位置、從客戶端讀入數據。
struct client_data
{
struct sockaddr_in address;
char* write_buf; //寫入客戶端段的數據的位置
char buf[BUF_SIZE]; //客戶端讀入的數據
};
int StartUp(int port,char *ip_addr)
{
assert(ip_addr);
int sock = socket(AF_INET, SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip_addr);
if(bind(sock,(struct sockaddr *)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock,5) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
//設置文件描述符爲非阻塞狀態
int setnoblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd,F_SETFL,new_option);
return old_option;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
printf("Usage: %s [local_ip] [local_port]\n",argv[0]);
return 1;
}
//建立監聽socket
int listen_sock = StartUp(atoi(argv[2]),argv[1]);
//創建users數組,分配client數據對象的文件描述符。利用它來進行索引用戶數據以及發數據
struct client_data* users = (struct client_data *)malloc(sizeof(struct client_data)*LIMIT_FD);
//client_data users[LIMIT_FD];
//雖然有足夠多的client_data,但是依然要限制用戶數量,事件最大
struct pollfd fds[LIMIT_USER+1];
int user_count = 0;
int i = 0;
for(i = 1; i <= LIMIT_USER; ++i)
{
fds[i].fd = -1;
fds[i].events = 0;
}
fds[0].fd = listen_sock;//設置監聽端口
fds[0].events = POLLIN|POLLERR;//監聽端口設置可讀和錯誤事件
fds[0].revents = 0;
while(1)
{
//永遠等待,當準備好再去提交給應用程序。
int ret = poll(fds, user_count+1, -1);
if(ret < 0)
{
printf("poll faile\n");
break;
}
for(i = 0; i < user_count+1; ++i)
{
//此時爲監聽套接字,有新連接來,監聽套接字接受到可讀事件
if((fds[i].fd == listen_sock) && (fds[i].revents & POLLIN))
{
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
int sock = accept(listen_sock,\
(struct sockaddr *)&peer,&peer_len);
if(sock < 0)
{
perror("accept");
continue;
}
printf("new user :ip:%s,port:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
// 如果請求太多,則關閉請求連接。
if(user_count >= LIMIT_USER)
{
const char *msg = "Too many users!!!\n";
printf("%s",msg);
write(sock, msg, sizeof(msg) );
close(sock);
continue;
}
//相對於新連接,同時去修改fds,和users數組對應連接的文件描述符sock的客戶數據。
user_count++;
users[sock].address = peer;
//設置非阻塞
setnoblocking(sock);
fds[user_count].fd = sock;
fds[user_count].events = POLLIN | POLLERR | POLLRDHUP;
fds[user_count].revents = 0;
printf("come a new user, now have %d users\n",user_count);
}
//對於出現錯誤信息
else if(fds[i].revents & POLLERR)
{
printf("get an error from %d\n",fds[i].fd);
//...
continue;
}
//如果客戶端關閉連接。此時檢測到客戶端斷開的請求,所以這個時候觸發這個事件
else if(fds[i].revents & POLLRDHUP)
{
//服務器也許要關閉連接,並且把user_count減1
//這裏的減相當於去移動了文件描述符,把最大的放到了需要減的那個了。
//
users[fds[i].fd] = users[fds[user_count].fd];
close(fds[i].fd);
fds[i] = fds[user_count];
i--;
user_count--;
printf("a client left\n");
}
//連接套接字可讀
else if(fds[i].revents & POLLIN)
{
int sock = fds[i].fd;
memset(users[sock].buf, 0,sizeof(users[sock].buf));
ret = read(sock, users[sock].buf, sizeof(users[sock].buf) - 1);
printf("client :%s\n",users[sock].buf);
if(ret > 0)
{
//收到客戶數據,此時通知其他的socket接受數據
users[sock].buf[ret] = 0;
int j = 0;
for(j = 1; j <= user_count; ++j)
{
if(fds[j].fd == sock)
{
continue;
}
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
users[fds[j].fd].write_buf = users[sock].buf;
}
}
else if(ret < 0)
{
//讀取錯誤,關閉連接
if(errno != EAGAIN)
{
perror("read");
close(sock);
users[fds[i].fd] = users[fds[user_count].fd];
fds[i] = fds[user_count];
i--;
user_count--;
}
}
}
else if(fds[i].revents & POLLOUT)
{
//連接套接字可寫
int sock =fds[i].fd;
//判斷是否可寫
if(! users[sock].write_buf)
{
continue;
}
ret = write(sock,users[sock].write_buf,\
BUF_SIZE-1);
users[sock].write_buf = NULL;
//寫完以後重新註冊fds[i]的可讀事件
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
close(listen_sock);
return 0;
}