通信本质,通信方法,匿名管道的原理和多个特点(访问控制,pipe_buf,原子性,半双工),pipe()+模拟实现代码,多个进程之间的通信(匿名管道,模拟实现代码)

本文详细介绍了进程通信的概念,着重讨论了管道(包括匿名管道)的原理、使用方法,以及SystemVIPC和POSIX标准在多进程通信中的角色。通过示例展示了如何在C++中利用管道进行父子进程间的通信,以及半双工和全双工的区别。
摘要由CSDN通过智能技术生成

目录

通信

介绍

为什么要有通信

通信的本质

如何通信

管道

system V

posix标准

信号

管道

引入

匿名管道

原理 

介绍

过程

 实现 -- pipe()

函数原型

参数

返回值

 模拟代码

特点

用于父子进程之间的通信

提供访问控制  

缓冲区被写满时

写入规定

pipe_buf

原子性

提供面向字节流的通信服务

管道的生命周期随进程

代码中添加退出信息

单向通信 -- 半双工的特殊情况

半双工

全双工

多个进程之间通信

介绍

代码 

头文件

主程序 -- 创建子进程+清理资源

 主程序 --  菜单界面+父进程派发任务

执行结果


通信

介绍

进程通信是指不同进程之间进行信息交换和共享数据的机制和方法

包括完成:

为什么要有通信

  • 在操作系统中,进程是独立运行的程序实体,每个进程拥有自己的内存空间和资源
  • 但我们有时候又需要使用多个进程来协同完成某种功能
  • 因此我们就需要进程通信这一概念

进程通信允许不同的进程之间进行协作和数据交换,以实现共同的目标

所以,通信的目的就是 -- 实现多进程协同

通信的本质

  • 通信的前提:让不同进程看到同一块"内存"(特定的结构组织的)
  • 只有这样,才能在它们之间传输数据,实现各种功能
  • 这块"内存"不属于任何一个进程
  • 如果属于某一个进程,其他进程该如何拿到它呢?(进程具有独立性嗷)
  • 因此,它是os中某一个模块提供的

如何通信

管道

  • 基于文件系统的概念
  • 分为匿名管道和命名管道,两者本质上都是文件

system V

  • System V Unix操作系统提供了一组进程间通信机制 -- System V IPC(Inter-Process Communication),用于实现不同进程之间的数据传递和同步
  • System V IPC提供了三种主要的通信机制 -- 消息队列,信号量,共享内存

posix标准

POSIX标准中,有关于多线程和网络通信的规范,允许开发者在遵循POSIX标准的系统上使用统一的接口进行多线程编程和网络通信

信号

信号是一种异步通信方式,用于通知进程发生了某种事件


管道

引入

  • 现实中的管道(天然气管道,石油管道等等),都是为了运输某种资源修建的,并且有一个出口,一个入口,进行单向运输
  • 而在计算机领域,资源指的就是数据
  • 为了实现通信,有设计者设计了一种单向通信的方式
  • 这种方式与现实中的管道功能类似,于是取名为管道

匿名管道

原理 

介绍
  • 管道通信 -- 进程之间通过管道进行通信
  • 管道是用文件描述符和我们建立联系的
过程

当一个进程A以某种方式打开管道,实际上是创建两个文件

一个文件作为管道读端,另一个文件作为管道写端

这两个文件描述符连接到一个内核中的缓冲区,它实际上是一块内存区域

如何让另一个进程B也可以看到这个管道呢?

  • 看到管道,也就是打开文件
  • 如图,我们只要拿到进程A中的file结构体,通过管道相关文件的fd就能操作管道了
  • 也就是说,只要我们可以拿到该进程的pcb,就可以看到管道
  • 但是,进程是具有独立性的,pcb该怎么拿呢?
  • 别忘了,子进程可是能突破独立性的,因为它可以继承父进程的pcb以及文件描述符表(也就是拷贝一份捏):
  • 这样子进程就可以和父进程进行通信噜(因为看到了同一个管道) 

 实现 -- pipe()

函数原型
#include <unistd.h>

int pipe(int pipefd[2]);
  • 用于创建一个匿名管道
参数
  • 参数pipefd是一个整型数组,包含两个文件描述符
  • pipefd[0]是读端fd,pipefd[1]是写端fd
返回值
  • 返回0,表示管道创建成功
  • 返回-1,表示出现错误

 模拟代码

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <assert.h>
#include <cstdio>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>

using namespace std;

int main()
{
    int pipefd[2] = {0}; // 会打开两个文件(用于读,写)
    int ret = pipe(pipefd);
    assert(ret != -1);
    (void)ret; // release下assert不会显示,如果不使用这个ret,会有警告

#ifdef DEBUG
    cout << pipefd[0] << endl;
    cout << pipefd[1] << endl;
#endif

    pid_t fd = fork(); // 创建子进程,让他和父进程通信
    assert(fd!=-1);
    if (fd == 0)
    {
        // 子进程 -- 读
        close(pipefd[1]);//关掉写端
        char buffer[1024];//用于存放数据
        memset(buffer, 0, sizeof(buffer));
        while (true){
            ssize_t size = read(pipefd[0], buffer, sizeof(buffer) - 1); //为了使有地方放\0
            if (size > 0)
            {
                buffer[size] = 0; //读到内容后,记得要加\0
                cout << "im child "
                     << "message: " << buffer << endl;  //打印出读到的内容
            }
            else if (size == 0) //如果为0,说明读到了文件末尾
            {
                cout << "read end" << endl;
                break;
            }
        }
        exit(0);
    }

    // 父进程 -- 写
    close(pipefd[0]);//关掉读端
    string message = "im parent , im writing";
    char buffer[1024]; //存放数据
    int count = 0;
    memset(buffer, 0, sizeof(buffer));
    while (true)
    {
        snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d", getpid(), message.c_str(), count++);  //先将要写入的内容格式化
        ssize_t size = write(pipefd[1], buffer, strlen(buffer)); 
        if (size < 0)  //小于0就说明发生错误
        {
            cout << "write fail" << endl;
        }
        sleep(1); //写入成功后,休息1s再继续写入
    }
    pid_t flag = waitpid(fd,nullptr, 0);  //等待子进程退出
    if (flag <= 0)
    {
        cout << "wait fail" << endl;
    }
    assert(flag > 0);
    (void)flag;

    cout << "wait success" << endl;
    return 0;
}

特点

用于父子进程之间的通信
  • 匿名管道的原理只适用于父子进程之间的通信
  • 因为其中一个进程必须拿到另一个进程的文件描述符表,只有父子进程才能实现
  • 每个进程都打开了两个文件,作为读端和写端
  • 但是管道是单向通信,所以每个进程只有其中一端就行
  • 所以,需要在通信前,确定好各自的分工,然后关闭不需要的文件
提供访问控制  

上面的代码中,我们让父进程在每次写入成功后停1s

可以看到是很秩序的输出信息:

说明 -> 读端会等待写端写入,而不会自己乱读,导致读一些垃圾数据啥的

缓冲区被写满时

如果父进程一直写,一直写,把我们定义的缓冲区写满了会怎么样

(让子进程睡眠20s,不让他读取数据)

// 父进程 -- 写
    close(pipefd[0]);
    string message = "im parent , im writing";
    char buffer[1024];
    int count = 0;
    memset(buffer, 0, sizeof(buffer));
    while (true)
    {
        snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d", getpid(), message.c_str(), count++);
        ssize_t size = write(pipefd[1], buffer, strlen(buffer));
        cout << count << endl;  //标识它写入的次数
        if (size < 0)
        {
            cout << "write fail" << endl;
        }
        // sleep(1);
    }

会发现父进程写入次数卡在了这里:

说明此时已经将缓冲区写满了,但它没有继续写下去

说明 -> 写端写到满时,会等待读端读出,而不会一直写入

写入规定

pipe_buf

规定了内核的管道缓冲区大小

linux下为4096字节

原子性

原子性是指一个操作要么完全执行,要么完全不执行,没有中间状态(在并发编程中被用到)

上面两个例子,就说明管道会对这两个进程进行访问控制

(没有访问控制的情况下,父子进程同时向显示器写入时,会互相干扰各自打印的信息)

提供面向字节流的通信服务

如果子进程每次读取前停几秒,会一次输出一堆信息:

说明 -> 写入次数和读入次数无关 -> 表明数据以字节流的方式存在,它是面向流式的通信服务

管道的生命周期随进程
  • 因为管道也是文件,而文件的生命周期是依靠进程的
  • 一旦没有进程打开这个文件,该文件就会被销毁

  •  当写端的fd被关闭后,读端的read就会返回0,表示读到了文件结尾
代码中添加退出信息
    if (fd == 0)
    {
        // 子进程 -- 读
        close(pipefd[1]);
        char buffer[1024];
        memset(buffer, 0, sizeof(buffer));
        while (true)
        {
            ssize_t size = read(pipefd[0], buffer, sizeof(buffer) - 1); 
            if (size > 0)
            {
                buffer[size] = 0;
                cout << "im child ,"
                     << "message: " << buffer;
            }
            else if (size == 0)  //当读取完数据就退出
            {
                cout << "read end , im quit" << endl;
                break;
            }
        }
        exit(0);
    }
    // 父进程 -- 写
    close(pipefd[0]);
    string message = "im parent , im writing";
    char buffer[1024];
    int count = 0;
    memset(buffer, 0, sizeof(buffer));
    while (true)
    {
        snprintf(buffer, sizeof(buffer) - 1, "pid:%d,%s,%d\n", getpid(), message.c_str(), count++);
        ssize_t size = write(pipefd[1], buffer, strlen(buffer));
        //cout << count << endl;
        if (size < 0)
        {
            cout << "write fail" << endl;
        }
        if(count==5){  //写了五次就关闭写端
            close(pipefd[1]);
            cout<<"parent quit success"<<endl;
            break;
        }
        sleep(1);
    }
    pid_t flag = waitpid(fd, nullptr, 0); //等待子进程退出
    if (flag <= 0)
    {
        cout << "wait fail" << endl;
    }
    assert(flag > 0);
    (void)flag;

    cout << "wait success" << endl;
    return 0;
}

虽然这份代码看不太出来管道是否被销毁,但大概看看知道就行

单向通信 -- 半双工的特殊情况
半双工
  • 半双工(Half-duplex)是一种通信方式,指在通信的两端之间只能单向传输数据
  • 且数据传输方向只能在发送和接收之间切换不能同时进行发送和接收
  • 在半双工通信中,通信双方交替进行数据传输
  • 当一方发送数据时,另一方必须处于接收状态,以接收发送方的数据
  • 一旦发送方完成数据传输,接收方可以切换为发送状态,并向发送方发送数据
全双工
  • 全双工(Full-duplex)是一种通信方式,指在通信的两端可以同时进行双向数据传输,即可以同时发送和接收数据
  • 在全双工通信中,通信双方可以同时进行发送和接收操作,不需要交替进行
  • 两个进程都有独立的发送和接收通道,可以同时进行双向数据传输,从而实现了并行的数据交换

多个进程之间通信

介绍

如果我们想要在多个进程之间通信,就可以创建多个子进程来完成

父进程通过在管道中写入命令,确定好要派任务给哪个进程,然后就美美交给子进程完成噜

代码 
头文件
#include <iostream>

#include <unistd.h>
#include <cstring>
#include <assert.h>
#include <cstdio>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <time.h>

#include <string>
#include <functional>
#include <vector>
#include <unordered_map>

#define num 5

using namespace std;

using func = function<void()>;

vector<func> callbacks; //存放任务方法
unordered_map<int, string> descripe; //将编号和任务名关联起来

//四种任务
void read_mysql()
{
    cout << "process: " << getpid() << " 执行访问数据库任务" << endl;
}
void execule_url()
{
    cout << "process: " << getpid() << " 执行url解析" << endl;
}
void cal()
{
    cout << "process: " << getpid() << " 执行加密任务" << endl;
}
void save()
{
    cout << "process: " << getpid() << " 执行访问数据持久化任务" << endl;
}

void load() //将任务初始化进两个容器中
{
    descripe.insert({callbacks.size(), "read_mysql"});
    callbacks.push_back(read_mysql);
    for (const auto &i : descripe)
    {
        cout << i.first << " : " << i.second << endl;
    }

    descripe.insert({callbacks.size(), "execule_url"});
    callbacks.push_back(execule_url);

    descripe.insert({callbacks.size(), "cal"});
    callbacks.push_back(cal);

    descripe.insert({callbacks.size(), "save"});
    callbacks.push_back(save);
}

void show_hander() //展示现在正在执行什么任务
{
    //cout<<"im show "<<endl;
    for (const auto &i : descripe)
    {
        cout << i.first << " : " << i.second << endl;
    }
}

int task_size(){ //返回任务数量
    return callbacks.size();
}

int wait_command(int fd, bool &quit) //读取父进程派发的任务并返回
{
    uint32_t command = 0;
    ssize_t size = read(fd, &command, sizeof(uint32_t));
    if (size == 0)
    {
        quit = true;
        return -1;
    }
    assert(size == sizeof(uint32_t));
    return command;
}

void send_wakeup(pid_t pid, int fd, uint32_t command)//将任务编号写入管道,让子进程读取
{
    // cout << "cin command: " << command << endl;
    // cout << "descripe : " << descripe[command] << endl;
    write(fd, &command, sizeof(command));
    cout << "call process : " << pid << ", execute : " << descripe[command] << ", though : " << fd << endl;
}
主程序 -- 创建子进程+清理资源
int main()
{
    vector<pair<pid_t, int>> mapping_table; //关联 执行任务的子进程和任务编号 
    load(); //初始化

    // 创建多个进程
    for (int i = 0; i < num; i++)
    {
        int pipefd[2] = {0};
        pipe(pipefd); // 创建管道
        pid_t id = fork();
        assert(id != -1);
        (void)id;

        if (id == 0)
        {
            // child
            close(pipefd[1]); //关闭写端
            while (true) // 等待命令+执行任务
            {
                bool quit = false;
                int command = wait_command(pipefd[0], quit);//等待命令
                if (quit) //判断是否退出
                    break;
                if (command >= 0 && command < task_size())
                {
                    callbacks[command]();//执行派发的任务
                    cout << "success" << endl;
                }
                else
                {
                    cout << "非法command" << endl;
                }
            }
            cout << "process exit : " << getpid() << endl;
            exit(0);
        }

        // parent
        close(pipefd[0]);//关闭读端
        // 保存[通过哪个fd发送命令到子进程]:
        mapping_table.push_back(pair<pid_t, int>(id, pipefd[1])); 

    }

    //派发任务

    // 关闭fd+进程
    for (const auto &i : mapping_table)
    {
        close(i.second); // 关闭每个进程之间通信的写端,使子进程读到0从而退出
    }
    for (const auto &i : mapping_table)
    {
        waitpid(i.first, nullptr, 0); // 等待每个退出的子进程,回收资源
    }
    return 0;
}
 主程序 --  菜单界面+父进程派发任务
    // 派发任务,需要使每个进程都能被使用 -- 单机版的负载均衡,所以使用随机数
    srand((unsigned int)time(nullptr) ^ getpid() ^ 1243225324);

    cout << "****************************************" << endl;
    cout << "*  1. show functions  2. send command  *" << endl;
    cout << "****************************************" << endl;
    cout << "please select : " << endl;

    while (true)
    {
        int flag = 0;
        int select = 0, command = 0;
        cin >> select;
        if (select == 1)
        {
            show_handler();
        }
        else if (select == 2)
        {
            cout << "please enter your command : " << endl;
            cin >> command; // 任务
            // cout << "command: " << command << endl;
            cout << endl;
            int i = rand() % mapping_table.size(); //随机指派一个子进程执行任务
             //派发任务后,需要叫醒对应的子进程
            send_wakeup(mapping_table[i].first, mapping_table[i].second, command);
        }
        else
        {
            cout << "select error" << endl;
        }

        sleep(1);
        while (true) //重复
        {
            cout << endl
                 << "continue : y/n ?" << endl;
            char c = 0;
            cin >> c;
            getchar();
            if (c == 'y' || c == 'Y')
            {
                cout << "****************************************" << endl;
                cout << "*  1. show functions  2. send command  *" << endl;
                cout << "****************************************" << endl;
                cout << "please select : " << endl;
                flag = 1;
                break;
            }
            else if (c == 'n' || c == 'N')
            {
                cout << "exit" << endl;
                flag = 2;
                break;
            }
            else
            {
                cout << "select error , please retry" << endl;
                continue;
            }
            getchar();
        }
        if (flag == 1)
        {
            continue;
        }
        else if (flag == 2)
        {
            break;
        }
    }
执行结果

运行起来就长这样,我们可以手动派发任务

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值