UNIX网络编程——I/O复用:select 和 poll 函数
概述
I/O复用典型使用在下列网络应用场合。
- 当客户处理多个描述符(通常是交互式输入和网络套接字)时,必须使用 I/O复用。
- 一个客户同时处理多个套接字是可能的,不过比较少见。
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用。
- 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
- 如果一个服务器要处理多个服务或者多个协议,一般就要使用I/O复用。
I/O模型
阻塞式I/O模型
默认情形下,所有套接字都是阻塞的。
把 recvfrom 函数视为系统调用,因为我们正在区分应用进程和内核。
进程调用recvfrom,其系统调用知道数据报到达且被复制到应用进程的缓冲区或者发生错误才返回。
最常见的错误是系统调用被信号中断。
进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。
recvfrom成功返回后,应用进程开始处理数据报。
非阻塞式I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。
当一个应用进程像这样对一个非阻塞描述符循环调用 recvfrom时,称之为轮询(polling)。
应用进程持续轮询内核,以查看某个操作是否就绪。缺点是会耗费大量CPU时间。
I/O复用模型
有了I/O复用,就可以调用 select 或 poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。
阻塞于 select 调用,等待数据报套接字变为可读。当 select 返回套接字可读这一条件时,我们调用 recvfrom 把所读数据报复制到应用进程缓冲区。
select 函数
select 函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
调用 select 告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间。
感兴趣的描述符不局限于套接字,任何描述符都可以使用 select 来测试。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timeval *timeout);
返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1
最后一个参数 timeout ,它告知内核等待所指定描述符中的任何一个就绪可花多长时间。
其中 timeval 结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec; /*seconds*/
long tv_usec; /*microseconds*/
};
这个参数有以下三种可能。
- (1)永远等待下去:仅在有一个描述符准备好 I/O 时才返回。为此,把该参数设置为空指针。
- (2)等待一段固定时间:在有一个描述符准备好I/O时返回,但是不超过由该参数所指向的 timeval 结构中指定的秒数和微秒数。
- (3)根本不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个 timeval 结构,而且其中的定时器值(由该结构指定的秒数和微秒数)必须为0。
timeout 参数的 const限定词表示它在函数返回时不会被 select 修改。
中间的三个参数 readset 、writeset 和 exceptset 指定我们要让内核测试读、写和异常条件的描述符。
目前支持的异常条件只有两个:
- (1)某个套接字的带外数据的到达。
- (2)某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
select 函数修改由指针 readset、writeset 和 exceptset 所指向的描述符集,因而这三个参数都是值-结果参数。
描述符就绪条件
shutdown 函数
终止网络连接的通常方法是调用close函数。
close有两个限制,却可以使用 shutdown 来避免。
- (1)close 把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。使用shutdown可以不管引用计数就激发TCP的整除连接终止序列。
- (2)close 终止读和写两个方向的数据传送。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
返回:若成功则为0,若出错则为-1
poll 函数
poll 提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
第一个参数是指向一个结构数组第一个元素的指针。
每个数组元素都是一个 pollfd 结构,用于指定测试某个给定描述符 fd 的条件。
struct pollfd {
int fd; /*descriptor to check*/
short events; /*events of interest on fd*/
short revents; /*events that occurred on fd*/
};
下图列出了用于指定 events 标志以及测试 revents 标志的一些常值。
该图分为三个部分:第一部分是处理输入的四个常值,第二部分是处理输出的三个常值,第三部分是处理错误的三个常值。
poll 识别三类数据:普通、优先级带和高优先级。
就TCP和UDP套接字而言,以下条件引起 poll 返回特定的 revent。
- 所有正规TCP数据和所有UDP数据都被认为是普通数据。
- TCP的带外数据被认为是优先级带数据。
- 当TCP连接的读半部分关闭时,也被认为是普通数据,随后的读操作将返回0。
- TCP连接存在错误即可认为是普通数据,也可以是错误(POLLERR)。
- 在监听套接字上有新的连接可用既可认为是普通数据,也可认为是优先级数据。
- 非阻塞式 connect 的完成被认为是使相应套接字可写。
timeout参数指定 poll 函数返回前等待剁成时间。它是一个指定应等待毫秒数的正值。
INFTIM 常值被定义为一个负值。如果系统不能提供毫秒级精度的定时器,该值就向上舍入到最接近的支持值。
当发生错误时,poll函数的返回值为-1,若定时器到时之前没有任何描述符就绪,则返回0, 否则返回就绪描述符的个数,即 revebts 成员值非0的描述符个数。
小结
学习参考资料:
《UNIX网络编程 卷1:套接字联网API》 第3版