详解:管道的读写规则以及原子性问题

本文详细解析了Linux管道的读写规则,包括阻塞与非阻塞模式下的行为,管道容量限制,以及PIPE_BUF对原子性的影响。通过具体示例展示了不同条件下管道操作的特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、管道为空时读取

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将不再保证写入的原子性。



注:本文参考 linux系统编程之管道(二):管道读写规则和Pipe Capacity、PIPE_BUF

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值