Zach的博客

I/O模型

I/O模型的种类

Unix环境下可用的5种I/O模型分别为:

  • 阻塞式I/Ob
  • 非阻塞式I/O
  • I/O复用(select和poll)
  • 信号驱动I/O(SIGIO)
  • 异步I/O(POSIX的aio_系列函数)

一个输入操作的通常包括两个步骤:

  1. 等待数据准备好
  2. 从内核向进程复制数据

阻塞式I/O模型

io_block.png

在图示中,进程调用recvfrom,其系统调用直到数据到达且被复制到应用进程的缓冲区中或者发生错误才返回,最常见的错误就是被信号中断。

非阻塞I/O模型

io_nonblock.png

从图示中我们可以看出,当recvfrom没有数据可返回时,内核立即返回一个EWOULDBLOCK错误;如果有数据准备好了,那么s内核开始将数据复制到应用进程缓冲区,于是recvfrom成功返回。

这样一种对一个非阻塞描述符循环调用recvfrom的方式,我们称之为polling

I/O复用模型

io_multiplexing.png

I/O复用模型中,我们的进程阻塞于select或者poll函数,而不是阻塞在真正的I/O系统调用上,当某一个描述符数据准备好时,我们的进程被唤醒,从而可以处理数据,通过I/O复用模型,我们可以让应用进程等待多个I/O的操作完成,而不是单单阻塞于一个I/O操作。

信号驱动的I/O模型

io_signal.png

信号驱动模型在数据就绪时通过发送SIGIO通知应用进程,应用进程收到信号后可以在信号处理函数中调用recvfrom读取数据。这种模式的优势在于等待数据报到达期间,进程不会被阻塞,主循环可继续执行。但是在内核复制数据到应用进程缓冲区期间,应用进程被阻塞。

异步I/O模型

POSIX定义同步和异步I/O操作如下:

  1. 同步I/O操作:导致请求进程阻塞,直到I/O操作完成(前面四个模型按照该定义都是同步I/O)
  2. 异步I/O操作:不导致请求进程阻塞。

异步I/O由POSIX规范定义,它告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到应用进程的缓冲区)完成后通知我们。它于信号驱动的I/O模型相比,区别在于异步模型由内核通知我们什么时候I/O操作完成,而信号驱动模型由内核通知我们什么时候可以启动一个I/O操作。

select函数

select函数允许进程指示等到多个时间中的任何一个发生,并只在有一个或者多个时间发生或经历一段指定时间后才唤醒它。

函数的原型为:

1
2
3
4
#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd1, fd_set *readset, fd_set *writeset, fd_set *excepset, const struct timeval *timeout);

中断三个参数readsetwritesetexceptset指定要让内核测试读、写和异常条件的描述符。maxfd1参数指定待测试的描述符的个数,它的值是待测试描述符的最大描述符加1。在函数返回后,描述符集内任何与未就绪描述符对应的位都会被清为0,为此,每次重新调用select函数时,我们都得再次把所有需要测试的描述符对应位置位。

一般来说,为了提升性能而引入缓冲机制会增加网络应用程序的复杂性。比如用fgets读取文本行,这转而会使得一可用的文本行被读入到stdio缓冲区中,然而fgets只返回1行,其余仍然在缓冲区中,当fgets完成任务后,select函数会被再次调用以等待新的工作,它不管stdio缓冲区中是否还有数据,究其原因是select不知道stdio使用了缓冲区,它只是从read系统调用的角度指出是否有数据可用。所以在混合使用stdio和select时需要格外小心。

shutdown函数

终止网络连接的通常方法是调用close函数,但是close函数有两个限制:

  1. close把描述符的引用计数见减1,仅仅在计数变为0时关闭套接字。
  2. close终止读和写两个方向的数据传送。

利用shutdown函数,可以避免这两个限制,shutdown函数可以不管引用计数就激发TCP的正常终止序列,也可以在我们发送完数据后,只关闭写半部,仍然等待远端数据接收(反之亦可)。

函数原型为:

1
2
3
#include <sys/socket.h>

int shutdown(int sockfd, int howto)

函数的行为依赖于howto参数:

  • SHUT_RD:关闭读半部,套接字中不再有数据可以接收,而且套接字接收缓冲区中的现有数据都被丢弃。进程不能再对这样的套接字调用任何读函数,对一个TCP套接字这样调用shutdown函数后,由该套接字接收的来自对端的任何数据都被确认,然后悄然丢弃。
  • SHUT_WR:关闭写半部,对于TCP套接字这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,之后发送TCP的FIN分节,不管套接字描述符引用是否为0,写半部关闭都会执行,进程不能再对这样的套接字调用任何写函数。
  • SHUT_RDWR:连接的读半部和写半部都关闭,这和调用两次shutdown等效。

一个I/O复用的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void select_strcli(FILE *fp, int sockfd) {
int maxfdp1, stdineof, n;
fd_set rset;
char buf[MAXLINE];

stdineof = 0;
FD_ZERO(&rset);
for (;;) {
/* 每次select返回后,任何未就绪的描述符清0,所以每次都要置位*/
if (stdineof == 0)
FD_SET(fileno(fp), &rset);
FD_SET(sockfd, &rset);
maxfdp1 = max(fileno(fp), sockfd) + 1;

n = select(maxfdp1, &rset, NULL, NULL, NULL);
if (n < 0)
err_sys("select error");

if (FD_ISSET(sockfd, &rset)) {
if ((n = Read(sockfd, buf, MAXLINE)) == 0) {
if (stdineof == 1)
return; /* terminated normally */
else
err_quit("str_cli: server terminated prematurelly");
}
Write(fileno(stdout), buf, n);
}

if (FD_ISSET(fileno(fp), &rset)) {
if ((n = Read(fileno(fp), buf, MAXLINE)) == 0) {
stdineof = 1;
Shutdown(sockfd, SHUT_WR); /* send FIN, still can read */
FD_CLR(fileno(fp), &rset);
continue;
}
Writen(sockfd, buf, n);
}
}
}