几种IO模型优缺点:
阻塞型:当资源临时不可获得时,调用者进程阻塞等待。节省系统资源,运行效率低。
非阻塞型(轮询):当资源不可获得时,系统调用出错返回,影响浪费系统资源,运行效率高。
多路IO复用型:既节省系统资源,服务效率又高
异步IO:
信号驱动IO:在不干扰主进程运行的情况下实现异步访问IO;
异步IO:类似于信号驱动IO,依赖底层驱动。
信号驱动I/O和异步I/O的主要区别在于,信号驱动I/O是由内核通知我们何时可以启动一个I/O操作,此时数据仍在内核空间。而异步I/O有内核通知我们IO操作何时完成,此时数据已经拷贝到了我们的用户空间。
上述五种不同I/O模型的比较:前四种I/O模型主要区别在于第一阶段,有的阻塞,有的不阻塞。第二阶段基本相同:在数据从内核空间拷贝到调用者的用户空间时,进程阻塞与读操作。而异步IO的两个阶段都没有阻塞,在数据拷贝完成后,才会让内核通知我们。
一、非阻塞IO
二、多路复用IO
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:实现IO多路转接,先构造一张存有多个描述符的表,然后这个函数阻塞,并且指示内核等待当多个描述中的任意一个准备好时,该函数才返回。
参数:
nfds:要检查的描述符数量,其值为三个描述符表中最大描述符编号的值。然后再加1.
readfds:在调用函数以前,将此值设置为要让内核测试 读是否准备好的描述符集。当函数返回时,这个值已经改变,用于指示哪些描述符字已经准备好读。可以用FD_ISSET来测试。
writefds:
在调用函数以前,将此值设置为
要让内核测试 写是否准备好的描述符集。
当函数返回时,这个值已经改变,用于指示哪些描述符字已经准备好写。可以用FD_ISSET来测试。
exceptfds
:
在调用函数以前,将此值设置为
要让内核测试异常条件的描述符集。
当函数返回时,这个值已经改变,用于指示哪些描述符字异常。可以用FD_ISSET来测试。
timeout:要等待的时间
struct timeval
{
long tv_sec; 表示要等待的秒数
long tv_usec; 表示要等待的微秒数
} ;
当timeout==NULL时,此函数永远等待,只有当指定描述符中的任意一个准备好或捕捉到一个信号时才返回。如果捕捉到一个信号。select返回-1
当timeout == 0时,完全不等待,立即返回。
当timeout >0时,等待指定的秒数或微秒数。当指定时间已经超时时立即返回,如果超时时还没有一个描述符准备好,则返回值是0.
返回值:
- 返回值-1表示出错。例如在所指定的描述符都没有准备好时捕捉到了一个信号,此时出错返回-1,这种情况下,不修改其中任何描述符集。
- 返回值0表示所有描述符都没有准备好,而且已经超时。此时,所有描述符集都被清为0.
- 返回值正值表示已经准备好的描述符个数,是这三个描述符集中已准备好的描述符之和。
注:
- 有三个描述符集,readfds、writefds、exceptfds,每个描述符集存放在fd_set数据类型中,这种数据类型中的每一位对应一个描述符。所以内核每次检查对该类型中的每一位(也就是每个描述符)进行检查,来判断当前有没有准备好的描述符。所以nfds实质上就是定义了一个内核要检查的范围(也就是要检查的描述符数量)。因为描述符时从0开始,所以nfds为描述符表中的最大描述符编号加一。
- 描述符在什么条件下准备好
对于普通文件描述符基本不会阻塞,但涉及到进程间或网络间通信时,便会阻塞,这时必须引起对select返回“准备好”的条件说得明确些
1、下列四个条件任何一个满足时,套接字准备好读:
a、可接收缓冲区的数据字节数大于0时,即有可读的数据时,将不阻塞。对于套接口而言,可以使用套接口选项SO_RCVLOWAT来设置一个低潮限度,当接收数据缓冲区的字节数大于等于此低潮限度时,套接口读操作将不阻塞并返回。
b、在面向连接的通信中,当连接到读端的写端关闭时,读操作将不阻塞且返回0. UDP不是面向连接的,所以不会返回。
c、当读操作出现错误时,这时读操作将不阻塞且返回错误。
d、调用accept,套接口是一个监听套接口且还有未完成的连接。
2、下列三个条件任何一个满足时。套接口准备好写:
a、发送缓冲区仍有可用空间时,将不阻塞。
对于套接口而言,可以使用套接口选项SO_SNDLOWAT来设置一个低潮限度,当发送数据缓冲区的字节数小于等于此低潮限度时,套接口读操作将不阻塞并返回。实际上UDP套接口没有发送缓冲区,内核只是拷贝应用进程数据并将其向协议栈的下层传递。因此一个阻塞UDP套接口上的输出操作不会阻塞。
b、在面向连接的通信中,当连接到写端的读端关闭时,写操作将产生信号SIGPIPE,终止进程。
c、当写操作出现错误时,这时读操作将出错返回。
3、如果一个套接口存在带外数据或者仍处于带外标记,那它有异常条件待
处理
注意当一个套接口出错时,它被select标记为即可读又可写。
通过调用以下四个宏函数来处理fd_set描述符集:
void FD_CLR(int fd, fd_set *set); 将描述符fd从描述符集中清除。若fd在描述符集中则返回非0值,否则返回0
void FD_SET(int fd, fd_set *set);
将描述符fd添加到描述符集中
int FD_ISSET(int fd, fd_set *set); 测试描述符fd是否被置位
void FD_ZERO(fd_set *set); 将描述符集清0
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);
除以下几点外,此函数和select相同:
- select的超时用timeval结构指定,但pselect使用timespec结构,timespec结构以秒和纳秒表示超时,而非微妙和秒。
- pselect的超时声明为const,保证了调用pselect不会改变此值
- 对于pselect可使用信号屏蔽字,若sigmask为空,那么在信号有关方面,pselect的运行状况和select相同。否则,sigmask指向一个信号屏蔽字,在调用pselect时,以原子方式安装该信号屏蔽字。在返回时恢复以前的信号屏蔽字。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:
实现IO多路转接,
poll函数用于监测多个等待事件,若事件未发生,进程睡眠,放弃CPU控制权,若监测的任何一个事件发生,poll将唤醒睡眠的进程,并判断是什么等待事件发生,执行相应的操作。poll函数退出后,struct pollfd变量的所有值被清零,需要重新设置。
参数:
fds:
指定了一个被监视的文件描述符,一般设置为一个结构体数组,指向结构体数组的第一个元素的指针。
struct
pollfd
{
int
fd
;
/* file descriptor */
描述符编号
short
events
;
/* requested events to watch */
要关心的描述符状态,其值详细见下图。
short
revents
;
/* returned events witnessed */
返回时,内核设置revents,以说明描述符发生了什么
}
;
nfds:说明了结构体数组中的个数,即描述符个数。
timeout:
当
timeout==-1
时,此函数永远等待,只有当指定描述符中的任意一个准备好或捕捉到一个信号时才返回。如果捕捉到一个信号。select返回-1
当timeout == 0时,完全不等待,立即返回。
当timeout >0时,等待指定的毫秒。当指定时间已经超时时立即返回,如果超时时还没有一个描述符准备好,则返回值是0.
返回值 :
- 返回值-1表示出错。
- 返回值0表示所有描述符都没有准备好,而且已经超时。此时,所有描述符集都被清为0.
- 返回值正值表示已经准备好的描述符个数
注意,第三部分的三个常值在events中是不能设置的,但是当相应条件存在时就在revents中返回。
三、异步IO
使用异步I/O(SIGIO)需要进程执行以下三个步骤:
1、给SIGIO信号注册处理函数。
2、设置要接受SIGIO信号的进程ID或进程组ID。以命令F_SETOWN调用fcntl函数来设置进程或进程组ID。
3、激活套接口的信号驱动
注:
对于管道文件和普通文件等用OPEN打开的文件,当以只读方式打开时,对描述符执行写操作会引起SIGIO信号。当以只写方式打开时,对描述符执行读操作,会引起SIGIO信号。(读写操作必须已经完成,即数据已经全部被读取到内核空间或者从内核空间将数据全部取出,才会发生信号)
UDP套接口上的SIGIO信号,当下述事件发生时产生SIGIO信号:
TCP套接口上的SIGIO信号,当下述事件发生时产生SIGIO信号:
- 信号驱动对TCP套接口几乎时没用的,原因是该信号产生过于频繁,并且该信号并没有告诉我们发生了什么事件。下列条件均可在TCP套接口上产生SIGIO信号。
- 在监听套接口上有一个连接请求已经完成
- 发起来一个连接拆除请求
- 一个连接请求拆除请求已经完成
- 一个连接的一半已经关闭
- 数据到达了套接口
- 数据已从套接口上发出(即输出缓冲区有空闲空间)
- 发生了一个异步错误