I/O 多路复用指的是监听多个 fd,并在其中任一 fd 可读写时通知进程进行操作。API 有:
select()
poll()
:性能差,可移植性好epoll()
:Linux 专有 API,性能好kqueue()
:BSD 专用 API,性能好
这里主要讨论 select()
poll()
及 epoll()
。
在继续之前,要先讲讲两种不同类型的 readiness notification,分别是水平触发(level-triggered notification)以及边缘触发(edge-triggered notification)。
Level-triggered and edge-triggered
做了个视频讲解:
select 和 poll
这两个函数功能类似,poll()
功能多一些,签名分别是:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
select()
是把读、写、出错的 fd 分别以 3 个 set 传入;poll()
则是将它们都合并成一个数组,再通过数组元素的属性来区分。
什么情况下 fd 才算是 ready?
仅在可以不 block 地进行 read()
/ write()
的情况下。对于不同类型的 fd(如文件、FIFO、socket)等,ready 代表的含义也不同。参考 The Linux Programming Interface 一书第 63 章。
epoll
不同于 select 及 poll 需要不停地传入关心的 fd 列表,epoll 将该列表维护在内核中。epoll 的 API 如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
epoll_create()
会返回这个 epoll 的 fd;再通过 epoll_ctl()
将关心的 fd 加入进来:
int epfd;
struct epoll_event ev;
epfd = epoll_create(5);
if (epfd == -1)
errExit("epoll_create");
ev.data.fd = fd;
ev.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
之后调用 epoll_wait()
时,系统会把 ready 的 fd 通过参数 evlist
返回。
性能对比
可以看出 select / poll 的 CPU 消耗,基本跟监听的 fd 数量成正比;而 epoll 则不太受数量影响。性能差距非常大。
对于 select / poll,kernel 在判断哪些 fd ready 时,
- 对于 select,kernel 需要扫描 [0, nfds-1] 一共 nfds 个 fd,即使三个 fd_set 中只有一个值为 nfds-1 的 fd
- 对于 poll,kernel 需要扫描 fds 中指定的一共 nfds 个 fd;这些 fd 都是被关心的
这就意味着,对于 select 的场景,假如 fd_set 中的 fd 非常稀疏,比如 [1, 3, 5, 10000],kernel 仍然需要扫描 10001 个 fd;这是非常低效的。poll 则没有此问题。
当监听的 fd 非常多时:
- 每次调用 select / poll 时,kernel 都必须 check 每个 fd 的 ready 情况,非常耗 CPU
- 对于 select,因为参数中的 fd_set 是 value-result,即调用结果也通过修改这些参数来返回,所以每次调用完都需要重新初始化 fd_set,这带来 CPU 消耗;对于 poll,在 user space 和 kernel 间传递 fds 数组的消耗也巨大
- 调用返回时,用户程序 需要检查每个 fd 来判断是否 ready
对于 epoll,下面的原因使得它性能高:
- 由于监听的 fd 放在 kernel 中维护,少了传递参数和遍历的消耗
- 由于 kernel 知道哪些 fd 是被关心的,因此当这些 fd 有状态变化时,kernel 将其放入 ready list 并等待下次
epoll_wait()
调用时返回即可
参考
- Chapter 63, The Linux Programming Interface