概述
- I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能很重要。
通常,网络程序在下列情况下需要使用I/O复用技术:- 客户端程序要同时处理多个socket。
- 客户端程序要同时处理用户输入和网络连接。
- TCP服务器要同时处理监听socket和连接socket。
- 服务器要同时处理TCP请求和UDP请求。
- 服务器要同时监听多个端口,或者处理多种服务。
select系统调用
select
- select系统调用的用途是,在一段时间内,监听用户感兴趣的文件描述符上的可读、可写或异常等事件。
- nfds参数指定被监听的文件描述符的总数。它通常被设置为select监听的所有文件描述符中最大值加1,因为文件描述符是从0开始计数的。
- readfds,writefds和exceptfds参数分别指向可读、可写和异常事件对应的文件描述符集合。
- 应用程序调用select时,通过这3个参数传入自己感兴趣的文件描述符。
select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。
- 可以看到,fd_set结构体仅包含一个整形数组,该数组的每一个元素的每一位标记一个文件描述符。
fd_set能容纳的文件描述符的数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
- timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。
不过不能完全信任select调用返回的timeout值,比如调用失败时timeout的值是不确定的。- 这两个参数都传0,select将立即返回。
- 如果给timeout传NULL,select将一直阻塞,直到某个文件描述符就绪。
- select成功时返回就绪(可读、可写和异常)的文件描述符的总数。
如果在超时时间内没有任何文件描述符就绪,select将返回0。
select失败时返回-1并设置errno。
如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。
文件描述符就绪条件
- 在网络编程中,下列情况下socket可读:
- socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT。
此时可以无阻塞地读socket,并且读操作返回的字节数大于0。 - socket通信的对方关闭连接。
此时对该socket的读操作将返回0。 - 监听socket上有新的连接请求。
- socket上有未处理的错误。
此时可以用getsockopt来读取和清除该错误。
- socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT。
- 下列情况下socket可写:
- socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。
此时可以无阻塞的写该socket,并且写操作返回的字节数大于0。 - socket的写操作被关闭。
对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号。 - socket使用非阻塞connect连接成功或失败(超时)之后。
- socket上有未处理的错误。
此时可以使用getsockopt来读取和清楚该错误。
- socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。
- 网络程序中,select能处理的异常只有一种:
- socket上接收到带外数据。
处理带外数据
- socket上接收到普通数据和带外数据都将使socket返回,但socket处于不同的就绪状态:
- 接收到普通数据处于可读状态
- 接收到带外数据处于异常状态
poll系统调用
- poll系统调用和select系统调用类似,也是在指定时间内沦陷一定数量的文件描述符,以测试其中是否有就绪者。
- fd成员指定文件描述符
events成员告诉poll监听fd上的哪些事件,它是一系列事件的按位或
revents成员则由内核修改,以通知应用程序fd上实际上发生了哪些事件(下图)。
nfds参数指定被监听事件集合fds的大小
timeout参数指定poll的超时值,单位是毫秒。当timeout为-1时,poll调用将永远阻塞,直到某个事件发生;当timeout为0时,poll调用将立即返回。 - poll系统调用的返回值的含义与select相同。
- 上表中:POLLRDNORM,POLLRDBAND,POLLWRNORM,POLLWRBAND由XOPEN规范定义。
它们实际上是将POLLIN事件和POLLOUT事件分得更细,以区别对待普通数据和优先数据。但Linux并不完全支持它们。 - 通常,应用程序需要recv调用的返回值来区分接收到的是有效数据还是对方关闭连接的请求,并做相应的处理。
不过,自Linux2.6.17开始,GNU为poll系统调用增加了一个POLLRDHUP事件,它在socket上接收到对方关闭连接的请求之后触发。这为我们区分上述两种情况提供了一种更简单的方式。但使用POLLRDHUP事件时,我们需要在代码最开始处定义_GNU_SOURCE。
epoll系统调用
内核事件表
- epoll是Linux特有的I/O复用函数。
它在实现和使用上和select,poll有很大差异。- 首先,epoll使用一组函数来完成任务,而不是单个函数。
- 其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和epoll那样每次调用都要重复传入文件描述符集或事件集。
- 不同的是,epoll需要一个额外的文件描述符,来唯一标识内核中的这个事件表(使用下面的epoll_create来创建这个额外的文件描述符)。
- epoll_create
- size参数并不起作用,只是给内核一个提示,告诉它事件表需要多大。
- 该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
- 操作内核事件表的函数:
- fd是要操作的文件描述符
- op是指定操作类型
EPOLL_CTL_ADD:往事件表中注册fd上的事件
EPOLL_CTL_MOD:修改fd上的注册事件
EPOLL_CTL_DEL:删除fd上的注册事件 - 结构体events成员描述事件类型。epoll支持的事件类型和poll基本相同。但epoll有两个额外的事件类型:EPOLLET和EPOLLONESHOT。它们对epoll的高效运作非常关键。
- epoll_data_t联合体,4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。
ptr成员可用来指定与fd相关的用户数据。
由于epoll_data_t是一个联合体,所以不能同时使用fd和ptr,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能使用其他手段,比如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。 - epoll_ctl成功时返回0,失败则返回-1并设置errno。
epoll_wait
- epoll_wait系统调用的主要接口是epoll_wait函数。
它在一段超时时间内等待一组文件描述符上的事件。 - 调用成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。
- epoll_wait如果检测到事件,就将所有就绪事件从内核事件表(由epfd指定)中复制到它的第二个参数events指向的数据中。
这个数据只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。
这就极大地提高了应用程序索引就绪文件描述符的效率。
LT和ET
- epoll对文件描述符的操作有两种模式:
- LT:水平触发
- ET:边沿触发
- LT模式是默认的工作模式,这种模式下的epoll相当于一个效率价高的poll。
当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。 - ET模式是epoll的高效工作模式。
- 对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。
这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。 - 而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即注册该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
- 可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT效率高。
EPOLLONESHOT事件
- 即使使用ET模式,一个socket上的某个事件还是可能被触发多次。
这在并发程序中可能会引起一个问题。比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据处理过程中该socket上又有新的数据可读(EPOLLIN再次被触发),此时另外一个线程(或进程)被唤醒来读取这些新的数据。
于是,就出现了两个线程同时操作一个socket的局面。然而,这并不是我们期望的。我们期望的是,一个socket连接在任一时刻都只能被一个线程处理。
这一点,可以使用epoll的EPOLLONESHOT事件实现。 - 对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,触发我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件。
- 这样,对于当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。
反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个socket下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux 高性能服务器编程:高级I/O函数11/28
- ♥ Linux_ 命令大全 系统设置03/16
- ♥ 【Manjaro】Vmware分辨率不能修改03/22
- ♥ Linux 高性能服务器编程:网络基础编程一11/27
- ♥ Linux 高性能服务器编程:高性能定时器12/18
- ♥ Linux 高性能服务器编程:TCP一11/21