三种I/O复用函数的比较总结

下列情况下可以用到I/O多路转接技术:
1.客户端程序要同时处理多个socket。(比如非阻塞的connect技术)
2.客户端程序要同时处理用户输入和网络连接。
3.TCP服务器要同时处理监听socket和连接socket。(I/O复用使用最多的场合)
4.服务器要同时处理TCP请求和UDP请求。
5.服务器要同时监听多个端口,或者处理多种服务。(xinetd服务)
I/O复用虽然可以同时监听多个文件描述符,但他本身是阻塞的,并且,当多个文件描述符同时就绪时。如果不采用额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这样I/O复用看起来就像是串行的,要实现兵法就需要用多进程或多线程实现。
select:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timecal* timeout);
nfds:指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件的描述符中的最大值加1.
readfds、writefds、excepfds:分别指向可读可写和异常事件对应的文件描述符集合。应用程序调用select函数时,通过这三个参数传入自己感兴趣的文件描述符select调用返回时,内核将修改他们来通知应用程序哪些文件描述符已经就绪fd_set类型结构体只包含一个整形数组,数组每一位标记一个文件描述符。这样就限制了select能同时处理的文件描述符的总量。
timeout:设置select函数的超时时间。它是一个timeval类型的指针,内核将修改它告诉应用程序select等待了多久。应为select调用完成后timeout的值是不能完全信任的。
文件描述符就绪条件:
在网络编程中,下列情况下socket可读:
socket内核接收缓冲区中接收缓冲区中的字节数大于或等于其低水位标记SO_RCVLOWAT。这时,我们可以无阻塞的读该socket,并读操作返回0。
socket通信双方关闭连接,此时对该socket的读操作将返回0。
监听socket上有新的连接请求。
socket上有未处理的错误。可以用getsockopt来清除错误。
下列情况下socket可写:
socket内核发送缓冲区中你给的可用字节数大于过等于其低水位标记SO_SNDLOWAT。
socket的写文件描述符被关闭,对写操作被关闭的socket执行写操作会触发一个SIGPIPE信号,这是,可以随意写。
socket使用非阻塞connect连接成功或者失败之后。
socket上有未处理的错误,同上。

poll
int poll(struct pollfd* fds, nfds_t nfds, int timeout)
与select类似,也是在指定时间内轮询一定量的文件描述符,以测试其中是否有已经就绪者。
fds是一个pollfd类型的数组。指定用户感兴趣的文件描述符上发生的可读可写和异常事件。pollfd类型结构体中的events告诉poll监听fd上的哪些事情,是一系列时间的按位或。revents成员由内核修改,通知应用程序上fd实际发生了哪些事件。
GUN为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。这样,应用程序就可以区分socket上接收到的是有效数据还是关闭连接的请求,并做出相应的处理。但是使用POLLRDHUP的事件,需要在代码最开始处定义_GUN_SOURCE。nfds指定被监听的事件集合fds的大小。

epoll
Linux特有的I/O复用函数。epoll使用一组函数来完成任务,而不是单个函数。epoll把用户关心的文件描述符放在内核里的一个时间表中。(事件表在内核中的实现其实就是一颗红黑树,红黑树的查找效率高,找到之后放到内核的就绪队列中)。epoll_create就是创建红黑树和就绪队列过程 。
int epoll_wait(int size)
size参数不起作用,只是告诉内核,它需要的事件表有多大。该函数返回的文件描述符将用作其他所有的epoll系统调用的第一个参数,表示要访问的内核事件表。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)
fd是要操作的文件描述符,op参数则指定操作类型。EPOLL_CTL_ADD(往事件表(epfd)上添加fd上的事),EPOLL_CTL_DEL,EPOLL_CTL_MOD。event是epoll_event类型的结构体指针。
epoll_wait
epoll系列函数调用的主要接口。在一组超时时间内等待一组文件描述符上的事件。
int epoll_wait(int epfd, struct epoll_event* event, int maxevents, int timeout)
timeout指超时时间,maxevents指定最多监听多少个事件,必须大于0。epoll_wait函数如果检测到事件,就将所有就绪的时间从内核事件表(epfd)中复制到它的第二个参数events指向的数组中去。这个数组只用于输出epoll_wait检测到的就绪事件。
LT和ET模式
epoll对文件描述符的操作有两种模式:LT(level trigger,水平触发)和ET(edge trigger,边沿触发)。LT是默认的工作模式,这种模式下,epoll相当于一个效率极高的poll。当往epoll内核事件表注册添加一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。
LT:当epoll_wait检测到其上有事件发生并将此事件通知到应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事。直到事件被处理。
ET:当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
ET在很大程度上降低了同一个epoll事件被重复触发的次数。因此,ET效率比LT模式高。

防止一个socket上的事件被触发多次:使用epoll的EPOLLONESHOT事件实现
即使我们使用ET工作模式,一个socket上的某个事件也可能被触发多次。这在并发程序就会引起一个问题。比如,一个线程(进程),在读取完某个socket上的数据后开始处理这些数据,而在数据处理的过程中该socket上又有新数据可读,这时EPOLLIN再次被触发,此时,又有一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个socket的局面。我们期望的是一个socket在任意一个时刻都只被一个线程处理。注册了EPOLLONESHOT事件
的文件描述符,操作系统最多触发其上注册的一个事件,且只触发一次,除非我们用epoll_wait函数重置该文件描述符上注册的EPOLLOONTSHOT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。注册了EPOLLONESHOT事件的socket一旦被被某个线程处理完毕,该线程就应该立即重置这个 socket傻昂的EPOLLONESHOT事件,以确保这个socket下次可读时,其EPOLLONESHOT能被触发,进而让其他工作线程有机会继续处理这个socket。
三种I/O复用函数的比较



系统调用 select poll epoll
事件集合 用户通过3个参数分别传入感兴趣的可读可写及异常事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数 统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait系统调用的参数events仅用来反馈就绪事件
应用程序索引就绪文件描述符的时间复杂度 O(n) O(n) O(1)
最大支持文件描述符个数 一般有最大限制 65535 65535
工作模式 LT LT 支持ET高效模式
内核实现和工作效率 采用轮询方式来检测就绪事件,算法复杂度为O(n) 采用轮询方式来检测就绪事件,算法复杂度为O(n) 采用回调方式来检测就绪事件,算法复杂度为O(1)

epoll高效体现在哪里?
1.底层用红黑树维护一个事件集,用一个就绪队列来存放就绪事件,查找效率很高。
2.使用mmap(内存映射技术)加速内核与用户空间的消息传递,减少拷贝,提高效率
3.内核采用callback回调机制,激活这个文件描述符(将节点放到就绪队列中去)调用epoll_wait时就会被通知。
4.I/O效率不随fd数目的增加而线性下降。






































你可能感兴趣的:(Linux,IO,C语言,Linux基础知识)