5种I/O模型
对于一个套接字上的输入操作而言,第一步通常为等待数据从网络中到达,当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
阻塞式I/O模型:
如图,当进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区中或者发生错误才返回。也就是说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。
非阻塞I/O模型
进程把一个套接字设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把进程投入睡眠,而是返回一个错误。
如图,应用进程对一个非阻塞描述符循环调用recvfrom,我们称之为轮询。应用进程持续轮询内核,以查看某个操作是否就绪。
I/O复用模型
在第5章给出的TCP回射程序中,我们遇到的主要问题在于客户阻塞与fgets调用期间,服务器会被杀死,服务器虽然正确的向客户发送了FIN,但客户看不到这个EOF直到从套接字读时为止。这样的进程需要一种预先告知内核的能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,他就通知进程,这个能力被称为I/O复用。
I/O复用通过调用select或poll阻塞在这两个系统调用中的某一个上而不是阻塞在真正的I/O系统调用上。
如图,我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,调用recvfrom把所读数据报复制到应用进程缓冲区。
信号驱动式I/O模型
用信号让内核在描述符就绪时发送SIGIO信号通知我们。
我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理。
此模型的优势某过于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
异步I/O模型
罕见,不做介绍。
各种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);
timeout参数:
timeout参数告知内核等待所指定描述符中的任何一个就绪可花多长时间。
其timeval结构用于指定这段时间的秒数和微秒数:
struct timeval{
long tv_sec; //秒数
long tv_usec; //微秒数
};
此参数有以下三种可能:
- 永远等待下去:仅在有一个描述符准备好I/O时才返回,把该参数设置为空指针即可。
- 等待一段固定时间:在有一个描述符准备好I/O时返回,但不超过参数结构指定的秒数和微秒数。
- 根本不等待:检查描述符后立即返回,即轮询。将参数结构中的定时器值设置为0。
由于内核支持的时间分辨率和调度延迟问题,定时器到后可能还会花点时间来调度相应进程运行
timeout参数不会被select修改,即即使定时器到之前select就返回了,timeou参数所指向的timeval结构不会被更新成该函数返回时剩余的秒数。
中间三fd_set类型参数:
readset、writeset、exceptset指定我们要让内核测试读、写和异常条件的描述符。
目前支持的异常条件只有两个:
1. 某个套接字的带外数据的到达
2. 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息
select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。我们分配一个fd_set数据类型的描述符集,并以下4个宏设置或测试该集合中的每一位,也可以直接赋值成另外一个描述符集:
void FD_ZERO(fd_set *fdset); //清除fdset中的所有bit
void FD_SET(int fd,fd_set *fdset); //打开fdset中的第fd个bit
void FD_CLR(int fd,fd_set *fdset); //关闭fdset中的第fd个bit
int FD_ISSET(int fd,fd_set *fdset); //测