Linux系统编程——管道篇

Pipe 管道

学习文章https://blog.csdn.net/skyroben/article/details/71513385

进程间的通信
每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

不同进程间的通信本质:进程之间可以看到一份公共资源;而提供这份资源的形式或者提供者不同,造成了通信方式不同,而 pipe就是提供这份公共资源的形式的一种。

进程间数据交换机制(IPC).

主要区别在于可访问性模型

  • 管道:“相关”进程
  • FIFO(命名管道):
    • 在文件系统中有名称
    • 可访问性:用户/组所有权+文件权限

对于这两种机制,数据都具有进程持久性

  • 当所有进程关闭指向管道/FIFO的fd时,未读数据将被丢弃

管道是常用的外壳特征;如

ls | wc -l

执行这条指令的时候,shell会有以下的步骤:

  1. 使用fork()创建两个执行进程ls和wc
  2. 将ls的标准输出和wc的标准输入连接到管道

image.png
管道中的数据保存在内核内存中

管道的特点

1.管道只允许具有血缘关系的进程间通信,如父子进程间的通信。
2.管道只允许单向通信
3.管道内部保证同步机制,从而保证访问数据的一致性。
4.面向字节流
5.管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。

:::tips

  1. 管道是字节流

    1. 数据是不受限制的字节序列
    2. 可以读取任意块的数据,而不管写的大小
    3. 数据依次通过管道(没有随机访问)
  2. 管道是单向的

    1. 管道有一个读端(fd[0])和一个写端(fd[1])
  3. 管道的容量有限 PIPE_BUF

    1. 限制因系统而异
  4. Linux下的管道容量:

    1. Linux <= 2.6.10: 4096字节
    2. Linux >= 2.6.11: 65,536字节
    3. **fcntl(fd, F_SETPIPE_SZ, size)**可用于更改管道容量(自Linux 2.6.35起)
  5. 应用程序的设计不应该关心容量

    1. 为了防止写入阻塞,请确保始终有一个活动的读取器
      :::
      管道的缓存区
      :::info
      管道的缓存区的大小是有限的,PIPE_BUF.
  6. 如果写入的数据小于等于PIPE_BUF,字节保证是原子的

    1. 如果没有足够的空间写入所有字节,则不写入任何字节
    2. 调用程序阻塞,直到有空间在一个操作中写入所有字节
    3. 字节不会与其他进程的写入混杂在一起
    4. PIPE_BUF == 4096;在某些系统上低至512
  7. 写入> PIPE_BUF字节可能不是原子的

    1. 数据可以分成/传送成更小的片段
    2. Write()在传输完所有数据后完成
      :::

实例验证计算管道的容量

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>

int main()
{
    int fd[2];
    int ret = pipe(fd);
    if (ret == -1)
    {
        perror("pipe error\n");
        return 1;
    }
    pid_t id = fork();
    if (id == 0)
    {//child
        int i = 0;
        close(fd[0]);
        char *child = "I am  child!";
        while (++i)
        {
            write(fd[1], child, strlen(child) + 1);
            printf("pipe capacity: %ld\n", i*(strlen(child) + 1));
        }
        close(fd[1]);    		//缓存区满了后就关闭写端
    }
    else if (id>0)
    {//father
        close(fd[1]);         	//关闭写端,但是不读取管道里的数据
        waitpid(id, NULL, 0);
    }
    else
    {//error
        perror("fork error\n");
        return 2;
    }
    return  0;
}

image.png
可以看到写到65520之后管道堵塞了,而65536即为64K大小即为管道的容量(由于代码问题,少统计一次数据)。

创建一个管道

管道是由调用pipe函数来创建

#include<unistd.h>
int pipe(int fd[2]);
 //返回:成功返回0,出错返回-1     

:::info
fd参数返回两个文件描述符,

  • fd[0]指向管道的读端,
  • fd[1]指向管道的写端。

fd[1]的输出是fd[0]的输入。
:::
image.png

管道如何实现进程间的通信

:::info

  1. 父进程创建管道,得到两个文件描述符指向管道的两端。
  2. 父进程fork出子进程,子进程也有两个文件描述符指向同一个管道的两端。
  3. 父进程关闭fd[0],子进程关闭fd[1],即 父进程关闭管道读端,子进程关闭管道写端。(因为管道只支持单向通信)。父进程可以往管道里写,子进程可以往管道里读,管道是用环形队列实现的。数据从写端流入,从读端流出,就这样形成了进程间的通信。

当前,相反也是可以的,父进程关闭写端,子进程关闭读端。要保证不能父进程和子进程同时在管道上读或写,因为管道是单向的。
:::
fork()之后,每个进程关闭未使用的文件描述符

代码实现管道通信

  1. 假设我们想要将数据从子进程传输到父进程,即子进程写入,父进程读取。那么就要保证父进程关闭写端,子进程关闭读端;然后子进程往管道里写入,父进程往管道里读取数据。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

int main(){
    int fd[2];
    int ret = pipe(fd);

    if(ret == -1){
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    pid_t childPid = fork();
    if(childPid == -1){
        perror("fork");
        exit(EXIT_FAILURE);
    }
    else if(childPid == 0){
        int i = 0;
        close(fd[0]);    //子进程关闭fd[0];关闭读端
        char *child = "I am child";
        while(i<5){
            write(fd[1],child,strlen(child)+1);
            sleep(2);
            i++;
        }
    }
    else{       //parent
        close(fd[1]);
        char msg[100];
        int j = 0;
        while(j < 5){
            memset(msg,'\0',sizeof(msg));
            ssize_t s = read(fd[0],msg,sizeof(msg));
            if(s > 0){
                msg[s-1] = '\0';
            }
            printf("%s\n",msg);
            j++;
        }
    }
    return 0;
}

运行结果:
每隔两秒钟,打印出一次I am child.
image.png

  1. 想要将数据从父进程传输到子进程. 那么过程就正好相反,父进程写入,子进程读出. 父进程关闭读端.子进程关闭写端

image.png

管道读取数据的四种的情况

:::info
只有当所有写描述符都关闭时,读取器才会看到EOF
只有当所有读描述符都关闭时,写入器才能获得EPIPE + SIGPIPE
:::

读端不读,写端一直写


如果持续这样,会导致管道里面写满数据,从而导致再次write的时候导致堵塞,直到管道里有空位置才写入数据并返回。测试PIPO_BUF的案例,就是这样

写端不写,但是读端一直读


管道里的数据被读取完之后,再次read的时候就会堵塞,直到管道里有数据写入,才能重新读取数据。

读端一直读,且fd[0]保持打开,而写端写了一部分数据不写了,并且关闭fd[1]


:::info
如果一个管道读端一直在读数据,而管道写端的引⽤计数⼤于0决定管道是否会堵塞,引用计数大于0,只读不写会导致管道堵塞。
读端一直读,但是写端突然写一半不写了,此时读端将会收到EOF的信号
:::
父节点通过管道将argv[1]字符串发送给子节点
image.png

读端读了一部分数据,不读了且关闭fd[0],写端一直在写且f[1]还保持打开状态

:::info
一旦读端的fd[0]被关闭,而子进程继续想管道里写端写入数据,那么子进程会受到信号SIGPIPE,通常会导致进程异常终止。
写端一直写,读端读一半不读了,此时写端会生成SIGPIPE信号,并出现EPIPE的错误。
:::
:::success
如果pipe没有读取器,则通知写入器

  • write()导致SIGPIPE信号的生成 (Kill 13号)
    • 默认动作:终止进程
    • 可以使处置“忽略”,在这种情况下…
  • write()失败,出现EPIPE错误
    :::

实例:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>

int main(){
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1){
        perror("pipe");
        return 1;
    }
    pid_t childPid = fork();
    if(childPid == 0){
        int i = 0;
        close(fd[0]);

        char *child = "I am child.";
        while(i < 10){
            write(fd[1],child,strlen(child) + 1);
            sleep(2);
            i++;
        }
    }
    else if(childPid > 0){
        int j = 0;
        close(fd[1]);
        char msg[100];
        int status = 0;
        while(j < 5){
            memset(msg,'\0',sizeof(msg));
            ssize_t s = read(fd[0],msg,sizeof(msg));
            if(s > 0){
                msg[s - 1] = '\0';
            }
            printf("%s\n",msg);
            j++;
        }
        //写端还在继续写,此时关闭读端
        close(fd[0]);
        pid_t ret = waitpid(childPid,&status,0);
        printf("existingsingle(%d), exit(%d)\n",status & 0xff,(status>>8) & 0xff);
        //低八位存放该子进程退出时是否收到信号 
        //此低八位子进程正常退出时,退出码是多少  
    }
    else{
        perror("fork");
        return 2;
    }
    return 0;
}

image.png
使用kill -l 查看13号信号,可以知道13号信号代表SIGPIPE。
image.png

总结

:::info

  • 如果一个管道的写端一直在写,而读端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只写不读再次调用write会导致管道堵塞;
  • 如果一个管道的读端一直在读,而写端的引⽤计数是否⼤于0决定管道是否会堵塞,引用计数大于0,只读不写再次调用read会导致管道堵塞;
  • 当他们的引用计数等于0时,只读不写会导致读端收到EOF的错误。
  • 当他们的引用计数等于0时,只写不读会导致写端的进程收到一个SIGPIPE信号,导致进程终止,只写不读会导致read返回0,就像读到⽂件末尾⼀样。
    :::

将过滤器连接到管道

Filter ==从标准输入读取和/或写入标准输出的程序
假设我们希望过滤器读取或写入管道

ls | wc -l

image.png
将会出现什么问题?
:::info

  1. 通常,文件描述符0、1和2已经在使用中
  2. Pipe()将使用另外2个描述符 【STDOUT_FILENO=fd[1]】,【STDIN_FILENO=fd[0]】
  • 解决方案是使用dup(fd)(或similar) [复制函数]
  • 将过滤器连接到管道的写端
    :::
int fd[2];
pipe(fd);

close(STDOUT_FILENO);   //关闭fd[1] 
dup(fd[1]);
  • 由于我们不再需要fd[1],我们应该关闭它:
close(fd[1]);

但是,如果描述符0在pipe()和(第一个)close()之间……?
:::info

  1. 使用**dup2(oldfd, newfd)**复制函数来解决
  • 关闭newfd,如果它是打开的
  • 使newfd成为oldfd的副本
    • 前两步是原子的;防止多线程应用程序中的FD竞争
  • 如果oldfd == newfd什么都不做
  1. 用dup2()替换对close()和dup()的调用
    :::
dup2(fd[1],STDOUT_FILENO); //关闭fd1,并重新打开绑定到管道写端的fd1

close(fd[1]);
  1. 但是,如果描述符0和1在pipe()之前被关闭:
pipe ( fd ); /* Uses FD 0 and FD 1 *

//假设使用FD 1的管道的写端
dup2 (fd[1], STDOUT_FILENO ); /* dup2 (1 ,1) [no -op] */
close (fd[1]); /* close (1) [!!] */
  1. dup2()没有做任何事情,close()关闭了我们唯一的描述符
  2. 解决方案:如果pipe()使用了我们想要的描述符,则不需要dup2() + close():
pipe(fd);

if (fd[1] != STDOUT_FILENO ) {
    dup2(fd[1], STDOUT_FILENO );
    close(fd[1]);
}

Exercise

实例: 实现 ls | wc -l (省略错误检查)
ls | wc -l 
#include<unistd.h>
#include<stdio.h>
#include<string.h>

int main(){
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1){
        perror("pipe");
        return 1;
    }
    pid_t childPid = fork();
    if(childPid == 0){
        if(fd[1] != STDOUT_FILENO){
            dup2(fd[1],STDOUT_FILENO);
            close(fd[1]);
        }
        execlp("ls","ls",(char*)NULL);
        perror("execlp ls");
    }
    else if(childPid > 0){
        close(fd[1]);  
        //在管道的读端重复标准输入
        if(fd[0] != STDIN_FILENO){
            dup2(fd[0],STDIN_FILENO);  #将fd[0]复制到STDIN_FILENO,可以标准输入
            close(fd[0]);
        }
        execlp("wc","wc","-l",(char*)NULL);
        perror("execlp wc");
    }
    else{
        perror("fork");
        return 2;
    }
    return 0;
}

image.png

Exercise2:实现tr指令
  1. 创建一个程序,它接受一个文件名参数,并使用fork(), exec(), dup2()和pipe()来实现以下管道:
$ tr 'h' 'w' < out.txt | sort -u

:::info

  1. tr命令将文本中的h转换成w。(输入重定向是必需的,因为tr不接受文件名参数)
  2. 如果要编写调试输出,请将其写入标准错误
  3. 要使tr从filename读取,只需open()文件并将(dup2())结果FD复制到STDIN_FILENO上
  4. Makefile提供了一个测试:make test_unique_tokens
    :::
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<fcntl.h>

int main()
{
    int pfd[2];
    int fd;
    int ret = pipe(pfd);
    if(ret == -1){
        perror("pipe");
    }
    pid_t childPid = fork();
    if(childPid == 0){
        fd = open("./out.txt",O_RDONLY);
        dup2(fd,STDIN_FILENO);						#将open的内容复制到标准输入

            if(pfd[1] != STDOUT_FILENO){
            dup2(pfd[1],STDOUT_FILENO);
            close(pfd[1]); 
        }
        execlp("tr","tr","h","w",(char*)NULL);
        perror("execlp tr");
    }
    else if(childPid > 0){
        close(pfd[1]);
        if(pfd[0] != STDIN_FILENO){
            dup2(pfd[0],STDIN_FILENO);
            close(pfd[0]);
        }
        execlp("sort","sort","-u",(char *)NULL);
        perror("execlp sort");
    }
    else{
        perror("fork");
    }
    return 0;
}
  1. 扩展前面的程序来创建一个新程序count_unique_tokens.c,它接受一个文件名参数并实现以下管道:
tr 'h' 'w' < out.txt | sort -u | wc -w

image.png

int main()
{
    int pfd[2];
    int pfd2[2];

    int fd;
    int ret = pipe(pfd);
    if(ret == -1){
        perror("pipe");
    }
    pid_t childPid = fork();
    if(childPid == 0){
        fd = open("./out.txt",O_RDONLY);
        dup2(fd,STDIN_FILENO);

        if(pfd[1] != STDOUT_FILENO){
            dup2(pfd[1],STDOUT_FILENO);
            close(pfd[1]); 
        }
        execlp("tr","tr","h","w",(char*)NULL);
        perror("execlp tr");

        pid_t childPid2 = fork();  				# 创建子进程2
        if(childPid2 == 0){
            if(pfd2[1] != STDOUT_FILENO){		# stdout
                dup2(pfd2[1],STDOUT_FILENO);
                close(pfd2[1]); 
            }
            execlp("sort","sort","-u",(char *)NULL);
            perror("execlp sort");
        }
        else if(childPid2 > 0){
            close(pfd2[1]);
        }
        else{
            perror("fork");
        }   
    }
    else if(childPid > 0){
        close(pfd[1]);
        if(pfd[0] != STDIN_FILENO){
            dup2(pfd[0],STDIN_FILENO);
            close(pfd[0]);
        }
        execlp("wc","wc","-w",(char *)NULL);
        perror("execlp wc");
    }
    else{
        perror("fork");
    }
    return 0;
}

image.png

  1. 泛化在上一个练习中创建的程序来创建一个新版本(pipeline_builder.c),该版本实现并使用以下函数:
int execlPipeline (int infd , bool makePipe , char *arg , ...)

此函数创建一个子进程,其标准输出连接到该函数创建的管道的写端。子进程执行在arg和后续参数中包含的可变长度参数列表中指定的命令。
除此之外,execlPipeline()应该做以下事情:
:::tips

  1. 在调用fork()之前,如果makePipe非零,则创建一个管道。
  2. 在子进程中
  3. 将文件描述符infd复制为标准输入,以便子进程将从该文件描述符读取。
  4. (如果makePipe为非零)复制管道的写端,使其成为子进程执行的命令的标准输出。
    :::
    作为它的函数结果,execlPipeline()返回它所创建的管道的读端文件描述符,如果没有创建管道则返回-1。在返回之前,execlPipeline()关闭infd。使用此函数,可以使用以下代码构建管道:
fd = open(argv[1] , O_RDONLY);
fd = execlPipeline (fd , true , "tr", "h", "w",(char *) NULL );
fd = execlPipeline (fd , true , "sort", "-u", ( char *) NULL );
(void) execlPipeline (fd , false , "wc", "-l", ( char *) NULL );

:::info

  1. (在子程序中),您将需要使用stdarg(3),以便解析变长参数列表。您可能会发现,检查procexec/execlp.c源文件以获取如何从变长参数列表构建argv风格向量的示例非常有用。

  2. 不要忘记关闭多余的管道文件描述符。
    :::

  3. 用下面的命令行参数写一个程序:

$ ./ pipe_speed num - blocks wblock -size rblock -size

:::tips

  1. 创建管道
  2. fork()创建子进程
  3. 子进程从管道中读取大小为rblock-size的数据块,直到文件结束
  4. 父进程:
    1. 将大小为wblock-size的num-blocks块写入管道
    2. 关闭管道
    3. 等待子进程终止
      :::
      计时程序的操作为num_blocks和wblock-size的不同值

FIFOs(First-In First Out)

FIFOs的特点:
:::info

  1. 语义上类似于管道
  2. 主要区别:FIFO在文件系统中有一个名称【命名管道】
  3. 任何具有打开FIFO权限的进程都可以执行I/O
  4. 在shell中创建:mkfifo [-m permissions] pathname
    :::
$ mkfifo -m u+rw ,g=,o= myfifo
$ ls -lF myfifo
prw -------. 1 mtk mtk 0 Oct 31 13:21 myfifo |

image.png
函数原型

#include<sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

:::info

  1. 当不再需要时,使用**unlink()或remove()**删除
    :::

打开一个fifo

  1. 可以使用open()
fd = open("myfifo", O_RDONLY); // 打开读端
fd = open("myfifo", O_WRONLY); // 打开写端

:::info

  1. 打开FIFO的一端会阻塞,直到另一个进程打开另一端
    1. 如果另一端打开,则open()立即成功
    2. 打开是同步的
  2. 基本原理:FIFO只有在有读取器和写入器时才有用
  3. 就像管道一样(read(), write())
  4. 注意:FIFO数据是内核中的缓冲区
    1. FIFO有文件系统路径名,但这只是一种允许多个进程访问相同缓冲区的机制
  5. 如果所有fd都关闭,未读数据将被丢弃
    1. (FIFO名称在文件系统中持续存在,但数据具有进程持久性)
      :::

Exercise

在shell会话中对你喜欢的文本文件尝试以下操作:

$ mkfifo myfifo
$ tr 'aeiou' 'aieuo' < myfifo &
$ man 2 pipe > myfifo

image.png
image.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值