读取端关闭
同步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#endifint main(){int pair[2] {-1, -1};if (pipe(pair) 0) {perror(pipe2);return 1;}// 设置为非阻塞iomacOS不支持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_BUF1] {0};int n write(pair[1], new_buf, PIPE_BUF1);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.outshort 1 bytesatomic write failed: Resource temporarily unavailable输出中的Resource temporarily unavailable就是EAGAIN的文字描述。可见即使还有空间只要不能容纳下原子写入要求的全部数据就会立即失败。总结尽量每次读写管道都使用PIPE_BUF大小的buf可以免去很多麻烦但我还是建议每次读写之后检查返回值和errno以免发生问题毕竟读写加起来差不多有20种情况存在了。这也是APUE这本书推荐的做法。有一点需要注意POSIX规定了所有errno被设置成EPIPE的场景进程都会收到SIGPIPE这个信号默认行为会导致进程崩溃。但这个信号并不意味着程序发生了无法挽回的错误所以常见的做法是彻底屏蔽它然后检查write调用的返回值和errno。UDP协议套接字上的读写行为终于来到最复杂的套接字了这里说的套接字包含网络类型为INET和UDS这两种尽管他们的实现完全不同处理数据的方式也大相径庭但在read、write、send、recv这些系统调用上的行为是一样的。POSIX规定read、write如果操作对象是socket那么效果等同于调用recv和send。所以在socket的两节里我们只讨论recv和send。对UDP套接字的操作是比较简单的每次recv和send都会读取/发送一个UDP数据报而且这个操作是原子的不可中断。这意味send会把buf中所有东西全部写入后才会成功返回而且写入一但开始就不可被中断。所以不存在部分写。而recv则会尽量把下一个待读取的数据报全部读入缓冲区如果数据报的大小超过缓冲区大小则会截断截断之后数据报剩余的数据会被全部丢弃recv在截断时也会正常返回。recv同样一但开始读取就不可中断所以不存在部分读。UDP有读写缓冲区的概念这会影响它在不同io模式下的行为如果读缓冲区是空的同步io时recv会一直阻塞到有数据进来才返回非阻塞io下则直接报错并设置EAGAIN如果读缓冲区有数据不管什么模式下都会立即读取一个数据报并返回如果发送缓冲区是满的同步io时send会一直阻塞到所有数据都能写入为止而非阻塞下会直接报错并设置EAGAIN如果发送缓冲区有空间但不足以写入所有内容同步io的send会阻塞到缓冲区有足够空间然后一次性写入所有内容非阻塞io时则直接报错并设置EAGAIN如果发送缓冲区有空间写入所有数据则任意模式都不会阻塞会立即把所有数据写入并返回向没有服务监听的地址端口写数据并不会发生错误这是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直接出错可中断-1EBADFrecv返回0EOF说明所有的数据都已经被读取连接的生命周期也应该正常结束了。由此可见除了部分异常情况TCP下几乎所有的读都是部分读而且可被信号中断因此必须去检查recv的返回值并做处理。写入时的情况类似io模式写缓冲区状态连接状态send行为是否能被中断send返回值errno是否是部分写同步缓冲区有足够空间写入全部数据正常连接不阻塞写入全部数据可中断 len(buf)不设置✔同步缓冲区有空间但不能写入全部数据或者满正常连接先写入数据然后阻塞到缓冲区有空间接着写入循环往复直到全部写入可中断 len(buf)不设置✔非阻塞缓冲区有足够空间写入全部数据正常连接不阻塞写入全部数据可中断 len(buf)不设置✔非阻塞缓冲区有空间但不能写入全部数据正常连接不阻塞尽可能多写入然后返回可中断 len(buf)不设置✔非阻塞缓冲区满正常连接直接出错可中断-1EAGAIN任意任意连接已经关闭直接出错可中断-1EPIPE任意任意连接异常终止收到RST直接出错可中断-1ECONNRESET任意任意本地close了socket然后继续调用send直接出错可中断-1EBADFsend要简单一些因为它对连接状态的要求更为严格。同步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;}