【Linux】21:进程间通信

目录

一、进程间通信介绍

[1.1 进程间通信的目的](#1.1 进程间通信的目的)

[1.2 进程间通信的分类](#1.2 进程间通信的分类)

二、管道

三、匿名管道

[3.1 背景](#3.1 背景)

[3.2 原理](#3.2 原理)

[3.3 测试接口的代码](#3.3 测试接口的代码)

[3.4 5种特性](#3.4 5种特性)

[3.5 4种情况](#3.5 4种情况)

四、命名管道

[4.1 创建一个命名管道](#4.1 创建一个命名管道)

[4.2 命名管道和匿名管道的区别](#4.2 命名管道和匿名管道的区别)

[4.3 测试代码](#4.3 测试代码)


一、进程间通信介绍

我们之前说过,进程之间是相互独立的。每一个进程都有自己的PCB,数据,代码,页表等等。一个进程出现Bug,不会影响另外一个进程。即使时父子进程也是相互独立的。

但是如果进程之间想要协助呢?那怎么实现呢?一个进程的数据如果给另一个进程呢?

1.1 进程间通信的目的

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

1.2 进程间通信的分类

1:管道

  • 匿名管道pipe
  • 命名管道

2:System V IPC

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

3:POSIX IPC

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

进程之间通信的本质:是让不同的进程,先看到同一块资源(内存),然后才有通信的条件。

这个资源(内存)不能由任何一个进程提供。即使是父子进程,也做不到,在父进程malloc一块空间,交给子进程,当子进程修改时,会发生写时拷贝,父进程时看不到子进程修改的。因此这个资源(内存)必须要由操作系统来提供。所以操作系统需要提供相应的系统调用。

不管后面的进程通信的种类多么复杂。本质就是让不同的进程,先看到同一块资源(内存)

二、管道

我们之前在命令行使用的 | 就是一种管道,那么我们在命令行使用管道,它肯定会在底层给我们转化成对应的操作;

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

三、匿名管道

3.1 背景

在计算机早期,人们是没有很强烈的进程通信需求的,当谈到两个进程之间需要通信的时候,我们首先想到的根本不是设计什么标准,设计什么接口,往往想到的是:能不能基于当前操作系统技术,来进行进程之间通信。所以就不需要专门在操作系统里修改源代码。

我们能不能在计算机里面挑选出我们已经由的功能,把这些功能稍微改一下,来实现进程之间通信呢?

答案是有的。那个时代的工程师想到了文件技术。因为文件打开之后,在操作系统内核结构中会生成一个file类型的对象,而文件天然就有自己的缓冲区,只不过我们之前写文件时,是把文件缓冲区的内容刷新到磁盘上,现在能不能把文件缓冲区里面的数据刷新或者交给另外一个进程呢?不把文件缓冲区的数据刷新到磁盘上,而交给另外一个进程,那一瞬间不久有进程间通信技术了吗?

基于这一个技术,实现了进程之间的通信,还可以将之前的技术复用起来。

3.2 原理

我们在父进程里面打开一个文件,图示如下:

当父进程想要创建一个子进程,就需要把自己的PCB,数据和代码拷贝给子进程。如下所示:

子进程和父进程的文件描述符3都能够找到父进程打开的文件,这两个进程不就可以看到同一块缓冲区了吗?就有了进程之间通信的前提条件了。

上面的就是管道通信的原理。一个进程将数据写道文件的缓冲区里面,另外一个进程从缓冲区里面读取,所以两个进程就可以通信了。但是这里的文件缓冲区里面的数据不是还有刷新到磁盘上吗?很明显父子通信的数据属于临时数据,是不需要刷新到磁盘上的。所以真正的管道的实现并不是基于我们之前将的文件,而真正的管道文件它是内存级的,不需要将数据刷新到磁盘上,甚至和磁盘都没有关系!上面所画的图是理解过程,采用上面的这一套原理,然后再操作系统当中稍微修改一下。衍生出来了一个管道技术。

因此需要将文件部分做一下修改形成管道技术如下所示:

父进程以读写的方式创建一个管道,然后创建一个子进程。父进程和子进程就能看到同一块内存空间了。然后父子进程需要关闭不需要的文件标识符。父进程想要往管道里面写数据,就需要关闭读的文件描述符,子进程需要读数据,就需要关闭写的文件描述符,这就形成了一个单项通信的信道,这个信道就被称作管道。

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

这个管道是操作系统为了进程之间通信单独设计的,所以需要配上单独的系统调用接口。如下所示:

这个参数是一个输出型参数,需要传入一个int类型的数组。fd0表示读端的文件描述符,fd1表示写端的文件描述符。

返回值:成功创建管道返回0,失败返回-1.

创建管道的时候我们没有提供文件路径,所以说这个管道是内存级别的,没有文件名字,所以也称为匿名管道。

那我们是怎么保证两个进程打开的是同一个管道文件呢?

答案是:子进程继承父进程的文件描述符表。

3.3 测试接口的代码

cpp 复制代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void ChildWrite(int wfd)
{
    char buffer[1024];
    int cnt = 0;
    while (1)
    {
        snprintf(buffer, sizeof(buffer), "i am child,pid: %d,cnt: %d", getpid(), cnt++);
        write(wfd, buffer, strlen(buffer));
        sleep(1);
    }
}
void FatherRead(int rfd)
{
    char buffer[1024] = {0};
    while (1)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "child say:" << buffer << std::endl;
        }
    }
}
int main()
{
    // 1:创建管道
    int fds[2] = {0};
    int n = pipe(fds);
    if (n < 0)
    {
        std::cerr << "pipe erorr" << std::endl;
        return 1;
    }
    std::cout << "fds[0]:" << fds[0] << std::endl;
    std::cout << "fds[1]:" << fds[1] << std::endl;
    // 2:创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程code
        //  3:关闭不需要的文件描述符,形成单向通信的信道
        // 父进程读,子进程写。
        close(fds[0]);
        ChildWrite(fds[1]);
        close(fds[1]);
        exit(0);
    }
    // 3:关闭不需要的文件描述符,形成单向通信的信道
    // 父进程读,子进程写。
    close(fds[1]);
    FatherRead(fds[0]);
    waitpid(id, nullptr, 0);
    close(fds[0]);
    return 0;
}

3.4 5种特性

    1. 匿名管道 ,可以允许具有血缘关系的进程之间进行进程间通信,常用与父子,仅限于此
    1. 匿名管道 ,默认给读写端要提供同步机制
    1. 管道数据是面向字节流
    1. 管道的生命周期是随进程的
    1. 管道是单向通信的,是半双工通信的一种特殊情况。

3.5 4种情况

    1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据了)
    1. 正常情况,如果管道被写满了,写端必须等待,直到有空间为止(读端读走数据)
    1. 写端关闭,读端一直读取, 读端会读到read()返回值为0时,表示读到文件结尾
    1. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标进程发送SIGPIPE(13)信号,终止目标进程

四、命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

进程A打开了一个a/b/c.txt文件

进程B打开了一个a/b/c.txt文件。

在操作系统内核中,不会把这个文件的inode,内容在内存中加载两次,因为没有必要,此时进程A和进程B就看到了同一个文件,就满足了进程之间通信的前提条件了,看到了同一块资源了。图示如下:

上面的匿名管道是通过子进程继承父进程的方式,让两个进程看到了同一份资源。

这里的方式是通过打开同一个路径下的同一个文件来让两个不同的进程看到了同一份资源。文件是有路径和名字的。路径是具有唯一性的,所以能看到同一份资源。又因为A,B进程打开文件是有名字的,原理也是基于文件的通信方式。所以也被称为命名管道。

又因为磁盘级文件的数据是需要刷新到磁盘之中的,所以A,B进程打开的文件不能是历史上所学的那种普通文件,需要让linux内核支持一种特殊的文件,叫做管道文件。

管道文件有一个特点,只会被打开,不需要将数据刷新到磁盘之中。

所以A,B进程想要通信,可以创建一个命名管道来通信。

4.1 创建一个命名管道

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

pipe就是一个管道文件,文件类型是p。

  • 命名管道也可以从程序里创建,相关函数有:
  • 删除管道文件可以使用unlink 管道名字或者rm -f 管道名字

4.2 命名管道和匿名管道的区别

4.3 测试代码

如果我们想要让两个进程通信,可以让一个进程把对应的管道文件创建出来,后面的工作就是文件操作了,一个进程把文件以写的方式打开,一个进程把文件以读的方式打开,就可以实现进程之间通信了。

server.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    umask(0);
    // 1:创建管道文件
    int n = mkfifo("FIFO_FILE", 0666);
    if (n != 0)
    {
        std::cerr << "mkdir fifo error" << std::endl;
        return 1;
    }

    int fd = open("FIFO_FILE", O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 1;
    }
    int cnt = 1;
    while (1)
    {
        char buffer[1024];
        int n = read(fd, buffer, sizeof(buffer) - 1);
        if (n == 0)
        {
            break;
        }
        buffer[n] = 0;
        std::cout << "client say:" << buffer << "[" << cnt++ << "]" << std::endl;
    }
    return 0;
}

client.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    int fd = open("FIFO_FILE", O_WRONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 1;
    }
    while (1)
    {
        std::cout << "Please Enter#";
        std::string message;
        getline(std::cin, message);
        write(fd, message.c_str(), message.size());
    }
    return 0;
}

运行如下所示:

当client按下ctrl+c终止进程时,client关闭写端,server端read就会返回0,结束server进程。
我们修改一下上面的代码,如下所示:

server.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    umask(0);
    // 1:创建管道文件
    int n = mkfifo("FIFO_FILE", 0666);
    if (n < 0)
    {
        std::cerr << "mkdir fifo error" << std::endl;
        return 1;
    }
    std::cout << "mkfifo sucess" << std::endl;

    // 2:打开管道文件
    int fd = open("FIFO_FILE", O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 1;
    }
    std::cout << "open sucess" << std::endl;

    // 3:读取管道文件的数据
    int cnt = 1;
    while (1)
    {
        char buffer[1024];
        int n = read(fd, buffer, sizeof(buffer) - 1);
        if (n == 0)
        {
            break;
        }
        buffer[n] = 0;
        std::cout << "client say:" << buffer << "[" << cnt++ << "]" << std::endl;
    }

    // 4:关闭fd文件描述符
    close(fd);

    // 5:删除管道文件
    n = unlink("FIFO_FILE");
    if (n == 0)
    {
        std::cout << "remove fifo suecess" << std::endl;
    }
    else
    {
        std::cerr << "remove fifo failed" << std::endl;
    }
    return 0;
}

client.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
    // 1:以写的方式打开文件
    int fd = open("FIFO_FILE", O_WRONLY);
    if (fd < 0)
    {
        std::cerr << "open error" << std::endl;
        return 1;
    }
    std::cout << "open sucess" << std::endl;
    // 2:写数据给管道文件
    std::string message;
    int cnt = 1;
    pid_t id = getpid();
    while (1)
    {
        std::cout << "Please Enter#";
        getline(std::cin, message);
        message += ",message number" + std::to_string(cnt) + "[" + std::to_string(id) + "]";
        write(fd, message.c_str(), message.size());
    }
    // 3:关闭文件描述符
    close(fd);
    return 0;
}

我们编译只会,单独运行server程序,如下所示:

我们发现成功创建了管道文件,但是进程没有输出open success/failed,这就说明代码阻塞到了open这里。这是为什么呢?这是因为server在等待一个进程以写的方式打开这个管道文件,如果没有没有进程以写的方式打开这个管道文件,那么server以读的方式打开有什么意义呢?

现在我们运行client程序:如下所示:

此时server端就会输出open success/failed。如下所示:

我们在客户端里面输入内容,在server端就都会显示出来,如下所示:

当client按下ctrl+c终止进程时,client关闭写端,server端read就会返回0,结束server进程。

通过上面的代码,我们可以得出一个结论:当写入方没有执行open的时候,读方就要在open内部进行阻塞,直到写入方以写入的方式打开,读方的open才会返回。

上面的代码是面向过程的,我们修改一下代码,把代码修改成面向对象的,如下所示:

comm.hpp

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

class NamedFifo
{
public:
    NamedFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        fifoname = _path + "/" + _name;
        umask(0);
        int n = mkfifo(fifoname.c_str(), 0666);
        if (n < 0)
        {
            std::cerr << "mkdir fifo error" << std::endl;
        }
        else
        {
            std::cout << "mkfifo sucess" << std::endl;
        }
    }
    ~NamedFifo()
    {
        int n = unlink(fifoname.c_str());
        if (n == 0)
        {
            std::cout << "remove fifo suecess" << std::endl;
        }
        else
        {
            std::cerr << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;
    std::string _name;
    std::string fifoname;
};

class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }
    void OpenForRead()
    {
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            std::cerr << "open error" << std::endl;
        }
        else
        {
            std::cout << "open sucess" << std::endl;
        }
    }
    void OpenForWrite()
    {
        _fd = open(_fifoname.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            std::cerr << "open error" << std::endl;
        }
        else
        {
            std::cout << "open sucess" << std::endl;
        }
    }
    void Write()
    {
        std::string message;
        int cnt = 1;
        pid_t id = getpid();
        while (1)
        {
            std::cout << "Please Enter#";
            getline(std::cin, message);
            message += ",message number" + std::to_string(cnt++) + "[" + std::to_string(id) + "]";
            write(_fd, message.c_str(), message.size());
        }
    }
    void Read()
    {
        while (1)
        {
            char buffer[1024];
            int n = read(_fd, buffer, sizeof(buffer) - 1);
            if (n == 0)
            {
                break;
            }
            buffer[n] = 0;
            std::cout << "client say:" << buffer << std::endl;
        }
    }
    void Close()
    {
        if (_fd > 0)
            close(_fd);
    }
    ~FileOper()
    {
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
    int _fd;
};

server.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    // 创建管道文件
    NamedFifo fifo(".", "fifo");

    // 文件操作
    FileOper readerfile(".", "fifo");

    readerfile.OpenForRead();
    readerfile.Read();
    readerfile.Close();
    return 0;
}

client.cc

cpp 复制代码
#include "comm.hpp"

int main()
{
    FileOper writerfile(".", "fifo");
    writerfile.OpenForWrite();
    writerfile.Write();
    writerfile.Close();
    return 0;
}
相关推荐
赵民勇2 小时前
systemd-sysusers命令详解
linux·运维
灿烂的贝壳2 小时前
FC SAN多路径负载均衡配置指南
linux
binqian2 小时前
【Linux】内核怎么管理侦听socket
linux·网络协议
j7~2 小时前
【Linux网络】三十七.TCP 协议通讯流程
linux·网络编程·三次握手·四次挥手·tcp 协议通讯流程
顶点多余2 小时前
9.15 面试问题总结(一面挂)
linux·面试·职场和发展
顺风尿一寸2 小时前
从 ls -U 到 Tomcat libs加载jar包:ext4 目录哈希序如何影响 Java 文件排序顺序
linux·jvm
qeen873 小时前
【Linux】操作系统之进程介绍(一)
linux·笔记·学习·进程
邪修king3 小时前
Re:Linux系统篇(二十五):文件系统(一):磁盘硬件底层原理:从物理结构到 CHS/LBA 寻址,搞懂硬盘数据的定位逻辑
java·linux·运维·gpt
小则又沐风a3 小时前
TCP协议讲解-----了解TCP可靠性的基石
linux