在一个 web server 程序中,我们需要读取 client 请求的输入,并且需要将服务端的数据返回给 client。在 linux(Unix)系统中,任何资源都是文件,即 fd(文件描述符),所以我们需要监控需要的 fd,以便可读时,读取 client 的输入,可写时,向 client 返回服务端的数据。

假设程序有 100 个 client 访问,那么就需要从 100 个 fd 中读取数据,那么怎么做呢?

可以为每一个 client 生成一个 thred 或者 process(线程或者进程)来处理请求,但是很显然当 client 越来越大的时候,就会非常消耗资源,因为(???)

select & poll

这两种方法是任何 Unix 系统都有的,epoll 只有 linux 有,他们的工作方式都是:

  • 告诉他们你想要你知道哪些 fd 的信息
  • 然后他们会在这些 fd 可读/可写的时候通知你

select 和 poll 的定义

poll 返回了很多可能的 fd 处理结果,而 select 只返回了输入/输出/错误(select 内部做了转换)

另外一个区别就是:如果只想知道 1,3,4,19 四个 fd 的信息,poll 会遍历所有的 19 个 fd,而 select 只会遍历这 4 个

虽然 select 好一点,但是他们实际上还是依靠循环实现的,所以时间负复杂度是O(N)

信号驱动 IO

当 fd 发生变化的时候,让内核给你发一个信号

什么时候会内核会发出信号呢,有两种模式

  • level-triggered:在某状态下触发,只要在这个状态,就会一直触发
  • edge-triggered:状态变化的时候,发出信号;

epoll

  • 调用epoll_create,返回一个 epfd 的 id
    #include <sys/epoll.h>
    int epoll_create(int size);
  • 调用epoll_ctl告知内核你想要知道的 fd(可以用很多不同的文件描述符的类型)
    #include <sys/epoll.h>
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

他的参数:

  • epfd,第一步返回的 id

  • op,可选值:添加,删除,修改一个 fd

  • fd,需要监控的 fd

  • event,指向 epoll_event 的指针,表明了想要监控的事件类型

  • 调用epoll_wait开始等待

    #include <sys/epoll.h>
    int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);

他的参数:

  • epfd:第一步返回的 id
  • evlist:一个 epoll_event 的 list,由调用者创建,并在 epoll_wait 中修改,包含了 ready list
  • maxevents:evlist 的长度
  • timeout:超时时间

timeout 参数的含义:

  • 如果是 0,非阻塞,立刻返回 ready 数据
  • 如果是-1,永远 block,会被内核调入 sleep,直到有 ready 数据、或者被中断
  • 如果大于 0,block,直到有 ready 数据、或者被中断、或者达到了超时时间 timeout

epoll_wait 的返回值

  • 如果发生错误,返回-1
  • 如果超时,返回 0
  • 如果有 ready 数据,返回有数据的 evlist 的个数,然后检查 evlist 确认事件发生在哪些 fd 上

poll select 和 epoll 的性能对比

# operations  |  poll  |  select   | epoll
10            |   0.61 |    0.73   | 0.41
100           |   2.9  |    3.0    | 0.42
1000          |  35    |   35      | 0.53
10000         | 990    |  930      | 0.66

参考