进程间通信之匿名和命名管道

1、验证vscode是否支持C11的方法:包含头文件#include<unoreder_map> std;;;;;;,出现红色波浪线就是不支持,需要自己手动进行配置,vscode新版本已经支持,可以不用检查。

2、c++文件的后缀可以是.cpp.cc.cxx

进程间通信的概念

因为进程具有独立性,那如果要进行交互,就只能通过进程通信。

**OS需要直接或者间接给通信的进程提供内存空间,用以数据交互;其次,要通信的进程必须都能看到同一份公共的资源。**不同的通信种类划分依据是OS的哪个模块提供的公共资源。如,由文件系统提供的–>管道,由系统system V的通信模块提供的–>system V,内存–>共享内存,计数器–>信号量,队列–>消息队列。

进程通信的目的有这些

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

综上,我们是需要多进程协同来完成业务内容。

我们所接触过的进程间通信,如管道。

进程间通信的发展

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

标准1:System V 致力于解决本地通信

  • System V 消息队列
  • System V 共享内存 //这个重要
  • System V 信号量

标准2:POSIX 致力于解决跨主机通信–重点

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

管道

把从一个进程连接到另一个进程的一个数据流称为一个“管道”。是基于文件系统完成数据交换。

复习文件系统,fd文件描述符。

struct file{}结构体中包括1、file的操作方法;2、自己的内核缓冲区,需要向磁盘刷新数据;3、struct Page{}。这些东西

进程PCBtask_struct{}结构体中有个指针指向struct files_struct{}结构体,里面有个指针数组struct file* fd_array[],根据fd能找到每个文件打开时加载到文件系统的信息,即struct file{}结构体。fork创建子进程后,子进程继承了文件描述符表的内容==>两个进程看到了同一份内核资源!这是通信的前提!因此文件系统有个专门供通信的文件–管道文件(内存级文件)。注意,并不是真的文件,因为文件还要定期向磁盘刷新(这效率太慢了),而且我们的目的是让数据在进程间交互,而不是要写到磁盘上。

所以磁盘是不会存在这个管道文件!管道文件是由OS直接在内核中打开,创建struct file{},里面有个字段_type,标明该文件是管道文件。

匿名管道

用于父子进程的内存级文件没有名称,故为匿名管道。

进程分别以读和写的形式打开同一个文件(用2个文件描述符),父进程fork出子进程,父子进程看到同一份管道文件,一般而言,管道文件只能用来进行单向数据通信!所以接着进程关闭读方式的文件描述符所指向的文件,进程关闭写方式打开的文件描述符所指向的文件。【同理,可以关父进程的写,子进程的读,就可以让父进程去拿子进程的数据9反向)】

因为子进程是直接继承父进程,所以父进程读和写都要有。

基于pipe函数实现通信

需要用到的函数pipe

#include <unistd.h>
int pipe(int pipefd[2]);//输出型参数,读和写方式各一个文件描述符,下标0为读read,下标1为写write
//返回值:成功返回0,失败返回1,且可根据错误码errno查看错误信息

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

int fds[2];//参数
int ret = pipe(fds);//返回读和写的文件描述符
assert(ret == 0);//pipe函数调用成功
//检查返回值是否为3 和 4(0,1,2固定占用)
//std::cout << "fds[0]:" << fds[0] << std::endl;//3--读
//std::cout << "fds[1]:" << fds[1] << std::endl;//4--写

2、fork出子进程,父进程关闭写入,子进程关闭读取,然后进行数据传输

整体代码如下

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    //1、创建管道文件,打开读写端
    int fds[2];//参数
    int ret = pipe(fds);//返回读和写的文件描述符
    assert(ret == 0);//pipe函数调用成功

    // std::cout << "fds[0]:" << fds[0] << std::endl;//3--读
    // std::cout << "fds[1]:" << fds[1] << std::endl;//4--写

    //2、fork出子进程
    pid_t id = fork();
    assert(id >= 0);
    if(id == 0)//子进程
    {
        //子进程写入,故关闭写入文件读描述符
        close(fds[0]);

        //子进程的通信代码
        const char* s = "我是子进程,我正在给你发信息";
        int cnt = 0;
        while(true)
        {
            //不断给父进程写入
            char buffer[1024];//当作缓冲区
            snprintf(buffer, sizeof(buffer), "child->parent :%s[%d][%d]", s, cnt, getpid());//给buffer写入
            //将缓冲区的字符串写入pipe文件
            write(fds[1], buffer, strlen(buffer));//strlen不包括\0
            sleep(5);//每5s写1次
        }

        //子进程退出
        close(fds[1]);
        exit(0);
    }
    //父进程读取,故关闭写入文件写描述符
    close(fds[1]);

    // 通信代码
    while(true)
    {
        char buffer[1024];//当缓冲区使用
        ssize_t size = read(fds[0], buffer, sizeof(buffer)-1);//-1为缓冲区预留\0
        if(size > 0) buffer[size] = 0;
        std::cout << "Get Msg# " << buffer << "| MyPid# " << getpid() << std::endl;
    }

    //父进程阻塞式等待
    ret = waitpid(id, nullptr, 0);
    assert(ret == id);

    //安全起见,程序结束也关闭另一个文件描述符
    close(fds[0]);
    return 0;
}

管道4种读写情况

1、如果写端不写,读端一直读,会怎么样?(即写端写一次休眠5s,读端一直读)答:**如果管道没数据了,读端在读,默认会直接阻塞当前正在读取的进程!**即父进程一直在read处阻塞等待读取pipe文件【read函数是阻塞式调用,没有读到数据就会阻塞等待,即把父进程的r状态改成S状态】

2、反过来,如果读端不读,写端一直写,会怎么样?答:管道是个固定大小的缓冲区,pipe文件是有大小限制的,退一步说buffer也是有大小限制的,缓冲区会出现被填满的情况,为避免原有数据被覆盖,写端会阻塞等待,等读端读取才能继续写。读端也是按固定大小来读取,如果缓冲区填满了,再去读的话,就会读出一长串。

3、写端不写了并且关闭了自己fds[1]写文件描述符,这种情况需要加一个判断条件:读端读到返回值为0【关闭了写文件描述符–>缓冲区已经清空–>返回值为0】就退出。

4、读端不读了并关闭了自己fds[0]读文件描述。这种情况下OS会给写端发送信号13) SIGPIPE(子进程是异常终止,父进程可拿到子进程的退出码),终止写端。

管道特征

  1. 管道依托于文件存在,故管道的生命周期随进程
  2. 管道可以用来进行具有血缘关系的进程之间通信(爷孙之间,父子之间)进行通信,常用于父子通信;
  3. 管道是面向字节流的(后续在网络部分详解) 【在本例代码中的表现就是不会按照写入的格式取出数据,就按照缓冲区最大存放字节数来读取】;
  4. 半双工通信,又叫做单向通信(任何时刻,只允许一个进程给另一个进程发消息);
  5. 内部有互斥和同步机制,是对共享资源进行保护的手段

管道命令剖析

执行第一条代码后,会发现命令sleep 10000sleep 20000的PPID一样,都是27972,即2个命令共为同个父亲(bash)的孩子,两个sleep是兄弟关系

[yyq@VM-8-13-centos mypipe]$ sleep 10000 | sleep 20000
[yyq@VM-8-13-centos mypipe]$ ps ajx | head -1 && ps ajx | grep -E '10000|20000'
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
27972 29433 29433 27972 pts/6    29433 S+    1001   0:00 sleep 10000
27972 29434 29433 27972 pts/6    29433 S+    1001   0:00 sleep 20000
29467 29681 29680 29467 pts/7    29680 S+    1001   0:00 grep --color=auto -E 10000|20000//最后一条命令是grep,正常来说,自己检测自己,STAT是R+

分析:bash按照|来分割命令,把第一行代码解析为sleep 10000sleep 20000,接着bash进程创建两个子进程和匿名管道,在fork之前把这两个进程级联起来,这样fork出的两个sleep子进程就可以访问同一个管道,前一条命令为写端,后一条命令为读端,以此类推。

基于pipe的进程池设计

就是用一个进程去控制另一个进程执行命令。

commandCode为1、2、3、4,每个命令表示不同的工作任务,由父进程发给子进程,子进程若未收到指令代码就会处于阻塞等待状态。

创建父进程(写端)–循环创建管道–循环fork子进程(读端)-

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

// 随机数第三个值0x38402561是自定的
#define MAKE_SEED() srand((unsigned long)time(nullptr) ^ getpid() ^ 0x38402591)
#define PROCESS_NUM 5 // 创建子进程的个数

//子进程要完成的任务//
// 函数指针类型
typedef void (*func_t)();

void downloadTask()
{
    std::cout << "子进程id:" << getpid() << "正在执行下载任务" << std::endl;
    sleep(1);
}

void ioTask()
{
    std::cout << "子进程id:" << getpid() << "正在执行IO任务" << std::endl;
    sleep(1);
}

void flushTask()
{
    std::cout << "子进程id:" << getpid() << "正在执行刷新任务" << std::endl;
    sleep(1);
}

void loadTaskFunc(std::vector<func_t> *funcMap)
{
    assert(funcMap);
    funcMap->push_back(downloadTask);
    funcMap->push_back(ioTask);
    funcMap->push_back(flushTask);
}

//多进程 程序代码//
// 子进程键值
class subEp // 子终端(子进程:名称+id+读fd)
{
public:
    subEp(pid_t subId, int writeFd)
        : _writeFd(writeFd), _subId(subId)
    {
        char nameBuffer[1024];
        snprintf(nameBuffer, sizeof nameBuffer, "process{%d}[id(%d)][fd(%d)]\n", num, _subId, _writeFd);
        ++num;
        // 格式化创建进程名“process{num}[id][fd]”
        _name = nameBuffer;
    }

public:
    static int num;
    std::string _name;
    int _writeFd;
    pid_t _subId;
};
int subEp::num = 0;

int recvTask(int readfd)
{
    int code = 0;
    ssize_t n = read(readfd, &code, sizeof code);
    if (n == sizeof(int))
        return code; // 表示正确接收到任务
    else if (n == 0)
        return -1; // 表示写端已退出,那么子进程读不到,故返回-1
    else
        return 0; // 不可能会走这条线,只是为了写全if-else
}

void sendTask(const subEp &subProc, int taskFuncIndex)
{
    std::cout << "进入发送任务,任务[" << taskFuncIndex << "]将被下发给进程" << subProc._name << std::endl;
    int n = write(subProc._writeFd, &taskFuncIndex, sizeof taskFuncIndex); // 按4字节发送
    assert(n == sizeof(int));                                              // 保证发送字节数是合法的
    (void)n;                                                               // 因为assert只在debug版本下有效,用release的话,n没人使用即失效,故改为void
}

void createSubProcess(std::vector<class subEp> *subs, std::vector<func_t> &funcMap)
{
    std::vector<int> deleteFd; // 8解决第n个进程继承前n-1个写端文件描述符的问题
    for (int i = 0; i < PROCESS_NUM; i++)
    {
        // 1、建立通信信道
        int fds[2];
        int ret = pipe(fds);
        assert(ret == 0); // 保证成功创建管道文件
        (void)ret;

        // 2、创建子进程
        pid_t id = fork();
        if (id == 0)
        {
            // 子进程拿到指令并处理任务
            close(fds[1]); // 子进程为读端,关闭写端
            while (true)
            {
                for (int i = 0; i < deleteFd.size(); i++) // 8解决第n个进程继承前n-1个写端文件描述符的问题
                {
                    close(deleteFd[i]); // 关闭继承下来的不属于该子进程的写端文件描述符
                }
                // a.获取命令码,如果没有收到,就应该阻塞等待
                int commandCode = recvTask(fds[0]);
                // b.得到命令名就完成任务
                if ((commandCode >= 0) && (commandCode < funcMap.size()))
                    funcMap[commandCode]();
                else if (commandCode == -1)
                {
                    std::cout << "父进程写端已退出,子进程读端即将退出" << std::endl;
                    break;
                }
                else
                    std::cout << "子进程获取任务代码有误!" << std::endl;
            }
            exit(0);
        }

        close(fds[0]); // 父进程为写端,关闭读端

        // 3、保存[子进程id, 写端文件描述符]
        subEp sub(id, fds[1]);
        subs->push_back(sub);
        deleteFd.push_back(fds[1]); // 8解决第n个进程继承前n-1个写端文件描述符的问题
    }
}

void loadBalanceControl(const std::vector<subEp> &subs, const std::vector<func_t> &funcMap, int taskCount)
{
    int subProNum = subs.size();
    int taskFuncNum = funcMap.size();
    bool forever = (taskCount > 0 ? false : true); //<=0表示永远执行
    while (taskCount > 0 || forever)
    {
        // 将任务均衡地分配给每一个子进程(单机版的子进程负载均衡)
        //  1、选择子进程-subs中选择一个下标
        int subProcIndex = rand() % subProNum;
        // 2、选择任务-funcMap中选择一个下标
        int taskFuncIndex = rand() % taskFuncNum;
        // 3、将任务发送给子进程
        sendTask(subs[subProcIndex], taskFuncIndex);

        sleep(1);
        if (forever == false)
            taskCount--;
    }

    // 走到在说明父进程写端退出,就是把键值中的写端关掉
    for (int i = 0; i < subProNum; i++)
    {
        close(subs[i]._writeFd);
    }
}

void waitProcess(const std::vector<subEp> &process)
{
    int size = process.size();
    for (int i = 0; i < size; i++)
    {
        waitpid(process[i]._subId, nullptr, 0);
        std::cout << "wait subprocess[" << process[i]._subId << "] success!" << std::endl;
    }
}

int main()
{
    MAKE_SEED();

    std::vector<func_t> funcMap; // 任务函数表
    std::vector<subEp> subs;     // 用于保存所有子进程的[id, 写端文件描述符]

    // 1、加载任务列表,建立PROCESS_NUM个子进程和pipe,用vector保存
    loadTaskFunc(&funcMap);
    createSubProcess(&subs, funcMap);

    // 2、执行以下代码的都是父进程,可以在此处写控制子进程的代码
    int taskCount = 3;                            // 父进程想让子进程执行的任务次数 <=0表示永远执行,>0表示限制执行次数
    loadBalanceControl(subs, funcMap, taskCount); // 让父进程负载均衡控制子进程

    // 3、回收子进程信息
    waitProcess(subs);
    return 0;
}

createSubProcess()函数处,有一个小bug,但是不影响目前的使用和运行。假设文件描述符从3开始。

共识:父进程打开的文件是被所有子进程共享的。

分析:代码中父进程先创建管道文件,然后fork出第1个子进程,此时父进程的文件描述符表有一个写端文件描述符3(父进程的文件描述符表),第1个子进程拿着读端文件描述符4(子进程的文件描述符表,因为继承了父进程的表已经有0123了)【wfd:3,rfd:4】;当fork第2个子进程时,第2个子进程会继承父进程的文件描述符表,此时父进程的文件描述符表有一个写端文件描述符4(父进程的文件描述符表),第2个子进程拿着读端文件描述符5(子进程的文件描述符表,因为继承了父进程的表已经有01234了)【wfd:3、4,rfd:5】…即父进程的写端管道的写文件描述符是不停地被继承的

正常来说,我们退出第1个子进程,就关闭对应的第1个管道的写端【但是实际上没关完,第2、第3…第n个子进程都还拿着第1个子进程的写端没关】,那么第1个进程就会一直处于阻塞等待状态!**即只有关掉最后一个子进程(因为它拿着前面所有子进程的写端描述符),才能让其他子进程正常退出!**如果从0开始关一个子进程,waitpid一个子进程,就会出bug,因为下标为0的子进程的写端还被其他子进程拿着,父进程是wait不到子进程退出的。

故解决方法是1、倒序挨个关闭子进程(本份代码样例createSubProcess()函数中的deleteFd);2、for循环一次性关闭所有子进程的读端(本份代码样例loadBalanceControl()函数最后)。

命名管道

一次性生成两个可执行程序的Makefile要用伪目标来完成。

.PHONY:all
all:
	server client
server:server.cc
	g++ -o $@ $^ -std=c++11
client:client.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f server client

实现命令行式的两进程间通信

介绍mkfifo

SYNOPSIS
       mkfifo [OPTION]... NAME...

DESCRIPTION
       Create named pipes (FIFOs) with the given NAMEs.

       Mandatory arguments to long options are mandatory for short options too.

       -m, --mode=MODE
              set file permission bits to MODE, not a=rw - umask

       -Z     set the SELinux security context to default type
[yyq@VM-8-13-centos named_pipe]$ mkfifo namedpipe//mkfifo 文件名
[yyq@VM-8-13-centos named_pipe]$ ll -i
total 12
1451013 -rw-rw-r-- 1 yyq yyq  89 Feb 14 15:02 client.cc
1451014 -rw-rw-r-- 1 yyq yyq 161 Feb 14 15:03 Makefile
1451016 prw-rw-r-- 1 yyq yyq   0 Feb 14 15:08 namedpipe//这个就是命名管道文件,文件类型是p,也有自己的inode
1451008 -rw-rw-r-- 1 yyq yyq  89 Feb 14 15:02 server.cc

打开两个ssh窗口,我们可以在第一个窗口中,向这个命名管道文件里重定向写入数据

cnt=0; while :; do echo "hello world -> $cnt"; let cnt++; sleep 1; done > namedpipe

再在第二个窗口做重定向输出

cat < namedpipe

在这个过程中我们通过ll命令,可以观察到管道文件的大小一直是0!原因:命名管道的数据不会被刷新到磁盘,故大小一直为0。

请问:命名管道是如何做的让不同的进程看到同一份资源呢?因为即使多个进程打开指定名称的同一个文件时,也只会有一个struct file,再增加一个引用计数就行。重点:**指定名称的文件是具备唯一性的,因为是由路径+文件名唯一确定的。**同一个路径下不会有名称相同的文件。

基于mkfifo函数实现通信

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>

       int mkfifo(const char *pathname, mode_t mode);//mode就是指文件权限,可以写成0666,读写执行均打开

DESCRIPTION
       mkfifo()  makes  a  FIFO  special  file  with name pathname.  mode specifies the FIFO's permissions.  It is modified by the process's umask in the usual way: the permissions of the created file are (mode & ~umask).
       A FIFO special file is similar to a pipe, except that it is created in a different way.  Instead of being an anonymous com‐munications channel, a FIFO special file is entered into the file system by calling mkfifo().
       Once  you  have created a FIFO special file in this way, any process can open it for reading or writing, in the same way as an ordinary file.  However, it has to be open at both ends simultaneously before you can proceed to do any input or  output operations on it.  Opening a FIFO for reading normally blocks until some other process opens the same FIFO for writing, and vice versa.  See fifo(7) for nonblocking handling of FIFO special files.

RETURN VALUE
       On success mkfifo() returns 0.  In the case of an error, -1 is returned (in which case, errno is set appropriately).//成功返回0,失败返回-1,可以查看错误码

创建命名管道

bool creatFifo(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 ret = unlink(path.c_str());
    assert(ret == 0);
    (void)ret;//需要使用ret,就不会出现warning
}

完整代码

//server.cc
#include "comm.hpp"

int main()
{
    // 创建管道文件
    bool ret = creatFifo(NAMED_PIPE);
    assert(ret == true);
    (void)ret;

    std::cout << "server begin" << std::endl;
    // 打开读端
    int readFd = open(NAMED_PIPE, O_RDONLY);
    std::cout << "server end" << std::endl;
    if (readFd < 0)
        exit(1); // 打开失败,退出码1

    // 读取数据
    char buffer[1024];
    while(true)
    {        
        ssize_t s = read(readFd, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << "client->server# " << buffer << std::endl;
        }
        else if(s == 0)
        {
            //写端已关闭,则读端也关闭
            std::cout << "检测到写端已关闭,程序即将退出..." << buffer << std::endl;
            break;
        }
        else
        {
            std::cout << "读取出错 " << strerror(errno) << std::endl;
            break;
        }
    }

    // 关闭读端
    close(readFd);

    // 删除命名管道
    removeFifo(NAMED_PIPE);
    return 0;
}

//client.cc
#include "comm.hpp"

int main()
{
    std::cout << "client begin" << std::endl;
    // 打开写端
    int writeFd = open(NAMED_PIPE, O_WRONLY);
    std::cout << "client begin" << std::endl;
    if (writeFd < 0)
        exit(1); // 打开失败,退出码1

    // 写入数据
    char buffer[1024];
    while(true)
    {
        std::cout << "Please say# ";
        fgets(buffer, sizeof(buffer) - 1, stdin);//从输入流中保存信息->即用户输入
        if(strlen(buffer) > 0) buffer[strlen(buffer) - 1] = 0;//去掉结尾的\n
        int ret = write(writeFd, buffer, strlen(buffer));
        assert(ret == strlen(buffer));
        (void)ret;
    }

    // 关闭写端
    close(writeFd);
    return 0;
}

//comm.hpp
#pragma once

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

#define NAMED_PIPE "/tmp/mypipe"

bool creatFifo(const std::string &path)
{
    umask(0);
    int ret = mkfifo(path.c_str(), 0600); // 只允许文件的拥有者进行通信
    if (ret == 0)
        return true;
    else
    {
        std::cout << "errno: " << errno << "err string: " << strerror(errno) << std::endl;
        return false;
    }
}

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

命名管道特征

  1. 读端会阻塞在open函数,直到写端也接入。表现为执行./server后,只输出server begin,再执行./client,才输出server end;而./client执行后,直接输出client beginclient end
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值