【Linux】进程间通信之管道

目录

  • 前言
  • 1、IPC介绍
    • 1.1、进程间通信的目的
    • 1.2、背景和发展
    • 1.3、进程间通信的分类
  • 2、管道
    • 2.1、概念
    • 2.2、管道的原理
    • 2.3、匿名管道
    • 2.4、管道的读写规则
    • 2.5、多进程间通信
    • 2.6、管道的特点
  • 3、命名管道
    • 3.1、概念
    • 3.2、创建命名管道
    • 3.3、命名管道的使用
    • 3.4、匿名管道与命名管道的区别
    • 3.5、命名管道的读写规则

前言

这篇文章给大家带来进程间通信的学习!!!


1、IPC介绍

1.1、进程间通信的目的

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

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

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

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


1.2、背景和发展

  • 我们都知道:进程间居有独立性,互不干扰。进程间想要通信(交互数据:需要多个进程协同处理一件事情!!!),成本会很高

  • 进程虽然具有独立性(写时拷贝),但不是彻底独立了,有时候,我们需要二个进程进行信息交互,这时候我们就需要打破它们的独立性,进行数据交互

进程间通信的发展:

  • 管道:它是Linux系统一下一种IPC通信机制,可以用于进程间通信,线程间通信

  • System V进程间通信:用于在操作系统层面上进行进程间通信的标准,它给用户提供了系统接口,调用它的接口就能完成进程间的通信

  • POSIX进程间通信:它是System V 进程间通信的变体


1.3、进程间通信的分类

管道:

  • 匿名管道

  • 命名管道

System V进程间通信:

  • System V消息队列

  • System V共享内存

  • System V信号量

POSIX进程间通信:

  • 消息队列

  • 共享内存

  • 信号量

  • 互斥量

  • 条件变量

  • 读写锁


2、管道

2.1、概念

  • 管道分为:匿名管道命名管道,管道是提供共享资源的一种手段!!!

  • 管道是Unix操作系统中最古老的进程间通信的形式

  • 我们把从一个进程连接到另一个进程的一个“数据流”称为一个“管道”

该图中:who进程以写的方式打开管道,向管道写入数据,wc进程以读的方式打开,读取who进程写入的数据,对这些数据进行处理!!!

【Linux】进程间通信之管道_第1张图片

  • 我们学习IPC,是让不同的进程看到同一份资源(文件,内存块,队列,网络等等)

  • 学习IPC,不是先学习如何通信,而是先让多个进程如何看到同一份资源!!!

  • 资源的不同,就决定了不同的通信方式(资源是队列时,就是以消息队列方式通信)


2.2、管道的原理

管道原理:

  • struct file结构体里面有一个缓冲区(struct address_space)

  • 一个文件想知道自己是什么文件,会找到struct inode中的union联合体里面的指针,如果是块设备,就会生效块设备的指针,该指针指向描述对应设备的结构体!!!

  • 知道自己是什么文件的时候,就可以打开对应的缓冲区!!!

pipe_inode_info是管道、block_device是块设备、cdev是字符设备

【Linux】进程间通信之管道_第2张图片

  • 对应设备的结构体里面就会包含对应的缓冲区,如下图:

下面是管道描述的结构体,有等待队列(wait)和管道缓冲区(bufs)等等…

【Linux】进程间通信之管道_第3张图片


管道原理:
【Linux】进程间通信之管道_第4张图片

当子进程执行完毕时,父进程是如何发现的呢???

  • 子进程退出,记录文件描述符指针的计数器会自减,这样父进程就知道子进程执行完了

  • 结论:通过引用计数的方式知道子进程退出了!!!


2.3、匿名管道

匿名管道:没有命名的管道,使用pipe系统调用来创建

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

使用匿名管道,实现一个简单的进程间通信:父进程写,子进程读取数据!!!

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    // 创建管道 -- 打开二个文件 -- 分别以读、写的方式打开
    int pipefd[2] = {0};
    int flag = pipe(pipefd);
    if (flag == -1)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid_t id = fork(); // 创建子进程

    // 父子进程分流执行各自代码
    if (id == 0)
    {
        // Chile Process -- 子进程进行读操作,需要关闭写端文件
        close(pipefd[1]);
        char buffer[64];
        while (true)
        {
            memset(buffer, 0, sizeof(buffer)); // 每次重新初始化buffer
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer));
            if (s > 0)
            {
                cout << time_t(time(nullptr)) << endl;
                cout << "子进程收到消息,内容是:" << buffer << endl;
            }
            else if (s == 0)
            {
                cout << "父进程写完了,我也退出了!!!" << endl;
                break;
            }
            else if (s == -1)
            {
                cerr << "read erron" << endl;
                exit(3);
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process -- 父进程写入数据,需要关闭读端文件
        close(pipefd[0]);
        const char *msg = "hello world!!!";
        int cnt = 0;
        while (cnt < 5)
        {
            write(pipefd[1], msg, strlen(msg));
            ++cnt;
            sleep(2);
        }

        close(pipefd[1]);
        cout << "父进程写完了!!!" << endl;
    }
    else
    {
        // Do Nothing
    }
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0); // 阻塞等待
    if (ret > 0)
    {
        cout << "等待子进程成功,退出码:" << WEXITSTATUS(status) << ", 退出信号:" << WTERMSIG(status) << endl;
    }
    // pipefd[0] --> 读
    // pipefd[1] --> 写
    // cout << "pipefd[0]: " << pipefd[0] << endl
    //      << "pipefd[1]: " << pipefd[1] << endl;
    return 0;
}

【Linux】进程间通信之管道_第5张图片

用fork来共享管道原理

  • 原理:创建管道后,父进程创建子进程,子进程继承了父进程的代码和数据

  • 父子进程都以读写的方式指向管道的两端

  • 如果父进程要读,子进程要写,那么父进程必须关闭写端,子进程关闭读端

【Linux】进程间通信之管道_第6张图片

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

【Linux】进程间通信之管道_第7张图片

  • 看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”

  • 管道被创建出来后,在磁盘中只是一个标识符,主要是在内存中进行通信(大小一般为:4KB)

为什么父进程要分别以读和写的方式创建管道文件?

  • 因为为了让子进程继承父进程,让子进程不用重新打开了!

为什么父子进程要关闭对应的读写?

  • 因为管道是单向的!

  • 只能一边流向一边,不能二端一起互相流动,跟管子一样!


2.4、管道的读写规则

当管道没有数据可读时:

  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止

  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN

当管道满的时候:

  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据

  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

测试:父进程一直写入数据,子进程一直不读,最后父进程write会不会阻塞?

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

int main()
{
    // 创建管道 -- 打开二个文件 -- 分别以读、写的方式打开
    int pipefd[2] = {0};
    int flag = pipe(pipefd);
    if (flag == -1)
    {
        cerr << "pipe error" << endl;
        exit(1);
    }
    pid_t id = fork(); // 创建子进程

    // 父子进程分流执行各自代码
    if (id == 0)
    {
        // Chile Process -- 子进程进行读操作,需要关闭写端文件
        close(pipefd[1]);
        char buffer[64];
        
        while (true)
        {}
        
        close(pipefd[0]);
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process -- 父进程写入数据,需要关闭读端文件
        close(pipefd[0]);
        const char *msg = "hello world!!!";
        int cnt = 0;
        while (1)
        {
            write(pipefd[1], msg, strlen(msg));
            ++cnt;
            cout << "父进程写入的次数:" << cnt << endl;
        }

        close(pipefd[1]);
        cout << "父进程写完了!!!" << endl;
    }
    else
    {
        // Do Nothing
    }
    int status = 0;
    pid_t ret = waitpid(-1, &status, 0); // 阻塞等待
    if (ret > 0)
    {
        cout << "等待子进程成功,退出码:" << WEXITSTATUS(status) << ", 退出信号:" << WTERMSIG(status) << endl;
    }
    return 0;
}

【Linux】进程间通信之管道_第8张图片

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0

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

原子性:在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行,多个操作是一个不可以分割的整体

  • 当要写入的数据量不大于PIPE_BUF(管道缓冲区最大值)时,Linux将保证写入的原子性

  • 当要写入的数据量大于PIPE_BUF(管道缓冲区最大值)时,Linux将不再保证写入的原子性

man 7 pipe手册中的PIPE_BUF介绍

【Linux】进程间通信之管道_第9张图片

总结:

  • 管道内部实现自带同步和互斥的机制! – 按顺序进行写数据和读数据

  • 正常的多进程在对硬件(比如:显示器)进行写入时,打印的顺序是乱的


2.5、多进程间通信

多进程间通信(进程池):

  • 创建一个父进程、多个管道和多个子进程(管道与子进程数量一致)

  • 父进程向不同的子进程发送任务码,子进程收到任务码后会执行对应的任务!

【Linux】进程间通信之管道_第10张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

typedef void (*method)(); // 定义一个函数指针类型
vector<method> methods;   // 方法集合(任务集合)

unordered_map<uint32_t, string> info;

// 用来保存父进程的PID和对应的读端文件描述符(pipefd[0])
vector<pair<uint32_t, uint32_t>> v;

void Func1()
{
    cout << "这是一个处理日志的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

void Func2()
{
    cout << "这是一个备份数据的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

void Func3()
{
    cout << "这是一个处理网络服务的任务, 执行进程的ID是: " << getpid()
         << ",执行时间是: " << time(nullptr) << endl;
}

// 初始化任务列表
void Methods_Init()
{
    info.insert(make_pair(methods.size(), "处理日志"));
    methods.push_back(Func1);
    info.insert(make_pair(methods.size(), "备份数据"));
    methods.push_back(Func2);
    info.insert(make_pair(methods.size(), "处理网络服务"));
    methods.push_back(Func3);
}

// 父进程写入数据
void Work(int fd)
{
    while (true)
    {
        uint32_t operatorType = 0;
        ssize_t s = read(fd, &operatorType, sizeof(uint32_t));
        if (s == 0)
        {
            cout << "父子进程的写端已经关闭, 那我子进程也退出了! ! !" << endl;
            break;
        }
        if (s == sizeof(uint32_t))
        {
            if (operatorType < methods.size())
            {
                methods[operatorType](); // 调用对应的方法!
            }
        }
    }
}

// 子进程读取数据
void DispatchTask(vector<pair<uint32_t, uint32_t>> &vv)
{
    srand((unsigned int)time(nullptr)); // 随机数种子
    int cnt = 3;
    while (cnt--)
    {
        // 选择一个进程
        uint32_t pick = rand() % vv.size();

        // 选择一个任务
        uint32_t task = rand() % methods.size();

        // 写入指定进程的管道中
        write(vv[pick].second, &task, sizeof(uint32_t));
        cout << "父进程指派任务, 该任务是: " << info[task]
             << ", 时间是: " << time(nullptr) << ", 编号是: " << pick << endl;
        sleep(1);
    }
}

// 多进程间通信 -- 父进程对一组子进程派发任务
int main()
{
    // 0、加载任务列表
    Methods_Init();
    // 2、创建多进程 并且 各自创建一个父子进程通信的管道(每个子进程配对一个管道)
    int Process_Num = 3;
    for (int i = 0; i < Process_Num; ++i)
    {
        // 1、创建管道
        int pipefd[2] = {0};
        if (pipe(pipefd) == -1)
        {
            cerr << "pipe erron: " << endl;
            return 1;
        }
        pid_t id = fork();
        if (id == 0)
        {
            // 3、Child Process -- 子进程读数据,关闭写端
            close(pipefd[1]);
            // 子进程执行父进程派发的任务
            Work(pipefd[0]);
            exit(0);
        }
        // if以外的都是父进程的操作!
        // 4、父进程写数据,关闭读端
        close(pipefd[0]);
        // 将父进程的PID和读端文件描述符写入到v中
        v.push_back(make_pair(id, pipefd[0]));
    }
    // 父进程派发任务给子进程
    DispatchTask(v);

    // 5、阻塞等待子进程退出
    for (int i = 0; i < Process_Num; ++i)
    {
        close(v[i].second); // 逐个关闭父进程写端
        int status = 0;
        pid_t ret = waitpid(v[i].first, &status, 0); // 阻塞等待
        if (ret > 0)
        {
            cout << "父进程等待子进程[" << v[i].first << "]成功, 退出码: "
                 << WEXITSTATUS(status) << ", 退出信号: " << WTERMSIG(status) << endl;
        }
    }
    return 0;
}

2.6、管道的特点

  • 只能用于具有共同祖先的进程(具有血缘关系的进程)之间进行通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道

  • 管道提供流式服务 – 单向通信(一个进程的数据流入到另一个进程进行处理)

  • 一般而言,进程退出,管道释放(管道也是文件),所以管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥(管道读写规则)

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

  • 管道是面向字节流进行传输数据的!!!

【Linux】进程间通信之管道_第11张图片


3、命名管道

3.1、概念

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

  • 如果我们想在不相关的进程(不是fork出来的进程)之间交换数据,可以使用FIFO文件(管道文件)来做进行通信,它经常被称为:命名管道

  • 命名管道是一种特殊类型的文件,文件类型为:p


3.2、创建命名管道

使用指令创建命名管道:

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

[lyh@localhost Test]$ ll
总用量 0
[lyh@localhost Test]$ mkfifo FIFO
[lyh@localhost Test]$ ll
总用量 0
prw-rw-r--. 1 lyh lyh 0 1219 01:51 FIFO

【Linux】进程间通信之管道_第12张图片

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

int mkfifo(const char *pathname,mode_t mode);
  • 返回值:成功时,mkfifo()返回0,如果出现错误,则返回-1(在这种情况下,错误号设置为:appropri‐ately)

  • pathname:在哪个路径下创建什么名字的管道文件

  • mode:该参数指定FIFO文件的权限(八进制设置),该参数会受到已继承父进程中umask的影响,因此:创建的文件的权限是(mode & ~umask)

简单创建一个命名管道:

int main()
{
    // 设置当前进程权限掩码
    umask(0);
    // 在进程当前工作路径(cwd)创建一个FIFO管道文件,文件权限为664
    if (mkfifo("./FIFO", 0664) != 0)
    {
        cerr << "mkdfifo error: " << endl;
        return 1;
    }
    return 0;
}

3.3、命名管道的使用

使用命名管道完成没有血缘关系的进程之间的通信:

  • 服务器写入数据,客户端读取数据,并且向显示器进程刷新

  • serverFifo是服务器文件,clientFifo是客户端文件

  • 主要知识用到了:mkfifo、open、read、write等接口

comm.h

#pragma once

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

// 命名管道创建的路径及管道的命名
#define PIPE_PATH "./pipe"

// 写入数据的最大数量
#define NUM 1024

serverFifo.cpp

#include "comm.h"

// 服务器读取客户端发送的数据
int main()
{
    // 设置当前进程权限掩码
    umask(0);
    // 1、创建命名管道
    if (mkfifo(PIPE_PATH, 0664) != 0)
    {
        cerr << "mkdfifo error: " << endl;
        return 1;
    }

    // 打开文件 -- 只写状态打开文件
    int pipefd = open(PIPE_PATH, O_RDONLY);
    if (pipefd < 0)
    {
        cerr << "open error: " << endl;
        return 2;
    }
    // 向命名管道读取数据
    char buffer[NUM];
    while (true)
    {
        ssize_t R = read(pipefd, buffer, sizeof(buffer));
        if (R > 0)
        {
            // 清除回车
            buffer[R] = '\0';
            cout << "客户端->服务器: " << buffer << endl;
        }
        else if (R ==0)
        {
            cout << "客户端退出了,我也退出了!!!" << endl;
            break;
        }
        else
        {
            cerr << "read error: " << endl;
            return 3;
        }
    }
    close(pipefd);
    // 删除管道文件
    unlink(PIPE_PATH);
    cout << "服务器退出!" << endl;
    return 0;
}

clientFifo.cpp

#include "comm.h"

// 客户端向命名管道写入数据
int main()
{

    // 打开命名管道文件,以只写的方式打开
    int pipefd = open(PIPE_PATH, O_WRONLY);
    if (pipefd < 0)
    {
        cerr << "open error: " << endl;
        return 2;
    }
    // 向命名管道写入数据
    char buffer[64];
    while (true)
    {
        memset(buffer, 0, sizeof(buffer));
        if (fgets(buffer, sizeof(buffer), stdin) != nullptr)
        {
            printf("请输入内容:");
            fflush(stdout); // 刷新输出缓冲区
            ssize_t W = write(pipefd, buffer, strlen(buffer));
            if (W > 0)
            {
                buffer[W] = '\0';
            }
        }
        else
        {
            break;
        }
    }
    close(pipefd);
    cout << "客户端退出了!!!" << endl;
    return 0;
}

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

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

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

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


3.5、命名管道的读写规则

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

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

  • O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

总结:

  • 命名管道和匿名管道只有创建与打开的方式不同之外,读写规则都是一样的

  • 命名管道和匿名管道都会互相同步和互斥!!!

你可能感兴趣的:(Linux,linux,服务器,unix)