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

一、管道为空时读取

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);    
    }    
     
    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 版本中,管道的容量与系统页面大小相同,但自 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 <string.h>
         
#define SIZE 64*1024 //64KB
         
int main() {
    char A[SIZE];
    char B[SIZE];
         
    memset(A, 'A', sizeof(A));
    memset(B, 'B', sizeof(B));
         
    int pipefd[2];    
    if(pipe(pipefd) < 0) {
        perror("pipe error");    
        return -1;    
    }    
         
    int ret = 0;
         
    pid_t pid = fork();
    if(pid < 0 ) {
        perror("fork error");
        return -1;
    }
    else if(pid == 0) { //子进程1写入A    
        close(pipefd[0]);    
        ret = write(pipefd[1], A, sizeof(A)); //阻塞写入,直到64kB的数据全部写完才返回    
        printf("Child process [%d] wrote %d bytes of character A to the pipeline.\n", getpid(), ret);    
        exit(0);    
    }        
             
    pid = fork();    
    if(pid < 0 ) {    
        perror("fork error");    
        return -1;    
    }        
    else if(pid == 0) { //子进程2写入B    
        close(pipefd[0]);    
        ret = write(pipefd[1], B, sizeof(B)); //阻塞写入,直到64kB的数据全部写完才返回    
        printf("Child process [%d] wrote %d bytes of character B to the pipeline.\n", getpid(), ret);    
        exit(0);    
    }        
             
    close(pipefd[1]);    
    sleep(1); //休眠1s,确保父子进程的写端都已关闭    
    int n = 0;    
    while(1) {    
        char buf[4*1024] = {0};    
        ret = read(pipefd[0], buf, sizeof(buf)); //每次读取4KB数据    
        if(ret == 0) { //若读完数据,则不再读取    
            printf("Pipeline data has been read out.\n");    
            break;    
        }    
        printf("%2d: Parent process [%d] read %d bytes from the pipeline, buf[4095] = %c\n", ++n, getpid(), ret, buf[4095]);           
    }        
             
    return 0;    
}

【执行结果】
在这里插入图片描述
【分析】
        在上述代码中,我们让两个子进程都分别写入64KB的数据,由于从 Linux 2.6.11 以来,管道容量为65536字节,所以此时写入的数据与PIPE_BUF相等。在默认阻塞模式下,每个子进程完全写入64KB数据才能返回,而父进程对管道进行阻塞式读取,当没有数据时就一直阻塞等待,直到有数据到达。
        从执行结果来看,在这种情况下,子进程1先向管道中连续写入了64KB的数据A,接着父进程从管道中读取这些数据,但此时子进程2还不能向管道中写入,因为管道的剩余空间还无法一次容纳64KB数据,必须等到父进程读取完管道中剩余的所有数据才能开始写入。父进程读取完子进程1写入的数据后,管道中已经没有数据可读,父进程则开始进入阻塞等待状态,此时子进程2开始一次连续地写入64KB的数据B,之后,管道中又有数据了,父进程继续读取管道中子进程2写入的数据,直到读取完后,程序退出。
        分析结果我们发现,这两个子进程写入的数据都是连续地写入到管道中,并没有产生和其他进程交替写入的问题,所以由此可见,在 n <= PIPE_BUF的情况下,Linux将保证写入的原子性。

【示例二】n > PIPE_BUF,O_NONBLOCK disable
【代码】

#include <stdio.h>                                                                                                                     
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
         
#define SIZE 68*1024 //68KB
         
int main() {
    char A[SIZE];
    char B[SIZE];
         
    memset(A, 'A', sizeof(A));
    memset(B, 'B', sizeof(B));
         
    int pipefd[2];    
    if(pipe(pipefd) < 0) {
        perror("pipe error");    
        return -1;    
    }    
         
    int ret = 0;
         
    pid_t pid = fork();
    if(pid < 0 ) {
        perror("fork error");
        return -1;
    }
    else if(pid == 0) { //子进程1写入A    
        close(pipefd[0]);    
        ret = write(pipefd[1], A, sizeof(A)); //阻塞写入,直到68kB的数据全部写完才返回    
        printf("Child process [%d] wrote %d bytes of character A to the pipeline.\n", getpid(), ret);    
        exit(0);    
    }        
             
    pid = fork();    
    if(pid < 0 ) {    
        perror("fork error");    
        return -1;    
    }        
    else if(pid == 0) { //子进程2写入B    
        close(pipefd[0]);    
        ret = write(pipefd[1], B, sizeof(B)); //阻塞写入,直到68kB的数据全部写完才返回    
        printf("Child process [%d] wrote %d bytes of character B to the pipeline.\n", getpid(), ret);    
        exit(0);    
    }        
             
    close(pipefd[1]);    
    sleep(1); //休眠1s,确保父子进程的写端都已关闭    
    int n = 0;    
    while(1) {    
        char buf[4*1024] = {0};    
        ret = read(pipefd[0], buf, sizeof(buf)); //每次读取4KB数据    
        if(ret == 0) { //若读完数据,则不再读取    
            printf("Pipeline data has been read out.\n");    
            break;    
        }    
        printf("%2d: Parent process [%d] read %d bytes from the pipeline, buf[4095] = %c\n", ++n, getpid(), ret, buf[4095]);           
    }        
             
    return 0;    
}

【执行结果】
在这里插入图片描述
【分析】
        在上述代码中,我们将示例一两个子进程写入的数据量由64KB提高到了68KB,此时写入的数据大于 PIPE_BUF。
        从执行结果来看,在这种情况下,子进程1先向管道中写入了64KB的数据A,待父进程读取完这些数据后,子进程2又向管道中写入了64KB的数据B,又待父进程读取完这些数据后,子进程1将剩余的4KB数据A写入,此时子进程1已将68KB的数据写完,紧接着,子进程2也将剩余的4KB数据B写入,此时子进程2同样也将68KB的数据写完,之后,父进程将管道中剩余的数据分两次读完,程序退出。
        分析结果我们发现,这两个子进程写入的数据并非连续地写入到管道中,而是两个进程交替写入(子进程1写入64KB数据 -> 子进程2写入64KB数据 -> 子进程1写入4KB数据 -> 子进程2写入4KB数据),所以由此可见,在 n > PIPE_BUF的情况下,Linux将不再保证写入的原子性。

总结

  • 当要写入的数据量不大于 PIPE_BUF 时,Linux将保证写入的原子性;
  • 当要写入的数据量大于 PIPE_BUF 时,Linux将不再保证写入的原子性。



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

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值