读取端关闭
2026/7/7 3:20:47 网站建设 项目流程
同步io,原子写,管道中容量足够容纳所有内容不阻塞,原子地写入所有数据,不可中断len(buf)不设置
同步io,原子写,管道没有容量或者容量不足以容纳所有内容阻塞到所有内容可被写入为止,不可中断len(buf)不设置
非阻塞io,原子写,管道中容量足够容纳所有内容不阻塞,原子地写入所有数据,不可中断len(buf)不设置
非阻塞io,原子写,管道没有容量或者容量不足以容纳所有内容不阻塞,直接出错-1EAGAIN
同步io,普通写,管道中容量足够容纳所有内容不阻塞,写入所有数据,可中断<= len(buf)不设置
同步io,普通写,管道没有容量或者容量不足以容纳所有内容阻塞到所有内容可都写入为止,可中断<= len(buf)不设置
非阻塞io,普通写,管道中容量足够容纳所有内容不阻塞,写入所有数据,可中断<= len(buf)不设置
非阻塞io,普通写,管道没有任何容量不阻塞,直接出错-1EAGAIN
非阻塞io,普通写,管道容量不足以写入所有数据不阻塞,有多少写入多少,可中断<= len(buf)不设置
任何模式,写入没开始前被信号中断直接出错-1EINTR

可以看到部分写主要发生在非原子写的情况下。看一个非阻塞io时容量不够导致部分写的例子:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#define PIPE_MAX (4096*16)
#ifdef __linux__
// #include <linux/limits.h>
#define PIPE_BUF 4096
#else
#define PIPE_BUF 512
#endif
int main()
{
int pair[2] = {-1, -1};
if (pipe(pair) < 0) {
perror("pipe2");
return 1;
}
// 设置为非阻塞io,macOS不支持pipe2,为了跨平台只能用这种原始办法
if (fcntl(pair[0], F_SETFL, fcntl(pair[0], F_GETFL) | O_NONBLOCK) < 0) {
perror("fcntl pair[0]");
return 1;
}
if (fcntl(pair[1], F_SETFL, fcntl(pair[1], F_GETFL) | O_NONBLOCK) < 0) {
perror("fcntl pair[1]");
return 1;
}
// 非原子写入,因此发生部分写
char buf[PIPE_MAX-1] = {0};
memset(buf, 'c', sizeof(buf));
if (write(pair[1], buf, PIPE_MAX-1) != PIPE_MAX-1) {
printf("this can not be a short write\n");
return 1;
}
char new_buf[PIPE_BUF+1] = {0};
int n = write(pair[1], new_buf, PIPE_BUF+1);
if (n < 0) {
perror("write");
return 1;
}
printf("short %d bytes\n", n);
if (read(pair[0], read_buf, PIPE_BUF-100) != PIPE_BUF-100) {
printf("this can not be a short read\n");
return 1;
}
// 原子写入会立即失败
char atomic_buf[PIPE_BUF];
memset(atomic_buf, 'c', PIPE_BUF);
n = write(pair[1], atomic_buf, PIPE_BUF);
if (n < 0) {
perror("atomic write failed");
return 0;
} else {
printf("no way!\n");
return 1;
}
}

程序首先写入数据,只留一字节给管道,然后非阻塞写入一个比原子写限制大一字节的数据,这时候程序就会发生部分写,只写入一字节。如果这里是同步io的话程序则会阻塞住直到剩下的所有数据都能写入。接着我们把读取PIPE_BUF-100的数据,现在管道的容量只有PIPE_BUF-100字节,然后又再往管道里原子写入PIPE_BUF长度的数据,这一步应该直接失败。

运行结果:

$ ./a.out
short 1 bytes
atomic write failed: Resource temporarily unavailable

输出中的Resource temporarily unavailable就是EAGAIN的文字描述。可见即使还有空间,只要不能容纳下原子写入要求的全部数据,就会立即失败。

总结:尽量每次读写管道都使用PIPE_BUF大小的buf可以免去很多麻烦,但我还是建议每次读写之后检查返回值和errno,以免发生问题,毕竟读写加起来差不多有20种情况存在了。这也是APUE这本书推荐的做法。

有一点需要注意,POSIX规定了所有errno被设置成EPIPE的场景,进程都会收到SIGPIPE,这个信号默认行为会导致进程崩溃。但这个信号并不意味着程序发生了无法挽回的错误,所以常见的做法是彻底屏蔽它然后检查write调用的返回值和errno。

UDP协议套接字上的读写行为

终于来到最复杂的套接字了,这里说的套接字包含网络类型为INETUDS这两种,尽管他们的实现完全不同处理数据的方式也大相径庭,但在readwritesendrecv这些系统调用上的行为是一样的。

POSIX规定readwrite如果操作对象是socket,那么效果等同于调用recvsend。所以在socket的两节里我们只讨论recvsend

对UDP套接字的操作是比较简单的,每次recv和send都会读取/发送一个UDP数据报,而且这个操作是原子的不可中断。

这意味send会把buf中所有东西全部写入后才会成功返回,而且写入一但开始就不可被中断。所以不存在部分写。

而recv则会尽量把下一个待读取的数据报全部读入缓冲区,如果数据报的大小超过缓冲区大小,则会截断,截断之后数据报剩余的数据会被全部丢弃,recv在截断时也会正常返回。recv同样一但开始读取就不可中断,所以不存在部分读。

UDP有读写缓冲区的概念,这会影响它在不同io模式下的行为:

  1. 如果读缓冲区是空的,同步io时recv会一直阻塞到有数据进来才返回;非阻塞io下则直接报错并设置EAGAIN
  2. 如果读缓冲区有数据,不管什么模式下都会立即读取一个数据报并返回
  3. 如果发送缓冲区是满的,同步io时send会一直阻塞到所有数据都能写入为止;而非阻塞下会直接报错并设置EAGAIN
  4. 如果发送缓冲区有空间但不足以写入所有内容,同步io的send会阻塞到缓冲区有足够空间,然后一次性写入所有内容;非阻塞io时则直接报错并设置EAGAIN
  5. 如果发送缓冲区有空间写入所有数据,则任意模式都不会阻塞,会立即把所有数据写入并返回
  6. 向没有服务监听的地址端口写数据并不会发生错误,这是udp协议的特性,除非你把套接字的对端地址进行了绑定

总体UDP很简单没有部分读写问题,只有数据截断需要特别注意。这在后文会讲。

TCP协议套接字上的读写行为

TCP是这些总结里面最复杂的,因为它受io模式和信号影响,同时也有读写缓冲区的概念,并且TCP是面向连接的协议,连接状态还会额外影响读写的行为。

场景实在太多,用文字描述会非常费篇幅,因此我们直接上表格:

io模式读缓冲区状态连接状态recv行为是否能被中断recv返回值errno是否是部分读
同步缓冲区空正常连接阻塞到有数据为止,然后尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
同步缓冲区有数据或者满正常连接不阻塞,尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
同步缓冲区空连接已经关闭不阻塞,直接返回可中断0不设置
同步缓冲区有数据或者满连接已经关闭不阻塞,尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
非阻塞缓冲区空正常连接直接出错可中断-1EAGAIN
非阻塞缓冲区有数据或者满正常连接不阻塞,尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
非阻塞缓冲区空连接已经关闭不阻塞,直接返回可中断0不设置
非阻塞缓冲区有数据或者满连接已经关闭不阻塞,尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
任意缓冲区空连接异常终止收到RST直接出错可中断-1ECONNRESET
任意缓冲区有数据或者满连接异常终止收到RST不阻塞,尽可能多读取信息,直到缓冲区里没数据或者buf填满可中断<= len(buf)不设置
任意任意本地close了socket,然后继续调用recv直接出错可中断-1EBADF

recv返回0(EOF)说明所有的数据都已经被读取,连接的生命周期也应该正常结束了。

由此可见,除了部分异常情况,TCP下几乎所有的读都是部分读而且可被信号中断,因此必须去检查recv的返回值并做处理。

写入时的情况类似:

io模式写缓冲区状态连接状态send行为是否能被中断send返回值errno是否是部分写
同步缓冲区有足够空间写入全部数据正常连接不阻塞,写入全部数据可中断<= len(buf)不设置
同步缓冲区有空间但不能写入全部数据或者满正常连接先写入数据,然后阻塞到缓冲区有空间,接着写入,循环往复直到全部写入可中断<= len(buf)不设置
非阻塞缓冲区有足够空间写入全部数据正常连接不阻塞,写入全部数据可中断<= len(buf)不设置
非阻塞缓冲区有空间但不能写入全部数据正常连接不阻塞,尽可能多写入然后返回可中断< len(buf)不设置
非阻塞缓冲区满正常连接直接出错可中断-1EAGAIN
任意任意连接已经关闭直接出错可中断-1EPIPE
任意任意连接异常终止收到RST直接出错可中断-1ECONNRESET
任意任意本地close了socket,然后继续调用send直接出错可中断-1EBADF

send要简单一些,因为它对连接状态的要求更为严格。同步io下send会尽量发生全部数据,但会被信号中断;非阻塞io下则是能写多少是多少,几乎都是部分写。

所以针对tcp必须检查所有读写操作的返回值和errno,这也是为什么UNP这本网络编程的名著会在头两章就给出下面这样的帮助函数:

/* Like write(), but retries in case of partial write */
ssize_t writen(int fd, const void *buf, size_t count)
{
size_t n = 0;
while (count > 0) {
int r = write(fd, buf, count);
if (r < 0) {
if (errno == EINTR)
continue;
return r;
}
if (r == 0)
return n;
buf = (const char *)buf + r;
count -= r;
n += r;
}
return n;
}

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询