一篇文章看懂select_poll_epoll
书上还有各种博客介绍select.poll.epoll基本上都是一上来就介绍数据结构,参数等等,这对新手很不友好啊,于是看到了知乎大神的帖子:https://www.zhihu.com/question/32163005
以下转自知乎:
——————————————————我是分割线——————————————————————–
假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。 现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
他们用这个东西
这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。这个东西现在还没有淘汰哦,只是变成电子的了而已。。是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:
于是大部分人都直接联想到”一根网线,多个sock复用” 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的。
重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。 是不是听起来好拗口,看个图就懂了.
在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。
什么,你还没有搞懂“一个请求到来了,nginx使用epoll接收请求的过程是怎样的”, 多看看这个图就了解了。提醒下,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
下面我们来看看这三个系统调用
了解这个基本的概念以后,其他的就很好解释了。
select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。select 被实现以后,很快就暴露出了很多问题。select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。 select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦. “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified” 霸不霸气于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。 poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:epoll 现在是线程安全的。 epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。 epoll 当年的patch,现在还在,下面链接可以看得到:/dev/epoll Home Page贴一张霸气的图,看看当年神一样的性能(测试代码都是死链了, 如果有人可以刨坟找出来,可以研究下细节怎么测的).
横轴Dead connections 就是链接数的意思,叫这个名字只是它的测试工具叫deadcon. 纵轴是每秒处理请求的数量,你可以看到,epoll每秒处理请求的数量基本不会随着链接变多而下降的。poll 和/dev/poll 就很惨了。可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。
其实有些国内知名厂商把epoll从安卓里面裁掉这种脑残的事情我会主动告诉你嘛。什么,你说没人用安卓做服务器,尼玛你是看不起p2p软件了啦。 而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。详细的在这里:Connection processing methodsPS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。 如果像是在欧朋数据中心里面的转码服务器那种动不动就是几万几十万的并发,不用epoll我可以直接去撞墙了。
作者:罗志宇
链接:https://www.zhihu.com/question/32163005/answer/55772739
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
看完了这个简介的说明之后,我们来看看这三个家伙
int select(int maxfd1,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct time val *tvptr)
参数0: maxfd1是我们关心的文件描述符的最大值+1
参数123:是指向描述符集合的指针,这些指针指向一个fd_set结构体,这个结构体中包含了我们关心的所有的描述符,并且依次是对这些描述符的动作的关心。
参数4:这个结构体如下:
struct timeval
{
long tv_sec;//秒
long tv_usec;//毫秒
}
参数4有三种情况:
*tvptr==NULL; //永远等待,当指定的描述符中的一个fd已经准备好,返回,如果捕捉到一个信号,返回-1
tvptr->tv_sec=0&&tv_ptr->usec==0;//完全不用等待,测试所有的指定的描述符并且立即返回,这是得到多个描述符的状态而不阻塞select函数的轮询方法。
tv_ptr->tv_sec!=0&&tv_ptr->usec!=0;//等待设定的时间,当有描述符准备好了或者超时则返回。如果超时还没有准备好的描述符,则返回0.
关于fd_set
为了说明方便,我们取fd_set长度为一个字节,其中每个bit对应一个文件描述符,则一个字节对应8个fd
fd_set set;
FD_ZERO(&set);//此时set为:(高位)<<<-*00000000*->>>(低位)
fd=5;
FD_SET(fd,&set);//此时set为:(高位)<<<-*00100000*->>>(低位)
fd1=0;
FD_SET(fd1,&set);//此时set为:(高位)<<<-*00100001*->>>(低位)
fd2=1;
FD_SET(fd2,&set);//此时set为:(高位)<<<-*00100011*->>>(低位)
select((5+1),&set,0,0,0)//阻塞等待
//如果fd1,fd2发生可读事件则select返回一个正值,此时set变为
(高位)<<<-*00000011*->>>(低位),没有事件的第五位被清空,此时需要我们自己去查找到底是那个fd发生了我们关心的可读事件,比如可以用>>检查。
我们来看一个客户端连接的例子:
以下图片来自:http://blog.csdn.net/lingfengtengfei/article/details/12392449
select的几大缺点:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(3)select支持的文件描述符数量太小了,默认是1024
poll
**以上就是select的有关知识,其实了解了select,poll和epoll也就不难啦。
我们继续来看看poll:**
int poll(struct pollef fdarray[],unsigned long nfds,int timeout);
和select不同的是,poll不是为每一个条件构造一个描述符集,而是构造一个pollfd结构数组,每个数组元素制定一个描述符编号以及对其关心的条件:
struct pollfd
{
int fd;
short events;//我们关心的事件
shrot revents;//实际发生的事件
}
并且poll没有限制可以监视的最大的fd的个数,你开心就好。
epoll
epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。
对于第一个缺点,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内部定义的等待队列)。这也能节省不少的开销。