内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程。这个能力称为I/O复用。(I/O multiplexing)
6.1 I/O模型
5种I/O模型的基本区别:
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(select和poll)
- 信号驱动式I/O(SIGIO)
- 异步I/O(POSIX的aio_系列函数)
一个输入操作通常包括两个不同的阶段:
(1)等待数据准备好;
(2)从内核向进程复制数据。
对于一个套接字上的输入操作:
第一步:涉及等待数据从网络中到达。
第二步:当所等待分组到达时,它被复制到内核中的某个缓冲区。
第三步:把数据从内核缓冲区复制到应用进程缓冲区。
- 阻塞式I/O模型
- 非阻塞式I/O模型
像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询(polling)。
- I/O复用模型(select和poll)
我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。
看似I/O复用并不显得有什么优势。但使用select的优势在于我们可以等待多个描述符。
- 信号驱动式I/O模型(SIGIO)
让内核在描述符就绪时发送SIGIO信号通知我们。这种模型的优势在于等待数据报到达期间进程不被阻塞。
- 异步I/O模型(POSIX的aio_系列函数)
工作机制:告知内核启动某个操作,并让内核在整个操作完成后通知我们。
异步I/O(asynchronous I/O)模型和信号驱动式I/O的区别:
(1)
信号驱动式I/O是由内核通知我们何时可以启动一个I/O操作。
异步I/O模型是由内核通知我们I/O操作何时完成。
(2)
信号驱动式I/O是当数据报准备好被读取时,才产生信号。
异步I/O模型是直到数据已复制到应用进程缓冲区,才产生信号。
- 各种I/O模型的比较
POSIX定义:
(1)同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成;
前4种模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用模型和信号驱动式I/O模型都是同步I/O模型。因为其中真的的I/O操作将阻塞进程。
(2)异步I/O操作(asynchronous I/O operation)不导致请求进程阻塞。
只有异步I/O模型与POSIX定义的异步I/O相匹配。
6.2 select函数
该函数允许进程指示内核等待多个事件(读、写、异常条件、等待多长时间)中的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
/* 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1 */
参数struct timeval *timeout:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
这个参数有以下三种可能:
(1)设置成NULL,永远等待下去:仅在有一个描述符准备好I/O时才返回。
(2)指定秒数和微秒数,等待一段固定时间:在有一个描述符准备好I/O时返回。
(3)秒数和微秒数为0,根本不等待:检查描述符后立即返回,这称为轮询(polling)。
注意:
- 前两种情形的等待通常会被进程在等待期间捕获的信号中断,并从信号中断函数返回。
- 粗糙的微秒级,许多UNIX内核把超时值向上舍入成10ms的倍数。另外涉及调度延迟,就是说定时器时间到后,内核还需要花一点时间调度相应进程运行。
- timeout参数的const表示它在函数返回时不会被select修改。
参数readset、writeset、exceptset
测试读、写和异常条件的描述符。
目前支持异常条件只有两个:
(1)某个套接字的带外数据的到达。
(2)某个已设置为分组模式的伪终端存在可从其主端读取的控制状态信息。
设置描述符集:
void FD_ZERO(fd_set *fdset); /* clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset); /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset); /* turn off the bit for fd in fdset */
void FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */
参数maxfdp1
maxfdp1指定待测试的描述符的个数。(而非最大值,描述符是从0开始的)。
FD_SETSIZE常值是数据类型fd_set中描述符总数,其值通常是1024.
select函数返回后,我们使用FD_ISSET宏来测试fd_set数据类型中的描述符。描述符集内任何与未就绪描述符对应的位返回时均清成0。为此,每次重新调用select函数时,我们都要再次把所有描述符集内所关心的位均置为1。
描述符就绪条件:
(1)满足下列任何一个时,一个套接字准备好读:
a)该套接字接收缓冲区中的数据字节数大于等于该套接字接收缓冲区低水位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(也就是返回准备好读入的数据)。我们可以使用SO_RCVLOWAT套接字选项设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值为1。
b)该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)。
c)该套接字是一个监听套接字且已完成的连接数不为0(已连接队列不为空)。对这样的套接字的accept通常不会阻塞。
d)其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误(pending error)也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
(2)满足下列任何一个时,一个套接字准备好写:
a)该套接字发送缓冲区中的可用空间字节数大于等于该套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)。这意味着如果我们把这样的套接字设置成非阻塞,写操作将不阻塞并返回一个正值。我们可以使用SO_SNDLOWAT套接字选项来设置该套接字的低水位标记。对于TCP和UDP套接字而言,其默认值通常为2048。
b)该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。
c)使用非阻塞式connect的套接字已建立连接,或者connect已经以失败告终。
d)其上有一个套接字错误待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回一个错误),同时把errno设置成确切的错误条件。这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取并清除。
(3)如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理。
注意:当某个套接字上发生错误时,它将由select标记为既可读又可写。
接收低水位标记和发送低水位标记的目的在于:允许应用程序控制在select返回可读或可写条件之前有多少数据可读或有大空间可用于写。
任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。
select的最大描述符数:
增大描述符集大小的唯一方法是先增大FD_SETSIZE的值,再重新编译内核。
6.3 shutdown函数
终止网络连接通常方法是调用close函数。不过close有两个限制:
(1)close把描述符的引用计数减1,仅在该计数变为0时才关闭套接字。
(2)close终止读和写两个方向的数据传递。
使用shutdown可以不管引用计数就激发TCP的正常连接终止序列。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
/* 返回:若成功则为0,若出错则为-1 */
howto参数的值:
SHUT_RD:关闭连接的读这一半——套接字中不再有数据可接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数。对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。
SHUT_WR:关闭连接的写这一半——对于TCP套接字,这称为半关闭(half-close)。当前留在套接字发送缓冲区中的数据将被发送掉,后跟TCP的正常连接终止序列。我们已经说过,不管套接字描述符的引用计数是否等于0,这样的写半部关闭照样执行。进程不能再对这样的套接字调用任何写函数。
SHUT_RDWR:连接的读半部和写半部都关闭——这与调用shutdown两次等效:第一次调用指定SHUT_RD,第二次调用指定SHUT_WR。
6.4 pselect函数
#include <sys/select.h>
#include <signal.h>
#include <time.h>
int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
const struct timespec *timeout, const sigset_t *sigmask);
/* 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1 */
(1)timespec结构:
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
(2)sigmask参数:
一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。
6.5 poll函数
poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);
/* 返回:若有就绪描述符则为其数目,若超时则为0,若出错则为-1 */
参数fdarray:
struct pollfd {
int fd; /* descriptor to check */
short events; /* events of interest on fd */
short revents; /* events that occurred on fd */
};
处理输入:POLLIN、POLLRDNORM、POLLRDBAND、POLLPRI。
处理输出:POLLOUT、POLLWRNORM、POLLWRBAND。
处理错误:POLLERR、POLLHUP、POLLNVAL。
其中处理错误的不能在events中设置,但是当相应条件存在时,就在revents中返回。
poll识别:普通(normal)、优先级带(priority band)、高优先级(high priority)。
- 所有正规TCP数据和UDP数据都被认为是普通数据;
- TCP的带外数据被认为是优先级带数据;
- 当TCP连接的读这一半关闭时(如接收了一个FIN),这也认为是普通数据,且后续的读操作将返回0;
- TCP连接存在错误既可以认为是普通数据,也可以认为是错误(POLLERR)。无论哪种情况,后续的读操作将返回-1,并将errno置为适当的值,这就处理了诸如接收到RST或超时等条件;
- 在监听套接口上新连接的可用性既可认为是普通数据,也可以认为是优先级带数据,大多数实现都将其作为普通数据考虑。
- 非阻塞式connect的完成被认为是使相应套接字可写。
参数nfds:
指定结构数组中元素的个数。
参数timeout:
指定poll函数返回前等待多长时间。指定应等待毫秒数的正值。
INFTIM被定义为一个负值。
如果不关心某个特定的描述符,可将其对应pollfd结构的fd成员置为一个负值,这样就可以忽略成员events,且返回时将成员revents的值置为0。
poll没有select存在的最大描述字数目问题。但可移植性select要好于poll。(支持select的系统比支持poll的系统要多)