进程间通信方式——管道

进程间通信方式——管道

  • 1、管道
  • 2、匿名管道
    • 2.1 创建匿名管道
    • 2.2 进程间通信
  • 3、有名管道
    • 3.1 创建有名管道
    • 3.2 进程间通信
  • 4、管道的读写行为

原文链接

1、管道

管道的是进程间通信(IPC - InterProcess Communication)的一种方式,管道的本质其实就是内核中的一块内存(或者叫内核缓冲区),这块缓冲区中的数据存储在一个环形队列中,因为管道在内核里边,因此我们不能直接对其进行任何操作。
在这里插入图片描述
因为管道数据是通过队列来维护的,我们先来分析一个管道中数据的特点:

  • 管道对应的内核缓冲区大小是固定的,默认为4k(也就是队列最大能存储4k数据)

  • 管道分为两部分:读端和写端(队列的两端),数据从写端进入管道,从读端流出管道。

  • 管道中的数据只能读一次,做一次读操作之后数据也就没有了(读数据相当于出队列)。

  • 管道是单工的:数据只能单向流动, 数据从写端流向读端。

  • 对管道的操作(读、写)默认是阻塞的

    • 读管道:管道中没有数据,读操作被阻塞,当管道中有数据之后阻塞才能解除
    • 写管道:管道被写满了,写数据的操作被阻塞,当管道变为不满的状态,写阻塞解除

管道在内核中, 不能直接对其进行操作,我们通过什么方式去读写管道呢?其实管道操作就是文件IO操作,内核中管道的两端分别对应两个文件描述符,通过写端的文件描述符把数据写入到管道中,通过读端的文件描述符将数据从管道中读出来。读写管道的函数就是Linux中的文件IO函数

// 读管道
ssize_t read(int fd, void *buf, size_t count);
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count);

最后分析一下为什么可以使用管道进行进程间通信,先看一下下面的图片:
在这里插入图片描述
在上图中假设父进程通过一系列操作可以通过文件描述符表中的文件描述符fd3写管道,通过fd4读管道,然后再通过 fork() 创建出子进程,那么在父进程中被分配的文件描述符 fd3, fd4也就被拷贝到子进程中,子进程通过 fd3可以将数据写入到内核的管道中,通过fd4将数据从管道中读出来。

也就是说管道是独立于任何进程的,并且充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的入口和出口(读端和写端的文件描述符),那么他们之间就可以通过管道进行数据的交互。

2、匿名管道

2.1 创建匿名管道

匿名管道是管道的一种,既然是匿名也就是说这个管道没有名字,但其本质是不变的,就是位于内核中的一块内存,匿名管道拥有上面介绍的管道的所有特性,额外的我们需要知道,匿名管道只能实现有血缘关系的进程间通信,什么叫有血缘的进程关系呢,比如:父子进程,兄弟进程,爷孙进程,叔侄进程。最后说一下创建匿名管道的函数,函数原型如下:

#include <unistd.h>
// 创建一个匿名的管道, 得到两个可用的文件描述符
int pipe(int pipefd[2]);
  • 参数:传出参数,需要传递一个整形数组的地址,数组大小为 2,也就是说最终会传出两个元素
    • pipefd[0]: 对应管道读端的文件描述符,通过它可以将数据从管道中读出
    • pipefd[1]: 对应管道写端的文件描述符,通过它可以将数据写入到管道中
  • 返回值:成功返回 0,失败返回 -1

2.2 进程间通信

使用匿名管道只能够实现有血缘关系的进程间通信,要求写一段程序完成下边的功能:

需求描述:
在父进程中创建一个子进程, 父子进程分别执行不同的操作:
- 子进程: 执行一个shell命令 “ps aux”, 将命令的结果传递给父进程
- 父进程: 将子进程命令的结果输出到终端

需求分析:

  • 子进程中执行shell命令相当于启动一个磁盘程序,因此需要使用 execl()/execlp()函数
    • execlp(“ps”, “ps”, “aux”, NULL)
  • 子进程中执行完shell命令直接就可以在终端输出结果,如果将这些信息传递给父进程呢?
    • 数据传递需要使用管道,子进程需要将数据写入到管道中
    • 将默认输出到终端的数据写入到管道就需要进行输出的重定向,需要使用 dup2()做这件事情
      • dup2(fd[1], STDOUT_FILENO);
  • 父进程需要读管道,将从管道中读出的数据打印到终端
  • 父进程最后需要释放子进程资源,防止出现僵尸进程

在使用管道进行进程间通信的注意事项:必须要保证数据在管道中的单向流动。这句话怎么理解呢,通过下面的图来分析一下:

第一步: 在父进程中创建了匿名管道,得到了两个分配的文件描述符,fd3操作管道的读端,fd4操作管道的写端。

第二步:父进程创建子进程,父进程的文件描述符被拷贝,在子进程的文件描述符表中也得到了两个被分配的可以使用的文件描述符,通过fd3读管道,通过fd4写管道。通过下图可以看到管道中数据的流动不是单向的,有以下这么几种情况:

  • 父进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出
  • 父进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出
  • 子进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出
  • 子进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出

前边说到过,管道行为默认是阻塞的,假设子进程通过写端将数据写入管道,父进程的读端将数据读出,这样子进程的读端就读不到数据,导致子进程阻塞在读管道的操作上,这样就会给程序的执行造成一些不必要的影响。如果我们本来也没有打算让进程读或者写管道,那么就可以将进程操作的读端或者写端关闭。
在这里插入图片描述

第三步:为了避免两个进程都读管道,但是可能其中某个进程由于读不到数据而阻塞的情况,我们可以关闭进程中用不到的那一端的文件描述符,这样数据就只能单向的从一端流向另外一端了,如下图,我们关闭了父进程的写端,关闭了子进程的读端:


根据上面的分析,最终可以写出下面的代码:

// 管道的数据是单向流动的:
// 操作管道的是两个进程, 进程A读管道, 需要关闭管道的写端, 进程B写管道, 需要关闭管道的读端
// 如果不做上述的操作, 会对程序的结果造成一些影响, 对管道的操作无法结束
#include <fcntl.h>
#include <sys/wait.h>

int main()
{
    // 1. 创建匿名管道, 得到两个文件描述符
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    // 2. 创建子进程 -> 能够操作管道的文件描述符被复制到子进程中
    pid_t pid = fork();
    if(pid == 0)
    {
        // 关闭读端
        close(fd[0]);
        // 3. 在子进程中执行 execlp("ps", "ps", "aux", NULL);
        // 在子进程中完成输出的重定向, 原来输出到终端现在要写管道
        // 进程打印数据默认输出到终端, 终端对应的文件描述符: stdout_fileno
        // 标准输出 重定向到 管道的写端
        dup2(fd[1], STDOUT_FILENO);
        execlp("ps", "ps", "aux", NULL);
        perror("execlp");
    }

    // 4. 父进程读管道
    else if(pid > 0)
    {
        // 关闭管道的写端
        close(fd[1]);
        // 5. 父进程打印读到的数据信息
        char buf[4096];
        // 读管道
        // 如果管道中没有数据, read会阻塞
        // 有数据之后, read解除阻塞, 直接读数据
        // 需要循环读数据, 管道是有容量的, 写满之后就不写了
        // 数据被读走之后, 继续写管道, 那么就需要再继续读数据
        while(1)
        {
            memset(buf, 0, sizeof(buf));
            int len = read(fd[0], buf, sizeof(buf));
            if(len == 0)
            {
                // 管道的写端关闭了, 如果管道中没有数据, 管道读端不会阻塞
                // 没数据直接返回0, 如果有数据, 将数据读出, 数据读完之后返回0
                break;
            }
            printf("%s, len = %d\n", buf, len);
        }
        close(fd[0]);

        // 回收子进程资源
        wait(NULL);
    }
    return 0;
}

3、有名管道

3.1 创建有名管道

有名管道拥有管道的所有特性,之所以称之为有名是因为管道在磁盘上有实体文件, 文件类型为p . 有名管道文件大小永远为0,因为有名管道也是将数据存储到内存的缓冲区中,打开这个磁盘上的管道文件就可以得到操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据。

有名管道也可以称为 fifo (first in first out),使用有名管道既可以进行有血缘关系的进程间通信,也可以进行没有血缘关系的进程间通信。创建有名管道的方式有两种,一种是通过命令,一种是通过函数。

通过命令

mkfifo xx

通过函数

#include <sys/types.h>
#include <sys/stat.h>
// int open(const char *pathname, int flags, mode_t mode);
int mkfifo(const char *pathname, mode_t mode);
  • 参数:
    • pathname: 要创建的有名管道的名字
    • mode: 文件的操作权限, 和open()的第三个参数一个作用,最终权限: (mode & ~umask)
  • 返回值:创建成功返回 0,失败返回 -1

3.2 进程间通信

不管是有血缘关系还是没有血缘关系,使用有名管道实现进程间通信的方式是相同的,就是在两个进程中分别以读、写的方式打开磁盘上的管道文件,得到用于读管道、写管道的文件描述符,就可以调用对应的read()、write()函数进行读写操作了。

小贴士 :
有名管道操作需要通过 open() 操作得到读写管道的文件描述符,如果只是读端打开了或者只是写端打开了,进程会阻塞在这里不会向下执行,直到在另一个进程中将管道的对端打开,当前进程的阻塞也就解除了。所以当发现进程阻塞在了open()函数上不要感到惊讶。·

写管道的进程

/*
	1. 创建有名管道文件 
		mkfifo()
	2. 打开有名管道文件, 打开方式是 o_wronly
		int wfd = open("xx", O_WRONLY);
	3. 调用write函数写文件 ==> 数据被写入管道中
		write(wfd, data, strlen(data));
	4. 写完之后关闭文件描述符
		close(wfd);
*/

#include <fcntl.h>
#include <sys/stat.h>

int main()
{
    // 1. 创建有名管道文件
    int ret = mkfifo("./testfifo", 0664);
    if(ret == -1)
    {
        perror("mkfifo");
        exit(0);
    }
    printf("管道文件创建成功...\n");

    // 2. 打开管道文件
    // 因为要写管道, 所有打开方式, 应该指定为 O_WRONLY
    // 如果先打开写端, 读端还没有打开, open函数会阻塞, 当读端也打开之后, open解除阻塞
    int wfd = open("./testfifo", O_WRONLY);
    if(wfd == -1)
    {
        perror("open");
        exit(0);
    }
    printf("以只写的方式打开文件成功...\n");

    // 3. 循环写管道
    int i = 0;
    while(i<100)
    {
        char buf[1024];
        sprintf(buf, "hello, fifo, 我在写管道...%d\n", i);
        write(wfd, buf, strlen(buf));
        i++;
        sleep(1);
    }
    close(wfd);

    return 0;
}

读管道的进程

/*
	1. 这两个进程需要操作相同的管道文件
	2. 打开有名管道文件, 打开方式是 o_rdonly
		int rfd = open("xx", O_RDONLY);
	3. 调用read函数读文件 ==> 读管道中的数据
		char buf[4096];
		read(rfd, buf, sizeof(buf));
	4. 读完之后关闭文件描述符
		close(rfd);
*/
#include <fcntl.h>
#include <sys/stat.h>

int main()
{
    // 1. 打开管道文件
    // 因为要read管道, so打开方式, 应该指定为 O_RDONLY
    // 如果只打开了读端, 写端还没有打开, open阻塞, 当写端被打开, 阻塞就解除了
    int rfd = open("./testfifo", O_RDONLY);
    if(rfd == -1)
    {
        perror("open");
        exit(0);
    }
    printf("以只读的方式打开文件成功...\n");

    // 2. 循环读管道
    while(1)
    {
        char buf[1024];
        memset(buf, 0, sizeof(buf));
        // 读是阻塞的, 如果管道中没有数据, read自动阻塞
        // 有数据解除阻塞, 继续读数据
        int len = read(rfd, buf, sizeof(buf));
        printf("读出的数据: %s\n", buf);
        if(len == 0)
        {
            // 写端关闭了, read解除阻塞返回0
            printf("管道的写端已经关闭, 拜拜...\n");
            break;
        }

    }
    close(rfd);

    return 0;
}

4、管道的读写行为

关于管道不管是有名的还是匿名的,在进行读写的时候,它们表现出的行为是一致的,下面是对其读写行为的总结:

  • 读管道,需要根据写端的状态进行分析:
    • 写端没有关闭 (操作管道写端的文件描述符没有被关闭)
      • 如果管道中没有数据 ==> 读阻塞, 如果管道中被写入了数据, 阻塞解除
      • 如果管道中有数据 ==> 不阻塞,管道中的数据被读完了, 再继续读管道还会阻塞
    • 写端已经关闭了 (没有可用的文件描述符可以写管道了)
      • 管道中没有数据 ==> 读端解除阻塞, read函数返回0
      • 管道中有数据 ==> read先将数据读出, 数据读完之后返回0, 不会阻塞了
  • 写管道,需要根据读端的状态进行分析:
    • 读端没有关闭
      • 如果管道有存储的空间, 一直写数据
      • 如果管道写满了, 写操作就阻塞, 当读端将管道数据读走了, 解除阻塞继续写
    • 读端关闭了,管道破裂(异常), 进程直接退出

管道的两端默认是阻塞的,如何将管道设置为非阻塞呢?管道的读写两端的非阻塞操作是相同的,下面的代码中将匿名的读端设置为了非阻塞:

// 通过fcntl 修改就可以, 一般情况下不建议修改
// 管道操作对应两个文件描述符, 分别是管道的读端 和 写端

// 1. 获取读端的文件描述符的flag属性
int flag = fcntl(fd[0], F_GETFL);
// 2. 添加非阻塞属性到 flag中
flag |= O_NONBLOCK;
// 3. 将新的flag属性设置给读端的文件描述符
fcntl(fd[0], F_SETFL, flag);
// 4. 非阻塞读管道
char buf[4096];
read(fd[0], buf, sizeof(buf));

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/207464.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

进程(process) vs 线程(Thread)

文章目录 前言一、进程&#xff08;process) vs 线程&#xff08;Thread&#xff09;引用自维基百科引用自CSDN INCOE AI引用自 geeksforgeeksOS( Operating System )如何调度线程的线程锁的核心原理是什么? 总结 前言 &#x1f680; 多方面理解进程(process) &#xff0c;线…

Python程序的计时

# -*- coding: UTF-8 -*- import timedef fun():time.sleep(5)sinceTime time.time() print("开始计时时刻&#xff1a;", sinceTime) fun() endTime time.time() print("结束时刻&#xff1a;", endTime) program_time endTime - sinceTime print(&quo…

振南技术干货集:各大平台串口调试软件大赏(5)

注解目录 &#xff08;串口的重要性不言而喻。为什么很多平台把串口称为 tty&#xff0c;比如 Linux、MacOS 等等&#xff0c;振南告诉你。&#xff09; 1、各平台上的串口调试软件 1.1Windows 1.1.1 STCISP &#xff08;感谢 STC 姚老板设计出 STCISP 这个软件。&#xf…

特殊二叉树——堆

&#x1f308;一、堆的基本概念 1.堆&#xff1a;非线性结构&#xff0c;是完全二叉树 2.堆分为大堆和小堆。 大堆&#xff1a;树中任意一个父亲都大于等于孩子&#xff0c;根节点值大于等于其所有子孙节点的值。 小堆&#xff1a;树中任意一个父亲都小于等于孩子&#xff0c;…

【pytorch】深度学习入门一:pytorch的安装与配置(Windows版)

请支持原创&#xff0c;认准DannisTang&#xff08;tangweixuan1995foxmail.com&#xff09; 文章目录 第〇章 阅读前提示第一章 准备工作第一节 Python下载第二节 Python安装第三节 Python配置第四节 Pycharm下载第五节 Pycharm安装第六节 CUDA的安装 第二章 Anaconda安装与配…

Kaggle-水果图像分类银奖项目 pytorch Densenet GoogleNet ResNet101 VGG19

一些原理文章 卷积神经网络基础&#xff08;卷积&#xff0c;池化&#xff0c;激活&#xff0c;全连接&#xff09; - 知乎 PyTorch 入门与实践&#xff08;六&#xff09;卷积神经网络进阶&#xff08;DenseNet&#xff09;_pytorch conv1x1_Skr.B的博客-CSDN博客GoogLeNet网…

Django-Redis

NoSQL&#xff1a;(不支持sql语句) Redis MongoDB Hbase hadoop Cassandra hadoop key-value数据库&#xff08;非关系性数据库&#xff09; redis优势 性能高&#xff0c;读取速度快&#xff0c;存在内存中 Redis应用场景 用来做缓存 在某些特定场景下替代传统数据库---社交…

数据爬取+可视化实战_告白气球_词云展示----酷狗音乐

一、前言 歌词上做文本分析&#xff0c;数据存储在网页上&#xff0c;需要爬取数据下来&#xff0c;词云展示在工作中也变得日益重要&#xff0c;接下来将数据爬虫与可视化结合起来&#xff0c;做个词云展示案例。 二、代码 # -*- coding:utf-8 -*- # 酷狗音乐 通过获取每首歌…

Python (十八) lambda

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

svn合并冲突时每个选项的含义

合并冲突时每个选项的含义 - 这个图片是 TortoiseSVN&#xff08;一个Subversion&#xff08;SVN&#xff09;客户端&#xff09;的合并冲突解决对话框。当你尝试合并两个版本的文件并且出现差异时&#xff0c;你需要解决这些差异。这个对话框提供了几个选项来处理合并冲突&…

Python中用于机器学习的Lazy Predict库

Python是一种多功能语言&#xff0c;你可以用它来做任何事情。Python的一个伟大之处在于&#xff0c;有这么多的库使它变得更加强大。Lazy Predict就是其中一个库。它是机器学习和数据科学的一个很好的工具。在本文中&#xff0c;我们将了解它是什么&#xff0c;它做什么&#…

adb连接Android手机

文章目录 一、adb连接Android手机1.USB连接调试&#xff08;方法一&#xff09;2.Wifi连接调试&#xff08;方法二&#xff09; 一、adb连接Android手机 1.USB连接调试&#xff08;方法一&#xff09; 使用usb数据线连接好电脑手机打开调试模式&#xff0c;勾选usb调试模式&a…

使用Pytorch从零开始构建Energy-based Model

知识回顾: [1] 生成式建模概述 [2] Transformer I&#xff0c;Transformer II [3] 变分自编码器 [4] 生成对抗网络&#xff0c;高级生成对抗网络 I&#xff0c;高级生成对抗网络 II [5] 自回归模型 [6] 归一化流模型 [7] 基于能量的模型 [8] 扩散模型 I, 扩散模型 II 在本教程中…

学生上课睡觉原因及对策

老师经常会遇到这样的情况&#xff1a;一些学生在课堂上昏昏欲睡&#xff0c;根本无法集中精力学习。所以怎么解决这个问题呢&#xff1f;接下来&#xff0c;我给大家一些实用的建议。 学生晚上熬夜&#xff0c;睡眠不足 引导学生养成良好的作息习惯&#xff0c;保证充足的睡眠…

“Python: Configure Tests“ not found解决方案

最近想尝试尝试学学软件测试。正好电脑上安装了vscode&#xff0c; 又懒得装pycharm&#xff0c;所以就用vscode了。 遇到的问题 跟着vscode运行unittest框架想运行一下测试用例文件。【前提是文件名一定要包含test&#xff0c;文件里要导入unittest的包&#xff0c;类要继承…

vue生命周期、工程化开发和脚手架

1、前言 持续学习记录总结中&#xff0c;vue生命周期、工程化开发和脚手架 2、Vue生命周期 Vue生命周期&#xff1a;就是一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个阶段&#xff1a;① 创建 ② 挂载 ③ 更新 ④ 销毁 1.创建阶段&#xff1a;创建响应式数据 2.挂…

JCRE-逻辑通道

概述 卡以APDU的形式接收来自CAD的服务请求。JCRE使用SELECT FILE APDU和MANAGE CHANNEL OPEN APDU来指定逻辑通道会话的活动Applet。一旦被选中&#xff0c;一个Applet实例将接收分派到该逻辑通道的所有后续APDU&#xff0c;直到该小程序实例被取消变成Desectected状态。 Ja…

机器人AGV小车避障传感器测距

一、A22超声波传感器 该模块是基于机器人自动控制应用而设计的超声波避障传感器&#xff0c;针对目前市场上对于超声波传感器模组盲区大、测量角度大、响应时间长、安装适配性差等问题而着重设计。 具备了盲区小、测量角度小、响应时间短、过滤同频干扰、体积小、安装适配性高…

【从删库到跑路 | MySQL总结篇】索引的详细使用

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【MySQL学习专栏】&#x1f388; 本专栏旨在分享学习MySQL的一点学习心得&#xff0c;欢迎大家在评论区讨论&#x1f48c; 目录 一、索引…

KMP基础架构

前言 Kotlin可以用来开发全栈, 我们所熟悉的各个端几乎都支持(除了鸿蒙) 而我们要开发好KMP项目需要一个好的基础架构,这样不仅代码更清晰,而且能共享更多的代码 正文 我们可以先将KMP分为前端和服务端 它们两端也能共享一些代码,比如接口声明,bean类,基础工具类等 前端和…