在一个 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 的定义
- here’s the definition of the select syscall and do_select
- and the definition of the poll syscall and do_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