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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

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 pipepipefd[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


总结

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

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

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux