我们都知道常见的IO复用有select、poll、epoll。现在来说说这几个的区别。
#include <sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
select的参数类型没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合。因此select需要三个参数分别传入可读、可写以及异常事件。因此select不能处理更多类型的数据。另一个方面,由于内核对fd_set集合的在线修改。因此下次调用要重置set集合。
fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位标记一个文件描述符。
#include <sys/select.h>
FD_ZERO(fd_set* fdset); /*清除fdset的所有位*/
FD_SET(int fd,fd_set* fdset); /*设置fdset的位fd*/
FD_CLR(int fd,fd_set* fdset); /*清除fdset的位fd*/
int FD_ISSET(int fd,fd_set* fdset); /*测试fdset的位fd是否被设置*/
假如是如图所示,nfds会返回6,告诉只检查到这,后面的不用检查。如果是3、5上有数据到达,采用偏移量记录1024个数字,在相应位置1。FD_ISSET告诉某个位是否被设置。
#include <poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
struct pollfd
{
int fd; /*文件描述符*/
short events; /*注册的事件,告诉poll监听fd上的哪些事件,它是一系列的按位或*/
short revents; /*实际发生的事件,由内核填充,以通知应用程序fd实际发生了哪些事件*/
};
poll统一事件源。把文件描述符和事件都定义其中。并且每次内核修改的是pollfd结构体的revents成员,而events成员保持不变。因此下次调用poll无需重置pollfd类型的参数。
select和poll都返回整个用户注册的事件集合。所以应用程序索引就绪文件描述符的时间O(N)。select内部是数组实现,poll内部是链表实现,所以select有最大的fd限制。poll没有限制。(系统资源假设无穷大的话)它们俩都有用户态到内核态的拷贝的过程,两者切换和数据拷贝都很消耗性能。
而epoll是一颗红黑树(过去是哈希表,现在是红黑树)。大家都知道红黑树效率很多高。另外epoll还没有内核和用户的切换,因为epoll采用了共享内存机制。epoll采用与select和poll完全不同的方式来管理用户注册的事件。它在内核中维护一张表。提供独立的epoll_ctl控制往其中添加、删除和修改事件。这样epoll_wait都直接从内核事件表中取得用户注册事件。无需反复从用户空间读入。epoll_wait调用event参数仅用来返回就绪的时间这样时间复杂度为O(1)。
select和poll都只能工作在相对低效的LT模式。而epoll则可以工作在高效的ET模式。而且epoll还支持EPOLLONESHOT事件。该事件能进一步减少可读可写和异常等事件被触发的次数。select和poll采用的都是轮询的方式。每次调用都要扫描整个注册文件描述符集合。epoll_wait不同。它采用的是回调的方式。内核检测到就绪的文件描述符时,就触发回调函数。回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时机将就绪事件队列中的内容拷贝到用户空间。因为epoll_wait无须轮询整个文件描述符集合来检测哪些事件已就绪。
epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。
#include<sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
LT模式和ET模式的区别:
LT(电平触发):不会丢失数据,是默认的工作模式。它之所以不会丢失数据是因为当有事件到达的时候,如果i不去处理这个数据,它会一直不停的提示你该去执行这个操作,直到你把事件处理后。
ET(边沿触发):会丢失数据但高效。是epoll的高效事件模式。他就是当你如果有事件到达,要求你必须马上处理,因为它只提醒你一次。如果你不去处理,之后它也不会再提醒你所以容易丢失数据。
ET模式很大程度上降低了同一个epoll事件被重复触发的次数,即使我们使用ET模式,一个socket上的某个事件还是有可能被触发多次。在并发的时候会引起问题。比如一个线程在读取完某个socket上的数据后开始处理这些数据,而数据处理的时候该socket上又有数据,此时另一个线程被唤醒来读取这些新的数据,于是就出现两个线程同时操作一个socket的局面。这个可以用epoll的EPOLLONESHOT事件来解决。注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发一次可读可写异常事件,且只触发一次。除非我们用epoll_ctl函数来重置这个文件描述符上注册的EPOLLONESHOT事件。处理完该线程要立即重置这个socket上的EPOLLONESHOT事件,确保下一次socket可读。