unix - Socket - Poll, Select and epoll
https://blog.csdn.net/jyy305/article/details/73012706
select的优缺点
优点:
(1)select的可移植性好,在某些unix下不支持poll.
(2)select对超时值提供了很好的精度,精确到微秒,而poll式毫秒。
缺点:
(1)单个进程可监视的fd数量被限制,默认是1024。
(2)需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。
(3)对fd进行扫描时是线性扫描,fd剧增后,IO效率降低,每次调用都对fd进行线性扫描遍历,随着fd的增加会造成遍历速度慢的问题。
(4)select函数超时参数在返回时也是未定义的,考虑到可移植性,每次超时之后进入下一个select之前都要重新设置超时参数。
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
static void Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int array[4096];
static int start_up(const char* _ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(1);
}
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(2);
}
if(listen(sock,10) < 0)
{
perror("listen");
exit(3);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return -1;
}
int listensock = start_up(argv[1],atoi(argv[2]));
int maxfd = 0;
fd_set rfds;
fd_set wfds;
array[0] = listensock; //监听套接字
int i = 1;
int array_size = sizeof(array)/sizeof(array[0]);
for(; i < array_size;i++)
{
array[i] = -1;
}
while(1)
{
FD_ZERO(&rfds);
FD_ZERO(&wfds);
for(i = 0;i < array_size;++i)
{
if(array[i] > 0)
{
FD_SET(array[i],&rfds);
FD_SET(array[i],&wfds);
if(array[i] > maxfd)
{
maxfd = array[i];
}
}
}
switch(select(maxfd + 1,&rfds,&wfds,NULL,NULL))
{
case 0:
{
printf("timeout\n");
break;
}
case -1:
{
perror("select");
break;
}
default:
{
int j = 0;
for(; j < array_size; ++j)
{
if(j == 0 && FD_ISSET(array[j],&rfds)) //array[0] 是监听套接字
{
//listensock happened read events
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listensock,(struct sockaddr*)&client,&len);
if(new_sock < 0)//accept failed
{
perror("accept");
continue;
}
else//accept success
{
printf("get a new client%s\n",inet_ntoa(client.sin_addr));
fflush(stdout);
int k = 1;
for(; k < array_size;++k)
{
if(array[k] < 0)
{
array[k] = new_sock; //普通套接字
if(new_sock > maxfd)
maxfd = new_sock;
break;
}
}
if(k == array_size)
{
close(new_sock);
}
}
}//j == 0
else if(j != 0 && FD_ISSET(array[j], &rfds))
{
//new_sock happend read events
char buf[1024];
ssize_t s = read(array[j],buf,sizeof(buf) - 1);
if(s > 0)//read success
{
buf[s] = 0;
printf("clientsay#%s\n",buf);
if(FD_ISSET(array[j],&wfds))
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\n<html><h1>yingying beautiful</h1></html>\r\n";
write(array[j],msg,strlen(msg));
}
}
else if(0 == s)
{
printf("client quit!\n");
close(array[j]);
array[j] = -1;
}
else
{
perror("read");
close(array[j]);
array[j] = -1;
}
}//else j != 0
}
break;
}
}
}
return 0;
}
poll函数的优缺点
优点:
(1)不要求计算最大文件描述符+1的大小。
(2)应付大数量的文件描述符时比select要快。
(3)没有最大连接数的限制是基于链表存储的。
缺点:
(1)大量的fd数组被整体复制于内核态和用户态之间,而不管这样的复制是不是有意义。
(2)同select相同的是调用结束后需要轮询来获取就绪描述符。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<poll.h>
static void usage(const char *proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = inet_addr(_ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(sock,10) < 0)
{
perror("listen");
return 4;
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
struct pollfd peerfd[1024];
peerfd[0].fd = sock;
peerfd[0].events = POLLIN;
int nfds = 1;
int ret;
int maxsize = sizeof(peerfd)/sizeof(peerfd[0]);
int i = 1;
int timeout = -1;
for(; i < maxsize; ++i)
{
peerfd[i].fd = -1;
}
while(1)
{
switch(ret = poll(peerfd,nfds,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("poll");
break;
default:
{
if(peerfd[0].revents & POLLIN)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(sock,\
(struct sockaddr*)&client,&len);
printf("accept finish %d\n",new_sock);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get a new client\n");
int j = 1;
for(; j < maxsize; ++j)
{
if(peerfd[j].fd < 0)
{
peerfd[j].fd = new_sock;
break;
}
}
if(j == maxsize)
{
printf("to many clients...\n");
close(new_sock);
}
peerfd[j].events = POLLIN;
if(j + 1 > nfds)
nfds = j + 1;
}
for(i = 1;i < nfds;++i)
{
if(peerfd[i].revents & POLLIN)
{
printf("read ready\n");
char buf[1024];
ssize_t s = read(peerfd[i].fd,buf, \
sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
fflush(stdout);
peerfd[i].events = POLLOUT;
}
else if(s <= 0)
{
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//i != 0
else if(peerfd[i].revents & POLLOUT)
{
char *msg = "HTTP/1.0 200 OK \
<\r\n\r\n<html><h1> \
yingying beautiful \
</h1></html>\r\n";
write(peerfd[i].fd,msg,strlen(msg));
close(peerfd[i].fd);
peerfd[i].fd = -1;
}
else
{
}
}//for
}//default
break;
}
}
return 0;
}
epoll
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
#include<sys/epoll.h>
static Usage(const char* proc)
{
printf("%s [local_ip] [local_port]\n",proc);
}
int start_up(const char*_ip,int _port)
{
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);
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if(listen(sock,10)< 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char*argv[])
{
if(argc != 3)
{
Usage(argv[0]);
return 1;
}
int sock = start_up(argv[1],atoi(argv[2]));
int epollfd = epoll_create(256);
if(epollfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sock;
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,sock,&ev) < 0)
{
perror("epoll_ctl");
return 6;
}
int evnums = 0;//epoll_wait return val
struct epoll_event evs[64];
int timeout = -1;
while(1)
{
switch(evnums = epoll_wait(epollfd,evs,64,timeout))
{
case 0:
printf("timeout...\n");
break;
case -1:
perror("epoll_wait");
break;
default:
{
int i = 0;
for(; i < evnums; ++i)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
if(evs[i].data.fd == sock \
&& evs[i].events & EPOLLIN)
{
int new_sock = accept(sock, \
(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
continue;
}//if accept failed
else
{
printf("Get a new client[%s]\n", \
inet_ntoa(client.sin_addr));
ev.data.fd = new_sock;
ev.events = EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,\
new_sock,&ev);
}//accept success
}//if fd == sock
else if(evs[i].data.fd != sock && \
evs[i].events & EPOLLIN)
{
char buf[1024];
ssize_t s = read(evs[i].data.fd,buf,sizeof(buf) - 1);
if(s > 0)
{
buf[s] = 0;
printf("client say#%s",buf);
ev.data.fd = evs[i].data.fd;
ev.events = EPOLLOUT;
epoll_ctl(epollfd,EPOLL_CTL_MOD, \
evs[i].data.fd,&ev);
}//s > 0
else
{
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}
}//fd != sock
else if(evs[i].data.fd != sock \
&& evs[i].events & EPOLLOUT)
{
char *msg = "HTTP/1.0 200 OK <\r\n\r\n<html><h1>yingying beautiful </h1></html>\r\n";
write(evs[i].data.fd,msg,strlen(msg));
close(evs[i].data.fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL, \
evs[i].data.fd,NULL);
}//EPOLLOUT
else
{
}
}//for
}//default
break;
}//switch
}//while
return 0;
}
epoll函数的优缺点
优点:
epoll的优点:
(1)支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
(2)IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行 操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
(3)使用mmap加速内核与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
(4)内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 — 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。
https://blog.csdn.net/xiangjai/article/details/93467925
select的调用过程如下所示:
(1)使用copy_from_user从用户空间拷贝fd_set到内核空间
(2)注册回调函数__pollwait
(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。
(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。
(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。
(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。
(8)把fd_set从内核空间拷贝到用户空间。
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
poll实现
poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
epoll
对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
总结:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。