一、五种I/O模型
1、阻塞I/O
我们在前面所说的I/O模型都是阻塞I/O,即调用recv系统调用,如果没有数据则阻塞等待,当数据到来则将数据从内核空间(套接口缓冲区)拷贝到用户空间(recv函数提供的buf),然后recv返回,进行数据处理。
2、非阻塞I/O
我们可以使用 fcntl(fd, F_SETFL, flag | O_NONBLOCK); 将套接字标志变成非阻塞,调用recv,如果设备暂时没有数据可读就返回-1,同时置errno为EWOULDBLOCK(或者EAGAIN,这两个宏定义的值相同),表示本来应该阻塞在这里(would block,虚拟语气),事实上并没有阻塞而是直接返回错误,调用者应该试着再读一次(again)。这种行为方式称为轮询(Poll),调用者只是查询一下,而不是阻塞在这里死等,这样可以同时监视多个设备:
while(1)
{
非阻塞read(设备1);
if(设备1有数据到达)
处理数据;
非阻塞read(设备2);
if(设备2有数据到达)
处理数据;
..............................
}
如果read(设备1)是阻塞的,那么只要设备1没有数据到达就会一直阻塞在设备1的read调用上,即使设备2有数据到达也不能处理,使用非阻塞I/O就可以避免设备2得不到及时处理。
非阻塞I/O有一个缺点,如果所有设备都一直没有数据到达,调用者需要反复查询做无用功,如果阻塞在那里,操作系统可以调度别的进程执行,就不会做无用功了,在实际应用中非阻塞I/O模型比较少用,经常与IO multiplexing 一起使用。
3、I/O复用
用select来管理多个I/O,当没有数据时select阻塞,如果在超时时间内数据到来则select返回,再调用recv进行数据的复制,recv返回后处理数据。
4、信号驱动I/O
先注册SIGIO信号的处理函数,进程继续执行其他操作,当数据到来时会发送SIGIO信号给进程,然后可以在信号处理函数中调用recv进行数据的复制,然后recv返回进行数据处理。
5、异步I/O
aio_read 函数也会提供一个buf,系统调用进入内核,如果没有数据则立即返回,进程继续执行其他操作,所以叫异步I/O,当数据到来时内核自动复制数据,然后推送给用户空间,通过在aio_read中指定的信号通知进程,让其处理数据。异步I/O跟信号驱动I/O的不同之处在于,它不用调用recv进行数据的复制,如果将后者比做”拉pull“,则前者可以认为是”push推“,push的效率会高点,其实异步I/O跟windows下面的完成端口差不多,但aio_read的实现或多或少存在问题,用得也比较少。
二、select函数简介
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数1:读写异常集合中的文件描述符的最大值加1;
参数2:读集合,关心可读事件;
套接口缓冲区有数据可读
对等连接的写一半关闭。即接收到FIN段,读操作将返回0
如果是监听套接口,已完成连接队列不为空时。
套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。
参数3:写集合,关心可写事件;
套接口发送缓冲区有空间容纳数据。
对等连接的读一半关闭。即收到RST段之后,再次调用write操作。
套接口上发生了一个错误待处理,错误可以通过getsockopt指定SO_ERROR选项来获取。
参数4:异常集合,关心异常事件;
套接口存在带外数据(TCP头部 URG标志,16位紧急指针字段)
参数5:超时时间结构体
对于参数2,3,4来说,如果不关心对应事件则设置为NULL即可。注意5个参数都是输入输出参数,即select返回时可能对其进行了修改,比如集合被修改以便标记哪些套接口发生了事件,时间结构体的传出参数是剩余的时间,如果设置为NULL表示永不超时。用select管理多个I/O,select阻塞等待,一旦其中的一个或多个I/O检测到我们所感兴趣的事件,select函数返回,返回值为检测到的事件个数,并且返回哪些I/O发送了事件,遍历这些事件,进而处理事件。注意当select阻塞返回后,此时调用read/write 是不会阻塞的,因为正是有可读可写事件发生才导致select 返回,也可以认为是select 提前阻塞了。
下面是4个可以对集合进行操作的宏:
void FD_CLR(int fd, fd_set *set); // 清除出集合
int FD_ISSET(int fd, fd_set *set); // 判断是否在集合中
void FD_SET(int fd, fd_set *set); // 添加进集合中
void FD_ZERO(fd_set *set); // 将集合清零
select函数的举例应用看这里。