Select和epoll
一、作用
这三个其实是三个函数,这三个函数是用来干什么的呐,我们举个例子吧,假设你的电脑是个高性能服务器,服务就会接收和处理很多消息,一个服务器肯定不止一个链接,很定有很多链接,但是不是任何时候链接都有内容需要我么去接收,我们这时候就需要判断出有内容的链接,并处理他。
我们自己来考虑的话,可能会想到多线程来处理,但是多线程来处理是不是会有问题呐,对,会带来相当多的上下文切换,上下文切换是很浪费性能的。
文件句柄:通俗来说,当我们使用一个文件,会首先返回一个文件句柄,通过句柄来处理文件
FD用于描述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
二、Select
sockfd = socket(AF_INET,sOCK_STREAM,0);
memset(&addr,0,sizeof (addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(2000);
addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd,(struct sockaddr*)&addr,sizeof(addr));
listen(sockfd,5);
for(i=0; i<5;i++)
{
memset(&client,0,sizeof (client));
addrlen = sizeof(client);
fds[i] = accept(sockfd,(struct sockaddr*)&client&addrlen);
if(fds[i] > max)
max = fds[i];
}
while(1){
FD_ZERO(&rset); //全部置位0
for (i = 0; i< 5; i++ ) {
FD_SET(fds[i],&rset); //将文件描述在rset让的位置置为1
}
puts( " round again");
// var1 被监听的文件描述符总数+1, var2 可读事件, var3 可写事件
// var3 异常事件 ,var 4超时时间
select(max+1,&rset,NULL,NULL,NULL);
for(i=0;i<5;i++) {
if (FD_ISSET(fds[i],&rset)) //测试某个位置是否被置位
{
//处理数据逻辑
memset(buffer,0,MAXBUF);
read(fds[i],buffer,MAXBUF);
puts(buffer);
}
}
}
while(1)上部分的代码是监听端口获取文件描述符(连接)。把这些文件描述符放到一个数组中,这些文件描述符是随机无序的,我们需要找出一个在里面找出一个最大的来。
我们再来看一下select函数 ,我们关注读文件描述符,其他的可以不传,超时时间有默认值。
读文件描述符 rset是什么呐,是一个bitmap 比如说 ,文件描述符为10,我们就把第10位置为1。bitmap默认1024位。
上面对reset的解释在应用上符合逻辑也比较好理解,但是reset在内核中的真实样貌是:
#undef __NFDBITS
#define __NFDBITS (8 * sizeof(unsigned long)) 计算一个long类型有几个bit
#undef __FD_SETSIZE
#define __FD_SETSIZE 1024
#undef __FDSET_LONGS
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
typedef struct {
unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;
reset里面创建了一个只能表示1024bit的long数组。1024是在代码里写死的,也可以改。
我们的select函数运行过程中会直接把rset拷贝到内核态来运行判断的。
没数据的时候:内核会一直遍历判断
有数据的时候,会将我们rset对应的位置置位,select不再阻塞会返回。
然后会执行for循环,判断哪个文件标识符(对应bitmap里面的位置,不是真正的文件描述符)被置位了,给读出来
这个max+1是什么意思那,是卡一下最少遍历长度,提高效率。
大小限制:
可监控的文件描述符个数取决与sizeof(fd_set)的值。我这边服务器上sizeof(fd_set)=512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。据说可调,另有说虽然可调,但调整上限受于编译内核时的变量值。
缺点
1、bitmap 默认1024,有上限
2、bitmap会置位,不可重用,每次执行需要恢复bitmap初始化
3、rset整体切换还是涉及到用户态内核态切换,交给内核态去修改。
4、有数据返回的时候还是会O(n)的复杂度去遍历
三、POll
// 我们发现poll的变化 是用结构体存储数据了,而不是使用bitmap了
struct pollfd {
int fd; //文件描述符
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
}
for (i=0; i<5;i++)
{
memset(&client,0,sizeof (client));
addrlen = sizeof(client);
pollfds[i].fd =
accept(sockfd,(struct sockaddr*)&client,&addrlen);
pollfds[i].events = POLLIN; //表示我们要监听读就绪事件
}
sleep(1);
while(1){
puts("round again");
// pollfds数组第零个元素的指针,pollfds结构体数组大小,超时事件单位ms
// 返回值小于0,表示出错,等于0,表示poll函数等待超时,大于0,表示poll由于监听的文件描述符就绪返回,返回就绪的个数。
poll(pollfds ,5,50000);
for(i=0; i<5;i++) {
if (pollfds[i].revents & POLLIN){
//处理逻辑
pollfds[i].revents = 0;
memset(buffer,0,MAXBUF);
read(pollfds[i].fd,buffer,MAXBUF);
puts(buffer);
}
}
}
上面是准备文件描述符,下边是操作,这次poll不是使用的bitmap了,而是做了一个结构体。
也是把这个结构体拷贝到内核态去操作数据
有事件的话会置位revents这个字段为发生的事件。
poll函数返回
找到变化之后,会首先初始化revents再读数据。
改进:
他是用数组来管理的没有最大连接数的限制。
缺点:
-
poll采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
-
在轮询期间,需要复制大量的句柄数据结构到内核空间,产生巨大的开销;
-
还是需要遍历整个数组才能发现哪些句柄发生了事件
-
触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程
select和poll的区别
1、select采用bitmap表示文件描述符,bitmap不可重用每次需要重新置位。poll函数传入pollfd结构体数组,内部将pollfd结构体数组转换为pollfd链表进行操作。
2、select需要将rset拷贝到内核态,poll需要复制大量的句柄数据结构到内核空间。
3、select大小受限于最大文件描述符,poll不受限制。
4、调用返回之后,两者都需要轮询判断
5、poll是水平触发
四、epoll
struct epoll_event events[5];
int epfd = epoll_create(10);
for (i=0;i<5;i+t){
static struct epoll_event ev;
memset(&client,0,sizeof (client);
addrlen = sigeof(client);
ev.data.fd = accept(sockfd,(struct sockaddr*)&client,&addrlen);
ev.events = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
}
while(1){
puts( "round again");
nfds = epoll_wait(epfd,events,5,10000);
for(i=0; i<nfds ;i++) {
memset(buffer,o,MAXBUF);
read(events[i].data.fd,buffer,MAXBUF);
puts(buffer);
}
}
第一步:使用epoll_create(10) ,我们会创建一个eventpoll的结构体,这个参数是需要监听的文件描述符大致的个数,意义不大。
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
首先明确一点epfd在epoll中是内核态和用户态共享的。比poll还要复杂,复杂带来的好处就是方便了我们快速定位有事件的文件描述符。
第二步:使用epoll_ctl方法在eventpoll结构上添加数据。
// var1 我们的eventpoll结构, var2 我们要执行的操作, var3 文件描述符, var4 与pollfd类似的结构体
epoll_ctl(epfd,EPOLL_CTL_ADD,ev.data.fd,&ev);
每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些新添加进来的事件会被封装为下面的结构体
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
这些事件结构体会挂载到红黑树上,重复事件依据红黑树的特性很快就会识别出来。所有添加在epoll中的事件都会与设备驱动程序建立回调关系,就是,当相应的文件描述符发生事件是,会调用这个回调方法,将我们的事件添加到eventpoll中的rdlist双链表中,这个地方就是为什么epoll不需要去遍历的原因了。
第三步:我们来看一下epoll_wait。
// var1 我们的eventpoll结构,var2 事件,var3文件描述符数量,var4超时事件
epoll_wait(epfd,events,5,10000);
成功时:返回为请求的I / O准备就绪的文件描述符的数目;
超时时:返回零。
错误时:返回-1。
epoll支持百万级别句柄的监听
小结
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
极其高效的原因:
这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket封装为一个结构放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄(这个socket文件描述符)的中断到了(有事件了),就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。(注:好好理解这句话!)
执行epoll_create时,创建了红黑树和就绪链表,执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。