【Linux】进程间通信——管道

img

进程间通信

1.1进程间通信介绍

什么是进程间通信?

答:进程具有独立性,每个进程都有自己的PCB,所以进程间需要通信,并且通信的成本一定不低(通信的本质:OS需要直接或者间接给通信双方的进程提供“内存空间”,并且要通信的进程,必须看到一份公共的资源)

而我们所说的不同通信种类本质就是:上面所说的资源,是OS中的哪一个模块提供的。如文件系统提供的叫管道通信;OS对应的System V模块提供的…

📝ps:成本不低是因为我们需要让不同的进程看到同一份资源

1.2进程间通信目的

进程间通信的目的在于

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程)

为什么要有进程间通信?

答:有时候我们需要多进程协同的,完成某种业务内容。比如管道

1.3进程间通信分类

如何去通信

答:1.采用标准的做法:System V进程间通信(聚焦在本地通信,如共享内存)、POSIX进程间通信(让通信过程可以跨主机)。

2.采用文件的做法:管道-基于文件系统(匿名管道、命名管道)

而本篇博客主要介绍管道,接着往下看把👇


管道

2.1管道介绍

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个"管道"

任何一个文件包括两套资源:1.file的操作方法 2.有属于自己的内核缓冲区,所以父进程和子进程有一份公共的资源:文件系统提供的内核缓冲区,父进程可以向对应的文件的文件缓冲区写入,子进程可以通过文件缓冲区读取,此时就完成了进程间通信,这种方式提供的文件称为管道文件。管道文件本质就是内存级文件,不需要IO

两个进程如何看到同一个管道文件:fork创建子进程完成

管道创建时分别以读和写方式打开同一个文件(如果只读或者只写,子进程也只会继承只读或只写,父子双方打开文件的方式一样,无法完成单向通信);父进程创建子进程,父进程以读写打开,子进程也是以读写打开(一般而言,管道只用来进行单向数据通信);关闭父子进程不需要的文件描述符,完成通信

image-20230124234013245

管道分为匿名管道和命名管道

2.2匿名管道

我们通过文件名区分文件,但是如果当前进程的文件没有名字,这样的内存级文件称为匿名管道。让两个进程看到同一个文件,通过父进程创建子进程,子进程继承文件地址的方式,看到同一个内存级文件,此时内存级文件没有名称就是匿名管道了。匿名管道能用来父进程和子进程之间进行进程间通信。

pipe

pipe:创建一个管道只需要调用pipe,注意头文件,返回值,以及函数的参数

头文件为#include <unistd.h>,调用成功返回0,调用失败返回-1。参数是输出型参数

SYNOPSIS
       #include <unistd.h>
       int pipe(int pipefd[2]);
DESCRIPTION
    pipe() creates a pipe,pipefd[0]  refers  to  the  read end of the pipe.  pipefd[1] refers to the write end of the pipe.
RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

创建管道文件,打开读写端

#include <iostream>
#include <unistd.h>
#include <cassert>

using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    //0,1,2->3,4
    //[0]:读取  [1]:写入
    cout<<"fds[0]:"<<fds[0]<<endl;//3
    cout<<"fds[1]:"<<fds[1]<<endl;//4
    return 0;
}

image-20230125000717496

所以[0]:3代表读取👄,[1]:4代表写入✍。

fork子进程:

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    //fork
    pid_t id = fork();
    assert(id>=0);
    if(id==0)
    {
        //子进程通信

        exit(0);
    }
    //父进程通信
    n = waitpid(id,nullptr,0);
    assert(n==id);
    return 0;
}

关闭父子进程不需要的文件描述符,完成通信

子进程写入,父进程读取:

#include <iostream>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);
    //fork
    pid_t id = fork();
    assert(id>=0);
    if(id==0)
    {
        //子进程通信:子进程进行写入,关闭读
        close(fds[0]);
        //通信
        const char*s = "这是子进程,正在进行通信";
        int cnt = 0;
        while(true)
        {
            cnt++;
            char buffer[1024];
            snprintf(buffer,sizeof buffer,"child->parent say:%s[%d][%d]",s,cnt,getpid());
            //写端写满的时候,在写会阻塞,等对方进行读取
            write(fds[1],buffer,strlen(buffer));//系统接口
            sleep(1);//一秒写一次
        }
        //退出前关闭子进程
        close(fds[1]);
        exit(0);
    }
    //父进程通信:父进程进行读取,关闭写
    close(fds[1]);
    //通信
    while(true)
    {
        char buffer[1024];
        //管道中如果没有数据,读端在读,默认会直接阻塞当前正在读取的进程
        ssize_t s = read(fds[0],buffer,sizeof(buffer)-1);
        if(s>0) buffer[s] = 0;
        cout<<"Get Message# "<<buffer<<"|mypid:"<<getpid()<<endl;
    }

    n = waitpid(id,nullptr,0);
    assert(n==id);
    
    //结束前关闭
    close(fds[0]);
    return 0;
}

image-20230125113529871

读写特征

管道读写特征

1.读快写慢

子进程休眠时,不在写入,父进程在读取(如果管道中没有数据,读端在读,此时默认会直接阻塞当前正在读取的进程)

image-20230125141424118

2.读慢写快

拿着管道读端不读,写端一直在写:写端往管道里写,而管道是有大小的,不断往写端写,会被写满

image-20230125145331180

管道是固定大小的缓冲区,当管道被写满,就不能再写了。此时写端会阻塞。

如果父进程只是sleep(2),稍微睡眠比较少:

image-20230125145632803

在这里不断读取的时候:写端是把数据塞到管道里,管道读取的是按照指定大小读取!而不是一行一行。而我们刚开始按行读取的是因为发送的慢,一次塞一行数据。

3.写入关闭,读到0

子进程写入端关闭:

image-20230125150745054

4.读取关闭,写入

管道是单向的:读端关闭,在写入就没有意义了:OS会终止写端,会给写进程发送信号,终止写端

image-20230125154034441

管道特征

1.管道的生命周期随进程,进程退出,管道释放

2.管道可以用来进行具有血缘关系的进程间通信(常用于父子通信)

3.管道是面向字节流的

4.半双工—单向通信(特殊)

5.互斥与同步机制——对共享资源进行保护的方案


2.3命名管道

我们前面已经知道:匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

那如果两个毫不相干的进程间通信交互呢?如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件

mkfifo
NAME
       mkfifo - make FIFOs (named pipes)

SYNOPSIS
    #include <sys/types.h>
    #include <sys/stat.h>
    int mkfifo(const char *pathname, mode_t mode);
RETURN VALUE
       On success mkfifo() returns 0.  In the case of an error, -1 is returned (in which case, errno is set appropriately).

在当前路径下直接创建命名管道:

mkfifo named_pipe

往管道文件写东西:

image-20230128133649932

image-20230128133743211

image-20230128133826611

两个进程打开同一个文件:站在内核的角度,第二个文件不需要继续创建struct file对象,因为OS会识别到打开的文件被打开了。在内核中,此时就看到了同一份资源,有着操作方法和缓冲区,不需要把数据刷新到磁盘上去,不需要IO。所以无论是匿名还是命名,本质都是管道

匿名管道通过继承的方式看到同一份资源。命名管道:通过让不同的进程打开指定名称(路径+文件名,具备唯一性)的同一个文件看到同一份资源。所以命名管道是通过文件的文件名来标定唯一性的。而匿名管道是通过继承的方式来标定的

创建管道文件

准备工作

分为三个文件:comm.hpp:公共文件(同一份资源),server.cc:读取端,clinet.cc:写入端

在目录tmp下创建文件:

server.cc:

#include "comm.hpp"
int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;
    return 0;
}

comm.hpp:

#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <cerrno>
#include <cstring>
#include <cassert>

#define NAMED_PIPE "/tmp/mypipe.name"

bool createFifo(const std::string &path)
{
    umask(0);
    int n = mkfifo(path.c_str(),0666);
    if(n==0) return true;
    else
    {
        std::cout<<"errno:"<<errno<<"err string:"<<strerror(errno)<<std::endl;
        return false;
    }
}

运行:

删除管道文件

unlink

注意头文件,函数的参数以及返回值这三个主要部分:

NAME
       unlink - remove a directory entry

SYNOPSIS
       #include <unistd.h>

       int unlink(const char *path);
RETURN VALUE
       Upon successful completion, 0 shall be returned. Otherwise, -1 shall be returned and errno set to indicate the error. If -1 is returned, the named file shall not be changed.

在comm.hpp中封装好删除的函数:

void removeFifo(const std::string &path)
{
    int n = unlink(path.c_str());
    assert(n==0);
    (void)n;//防止n没使用而警告
}

在server.cc中进行调用:

#include "comm.hpp"

int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;

    removeFifo(NAMED_PIPE);
    return 0;
}

至此,创建和删除管道文件的操作我们实现完毕。下面进入通信环节

通信

其实在了解完了匿名管道之后,对于命名管道我们能够更好的理解:

client.cc(写端):

#include "comm.hpp"

int main()
{
    int wfd = open(NAMED_PIPE,O_WRONLY);
    if(wfd<0) exit(1);
    //write
    char buffer[1024];
    while(true)
    {
        std::cout<<"Please Say:";
        fgets(buffer,sizeof(buffer),stdin);
        //if(strlen(buffer)>0) buffer[strlen(buffer)-1] = 0;
        ssize_t n = write(wfd,buffer,strlen(buffer));
        assert(n==strlen(buffer));
        (void)n;
    }
    close(wfd);
    return 0;
}

server.cc(读端):

#include "comm.hpp"

int main()
{
    bool ret = createFifo(NAMED_PIPE);
    assert(ret);
    (void)ret;

    int  rfd = open(NAMED_PIPE,O_RDONLY);
    if(rfd<0) exit(1);

    //read
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(rfd,buffer,sizeof(buffer)-1);
        if(s>0)
        {
            buffer[s] = 0;
            std::cout<<"client->server" <<buffer<<std::endl;
        }
        else if(s==0)
        {
            std::cout<<"client quit,俺也一样"<<std::endl;
            break;
        }
        else
        {
            std::cout<<"err string:"<<strerror(errno)<<std::endl;
            break;
        }
    }
    close(rfd);
    removeFifo(NAMED_PIPE);
    return 0;
}

进行通信:

image-20230128145203280

读端多出一行空行:写端输入之后多按了回车,修改为buffer[strlen(buffer)-1] = 0;

image-20230128154255185


总结

进程间通信的内容是比较多的,在这里,本文只是对进程间通信——管道这一部分进行介绍,后续会继续更新其他部分。

我们从进程间通信开始介绍,而后进入了进程间通信——管道这部分,管道又分为匿名管道和命名管道,以及之间的区别,匿名管道需要具有血缘关系的进程,而命名管道则不需要,同时,匿名管道通过子进程继承文件地址的方式,看到同一个内存级文件,而命名管道通过不同进程打开同一个文件,看到同一份资源。至此,对于管道的理解我们就先到这里结束。

  • 99
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 103
    评论
本PDF电子书包含上下两册,共1576页,带目录,高清非扫描版本。 作者: 毛德操 胡希明 丛书名: Linux内核源代码情景分析 出版社:浙江大学出版社 目录 第1章 预备知识 1.1 Linux内核简介. 1.2 Intel X86 CPU系列的寻址方式 1.3 i386的页式内存管理机制 1.4 Linux内核源代码中的C语言代码 1.5 Linux内核源代码中的汇编语言代码 第2章 存储管理 2.1 Linux内存管理的基本框架 2.2 地址映射的全过程 2.3 几个重要的数据结构和函数 2.4 越界访问 2.5 用户堆栈的扩展 2.6 物理页面的使用和周转 2.7 物理页面的分配 2.8 页面的定期换出 2.9 页面的换入 2.10 内核缓冲区的管理 2.11 外部设备存储空间的地址映射 2.12 系统调用brk() 2.13 系统调用mmap() 第3章 中断、异常和系统调用 3.1 X86 CPU对中断的硬件支持 3.2 中断向量表IDT的初始化 3.3 中断请求队列的初始化 3.4 中断的响应和服务 3.5 软中断与Bottom Half 3.6 页面异常的进入和返回 3.7 时钟中断 3.8 系统调用 3.9 系统调用号与跳转表 第4章 进程与进程调度 4.1 进程四要素 4.2 进程三部曲:创建、执行与消亡 4.3 系统调用fork()、vfork()与clone() 4.4 系统调用execve() 4.5 系统调用exit()与wait4() 4.6 进程的调度与切换 4.7 强制性调度 4.8 系统调用nanosleep()和pause() 4.9 内核中的互斥操作 第5章 文件系统 5.1 概述 5.2 从路径名到目标节点 5.3 访问权限与文件安全性 5.4 文件系统的安装和拆卸 5.5 文件的打开与关闭 5.6 文件的写与读 5.7 其他文件操作 5.8 特殊文件系统/proc 第6章 传统的Unix进程间通信 6.1 概述 6.2 管道和系统调用pipe() 6.3 命名管道 6.4 信号 6.5 系统调用ptrace()和进程跟踪 6.6 报文传递 6.7 共享内存 6.8 信号量 第7章基于socket的进程间通信 7.1系统调用socket() 7.2函数sys—socket()——创建插口 7.3函数sys—bind()——指定插口地址 7.4函数sys—listen()——设定server插口 7.5函数sys—accept()——接受连接请求 7.6函数sys—connect()——请求连接 7.7报文的接收与发送 7.8插口的关闭 7.9其他 第8章设备驱动 8.1概述 8.2系统调用mknod() 8.3可安装模块 8.4PCI总线 8.5块设备的驱动 8.6字符设备驱动概述 8.7终端设备与汉字信息处理 8.8控制台的驱动 8.9通用串行外部总线USB 8.10系统调用select()以及异步输入/输出 8.11设备文件系统devfs 第9章多处理器SMP系统结构 9.1概述 9.2SMP结构中的互斥问题 9.3高速缓存与内存的一致性 9.4SMP结构中的中断机制 9.5SMP结构中的进程调度 9.6SMP系统的引导 第10章系统引导和初始化 10.1系统引导过程概述 10.2系统初始化(第一阶段) 10.3系统初始化(第二阶段) 10.4系统初始化(第三阶段) 10.5系统的关闭和重引导

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平凡的人1

佬的鼓励是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值