select / poll / epoll 结合源码分析

基于对网络编程和基本I/O模型了解的基础上,进一步分析I/O复用的系统调用函数。

1 select/poll实现

(1) int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
nfds: 所有描述符的总数,受限于FD_SETSIZE
fd_set: 按bit位标记句柄的队列,如fd_set *readfds这个集合中包括文件描述符,监视这些文件描述符的读变化。如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时。可以传入NULL值,表示不关心任何文件的读变化。  

(2) int poll(struct pollfd *fds, nfds_t nfds, int timeout) 构造一个pollfd结构体数组,每个数组元素指定一个描述符标号及其所关心的条件
struct pollfd {
int fd; /* file descriptor */
short events; /* 由用户来设置,告诉内核我们关注的是什么 */
short revents; /* revents域是返回时内核设置的 */
}

大致流程

a 依次调用fd对应的struct file.f_op->poll()方法,检查每个提供待检测IO的fd是否已经有IO事件就绪
b 如果已经有IO事件就绪,则直接所收集到的IO事件返回,本次调用结束
c 如果暂时没有IO事件就绪,则根据所给定的超时参数,选择性地进入等待
d 如果超时参数指示不等待,则本次调用结束,无IO事件返回
e 如果超时参数指示等待(等待一段时间或持续等待),则将当前select/poll/epoll的调用任务挂起
f 当所检测的fd任何一个有新的IO事件发生时,会将上述的处于等待的任务唤醒。任务被唤醒之后,重新执行1中的IO事件收集过程,将此时收集到的IO事件返回,本次的调用过程结束。

2 epoll实现

epoll的API

(1). int epoll_create(int size)
    创建一个epoll句柄
    param size: 监听fd的数目
    return: 创建epoll实例的文件描述符
(2). int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
    注册要监听的事件类型
    param epfd:create的返回值
    param op: 动作(注册,修改,删除fd监听事件)
    param fd: 监听fd
    param event: 需要监听的事件
(3). int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件的产生
    return :准备好的文件fd
    检测到事件,就将所有就绪的事件从内核事件表中复制到events指向的数组中

epoll_create的创建过程做了什么

  • 文件系统里建file结点(占用一个fd值)
  • 内核cache里建红黑树,以存储epoll_ctl传来的socket(fd)
  • 建立一个双向链表,以存储准备就绪的事件

epol_ctl新事件如何注册(红黑树和链表有什么用)

  • 增加socket句柄时,检查在红黑树中是否存在,不存在则添加,然后向内核注册回调函数;回调函数的作用是在中断事件来临时,向就绪链表中插入数据
  • 其中,红黑树以其自平衡的特点,降低fd增删改查的复杂度(lgn)
  • 此处双向链表效果相当于队列,就绪fd先入先出

可以偷懒的epol_wait

  • 执行epoll_wait时,只需要判断就绪链表是否为空,将准备就绪的socket拷贝到用户态内存,清空就绪链表
  • LT模式下还需加个班,如果该节点确实有事件未处理,那么就会把该节点重新放入到刚刚删除掉的且刚准备好的就绪链表

epoll的两种工作模式
在这里插入图片描述

  • 水平触发(LT):在1处,不做任何操作,内核会不断通知进程文件描述符准备就绪;因此可以不立即处理
  • 边缘触发(ET):只有在0 – 1处时,内核才会通知进程文件描述符准备就绪。之后如果不在发生文件描述符状态变化,内核就不会再通知进程文件描述符已准备就绪。只支持no-block socket(重复read,直到EAGAIN)

3 select/poll与epoll有什么区别(epoll高效的原因)

从二者的实现看,区别已经很明显了,可以再结合源码看一下

区别一 epoll, select 支持的并发数

  • selct:一个进程所打开的fd(文件描述符)是有限制的,由FD_SETSIZE设置,默认值是1024/2048
  • epoll:fd上限是最大可以打开文件的数目

区别二 epoll, select/poll唤醒过程

epoll 执行回调将就绪的fd加入就绪链表,使epol_wait可以直接获取
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
    list_add_tail(&epi->rdllink, &ep->rdllist);
    // list_add_tail,先加入的节点左移,先入先出
}

select 直接唤醒(do_select实现成本增加,通过三层循环找到可操作的fd)
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync,
              void *key)
{
    return try_to_wake_up(curr->private, mode, sync);
}
此过程,epoll的实现有明显优势

区别三 维护套接字集合的方式

select/poll: 
服务器每次循环调用select()时首先需要使用FD_ZERO宏来初始化fd_set对象;
然后调用FD_SET将我们维护的这个套接字集合中的套接字加入fd_set这个集合中;
即每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
epoll,只需在注册时进行一次拷贝,后续的epol_ctl只是进行简单的维护
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章