select和epoll的过程分析与比较
进程监听一个socket
socket有发送和接收缓冲区,读一个空缓冲区或写一个满缓冲区都会阻塞进程
进程原本在工作队列上被调度,当因读/写socket而阻塞时,进程被移出工作队列,而加入这个socket的等待队列中;
当socket可读/写时,把进程从等待队列中重新放入工作队列
select
select
是为了解决如何让一个进程一次性监听多个socket
- 首先在用户态确定要监听的socket,用fd_set来标识要监听的socket(位图bitmap)
- 调用select api,会把fd_set拷贝到内核,然后系统遍历集合把进程加入每一个要监听的socket的等待队列中
- 当任意一个socket可读/写时,进程被唤醒,遍历监听的所有socket并根据socket是否就绪重新设置fd_set,然后将进程从集合中每一个socket的等待队列中移除
- 系统将fd_set拷贝回用户态
- 在用户态程序遍历fd_set,使用fd_isset来测试fd是否就绪
select
存在的主要问题:
select
最大可监视的文件描述符数量是 1024。因为select
采用的轮询的方式,文件描述符数目越大性能越差,为了保证效率限制了最大数量。- 调用
select
时,每次都需要将fd_set从用户空间和内核空间来回拷贝,数据拷贝具有一定的开销。 select
返回的是含有所有文件描述符的集合,用户需要遍历整个集合才能发现哪些socket
产生了事件。
参阅:https://blog.csdn.net/zhougb3/article/details/79792089
epoll
- 调用
epoll_create()
创建一个eventpoll
对象(对应返回的epfd),这个对象拥有一个rdlist
双链表和一个红黑树的root - 调用
epoll_ctl
增删改fd以及其关注的事件,会在红黑树增删改fd对应结点的epitem
对象(此处已经让内核监听这个fd了);同时系统会将eventpoll对象加入socket的等待队列而非加入进程 - 当socket就绪,中断程序将socket的引用加入到
eventpoll
对象的rdlist
就绪列表中 - 任意时刻进程调用了
epoll_wait
,如果rdlist不为空则直接返回,否则阻塞进程并且将进程加入eventpoll
的等待队列中,直到超时或者(rdlist不为空?) - 进程从
epoll_wait
返回,遍历rdlist
获取就绪的socket
参考:https://zhuanlan.zhihu.com/p/64746509
有个点还没弄明白,当epoll_wait阻塞时如果有socket就绪,那么修改rdlist到唤醒进程这一系列过程是怎么样的?
为什么epoll更高效
- select需要遍历整个fd集合来检查哪些fd是就绪的,而epoll直接得到的是已就绪fd的集合
- select需要把整个集合拷贝到内核,又拷贝回来,epoll则只需要把就绪的fd集合拷贝到用户空间
- epoll将阻塞与监听fd是否就绪(或者叫维护等待队列)分开了,在没有调用
epoll_wait
时内核已经在监听socket是否就绪并且可以把已就绪的socket加入就绪列表了,而select要调用时才去监听
LT & ET
做过FPGA编程类似课的应该知道,电信号有高电平和低电平,从低电平到高电平的过程被称为上升沿,如果知道这个那ET边沿触发一定很好理解。像电信号一样,我们可以把文件描述符分成两种状态:可读、不可读(可写、不可写也是一样的故不分开讨论了),那么
水平触发LT
即是指当缓冲区有数据可读时就会触发可读事件 / 缓冲区不满有位置可写时就会触发可写事件边沿触发ET
指当缓冲区从不可读变化到可读时会触发可读事件 / 缓冲区从满不可写变化到到不满可写时会触发可写事件
如果使用LT,我们可以只读取一部分数据,剩下未读的数据还会触发可读事件;而对于ET,我们需要循环读取出所有数据直到EAGAIN,其实就是要读到缓冲区状态从可读变化为不可读才行,否则缓冲区一直是可读状态没有状态变化无法触发ET事件,我们就可能会遗漏一部分事件。
当然很容易看出采取ET的话,事件触发的次数会更少,这也使得ET理论上效率比LT更高。
抉择
这一部分摘自https://segmentfault.com/a/1190000004597522
对于读事件而言,总体而言, 采用水平触发方式较好。应用程序在读取数据时,可能会一次无法读取全部数据,边沿触发在下一次可能不会触发。如果能够保证一次读取缓存的全部数据,可以采用边沿触发,效率更高, 但同时编程复杂度也高。
对于写事件,当客户端服务端采用短连接或者采用长连接但发送的数据量比较少时(例如: Redis), 采用水平触发即可。当客户端与服务端是长连接并且数据写入的量比较大时(例如: nginx), 采用边沿触发, 因为边沿触发效率更高。
目前,linux不支持读写事件分别设置不同的触发方式,具体采用哪种方式触发,需要根据具体需求。
监听套接字事件设置
监听套接字不需要监听写事件,只需要监听读事件。
监听套接字一般采用水平触发方式。(nginx开启multi_accept时,会把监听套接字所有可读的事件全部读取,此时可以使用边沿触发。但为了保证连接不丢失,nginx仍然采用水平触发)
通信套接字设置
redis对于与客户端通信使用的套接字默认使用水平触发。
nginx对于与客户端通信使用的套接字默认采用边沿触发。
文章仅仅从是过程来分析,具体使用细节查其他博客吧,底层实现的话等我看了再更吧。
求赞求关注(>人<;)