【Linux】进程间通信(1)——匿名管道

文章目录

  • 前言
  • 进程间通信的目的
  • 进程间通信的发展
  • 进程间通信分类
  • 管道
    • 什么是管道?
    • 站在内核角度-管道本质
    • 匿名管道pipe函数
    • 管道的特点(重要)
    • 用fork来共享管道原理
    • 匿名管道的使用步骤
    • 管道的读写规则
    • 管道的四种场景
  • 如何使用管道进行进程间通信?
    • makefile
    • Task.hpp
    • ctrlPipe.cc


前言

之前所学的进程,一般进程和进程之间是独立的关系,最多产生一些耦合,比如父进程创建子进程,父进程等待子进程等等。但实际在很多问题上进程和进程之间是需要相互协同的,那么进程间通信就是在不同进程之间传播或交换信息,接下来让我们一起走进进程间通信的大门!

进程间通信的目的

  1. 数据传输:一个进程需要将它的数据发送给另一个进程。
  2. 资源共享:多个进程之间共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发送了某种事件(如子进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如debug调试进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时直到它的状态改变。

可以形象理解成:两个人之间互相打电话,不管你们的目的是什么,是A打电话给B,叫他拿外套(进程控制),还是A通知B,今天晚上要开会(通知事件),本质都要“打电话”。

进程间通信的发展

  • 管道
  • System V进程间通信(可以实现本地通信)
  • POSIX进程间通信(可以实现跨网络通信)

进程间通信分类

管道:

  • 匿名管道pipe
  • 命名管道

System V IPC:

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC:

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

任何进程通信的手段:

  1. 想办法,先让不同的进程,看到同一份资源(这一步至关重要)!!!
  2. 让一方写入,一方读取完成通信的过程,至于,通信的目的和后续的工作,要结合具体场景。
  3. 不能说让A进程在它的内存中开辟一块空间,让B进程可以访问,因为这样就破坏了进程之间具有独立性这一说。抽象如图:
    【Linux】进程间通信(1)——匿名管道_第1张图片

管道

什么是管道?

  • 管道是Unix中最古老的进程间通信的形式。
  • 我们把一个进程连接到另一个进程的一个数据流称为一个“管道”。
  • 其本质是一个伪文件(管道实为内核使用环形队列机制,借助内核缓冲区4k实现);

在这里插入图片描述
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l是计算行数的一个命令,说明当前只有一个用户在线,
【Linux】进程间通信(1)——匿名管道_第2张图片

站在内核角度-管道本质

【Linux】进程间通信(1)——匿名管道_第3张图片

匿名管道pipe函数

pipe函数用于创建匿名管道,pipe函数的函数原型如下:

int pipe(int pipefd[2]);

pipe函数的参数是一个输出型参数,所以用户需要定义两个数组,并传过来,数组pipefd用于返回两个指向管道读端和写端的文件描述符:

数组元素 代表含义
pipefd[0] 管道读端的文件
pipefd[1] 管道写端的文件
pipe函数调用成功时返回0,调用失败时返回-1。

【Linux】进程间通信(1)——匿名管道_第4张图片

管道的特点(重要)

  1. 两个文件描述符引用,一个表示读端,一个表示写端
  2. 管道提供流式服务,规定数据从管道的写端流入管道,从读端流出。
  3. 管道是单向通信的,如果想要使用双向通信,那就创建两个管道吧;
  4. 管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用于父子通信,因为具有了血缘关系,fork创建子进程后才能看到同一份资源—pipe函数打开管道,并不清楚管道的名字,称之为匿名管道。
  5. 在管道通信中,写入的次数,和读取的次数,不是严格匹配的,读写次数的多少没有强相关。——表现:面向字节流。
  6. 管道具有一定的协同能力,让读端和写端能够按照一定的步骤进行通信——自带同步进制。
  7. 一般而言,进程退出,管道就会被释放,所以管道的生命周期随进程。
  8. 一般而言,内核会对管道操作进行同步与互斥;
  9. 管道是半双工的,数据只能向一个方向流动(ps:吵架是全双工的!哈哈哈!)

用fork来共享管道原理

图上模拟的是子进程向管道写入数据,父进程从管道读取数据。在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
【Linux】进程间通信(1)——匿名管道_第5张图片

匿名管道的使用步骤

站在文件描述符角度-深度理解管道
【Linux】进程间通信(1)——匿名管道_第6张图片
【Linux】进程间通信(1)——匿名管道_第7张图片
【Linux】进程间通信(1)——匿名管道_第8张图片
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。

管道的读写规则

  • 当没有数据可读时
    O_NONBLOCK disable: read调用阻塞,即进程暂停执行,一直等待有数据来为止;
    O_NONBLOCK enable: read调用返回-1,errno值为EAGAIN。

  • 当管道满的时候
    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据;
    O_NONBLOCK enable: 调用返回-1,errno值为EAGAIN。

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0,表面读到文件结尾;

  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性(即要么全做,要么都不做)。

  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道的四种场景

  1. 数据一旦被读走,便不再管道中存在,不可反复读取。如果我们读取完毕了所有的管道数据,如果对方不写入,我们就只能等待
  2. 如果我们写端将管道写满了,我们就不能再写了
  3. 如果我关闭了写端,读取完管道数据后,再读,read就会返回0,表明读到了文件结尾。
  4. 如果写端一直写,读端关闭,那么写端继续写就没有意义了,OS不会维护无意义,低效率的事情,OS会杀死一直在写入的进程!通过信号来终止进程:13)SIGPIPE

下列代码演示:子进程通过写端写数据到管道,父进程读取管道数据。
实例代码:

#include
#include
#include  
#include
#include
#include
#include
#include

int main()
{
    //父进程需要以读写的方式打开管道文件,父进程和子进程之间进行相互通信,父进程打开的管道文件地址是会被子进程继承下来的
    //所以它们指向的是同一份管道文件
    //【一定要让不同的进程看到同一份资源】
    //1.创建管道
    int pipefd[2] = {0};//将其初始化为0
    int n = pipe(pipefd);//pipefd是一个输出型参数
    if(n<0)
    {
        std::cout<<"pipe error, "<<errno<<":"<<strerror(errno)<<std::endl;
    }
    //pipe函数创建管道文件,将数组pipefd初始化成3和4,并返回,因为0,1,2已被占用
    std::cout<<"pipefd[0]: "<<pipefd[0]<<std::endl;//3->读端
    std::cout<<"pipefd[1]: "<<pipefd[1]<<std::endl;//4->写端
    
    //2.创建子进程
    pid_t id = fork();
    assert(id!=-1);//正常应该应用判断,我这里就断言

    if(id==0) //子进程
    {
        //3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
        //我们想让子进程进行写入,所以关闭的是pipefd[0]读,保留pipefd[1]写
        close(pipefd[0]);

        //4.开始通信--结合某种场景---子进程将数据写入管道
        const std::string namestr = "嗨!我是子进程";
        int cnt = 1;
        char buffer[1024];
        while(true)
        {
            //先向buffer字符缓冲区格式化写入数据
            snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的PID:%d\n",namestr.c_str(),cnt++,getpid());
            //然后再将缓冲区的数据写到管道里
            write(pipefd[1],buffer,strlen(buffer));
            sleep(1);
        }

        close(pipefd[1]);

        //子进程执行完退出
        exit(0);
    }

    //父进程
    //3.为实现单向通信,需要关闭不需要的文件描述符fd
    close(pipefd[1]);

    //4.开始通信--结合某种场景---父进程从管道中读取数据
    char buffer[1024]={0};
    while(true)
    {
        //父进程先把管道里的数据读出来,放到缓冲区中
        int n = read(pipefd[0],buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]='\0';
            std::cout<<"我是父进程,子进程给我的消息是:"<<buffer<<std::endl;
        }
        else if(n==0)
        {
            std::cout<<"我是父进程,我读到文件结尾了"<<std::endl;
            break;
        }
        else
        {
            std::cout<<"我是父进程,我读异常了"<<std::endl;
        }
    }

    close(pipefd[0]);//关闭管道读端

    //父进程等待子进程
    waitpid(id,&status,0);
    std::cout<<"sig: "<<(status & 0x7) << std::endl;//如果子进程一直写,但是父进程读取一段时间后退出
    //OS就会杀掉子进程

    return 0;
}

如何使用管道进行进程间通信?

【Linux】进程间通信(1)——匿名管道_第9张图片

makefile

ctrlPipe:ctrlPipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -rf ctrlPipe

Task.hpp

#pragma once

#include 
#include 
#include 
#include 

// typedef std::function func_t;

typedef void (*fun_t)(); //函数指针

void PrintLog()
{
    std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}

void InsertMySQL()
{
    std::cout << "执行数据库任务,正在被执行..." << std::endl;
}

void NetRequest()
{
    std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}

// void ExitProcess()
// {
//     exit(0);
// }

//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Execute(int command)
    {
        if(command >= 0 && command < funcs.size()) funcs[command]();
    }
    ~Task()
    {}
public:
    std::vector<fun_t> funcs;
};

ctrlPipe.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "Task.hpp"
using namespace std;

const int gnum = 3;
Task t;

class EndPoint
{
private:
    static int number;
public:
    pid_t _child_id;
    int _write_fd;
    std::string processname;
public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
        //process-0[pid:fd]
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    std::string name() const
    {
        return processname;
    }
    ~EndPoint()
    {
    }
};

int EndPoint::number = 0;

// 子进程要执行的方法
void WaitCommand()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(int));
        if (n == sizeof(int))
        {
            t.Execute(command);
        }
        else if (n == 0)
        {
            std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl; 
            break;
        }
        else
        {
            break;
        }
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    vector<int> fds;
    for (int i = 0; i < gnum; i++)
    {
        // 1.1 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        // 一定是子进程
        if (id == 0)
        {
            for(auto &fd : fds) close(fd);

            // std::cout << getpid() << " 子进程关闭父进程对应的写端:";
            // for(auto &fd : fds)
            // {
            //     std::cout << fd << " ";
            //     close(fd);
            // }
            // std::cout << std::endl;
            
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[0], 0);
            // 1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        // 一定是父进程
        //  1.3 关闭不要的fd
        close(pipefd[0]);

        // 1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));

        fds.push_back(pipefd[1]);
    }
}


int ShowBoard()
{
    std::cout << "##########################################" << std::endl;
    std::cout << "|   0. 执行日志任务   1. 执行数据库任务    |" << std::endl;
    std::cout << "|   2. 执行请求任务   3. 退出             |" << std::endl;
    std::cout << "##########################################" << std::endl;
    std::cout << "请选择# ";
    int command = 0;
    std::cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint> &end_points)
{
    // 2.1 我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 0;
    while(true)
    {
        //1. 选择任务
        int command = ShowBoard();
        if(command == 3) break;
        if(command < 0 || command > 2) continue;
        
        //2. 选择进程
        int index = cnt++;
        cnt %= end_points.size();
        std::string name = end_points[index].name();
        std::cout << "选择了进程: " <<  name << " | 处理任务: " << command << std::endl;

        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    // 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
    // for(const auto &ep : end_points) 
    // for(int end = end_points.size() - 1; end >= 0; end--)
    for(int end = 0; end < end_points.size(); end++)
    {
        std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0);
        std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
    } 
    sleep(10);

    // 2. 父进程要回收子进程的僵尸状态
    // for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
    // std::cout << "父进程回收了所有的子进程" << std::endl;
    // sleep(10);
}


// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int main()
{
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    createProcesses(&end_points);

    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);

    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

你可能感兴趣的:(Linux操作系统,linux,jvm,java)