select、poll、epoll是用来实现IO多路复用的函数。通过这些函数,一个线程可以同时监听多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。
select
c
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
- readfds:内核检测该集合中的文件描述符是否可读。如果想让内核检测某个IO是否可读,需要手动把文件描述符加入到该集合。
- writefds:内核检测该集合中的文件描述符是否可写。同readfds,需要手动把文件描述符加入到该集合。
- exceptfds:内核检测该集合中的文件描述符是否异常。同readfds,需要手动把文件描述符加入到该集合。
- nfds:以上三个集合中最大的文件描述符数值+1。例如集合是{0,1,5,10},那么maxfd就是11。
- timeout:用户线程调用select的超时时长。设置为null,表示如果线程会一直阻塞直到I/O事件发生;设置为非0的值,表示阻塞固定一段时间后返回;设置为0,表示检测完毕立即返回。
函数返回值:
- 等于-1:表示调用失败
- 大于0:成功,返回集合中就绪的IO总个数
- 等于0:表示没有就绪的IO
从上述的select函数声明来看,fd_set本质是一个数组,为了方便我们操作该数组,操作系统提供了以下函数:
c
// 将文件描述符fd从set集合中删除
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符是否在set集合中
void FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到fd集合中
void FD_SET(int fd, fd_set *set);
// 将set集合中,所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set);
select底层使用位图来存储文件描述符,也就是说每一位来存储文件描述符的状态。上面这些函数其实就是对位图的操作。
select执行逻辑:
- 当用户线程调用select的时候,会将fd_set拷贝到内核中。(这里发生了一次复制)
- 然后内核会遍历文件描述符检查是否数据是否就绪。
- 若发现文件描述符数据就绪,返回就绪的文件描述符数量。
select的缺点:
- select可以监听的文件描述符数量有上限。32位机默认上限为1024,64位机默认上限为2048。
- 每次调用select都需要将被监控的fds集合拷贝到内核中,高并发场景下这样的拷贝对于资源的消耗是很大的。
- select返回的时候,用户线程并不知道具体是哪些文件描述符就绪了,需要遍历被监听的文件描述符来检查。那么被监听的文件描述符越多,遍历检查的耗时越长。
poll
poll的实现和select非常相似,只是描述fd集合的方式不同。poll使用pollfd结构来存储文件描述符的状态。
c
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};
pollfd成员:
- fd:委托内核检测的文件描述符
- events:委托内核检测的fd事件(输入、输出、异常)
- revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
c
int poll(struct pollfd *fds, unsigned nfds, int timeout);
函数参数:
- fds:struct pollfd类型的数组,存储了待检测的文件描述符
- nfds:数组fds的大小
- timeout:指定poll函数的阻塞时长。等于-1表示阻塞直到IO就绪才返回;等于0表示不阻塞,不管是否有IO就绪,立即返回;大于0表示等待指定的毫秒数后返回。
函数返回值:
- -1:失败
- 大于0:表示检测的集合中已就绪的文件描述符的总个数
跟select相比,因为poll底层使用链表来存储文件描述符的状态,所以poll监听的文件描述符数量没有上限。但是跟select一样,调用poll的时候需要将所有的文件描述符拷贝到内核中,内核需要遍历检查。当poll返回之后,用户线程也需要遍历所有被监听的文件描述符来检查具体是哪些文件描述符的IO就绪了。
epoll
epoll的接口一共有三个函数:
- epoll_create:创建一个epoll句柄
- epoll_ctl:向epoll对象中添加/修改/删除要管理的文件描述符
- epoll_wait:等待其管理的文件描述符上的IO事件
epoll_create函数
c
int epoll_create(int size);
- 功能:该函数生成一个epoll专用的文件描述符。
- 参数:用来告诉内核监听的数目有多大。参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
- 返回值:如果成功,返回epoll专用的文件描述符。否则失败返回-1。
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll_ctl函数
c
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl函数是epoll的事件注册函数。在调用epoll_wait之前,需要先调用该函数来注册需要监听的描述符及对应事件。
参数:
- epfd:epoll专用的文件描述符,epoll_create的返回值。
- op:表示动作用3个宏来表示。EPOLL_CTL_ADD,注册新的fd到epfd中;EPOLL_CTL_MOD,修改已经注册的fd的监听事件;EPOLL_CTL_DEL,从epfd中删除一个fd。
- fd:需要监听的文件描述符
- events:告诉内核需要监听的事件。
返回值:0表示成功,-1表示失败。
epoll_wait函数
c
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait函数用于等待监听事件的发生。
参数:
- epfd:epoll专用的文件描述符,epoll_create的返回值。
- events:分配好的epoll_event结构体数组,epoll会将发生的事件赋值到events数组中。
- maxevents:告诉events有多少个。
- timeout:超时时间,单位为毫秒。-1表示阻塞直到IO事件发生。
返回值:
- 等于0,表示已超时。
- 大于0,表示需要处理的事件数量。
- 等于-1,表示失败。
epoll比select、poll都高效的原因:
- epoll调用epoll_create就在内核创建了一个事件监听表。后面就通过epoll_ctl往事件监听表注册需要监听的fd及对应事件。这样调用epoll_wait的时候,就可以节省要监听的文件描述符从用户空间拷贝到内核空间的开销。
- epoll会将IO就绪的文件描述符放到ready_list双向链表中,所以epoll_wait返回的时候,用户线程不需要遍历所有被监听的文件描述符,只需要遍历这个ready_list就可以知道哪些文件描述符就绪。
参考
IO多路复用------深入浅出理解select、poll、epoll的实现 - 知乎 (zhihu.com)