有些进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪(也就是说输入已准备好被读取,或者描述符已能承受更多的输出),他就通知进程,这个能力称为I/O复用
5种基本I/O模型
阻塞式I/O
非阻塞式I/O
I/O复用(select和poll)
信号驱动式I/O(SIGIO)
异步I/O
一个输入操作通常包括两个不同的阶段
(1)等待数据准备
(2)从内核向进程复制数据
对于一个套接口上的输入操作,第一步一般是等待数据到达网络,当分组到达时,它被拷贝到内核中的某个缓冲区,第二步是将数据从内核缓冲区拷贝到应用缓冲区。
(1)阻塞式I/O
最流行的I/O模型是阻塞式I/O(blocking I/O)模型,默认情形下,所有的套接字都是阻塞的
上图中进程在从调用recvfrom开始到它返回的整段时间内被阻塞,recvfrom成功返回后,应用进程开始数据处理
(2)非阻塞式I/O
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成,不能把本进程投入睡眠,而是返回一个错误。
前三次调用recvfrom时没有数据可以返回,因此内核转而立即返回一个EWOULDBLOCK错误,第四次调用recvfrom时已经有数据报准备好,它被复制到应用程序缓冲区,于是recvfrom成功返回
当一个应用程序使用了非阻塞模式的套接字,它需要使用一个循环来不停的测试是否一个文件描述符有数据可读(称做 polling,轮询) 。应用程序不停的 polling内核来检查是否 I/O操作已经就绪。这将是一个极浪费CPU资源的操作。这种模式使用中不是很普遍。
(3)IO复用模型
有了I/O复用,我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞真正的I/O系统之上
我们阻塞于select调用,等待数据报套接字变为可读,当select返回套接字可读这一条件时,调用recvfrom把所读的数据复制到应用程序缓冲区内。另外使用select的优势在于我们可以等待多个描述符就绪
(4)信号驱动IO模型
可以用信号让内核在描述符就绪时发送SIGIO信号通知我们
无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已处理好被处理,也可以是数据已准备被读取
(5)异步IO模型
异步 I/O 和 信号驱动I/O的区别是:
a) 信号驱动 I/O 模式下,内核在操作可以被操作的时候通知给我们的应用程序发送SIGIO 消息。
b) 异步 I/O 模式下,内核在所有的操作都已经被内核操作结束之后才会通知我们的应用程序。
该函数允许进程指示内核等待多个事件的任何一个发生,并只在有一个或多个事件发生或经历一段指定的时间才唤醒它,也就是说我们调用select告知内核对哪些描述符(就读、写或异常条件)感兴趣以及等待多长时间,当然感兴趣的描述符可以不仅局限于套接字,任何描述符都可以用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 -出错
我们从此函数的最后一个参数开始介绍,它告诉内核等待一组指定的描述字中的任一个准备好可花多长时间,结构timeval指定了秒数和微秒数成员
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
这个参数有以下三种可能:
a. 永远等待下去:仅在有一个描述字准备好I/O时才返回,为此,我们将参数timeout设置为空指针。
b. 等待固定时间:在有一个描述字准备好I/O是返回,但不超过由timeout参数所指timeval结构中指定的秒数和微秒数。
c. 根本不等待:检查描述字后立即返回,这称为轮询(polling)。为了实现这一点,参数timeout必须指向结构timeval,且定时器的值(由结构timeval指定的秒数和微秒数)必须为0
在前两者情况的等待中,如果进程捕获了一个信号并从信号处理程序返回,那么等待一般被中断。
中间三个参数readset,wirteset和exceptset指定我们要让内核测试读写和异常条件所需的描述字,参数maxfdp1说明了被测试的描述符的个数,它的值是要被测试的最大的描述符+1
为了分配一个fd_set数据类型的描述符集,并用这些宏初始化,设置或测试该集合的每一位,有下面是四个宏函数:
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 on the bit for fd in fdset */
int FD_ISSET(int fd, fd_set * fdset); /* is the bit for fd on in fdset */
描述符就绪条件
1).满足下面四个中任意条件,则一个套接字准备好读:
a.套接字接收缓冲区的数据字节数大于等于,套接字接收缓冲区低水位线,可以用SO_RCVLOWAT套接选项来设置低水位线,对于TCP和UDP套按字,默认值为1
b.该连接的读半部分关闭(接收到了FIN的TCP连接).对这样的套接字读操作,返回0(EOF)
c.该套接字是一个监听套接字且已经完成的连接数不为0.对这样的套按字的accept通常不会阻塞
d.其上有一个套接字错误待处理.对这样的套按字的读操作将不阻塞并返回-1(错误),同时把errno设置成错误条件,这些待处理错误也可以通过指定SO_ERROR套接字选项调用getsockopt获取.
2).满足下面四个中任意条件,则一个套接字准备好写:
a.该套接字发送缓冲区的可用字节数大于等于套接字发送缓冲区低水位线的当前大小.并且或者该套接已经连接,或者套按字不需要连接(UDP),如果我们把这套接字设置成非阻塞,写操作将不阻塞并返回一个正值.可以使用SO_SNDLOWAT设置一个该套接字的低水位标记.对于TCP和UDP默认值通常为2048.
b.该连接的写半部关闭.对这样的套接写的写操作将产生SIGPIPE信号.
c.使用非阻塞式的connect的套按字已经建立连接,或者connect已经失败.
d.其上有一个套接字错误等处理。对这样的套接字进行写操作会返回-,且,把ERROR设置成错误条件,可以通过指定SO_ERROR套按选项调用getsockopt获取并清除.
3).如果一个套接字存在带外数据或者仍处于带外标记,那么它有异常条件待处理
终止网络连接的正常方法是调用close,但close有两个限制可由函数shutdown来避免:
1). close将描述字的访问计数减1,仅在此计数为0时才关闭套接口。用shutdown我们可以激发TCP的正常连接终止序列,而不管访问计数。
2). close终止了数据传送的两个方向:读和写。由于TCP连接是全双工的,有很多时候我们要通知另一端我们已经完成了数据发送,即使那一端仍有许多数据要发送也是如此。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
// 返回: 0-成功, -1-出错
该函数的行为依赖于howto参数的值:
SHUT_RD – 关闭套接字的读取数据方向的连接
SHUT_WR – 关闭套接字的写入数据方向的连接
SHUT_RDWR – 关闭套接字双向的连接
#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 sygset_t * sigmask);
//返回: 准备好描述字的个数, 0-超时 -1-出错
pselect相对于select有两个变化
1).pselect函数采用timespec结构,这个结构支持纳秒
struct timespec{
time_t tv_sec; // seconds
long tv_nsec; // nanoseconds
};
2).pselect函数增加了第六个函数:一个指向信号掩码的指针
poll提供了与select相似的功能,但当涉及到流设备时,它还提供了附加信息
#include <poll.h>
int poll(struct pollfd * fdarray, unsigned long nfds, int timeout);
// 返回: 准备好描述字的个数, 0-超时, -1-出错
第一个参数是指向一个结构数组第一个元素的指针,每个数组元素都是一个pollfd结构,它规定了为测试一给定描述字fd的一些条件。
struct pollfd{
int fd; /* descriptor to check */
short events /* events of interest on fd */
short revents /* events that occurred on fd */
};
参数nfds说明我们关心的描述字的个数,参数timeout 超时等待的时间,单位是毫秒