Linux进程间通信——匿名管道|命名管道

目录

进程间通信介绍

进程间通信目的

 进程间通信发展

 进程间通信分类

管道(基于文件)

 System V IPC(基于本地通信,不能跨网络)

POSIX IPC 

管道 

什么是管道

匿名管道

匿名管道的原理

任何进程通信的手段

用fork来共享管道原理 

 站在文件描述符角度-深度理解管道

站在内核角度-管道本质

​编辑

pipe函数 

pipe2

 进程通信的步骤

第一步,创建管道 

第二步,创建子进程

 第三步,关闭不需要的fd

 第四步,开始通信

管道的特点

 管道的四种场景

基于匿名管道设计一个小的进程池

命名管道 

使用命令创建命名管道 

创建一个命名管道

mkfifo

mkfifo函数的返回值: 

命名管道的打开规则

 命名管道的应用——客户端和服务端的通信

 匿名管道与命名管道的区别


进程间通信介绍

进程间通信目的

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

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

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

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。

 进程间通信发展

管道

System V进程间通信

POSIX进程间通信

 进程间通信分类

管道(基于文件)

匿名管道pipe

命名管道

 System V IPC(基于本地通信,不能跨网络)

System V 消息队列

System V 共享内存

System V 信号量

POSIX IPC 

消息队列

共享内存

信号量

互斥量

条件变量

读写锁

管道 

什么是管道

管道是Unix中最古老的进程间通信的形式。 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

匿名管道

匿名管道的原理

匿名管道用于进程间通信,且仅限于本地父子进程之间的通信

任何进程通信的手段

a.想办法,先让不同的进程,看到同一份资源

b.让一方写入,一方读取,完成通信过程,至于通信目的与后续工作,要结合具体场景

进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让两个父子进程先看到同一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。 

——创建子进程的时候fork子进程,只会复制进程相关的数据结构对象 ,不会复制父进程曾经打开的文件对象

现象:这就是为什么fork之后,父子进程都printf,cout,都会向同一个显示器终端打印数据的原因

用fork来共享管道原理 

 站在文件描述符角度-深度理解管道

站在内核角度-管道本质

pipe函数 

 

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

 

pipe2

pipe2函数与pipe函数类似,也是用于创建匿名管道,其函数原型如下:

int pipe2(int pipefd[2], int flags);

pipe2函数的第二个参数用于设置选项。

1、当没有数据可读时:

O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
2、当管道满的时候:

O_NONBLOCK disable:write调用阻塞,直到有进程读走数据。
O_NONBLOCK enable:write调用返回-1,errno值为EAGAIN。
3、如果所有管道写端对应的文件描述符被关闭,则read返回0。
4、如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
5、当要写入的数据量不大于PIPE_BUF时,Linux将保证写入的原子性。
6、当要写入的数据量大于PIPE_BUF时,Linux将不再保证写入的原子性。

 进程通信的步骤

 用管道通信,大致分为四个步骤

第一步,创建管道 

查看一下管道文件是否创建成功

第二步,创建子进程

 第三步,关闭不需要的fd

 我们一般把pipefd[0]作为读端,pipefd[1]作为写端——0对应嘴巴用来读,1对应笔用来写

 

 第四步,开始通信

 

#include <iostream>
#include <string>
#include <cerrno>
#include <cassert>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// 让不同的进程看到同一份资源!!!!
    // 任何一种任何一种进程间通信中,一定要 先 保证不同的进程之间看到同一份资源
    int pipefd[2] = {0};
    //1. 创建管道
    int n = pipe(pipefd);
    if(n < 0)
    {
        std::cout << "pipe error, " << errno << ": " << strerror(errno) << std::endl;
        return 1;
    }
    std::cout << "pipefd[0]: " << pipefd[0] << std::endl; // 读端, 0->嘴巴->读书
    std::cout << "pipefd[1]: " << pipefd[1] << std::endl; // 写端, 1->笔->写东西的

    //2. 创建子进程
    pid_t id = fork();
    assert(id != -1); //正常应该用判断,我这里就断言:意料之外用if,意料之中用assert

    if(id == 0)// 子进程
    {
        //3. 关闭不需要的fd,让父进程进行读取,让子进程进行写入
        close(pipefd[0]);

        //4. 开始通信 -- 结合某种场景
        const std::string namestr = "hello ,我是子进程";
        int cnt = 1;
        char buffer[1024];
        while(true)
        {
            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];
    while(true)
    {
        int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = '\0';
            std::cout << "我是父进程, child give me message: " << buffer << std::endl;
        }

    }
    close(pipefd[0]);

    return 0;

}

 

 这要一个简单的管道通信就写好了

这里有一个细节,在子进程写的时候每写一次sleep一秒

管道的特点

1.单向通信

2.管道的本质是文件,因为fd的生命周期随进程,管道的生命周期是随进程的

3.管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用与父子通信——pipe打开管道,并不清楚管道的名字,匿名管道

4.在管道通信中,写入的次数,和读取的次数不是严格匹配的,读写次数的多少没有强相关--表现--字节流

5.具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信--自带同步机制

 管道的四种场景

1.如果我们read读取完毕了所有的管道数据,如果对方不发,我们只能等待

2.如果我们write端将管道写满了,我们还能写吗?不能

3.如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾

4.写端一直写,读端关闭,会发生什么呢?没有意义OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程!OS会通过信号来终止进程   13)SIGPIPE

基于匿名管道设计一个小的进程池

父进程向子进程写入特定的消息,唤醒子进程,甚至让子进程定向的执行某周任务

 创建进程部分

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]);
    }
}

在创建管道for循环中,在下一次循环管道的文件描述符会覆盖掉,作为父进程怎么知道哪一个进程对应哪一个管道呢?

所以定义一个类(或者结构体)来管理创建创建出来的子进程和管道,先描述在组织,

父进程需要的是有哪些子进程,哪些管道,子进程和管道的关系

再用一个vector容器来管理所有这个类

class EndPoint
{
public:
    pid_t _child_id;//子进程的pid
    int _write_fd;//向哪一个管道里写

public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
    }
    ~EndPoint()
    {
    }
};

子进程要执行的方法

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;
        }
    }
}

 总代码

Task.hpp

#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>

// typedef std::function<void ()> 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;
}

//约定,每一个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;
};





 ctrlProcess.cc

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include "Task.hpp"
using namespace std;

const int gnum = 5;//表示未来一共想要创建几个子进程
Task t;

class EndPoint
{
public:
    pid_t _child_id;
    int _write_fd;

public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
    }
    ~EndPoint()
    {
    }
};

// 子进程要执行的方法
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)
        {
            break;
        }
        else
        {
            break;
        }
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    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)
        {
            // 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]));
    }
}

int main()
{
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    vector<EndPoint> end_points;
    createProcesses(&end_points);
    // 2. 我们的得到了什么?end_points
    int num = 0;
    while(true)
    {
        //1. 选择任务
        int command = COMMAND_LOG;

        //2. 选择进程
        int index = rand()%end_points.size();

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

        sleep(1);
    }
    return 0;
}

 

匿名管道只能用于具有血缘关系的进程,如父子进程来进行进程间通信,如果我想要让毫不相干的两个进程来进程通信呢?——这里就可以用到命名管道了

----------------------------------------------------

命名管道 

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

命名管道是一种特殊类型的文件

使用命令创建命名管道 

 我们可以使用mkfifo命令创建一个命名管道。

这里我从另一端输入,从另一端读取,这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

 如果两个不相干的进程打开同一个文件,要不要再打开一个stuct file 结构体?

答案是不用,操作系统只会将指向改文件的引用计数ret加一

而且用管道文件写入读取不会进行刷盘,也没有对应的data block

如何保证两个毫不相关的进程,看到的是同一个文件并打开?

文件是有唯一性的,用路径表示

让不同的进程通过文件路径+文件名看到同一个文件,并打开——就是看到了同一资源——具备了进程间通信的前提

创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

命名管道也可以从程序里创建,相关函数有:

mkfifo

int mkfifo(const char *filename,mode_t mode);

mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限

若想创建出来命名管道文件的权限值不受umask的影响,则需要在创建文件前使用umask函数将文件默认掩码设置为0

umask(0); //将文件默认掩码设置为0
mkfifo函数的返回值: 

命名管道创建成功,返回0。

命名管道创建失败,返回-1。

命名管道的打开规则

1、如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO。
O_NONBLOCK enable:立刻返回成功。
2、如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO。
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO。

注意这里的权限变成了664,但是我给的权限是666,这里是因为没有设置权限掩码

再重新创建文件

 命名管道的应用——客户端和服务端的通信

服务端

第一步,创建管道

第二步,让服务端直接开启管道文件

 第三步,正常通信

第四步,关闭不需要的fd 

客户端

打开服务端,会发现server会卡在这里,原因客户端没有打开命名管道而导致的阻塞

 

继续打开客户端

这里简单的通信就做好了,但是这里服务端退出并不会自动把管道文件删除,在下次启动的时候就会报错,所以要对这里进行一些优化,用unlink函数

这里直接给出最后的全部代码(不用回车就能实现字符输入)

client.cc

#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
// #include <ncurses.h>
#include "comm.hpp"

int main()
{
    //1. 不需创建管道文件,我只需要打开对应的文件即可!
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }

    // 可以进行常规通信了
    char buffer[NUM];
    while(true)
    {
        // std::cout << "请输入你的消息# ";
        // char *msg = fgets(buffer, sizeof(buffer), stdin);
        // assert(msg);
        // (void)msg;
        // int c = getch();
        // std::cout << c << std::endl;
        // if(c == -1) continue;

        system("stty raw");
        int c = getchar();
        system("stty -raw");

        //std::cout << c << std::endl;
        //sleep(1);

        //buffer[strlen(buffer) - 1] = 0;
        // abcde\n\0
        // 012345
        //if(strcasecmp(buffer, "quit") == 0) break;

        ssize_t n = write(wfd, (char*)&c, sizeof(char));
        assert(n >= 0);
        (void)n;
    }

    close(wfd);

    return 0;
}

server.cc

#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

//少年们, 我刚刚写了一个基于匿名管道的进程池
// 可不可以把它改成使用命名管道呢??
int main()
{
    // 1. 创建管道文件,我们今天只需要一次创建
    umask(0); //这个设置并不影响系统的默认配置,只会影响当前进程
    int n = mkfifo(fifoname.c_str(), mode);
    if(n != 0)
    {
        std::cout << errno << " : " << strerror(errno) << std::endl;
        return 1;
    }
    std::cout << "create fifo file success" << std::endl;
    // 2. 让服务端直接开启管道文件
    int rfd = open(fifoname.c_str(), O_RDONLY);
    if(rfd < 0 )
    {
        std::cout << errno << " : " << strerror(errno) << std::endl;
        return 2;
    }
    std::cout << "open fifo success, begin ipc" << std::endl;

    // 3. 正常通信
    char buffer[NUM];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if(n > 0)
        {
            buffer[n] = 0;
            //std::cout << "client# " << buffer << std::endl;
            printf("%c", buffer[0]);
            fflush(stdout);
        }
        else if(n == 0)
        {
            std::cout << "client quit, me too" << std::endl;
            break;
        }
        else 
        {
            std::cout << errno << " : " << strerror(errno) << std::endl;
            break;
        }
    }

    // 关闭不要的fd
    close(rfd);

    unlink(fifoname.c_str());

    return 0;
}

comm.hpp

#pragma once

#include <iostream>
#include <string>

#define NUM 1024

const std::string fifoname = "./fifo";
uint32_t mode = 0666; 

 服务端和客户端之间的退出关系

当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了)。

当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE),此时客户端就被操作系统强制杀掉了。

 匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开

命名管道由mkfifo函数创建打开用open

FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

酷帅且洋仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值