当前位置: 首页 > news >正文

Linux: 进程间通信

目录

一 前言 

二 进程间通信目的

三 进程间通信方法

四 管道通信 

1. 进程如何通信

2.管道概念

2.1匿名管道 

2.2 匿名管道对多个进程的控制

2.3 命名管道

 2.4 命名管道原理


一 前言 

在我们学习进程的时候,我们知道正是因为程序地址空间的存在,所以进程之间具有独立性,他们互不影响,但是在我们的实际应用中,进程之间总会有需要通信(交流获取对方数据)。


二 进程间通信目的

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

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

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

  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态的转变。比如我们在程序调试代码的时候就是一个进程完全控制了另一个进程。


三 进程间通信方法

Linux为用户提供了三种进程间通信的方法

  • pipe管道通信:比如我们在进程一篇中用到的命令 ‘|’ 就是使用管道,该命令是和其他命令组合起来使用的。如搭配 grep 文本內容过滤工具使用。" ps -ajx | grep lrk "该命令就是将ps进程执行的数据通过管道传输给了 grep,才能筛选出指定的內容。管道一般是用于本地进程之间的数据传输。其又分为 匿名管道 和 命名管道
  • POSIX进程通信:是一套进程通信的标准,可以为操作系统提供进程通信的接口。
  • System V进程通信:也是一套进程通信的标准,可以为操作系统提供进程通信的接口。

四 管道通信 

1. 进程如何通信

 我们知道进程的程序地址空间决定了进程之间的独立性,这就给进程之间的通信造成了极大的困难,但是我们思考一下通信的本质是什么,即就是一个进程向另一个进程传递数据,而我们的进程始终是在操作系统内运行着的,那么进程是不是可以通过操作系统中的资源进行通信呢?

就像一个进程向同一份文件中写入数据,另一个进程去该文件中读取。这样的情况下,我们就通过访问同一个资源达成了数据传输的目的。即进程之间的通信的前提其实是不同的进程需要先能够看到、能够获取到同一份资源(文件、内存等)。该资源的种类其实就决定了进程通信的方式。

2.管道概念

管道是Unix中最古老的进程间通信的方式了。管道顾名思义,就是类比于生活中的管道才得名的。只不过生活中的管道输送的是谁、天然气等现实生活中的资源,而系统中的管道则是传输数据的,一个进程链接到另一个进程的数据流。

事实上,管道就是一个被打开的文件(内存级文件),但是这个文件很特殊,向这个文件内写入的数据实际上并不会放入磁盘中,管道是在内存中实现的,并由操作系统的内核管理。当一个进程向管道写入数据时,这些数据被存储在内核中的缓冲区;然后,另一个进程可以从同一管道读取这些数据。一旦数据被读取,它们就从缓冲区中移除,符合现实中的管道特征:只传输资源,不存储资源。且需要注意的是管道是单项传输的。示意图 如下:

管道分为两种:匿名管道命名管道 ,其实就是根据两种管道所打开的方式不同而做的分类。 

2.1匿名管道 

    匿名管道从名字来说,这个管道没有特定的名称,即在创建它的时候,不会指定打开文件的文件名、文件路径等不会创建实际文件在文件系统中,纯粹存在于内存中,由操作系统内核进行管理。用于进程间通信。由于匿名管道是非明确目标的文件对于两个毫不相关的进程是无法一起找到这个管道文件的(即两个无关的进程是无法找到公共资源),也就是说只有具有“血缘”关系的进程才能使用匿名管道进行通信。而父子进程共享数据,因此父子进程可以通过创建匿名管道进行通信。

💊下面就图解一下父子进程如何创建匿名管道进行通信

1.首先父进程分别以只读和只写打开管道文件(创建管道文件过程):

2.接着父进程创建子进程,子进程会继承父进程的文件打开方式: 

3.接下来父进程关闭读端,子进程关闭它的写端,父进程只负责往管道文件里面写入,子进程读取就行了 

这样就创建了一个匿名管道。

🚀:为什么父进程需要以两种方式打开管道文件呢?不能以想要的方式打开管道文件然后子进程再以它想要的方式打开文件不就行了?这种太过于麻烦了,子进程在创建的时候会自动继承父进程的文件打开方式,这时候我们只需要各自关闭一个文件即可。

🎯匿名管道的创建

系统调用函数:pipe()

 int pipe(int pipefd[2]),中 pipefd[2],这个数据存放的是什么呢?我们接下来打印一下看看。

 运行结果:

  1. pipe[0]=3:存储的是以只读方式打开管道时获取的fd
  2. pipe[1]=4:存储的是以只写方式打开管道时获取的fd 

🏃接下来我们根据父子进程创建匿名管道进行通信的三个步骤写一个测试:

1.首先父进程分别以只读和只写打开管道文件(创建管道文件过程,通过系统调用函数pipe()---------------------->2.接着父进程创建子进程,子进程会继承父进程的文件打开方式----------->3.接下来父进程关闭读端,子进程关闭它的写端,父进程只负责往管道文件里面写入,子进程读取就行了

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
using namespace std;
//mypipe.cpp
//父进程进行读取,子进程进入写入
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]\n",s,cnt,getpid());
            //子进程向管道写入端写入
            write(fds[1],buffer,strlen(buffer));//将buffer的数据写入管道
            sleep(5);//每隔1秒写入一次
            //sleep(50);
        }
        //写入完成结束之后,把子进程写入端关闭
        close(fds[0]);
        exit(0);
    }
    //父进程进行读取
    close(fds[1]);
    //父进程通信代码
    while(true)
    {
        char buffer[1024];
        //cout<<"AAAAAAAAAAAAAAAAAAAAAAAAAA"<<endl;
        ssize_t s=read(fds[0],buffer,sizeof(buffer)-1);//将读取的数据放到buffer中,读取大小为sizeof(buffer)
        //cout<<"BBBBBBBBBBBBBBBBBBBBBBBBBB"<<endl;
        if(s>0) buffer[s]=0;
        cout<<"Get Message"<<buffer<<"| my pid "<<getpid()<<endl;//这样父进程就读到了子进程传输的信息。
    }
    //进程等待
    n=waitpid(id,nullptr,0);
    assert(n==id);
    //读取完成结束之后,把父进程读取端关闭
    close(fds[0]);
    return 0;
}

运行结果 

接下来我们对代码进行一点改动。来探索,子进程是依何种状态来与父进程进行通信的

 运行结果

从测试结果可知,当父进程sleep(50)之后向管道写入的时候,子进程会进入阻塞状态一直等待父进程向管道传输数据。

 ⛳pipe文件具有访问控制机制,必须先写入才能读取。父子进程在对管道文件进行读写操作是阻塞式I\O,即管道文件中必须先有数据,读取端才能去读取,否则调用read时就会发生阻塞;同样,如果管道中被写满了数据,此时再调用write也会发生阻塞,直到管道中有足够的空间来写入。

2.2 匿名管道对多个进程的控制

🍰上面我们看到了只由一个父进程通过管道文件派发任务控制一个子进程的例子,那我们当然也可以通过对多个子进程派发任务来控制多个子进程。此时一个匿名管道就不够用了,我们需要多个匿名管道,既然需要创建多个匿名管道,那么我们就需要让父进程知道不同的子进程对应的不同的管道的写端。即我们需要让父进程知道所要派发任务的子进程的匿名管道的写端。

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
#include <vector>
#include <string>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>

#define PROCSS_NUM 5//子进程个数
#define MakeSeed() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x124587 ^ rand() % 1234)//定义随机种子,用来随机选择进程

///子进程要完成的任务///
typedef void (*func_t)(); // 函数指针,表示进程任务
void downLoadTask()
{
    std::cout <<getpid()<< ":下载任务" << std::endl;
    sleep(1);
}
void ioTask()
{
    std::cout <<getpid()<< ":IO任务" << std::endl;
    sleep(1);
}
void flushTask()
{
    std::cout << getpid()<<":刷新任务" << std::endl;
    sleep(1);
}
// 加载任务函数:将3个任务加载起来,放到vector中,组成加载任务表
void loadTaskFunc(std::vector<func_t> *out)
{
    assert(out);
    out->push_back(downLoadTask);
    out->push_back(ioTask);
    out->push_back(flushTask);
}

/多进程程序/
// 子进程端点
class subEp
{
public:
    // 构造函数
    subEp(pid_t subId, int writeFd)
        : subId_(subId), writeFd_(writeFd)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process-%d[pid(%d)-fd(%d)]", num++, subId, writeFd);
        name_ = nameBuffer;
    }

public:
    static int num;
    std::string name_; // 子进程名称
    pid_t subId_;      // 子进程的pid
    int writeFd_;      // 子进程写端的文件描述符
};
int subEp::num = 0; // 静态变量初始化要在类外


//读取父进程传递给子进程命令码:即读取管道的读端
int recvTask(int readFd)
{
    int code=0;
    ssize_t s=read(readFd,&code,sizeof(code));//读取到&code中,大小4个字节
    if(s==4) return code;//返回读取结果
    else if(s<=0) return  -1;
    else  return 0;
}
// 创建子进程
void createSubProcess(std::vector<subEp> *subs, std::vector<func_t> &funcMap)
{
    for (int i = 0; i < PROCSS_NUM; i++)
    {
        // 为每个进程建立对应的管道
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        (void)n;
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程,进行处理任务
            close(fds[1]); // 子进程进行读取
            while (true)
            {
                // 1.获取命令码,如果没有发送,进程阻塞
                int commandCode=recvTask(fds[0]);//读取过程//父进程如果不写,子进程处于阻塞状态,父进程代码持续执行
                // 2.完成任务
                if(commandCode>=0 && commandCode < funcMap.size()) funcMap[commandCode]();//执行对应函数
                else if(commandCode==-1) break;
            }
            exit(0);
        }
        //父进程对于代码
        close(fds[0]); // 父进程关闭对应管道的读端进行写入
        subEp sub(id, fds[1]);
        subs->push_back(sub);//走到这里有5个子进程且有对于的pid 和writeFd
    }
}
// 发送任务
void sendTask(const subEp &process, int taskNum)
{
    std::cout << "send task num:" << taskNum << "send to-> " << process.name_ << std::endl;
    int n = write(process.writeFd_, &taskNum, sizeof taskNum);//父进程向子进程写入
    //write(fd,buffer,sizeof buffer)//将buffer的内容写入fd对应的文件
    //父进程向那个子进程的writeFd写入了,那个子进程开始读取
    assert(n == sizeof(int));
    (void)n;
}

void loadBlanceControl(const std::vector<subEp>& subs,const std::vector<func_t> & funcMap, int count)
{
     // 2. 走到这就是父进程,控制子进程
     int processnum = subs.size();//子进程个数5个
     int tasknum = funcMap.size(); // 任务个数
     bool forever= (count==0 ? true :false);
     while (true)
     {
         // 1.选择一个进程--------->std::vector<subEp>--->index------>随机数
         int subIdx = rand() % processnum; // 一共有多少子进程
         // 2. 选择一个任务-------->std::vector<func_t>---index
         int taskIdx = rand() % tasknum;
         // 3. 任务发给选择的进程(父进程进行写)
         sendTask(subs[subIdx], taskIdx);//父进程向管道写入任务几的时候,此时子进程就不是阻塞状态了开始读取
         sleep(1);
         if(!forever)
         {
            count--;
            if(count==0) break;
         }
     }
     //父进程退出,关闭各个子进程写端
     for(int i=0;i<processnum;i++) close(subs[i].writeFd_);
}
//回收子进程
void waitProcess(std::vector<subEp> process)
{
    int processnum=process.size();
    for(int i=0;i<processnum;i++)
    {
        waitpid(process[i].subId_,nullptr,0);
        std::cout<<"wait subprocrss success ..."<<process[i].subId_<<std::endl;
    }
}

int main()
{
    MakeSeed();//随机种子,用来随机分配任务
    // 1建立5个子进程并建立和子进程通信的匿名管道
    std::vector<func_t> funcMap; // 任务表
    loadTaskFunc(&funcMap);//将任务都放在vector中
    std::vector<subEp> subs;//子进程,subEp是一个类
    createSubProcess(&subs, funcMap);//创建子进程,且这个函数会给每个进程分配一个任务
    // 2. 走到这就是父进程,控制子进程
    int taskCnt=20;//0:永远进行
    loadBlanceControl(subs,funcMap,taskCnt);
    // 3.回收子进程信息。
    waitProcess(subs);
    return 0;
}

运行结果 

2.3 命名管道

🍉:上述管道应用的一个限制:就是只能在具有共同祖先(具有亲缘关系)的进程间通信。因父子进程共享资源,所以彼此能找到公共资源。 如果我们想在不相关的进程之间交换数据,该怎么做呢?

我们可以使用FIFO文件来做这项工作,它经常被称为命名管道。 命名管道是一种特殊类型的文件

创建命名管道函数mkfifo()

接下来我们写一个例子,来初步理解命名管道

然后我们用系统调用函数mkfifo(),创建一个命名管道 

上述-------------------done > named_pipe 向命名管道named_pipe  输入hello linux

>  命令行可以看成一个进程,向named_pipe  输入数据。   cat <  可以看出另外一个不相关的进程 ,通过cat < named_pipe 

     cat <named_pipe  输出了hello Linux   ,我们可以理解为完成了两个不相关的进程通过named_pipe完成了通信。

 2.4 命名管道原理

🌔:要想进行进程间的通信,必须让两个进程看到同一份资源,那命名管道是如何做到让两个不同的进程看到同一份资源的?

我们可以让不同的进程打开指定名称(路径+文件名)的同一文件

路径+文件名=唯一性

命名管道是操作系统提供的可以共享的资源,不同的是命名管道是一个特殊的文件,它与普通文件有所不同,命名管道文件的内容不需要刷新到磁盘中,因为它仅需要进行通信即可,不需要耗费时间空间去将内容保存在磁盘中,所以它的文件大小一直是0。 

接下来我们写一个测试,让不同的进程通过命名管道进行通信

#include"common.hpp"
//客户端   client.cpp
int main()
{

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

    //WRITE
    char buffer[1024];
    while(true)
    {
        std::cout<<"please say#"<<std::endl;
        fgets(buffer,sizeof(buffer),stdin);
        buffer[strlen(buffer)-1]=0;
        ssize_t n= write(rfd,buffer,strlen(buffer));
    }

    close(rfd);
    return 0;
}
#include "common.hpp"
//服务端 server.cpp
int main()
{
    //服务端,创建命名管道
    bool r =createFifo(NAMED_PIPE);
    assert(r);

    //通信
    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<<"client error"<<std::endl;

        }
    }
    //移除管道
    close(rfd);
    removeFifo(NAMED_PIPE);
    
    return 0;
}

 server.cpp 和client.cpp通过 共同文件common.cpp进行通信。

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

#define NAMED_PIPE "/tmp/mypipe.100"    //定义一个命名管道路径
common.cpp
bool createFifo(const std::string& path)
{
    umask(0);
    int n=mkfifo(path.c_str(),0600);
    if(n==0) return true;
    else
    {
        std::cout<<"errno:"<< errno <<"err string:"<< strerror(errno) <<std::endl;
        return false;
    }
}

void removeFifo(const std::string &path)
{
   int n= unlink(path.c_str());
   assert(n==0);
   (void)n;
}

运行结果如下:


http://www.dtcms.com/a/98784.html

相关文章:

  • Python 序列构成的数组(对序列使用+和_)
  • sqlmap基础命令总结
  • [C++] 智能指针 进阶
  • Mysql练习题
  • RPCGC阅读
  • 算法刷题记录——LeetCode篇(11.1) [第1001~1010题]
  • Linux进程管理之进程的概念、进程列表和详细的查看、进程各状态的含义
  • C 语言的未来:在变革中坚守核心价值
  • vue搭建一个树形菜单项目
  • 坚持“大客户战略”,昂瑞微深耕全球射频市场
  • 计算机网络 第二章:应用层(2)
  • 项目实战-角色列表
  • SQLAlchemy系列教程:事件驱动的数据库交互
  • vue3实现router路由
  • 用Python实现简易的命令行工具
  • 【Java集合夜话】第9篇下:深入剖析TreeMap源码:红黑树实现原理与面试总结(建议收藏)
  • day1_Flink基础
  • 【Git教程】将dev分支合并到master后,那么dev分支该如何处理
  • Promise使用
  • 【题解】AtCoder At_abc399_d [ABC399D] Switch Seats
  • .NET开发基础知识21-30
  • [GXYCTF2019]禁止套娃1 [GitHack] [无参数RCE]
  • Matplotlib基本使用
  • 数据库监控 | openGauss监控解析
  • 小程序API —— 56页面处理函数 - 下拉刷新
  • 前端常问的宏观“大”问题详解(二)
  • 编译原理课设工作日志
  • 一些练习 C 语言的小游戏
  • 探索Scala基础:融合函数式与面向对象编程的强大语言
  • 在 Unreal Engine 5 中制作类似《鬼泣5》这样的游戏时,角色在空中无法落地的问题可能由多种原因引起。