IO多路复用
1、前言
IO多路复用指的是在单个线程中以阻塞的方式同时监控多个文件描述符上的IO事件。Linux中常用的IO多路复用方法主要为:select
、poll
和epoll
,三者各有优缺点。
2、原理
2.1 select
select
的基本原理是通过将要监控的fd
集合以fd_set
的方式传递给内核,其中fd_set
可以理解为内核中描述fd
的位图,被置位的位意味着要监控的fd
。这样的句柄位图,它可以向内核传递三个,分别是用来监控读、写、异常三个事件的,原型如下所示:
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
nfds
:要监控的fd
集合中,最大的fd+1
。内核会从0开始,扫描这个位图,直到nfds
,从而避免不必要的扫描readfds
:当句柄集合中的句柄可读时(有数据到来了),进程将被唤醒writefds
:监控可写事件,与readfds
类似exceptfds
:监控异常事件,与readfds
类似
标准库提供了一些辅助宏定义,包括FD_ZERO
、FD_SET
、FD_ISSET
、FD_CLR
,分别用于位图的清零、fd
的置位,fd
是否置位检测、fd
取消置位。
- 优点:使用简单、方便,跨平台性良好
- 缺点:监控的
fd
数量有上限(一般为1024个);需要遍历位图来确定哪个句柄上事件到来了,效率低。
2.2 poll
poll
的原理与select
类似,不同的是poll
传递给内核的是一个struct pollfd
类型的数组,该结构体定义如下:
struct pollfd {
int fd; //要监控的fd
short events; //要监控的事件,如POLLIN(read)、POLLOUT(write)等
short revents; //返回(命中)的事件
};
poll
函数的原型如下:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
-
fds
:struct pollfd
类型的数组 -
nfds
:数组的长度,即要监控的fd
的个数 -
timeout
:超时时间 -
优点:相比于
select
,能监控更多的事件,且没有长度限制 -
缺点:没有摆脱需要遍历所有的
fd
来检查谁触发了事件,效率低
在内核实现方面,poll
和select
底层实现基本相同。以UDP
套接字为例,每个sock
都有一个等待队列,poll
会将当前进程添加到要监控的等待队列上,一旦有数据到达,等待队列上的进程就都会被唤醒。然后,内核会根据被监控sock
的状态,判断自己所关注的事件是否触发,并将所触发的事件返回给用户态。其流程如下图所示:
2.3 epoll
epoll
在机制上与poll
和select
差距比较大,后面的两种方式属于“即时型”的,即每次调用的时候都需要把我们要监控的句柄和事件信息传递给内核。这部分的工作看起来是多余的,因为一般来说我们关注的fd
和事件都不会每次都不一样,因此这种数据拷贝实际上是不必要的,特别是在fd
数量比较多的时候,会产生大量的无用拷贝。
epoll
机制改变了这种状况,其实现机制为:创建一个新的类型为epoll
的句柄,并将要监控的句柄和事件绑定到这个句柄上,然后在这个句柄上进行等待事件触发。从这里可以看出,要监控的句柄只需要传递给内核一次,而且内核会把触发了事件的句柄传递给用户态,从而用户态不需要遍历所有句柄。该机制提供了三个接口,包括:
-
epoll
句柄创建。下面的函数会返回一个文件句柄,代表epoll
的句柄。参数size
会被忽略,但是不能为0。int epoll_create(int size);
-
句柄和事件的管理。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
这个函数用于管理
epoll
上的句柄和事件,各个参数含义为:epfd
:使用epoll_create
创建的句柄op
:要进行的操作,包括EPOLL_CTL_ADD
、EPOLL_CTL_MOD
和EPOLL_CTL_DEL
,意思很明显fd
:要进行操作的fd
event
:要操作的事件
-
事件的等待。下面的函数会在
epfd
上等待,直到有事件触发,与poll
类似。不同的是,它会将被触发了的fd
通过events
传递给用户态,其中maxevents
为events
数组的长度。int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
优点:select
和poll
的增强版,摆脱了前两者效率低、需要重复传参数的问题,没有句柄数量的限制。
缺点:需要调用额外的接口,操作比较繁琐。