linux下非阻塞I/O

平时在使用socket时,经常要把文件描述符设置为非阻塞,这样当没有数据到达时,就不会使进程阻塞,而是直接返回一个错误。在linux网络方面,有四个函数会涉及阻塞与非阻塞IO流,即读取操作,写入操作,接受外来连接,发起外出连接。接下来,就把网络编程书本上的知识点总结下。

读取操作


在linux系统中,磁盘文件可以使用read和readv函数,而网络socket则可以使用read,readv,recv,recvfrom和recvmsg五个。如果某个进程对阻塞TCP套接字读取数据,而内核缓冲区没有的数据,则进程会阻塞,知道缓冲区有数据达到,即使只有一个字节也可以唤醒tcp读取。如果是UDP数据协议,如果一个阻塞的UDP套接字接收缓冲区为空,对他调用读取操作,进程将睡眠,直到UDP数据报达到。

对于非阻塞的套接字,如果输入操作不能满足(TCP套接字至少有一个字节数据可读,UDP套接字即有一个完整的数据报可读),相应的调用将立即返回一个EWOULDBLOCK错误。

写入操作


linux对于磁盘的写入操作,有write和writev,而对于网络socket则可以使用write,writev,send,sendto和sendmesg。对于一个TCP写操作,其实就是将用户程序的数据拷贝到套接字缓冲区中,如果TCP套接字缓冲区没有空间,则进程将投入睡眠,直到有空间为止。

对于非阻塞的TCP套接字,如果缓冲区根本就没空间,则返回一个EWOULDBLOCK错误。如果缓冲区有一些空间,返回值是内核能够复制到该缓冲区的字节数。这个字节数也叫作不足计数。

UDP不存在真正的发送缓冲区。内核只是复制用户程序的数据,并把它沿着协议栈往下传送。因此对于一个阻塞的UDP套接字输出函数不会因和TCP套接字一样的原因阻塞,可能因为其他原因阻塞。

接受外来连接


接受外来连接,即accept函数。如果对于一个阻塞的套接字调用accept函数,并且尚无新的连接达到,调用进程将投入睡眠;

如果对于一个非阻塞的套接字调用accept函数,并且尚无新的连接到达,accept函数将立即返回一个EWOULDBLOCK错误。

发起外出连接


对于阻塞TCP套接字,connect调用将会阻塞,直到内核三次握手成功才可返回。而对于udp套接字而言,connect并没有三次握手的过程,只是使内核保存对端的IP地址和端口号。

对于非阻塞的TCP套接字,调用connect函数,如果返回0,则表示连接建立;如果返回小于0且errno为EINPROGRESS,则可能是正在三次握手的过程中,如果errno不是EINPROGRESS,则连接出错。

对于都是返回小于0的情况,我们可以用select函数来判断是否连接成功还是连接失败。

  1. 如果连接建立好了,对方没有数据到达,那么sockfd是可写的
  2. 如果select之前,连接就建立好了,而且对方的数据已到达,那么sockfd是可读和可写的。
  3. 如果连接发生错误,sockfd也是可读和可写的。

判断connect是否成功,就是区别第二和第三中情况,两者sockfd都是可读和可写,区分的方法是调用getsockopt检查是否错误。

如果发生错误,getsockopt 源自 Berkeley 的实现将在变量 error 中 返回错误,getsockopt 本身返回0;然而 Solaris 却让 getsockopt 返回 -1, 并把错误保存在 errno 变量中。所以在判断是否有错误的时候,要处理这两种情况。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
int conn_nonb(int sockfd, const struct sockaddr_in *saptr, socklen_t salen, int nsec)
{
int flags, n, error, code;
socklen_t len;
fd_set wset;
struct timeval tval;
flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
error = 0;
if ((n == connect(sockfd, saptr, salen)) == 0) {
goto done;
} else if (n < 0 && errno != EINPROGRESS){
return (-1);
}
/* Do whatever we want while the connect is taking place */
FD_ZERO(&wset);
FD_SET(sockfd, &wset);
tval.tv_sec = nsec;
tval.tv_usec = 0;
if ((n = select(sockfd+1, NULL, &wset,
NULL, nsec ? &tval : NULL)) == 0) {
close(sockfd); /* timeout */
errno = ETIMEDOUT;
return (-1);
}
if (FD_ISSET(sockfd, &wset)) {
len = sizeof(error);
code = getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
/* 如果发生错误,Solaris实现的getsockopt返回-1,
* 把pending error设置给errno. Berkeley实现的
* getsockopt返回0, pending error返回给error.
* 我们需要处理这两种情况 */
if (code < 0 || error) {
close(sockfd);
if (error)
errno = error;
return (-1);
}
} else {
fprintf(stderr, "select error: sockfd not set");
exit(0);
}
done:
fcntl(sockfd, F_SETFL, flags); /* restore file status flags */
return (0);
}

代码有助于理解是怎么解决这些问题冲突的。