网络应用中I/O多路复用的应用场景
- 客户端处理多个描述符(通常是交替输入+网络套接字,可以及时获取服务端发送的FIN包)
- 客户端同时处理多个套接字
- TCP服务端处理监听套接字和连接套接字
- 服务端同时处理TCP和UDP
- 服务端处理多个服务以及多种协议
I/O模型
阻塞I/O
最常见的I/O模型。默认情况下,socket是阻塞的。使用UDP数据包套接字为例,在数据就绪之前,recvfrom一直处于等待状态。
非阻塞I/O
当I/O操作不能完成时,内核不会让进程睡眠,而是返回错误。进程采用轮询方式对非阻塞描述符循环采取操作。
I/O多路复用
使用select/poll/epoll系统调用,当描述符准备就绪时,系统调用返回,并执行I/O操作。
信号驱动I/O
进程设置了信号处理程序后立即返回,当描述符就绪时,可以执行I/O操作时,内核发送SIGIO信号通知进程。
异步I/O
POSIX标准中定义了异步I/O。通常的做法是通知内核开始执行I/O操作,全部完成后发送通知。aio_或lio_设置通知的方式。
五种模型的比较
同步I/O:进程在I/O完成前会被阻塞
异步I/O:进程不会阻塞
因此,前四种模型属于同步I/O
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指定了等待时间。总共有三种方式,永久等待(直到有描述符就绪);等待(不超过)一段时间;不等待。如果进程捕捉到信号,那么前两种等待会被信号处理程序中断,会返回EINTR。有些系统的select调用返回时,会改变timeout参数的值,表示剩余的等待时间,为了可移植性,POSIX将timeout设为const。当select被中断时,为了获取剩余等待时间,只能在调用前和返回后两次获取系统时间,但是时间可能被更改。
中间的三个参数readset, writeset, exceptset指定了描述符集合,分别对read,write,excpetion进行测试。select调用修改了三个描述符集合,那些已经就绪的描述符在描述符集合中被置位。每次调用select前都要重新设置描述符集合。第一个缺点是为了检测哪些描述符已经就绪,每次需要遍历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 off the bit for fd in fdset */
int FD_ISSET(int fd, fd_set *fdset); /* is the bit for fd on in fdset ? */
maxfdp1参数指定了测试描述符的数量,它的值是被测试的最大描述符的值加1,相当于可接受从0开始的描述符个数。FD_SETSIZE是fd_set结构中描述符的数量,一般为1024,即select调用能够测试的最大描述符的值是1024,这也是select调用的第二个缺点。普通应用程序不会使用如此多数量的描述符,maxfdp1这个参数的存在可以使内核不从进程拷贝不必要的那部分fd_set,相当于提高效率。每次调用都需要拷贝描述符集合到内核空间是select的第三个缺点。
函数返回值表示所有被置位的数量,如果一个就绪的描述符在超过一个集合中被关注,计相应的次数而不是1次。0代表没有描述符就绪,-1代表错误,通常是被捕获的信号中断。
描述符就绪条件
- 读就绪:
- 套接字接收缓冲区中数据字节数在低水位线(默认为1)之上。此时read返回值大于0
- 读方向连接关闭(如TCP收到FIN包)。此时read返回0
- 对于监听套接字,已完成的连接数非0。此时accept不会阻塞
- 套接字发生错误。read返回-1
- 写就绪:
- 套接字发送缓冲区可用空间大小在低水位线之上(默认2048)并且套接字已经建立连接(TCP)或不需要连接(UDP)
- 写方向套接字关闭,write操作会产生SIGPIPE信号
- 非阻塞的connect完成连接或失败
- 套接字产生错误,write返回-1
- 当套接字产生错误时,同时标记为可读/可写
- 套接字异常:带外数据
shutdown()
客户端发送FIN包仅仅表示不会再发送数据,而读数据可能还没结束。所以需要shutsown函数。
- close()减少描述符的引用计数,只有引用计数为0才真正关闭套接字。shutdown()直接开始四次挥手
- close()关闭套接字的读写方向
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
/* SHUT_RD(0):关闭读方向
* SHUT_WR(1):关闭写方向
* SHUT_RDWR(2):关闭两个方向,相当于调用两次shutdown()
*/