一、管道为空时读取
1. 阻塞模式(默认)
【示例】:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid < 0) {
perror("fork error");
return -1;
}
else if(pid == 0) {
sleep(3); //子进程休眠3s
close(pipefd[0]);
write(pipefd[1], "Hello pipe", 10);
printf("Child process write:Hello pipe\n");
close(pipefd[1]);
}
else {
close(pipefd[1]);
char buf[20] = {0};
read(pipefd[0], buf, 20);
printf("Parent process read:%s\n", buf);
close(pipefd[0]);
}
return 0;
}
【执行结果】:
【分析】:
程序执行后,我们发现并没有立即显示打印信息,而是等待了一段时间后才显示,这是为什么呢?
在上述代码中,因为我们让子进程先休眠了3s,所以在这3s期间若父进程调用了read函数,则会陷入阻塞等待,此时程序暂停运行,直到子进程休眠时间到后向管道写入了数据才开始继续运行。
2. 非阻塞模式
【示例】:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
int main() {
extern int errno;
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid < 0) {
perror("fork error");
return -1;
}
else if(pid == 0) {
sleep(3); //子进程休眠3s
close(pipefd[0]);
write(pipefd[1], "Hello pipe", 10);
printf("Child process write:Hello pipe\n");
close(pipefd[1]);
}
else {
close(pipefd[1]);
char buf[20] = {0};
int flags = fcntl(pipefd[0], F_GETFL, 0);
if(fcntl(pipefd[0], F_SETFL, flags | O_NONBLOCK) < 0) { //设置为非阻塞
perror("fcntl error");
return -1;
}
int ret = read(pipefd[0], buf, 20);
if(ret == -1) { //read若返回-1,则打印错误信息
perror("read error");
printf("errno: %d\n", errno);
return -1;
}
printf("Parent process read:%s\n", buf);
}
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,设置为非阻塞后,父进程调用 read 并没有陷入等待,而是直接返回了 -1
,并且 errno
的值被置为11
,这个值的具体定义我们可以在/usr/include/asm-generic/errno-base.h
这个头文件中查看,其定义为EAGAIN
。
3. 总结
模式 | 结果 |
---|---|
阻塞模式(O_NONBLOCK disable) | read 调用阻塞,即进程暂停执行,直到有进程写入数据 |
非阻塞模式(O_NONBLOCK enable) | read 调用返回 -1,errno 值被置为 EAGAIN |
二、管道为满时写入
1. 阻塞模式(默认)
【示例】:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
int count = 0;
while(1) {
write(pipefd[1], "0", 1);
++count;
printf("count:%d\n", count);
}
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,在默认阻塞模式下,当管道写满(65536B)时,程序调用 write 陷入了阻塞等待,此时程序暂停运行,直到有其他进程从管道读走了数据才开始继续运行。
2. 非阻塞模式
【示例】:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
int main() {
extern int errno;
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
int flags = fcntl(pipefd[1], F_GETFL, 0);
if(fcntl(pipefd[1], F_SETFL, flags | O_NONBLOCK) < 0) { //设置为非阻塞
perror("fcntl error");
return -1;
}
int count = 0;
while(1) {
int ret = write(pipefd[1], "0", 1);
if(ret == -1) { //write若返回-1,则打印错误信息
perror("write error");
printf("errno:%d\n", errno);
break;
}
++count;
}
printf("count:%d\n", count); //打印管道最大容量
return 0;
}
【执行结果】:
【分析】:
从执行结果来看,设置为非阻塞后,当管道写满时,程序再次调用 write 并没有陷入等待,而是直接返回了 -1
,并且 errno
的值被置为11
,这个值的具体定义我们在前面已经查看过,其定义为EAGAIN
。
3. 总结
模式 | 结果 |
---|---|
阻塞模式(O_NONBLOCK disable) | write调用阻塞,即进程暂停执行,直到有进程读走数据 |
非阻塞模式(O_NONBLOCK enable) | write 调用返回 -1,errno 值被置为 EAGAIN |
三、所有管道写端关闭时读取
【示例】:
#include <stdio.h>
#include <unistd.h>
int main() {
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid < 0 ) {
perror("fork error");
return -1;
}
else if(pid == 0) {
write(pipefd[1], "Hello pipe", 10); //子进程先向管道中写入数据
close(pipefd[1]); //接着关闭写端
}
else {
close(pipefd[1]); //父进程关闭写端
sleep(1); //休眠1s,确保父子进程的写端都以关闭
char buf[5];
while(1) {
int ret = read(pipefd[0], buf, 5);
printf("ret: %d\n", ret);
if(ret == 0) {
break;
}
}
}
return 0;
}
【执行结果】:
【分析】:
在上述代码中,子进程先向管道中写入了些数据,接着父子进程都关闭写端,此时让父进程从管道中读取数据,当管道中的数据都被读取完后,再次 read 会返回 0。
总结
- 如果所有管道写端对应的文件描述符被关闭(管道写端的引用计数等于0),那么管道中剩余的数据都被读取完后,再次 read 会返回 0。
四、所有管道读端关闭时写入
【示例】:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
void Handler(int sig) { //信号处理函数
printf("Received signal: %d\n", sig);
}
int main() {
extern int errno;
signal(SIGPIPE, Handler);
int pipefd[2];
if(pipe(pipefd) < 0) {
perror("pipe error");
return -1;
}
pid_t pid = fork();
if(pid < 0 ) {
perror("fork error");
return -1;
}
else if(pid == 0) {
close(pipefd[0]); //子进程关闭读端
}
else {
close(pipefd[0]); //父进程关闭读端
sleep(1); //休眠1s,确保父子进程的读端都已关闭
int ret = write(pipefd[1], "Hello pipe", 10);
if(ret == -1) {
perror("write error");
printf("errno: %d\n", errno);
}
}
return 0;
}
【执行结果】:
【分析】:
在上述代码中,父子进程的读端都被关闭,此时让父进程向管道中写入数据,则会产生 13 号信号,通过 kill -l
命令可以查看这个信号就是 SIGPIPE
信号,进而导致父进程 write 出错返回 -1
,并置 errno 为 EPIPE
,对应的出错信息是Broken pipe
。
总结
- 如果所有管道读端对应的文件描述符被关闭,则 write 操作会产生 SIGPIPE 信号,进而可能导致 write 进程退出。
五、Pipe capacity
在 Linux 下,我们可以通过 man 7 pipe
命令来查看 pipe capacity 的具体信息,如下:
A pipe has a limited capacity. If the pipe is full, then a write(2) will block or fail, depending on whether the O_NONBLOCK flag is set (see below). Different implementations have different limits for the pipe capacity. Applications should not rely on a particular capacity: an application should be designed so that a reading process consumes data as soon as it is available, so that a writing process does not remain blocked.
In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386). Since Linux 2.6.11, the pipe capacity is 65536 bytes.
上述信息主要给我们说明了管道的容量是有限的,在 2.6.11 之前的 Linux 版本中,管道的容量与系统页面大小相同(例如,i386上为4096字节)。但自 Linux 2.6.11 以来,管道容量为 65536 字节。
在这里,我们可以通过 ulimit -a
命令来查看系统页面上管道容量的大小,如下:
我们发现在系统页面上显示的管道容量是4096字节(512B*8),而在前面我们测试管道为满时写入时的问题时就发现管道的实际容量为65536字节,这是因为测试时我使用的Linux版本是3.10.0(通过 cat /proc/version
命令可查看版本信息)。
六、PIPE_BUF与原子性问题
在 Linux 下,我们可以通过 man 7 pipe
命令来查看 PIPE_BUF 的具体信息,它的确切含义如下:
POSIX 规定,小于 PIPE_BUF 的写操作必须是原子的:要写的数据应被连续地写到管道;大于 PIPE_BUF 的写操作可能是非原子的:内核可能会将数据与其它进程写入的数据交织在一起。POSIX 规定 PIPE_BUF 至少为512字节(Linux 中为4096字节),具体的语义如下:(其中n为要写的字节数)
- n <= PIPE_BUF,O_NONBLOCK disable
写入具有原子性。如果没有足够的空间供 n 个字节全部立即写入,则阻塞直到有足够空间将n个字节全部写入管道。- n <= PIPE_BUF,O_NONBLOCK enable
写入具有原子性。如果有足够的空间写入 n 个字节,则 write 立即成功返回,并写入所有 n 个字节;否则一个都不写入,write 返回错误,并将 errno 设置为 EAGAIN。- n > PIPE_BUF,O_NONBLOCK disable
写入不具有原子性。可能会和其它的写进程交替写,直到将 n 个字节全部写入才返回,否则阻塞等待写入。- n > PIPE_BUF,O_NONBLOCK enable
写入不具有原子性。如果管道已满,则写入失败,write 返回错误,并将 errno 设置为 EAGAIN;否则,可以写入 1 ~ n 个字节,即部分写入,此时 write 返回实际写入的字节数,并且写入这些字节时可能与其他进程交错写入。
接下来,让我们通过下面的示例来理解一下原子性的问题。
【示例】:n <= PIPE_BUF,O_NONBLOCK disable
【代码】:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#define BUF_SIZE 4096 //PIPE_BUF通常是4096(将这里的值修改为大于4096将不保证原子性)
#define CHILD_NUM 5 //子进程数量
int main() {
int pipe_fd[2];
if (pipe(pipe_fd) == -1) {
perror("pipe error");
exit(1);
}
//创建多个子进程同时写入
for (int i = 0; i < CHILD_NUM; ++i) {
pid_t pid = fork();
if (pid == -1) {
perror("fork error");
exit(1);
}
else if (pid == 0) {
close(pipe_fd[0]);
char buf[BUF_SIZE];
memset(buf, 'A' + i, BUF_SIZE); //每个子进程写入不同的字符(如'A', 'B', ...)
int written = write(pipe_fd[1], buf, BUF_SIZE); //写入不超过PIPE_BUF,保证原子性
if (written == -1) {
perror("write error");
exit(1);
}
printf("Child %d wrote %d bytes\n", i, written);
close(pipe_fd[1]);
exit(0);
}
}
//父进程读取数据
close(pipe_fd[1]);
char read_buf[BUF_SIZE * CHILD_NUM]; //存储所有子进程写入的数据
int total_read = 0;
while (total_read < BUF_SIZE * CHILD_NUM) {
int n = read(pipe_fd[0], read_buf + total_read, BUF_SIZE);
if (n == -1) {
perror("read error");
exit(1);
}
else if (n == 0) {
break; //管道关闭
}
total_read += n;
}
close(pipe_fd[0]);
//等待所有子进程结束
for (int i = 0; i < CHILD_NUM; i++) {
wait(NULL);
}
//检查每一个大小为BUF_SIZE的连续块是否由相同字符组成
int is_corrupted = 0;
for (int i = 0; i < total_read; i += BUF_SIZE) {
char expected_char = read_buf[i];
for (int j = 1; j < BUF_SIZE; ++j) {
if (read_buf[i + j] != expected_char) {
printf("Data corruption at block %d, position %d!\n", i / BUF_SIZE, j);
is_corrupted = 1;
break;
}
}
}
if (!is_corrupted) {
printf("All writes were atomic (no corruption)!\n");
}
return 0;
}
【执行结果】:
总结
- 当要写入的数据量不大于 PIPE_BUF 时,Linux将保证写入的原子性;
- 当要写入的数据量大于 PIPE_BUF 时,Linux将不再保证写入的原子性。