I/O多路复用是这样一种机制:通过一个进程去监视多个文件描述符,一旦其中某个描述符就绪(通常是读就绪或者写就绪),就去通知程序进行相应的读或写操作,如果始终没有描述符就绪,则一直阻塞直到超时。
目前支持I/O多路复用的常见系统调用有select、poll和epoll。注意,这三者本质上还是属于同步I/O。
一、select
select函数监视的文件描述符有三类,分别是readset、writeset和exceptset。调用该函数后,函数就处于阻塞状态,直到有描述符就绪(有数据可读、可写、异常),或者超时,这时函数返回后,就可以通过遍历数据结构fd_set来找到已就绪的文件描述符。函数原型如下:
//若有就绪描述符则返回其数目,若超时则返回0,若出错则返回-1
int select( int maxfdpl, fd_set *readset, fd_set *writeset,
fd_set *exceptset, const struct timeval *timeout);
在连接的文件描述符数量不大时,select函数性能是可以的,但是一旦描述符数量过大,而实际活跃的描述符数量又极少时,性能就有问题了。所以select函数一般有以下几个缺点:
- 每次调用select都需要把所有的fd从用户态拷贝到内核态,当fd很多时开销比较大;
- 每次调用select都需要在内核线性扫描所有的socket,不管这个socket是否是活跃的,这在幅度很多时开销也比较大;
- select一次可以监视的fd数目是有限制的,默认值FD_SETSIZE是1024。
二、poll
poll本质上和select没什么区别,它将文件描述符数组传到内核态,然后查询每个fd对应的socket状态,如果某个文件描述符就绪,则将该描述符添加到该函数开始时初始化的等待队列中,并继续遍历,或者超时。之后就可以通过遍历数据结构pollfd来找到就绪的文件描述符了。函数原型如下:
//若有就绪描述符则返回其数目,若超时之前没有任何描述符就绪则返回0,若出错则返回1
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
poll相对select改进的一点是取消了文件描述符最大连接数的限制,不再局限于1024,但同时又多了一个缺点,就是如果报告了就绪的fd后却没有处理,在下次调用poll时会再次报告这个fd。
以上,select和poll返回后,都需要通过遍历所有连接着的文件描述符来获取已经就绪的socket。而事实上,同一时间连接的socket一般只有很少数目的描述符处于就绪状态,因此随着监视的描述符数量的增长,效率会直线下降。
三、epoll
epoll在linux内核中申请了一个简易的文件系统,把原先的select调用或者poll调用分成了三个部分:
- 调用epoll_create建立一个epoll对象,返回的文件描述符将用作其它所有epoll系统调用的第一个参数,指定要访问的内核事件表;
- 调用epoll_ctl向内核事件表中添加、删除或修改文件描述符;
- 调用epoll_wait收集就绪的文件描述符的个数。
因此在实际获取就绪的文件描述符时,只需要调用epoll_wait就可以,无需遍历所有的连接。函数原型如下:
//size参数目前并不起作用,只是给内核一个提示,告诉它事件表需要多大
int epoll_create(int size);
//epfd是epoll_create函数返回的文件描述符,fd参数是要操作的文件描述符,op参数指定操作类型:
//EPOLL_CTL_ADD:往事件表中添加fd上的事件
//EPOLL_CTL_MOD:修改事件表中fd上注册事件
//EPOLL_CTL_DEL:删除事件表中fd上注册事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//成功时返回就绪的文件描述符个数,失败时返回-1并设置errno
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
从数据结构角度讲,每一个epoll对象都有一个独立的eventpoll结构体,这个结构体会在内核空间中创造独立的内存,用于存储epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂到rbr红黑树中,这样如果有重复的事件添加进来就可以通过红黑树而高效地识别出来。另外所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说当相应的事件发生时会调用这里的回调方法,回调方法会把这样的事件放到双向链表rdllist中。
当调用epoll_wait检查是否有发生事件的连接时,就只是检查eventpoll对象中的双向链表rdllist是否有epitem元素而已,如果rdllist链表不为null,则把这里的事件复制到用户态内存,并将事件数量返回给用户。同时当epoll_ctl调用向epoll对象中添加、修改、删除事件时,从红黑树rbr中查找事件也非常快。也就是说,epoll是非常高效的,可以轻易地处理百万级别的并发连接。
epoll对文件描述符的操作有两种模式:LT(Level Trigger,水平触发)模式和ET(Edge Trigger,边缘触发)模式。默认情况下,epoll采用LT模式。这时当epoll_wait检测到有事件发生并将此事件通知应用程序后,应用程序如果这次不立即处理该事件,当应用程序下一次调用epoll_wait时,epoll_wait会再次将该事件通知给应用程序,直到该事件被处理为止。若为ET模式,当epoll_wait检测到有就绪事件并通知应用程序时,应用程序会立即处理该事件,因为如果这次不处理,后续的epoll_wait又不再会报告这一事件,该事件就永远不会再处理了。因此ET模式很大程度上降低了同一个epoll事件被重复触发的次数,效率相比LT模式要高。
来源:CSDN
作者:yang1018679
链接:https://blog.csdn.net/yang1018679/article/details/104413047