当前位置: 首页 > article >正文

【Linux系统】进程间通信一




在这里插入图片描述



初识进程间通信


1、进程间通信目的

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



2、如何通信(核心)

前提: 先得让不同的进程,看到同一份资源!

同一份资源: 某种形式的内存空间

提供资源的人: 只能是操作系统!

OS 的系统调用: 因为需要先保证进程间独立性,就需要为进程间通信设计一套通信方案,使不同进程可以操作同一份资源,这个资源一定是操作系统提供 的,想要操作这个资源就免不了使用 OS 的系统调用


在这里插入图片描述




3、进程间通信的分类


IPC 全称是 Inter-Process Communication,即为“进程间通信”

(若在平台上看到IPC,就表示进程间通信)


进程间通信大体分为两种:

  • 本地通信: 同一台主机、同一个OS,不同进程间的通信
  • 网络通信: 不同主机,同一网络中不同的两个进程之间的通信


进程间通信实现方式的具体分类如下:

  • 管道
    • 匿名管道pipe
    • 命名管道

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

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

下面文章会挑选几个相对重要的讲解:



匿名管道

1、何为管道

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


在这里插入图片描述





文件描述符表被继承,共享同一 struct file

当一个进程通过 fork() 创建子进程时,子进程会获得父进程打开文件的副本,具体来说是复制了父进程的文件描述符表,而这些文件描述符指向的是内核中的同一个 struct file 结构体实例。这意味着父子进程共享同一 struct file,父子进程将通过这同一个 struct file 对他们打开的同一个文件进行操作

因此,如果父子进程同时对同一文件进行读写操作,它们会相互影响。因为 struct file 是父子共享使用的,所以该结构中的文件内核缓冲区也是共享使用的,父子进程都可以像该缓冲区读写数据,若不进行进程同步等限制,则会出现数据不一致的问题,例如同一文件数据排列顺序是乱序,例如显示器文件,若父子进程对同一显示器进行写操作,则显示器则会同时输出父子进行输入的内容,显示的数据则按照父子进行的写入顺序显示


构成进程间通信

从上面这段话可以看出,若子进程继承父进程的文件描述符表,就能实现父子共享操作某个文件的行为

这就是父子进程两个进程看到同一个资源:同一文件

这刚好构成进程间通信!


我们前面讲解过,文件 struct file 结构中,存在文件内核缓冲区,进程写入缓冲区的内容会被系统刷新到磁盘中。

问题:若该文件用于进程间通信,是否存在与磁盘进行交互的需求?

答:进程间通信是内存级的操作,通信的数据都在进程之间,在内存之中传输,并没有和磁盘交互的需求,和磁盘交互一是比较耗时,二是没必要。

因此设计师设计了一种纯内存级的文件,该文件其他属性和普通文件差不多,但是该文件不用和磁盘交互(和磁盘无关,不用刷新缓冲区到磁盘中),通过这样的文件使进程在内存中通信,拷贝传输数据!

这就是用于进程间通信的特殊文件:管道文件!



2、匿名管道的使用:概念层面



在这里插入图片描述



下面是对这张管道使用的步骤解释:

1、父进程创建一个这样纯内存级别的管道文件,同时以读和写的方式打开该文件(因此分配有两个文件描述符)

2、子进程继承父进程的文件描述符表时,自然也拥有对同一管道文件的读写端

3、父进程关闭写端,子进程关闭读端:子进程通过写端向该管道中写入数据,父进程通过读端向该管道中读出数据,通过这样单向的读写交互,即通过管道完成进程间通信!



问题:

问题 1:是否可以不关闭进程的读或写端,使得每个进程同时拥有管道文件的读写端呢?

答:可以,但有风险!

(1)造成 fd 泄漏:我们使用管道,一个进程只会使用读或写端的其中一个,另一个用不上,就需要关闭,若不关则占用文件描述符资源,造成fd 泄漏问题(简单来说就是 fd 的数量有限,若不关闭一端则造成浪费占用)。

(2)误操作:但不关闭读或写端,那天一个本该操作写端的进程,误操作了读端,两个进程同时读,造成出错


问题 2:能不能以读写的方式打开打开一个fd,而不是读与写打开两个fd?

答:不能。 操作系统的设计是严谨的,你该是写端操作,就不建议你拥有读端的权限,一个 fd 代表一种权限。


问题 3:能不能先创建进程,再创建管道文件呢?

答:不能

本身就是利用了子进程会继承父进程资源的这种特性,才达到父子不同进程看到同一份资源(管道),如果先创建子进程,则父子进程其实打开管道文件,就不是同一份了(此时已经产生进程独立性的“隔离”了!)


问题 4:打开该管道文件是否需要指定磁盘路径,是否需要文件名打开?

答: 管道文件是内存级别文件,和磁盘无关了,就没有所谓路径,也没有所谓使用文件名打开

因此该管道没有名字,因此称为匿名管道!!!


2、匿名管道的理解:内核层面

在这里插入图片描述

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



3、管道创建接口

int pipe(int fd[2]);
  • 参数:

    • fd: 文件描述符数组,用户传入一个两元素数组,管道创建好后会将两个文件描述符表放到该数组中给你
      其,中 fd[0] 表示读端,fd[1] 表示写端。
  • 返回值:

    • 成功返回 0
    • 败返回错误代码。

在这里插入图片描述




4、再谈重定向

默认 fd 重定向

我们看一个样例:使用 cout 打印正常信息,使用 cerr 打印错误信息,将打印结果重定向到一个文本文件中

注:cerr 是 C++ 中,用于指定向显示器打印错误信息。

代码如下:

#include<iostream>
#include<unistd.h>
using namespace std;

int main()
{
    int fds[2] = {0};
    int ret = pipe(fds);
    if(ret == 0)
    {
        cout << "OK!" << '\n';
        cout << "OK!" << '\n';
        cout << "OK!" << '\n';
        cout << "OK!" << '\n';
        cerr << "pipe error!" << '\n';
        cerr << "pipe error!" << '\n';
        cerr << "pipe error!" << '\n';
        cerr << "pipe error!" << '\n';
    }
    return 0;
}



运行结果如下:

在这里插入图片描述

为什么 cerr 的结果不会重定向写入文件 log.txt 中?

之前说过:

文件描述符 fd = 1 的文件为标准输出流,对应显示器文件

文件描述符 fd = 2 的文件为标准错误流,也对应显示器文件

重定向 > :默认改变的是 fd = 1 的标准输出流文件,将该文件替换指向文件 log.txt


如果没有重定向cout 标准输出流, cerr 标准错误流,都会向对应的显示器文件打印出来信息

如果重定向了, 默认修改的只是 fd = 1 的标准输出流文件,因此 cout 输出到 文件 log.txt

而重定向不会影响 fd = 2 的标准错误流,因此 cerr 照样向显示器打印。


重定向是默认对 fd = 1 标准输出流文件进行处理,那是否有方式可以处理指定的 fd



指定 fd 重定向

实际上,./mypipe > log.txt 这样重定向的写法是默认重定向 fd = 1 的文件

完整写法是:指定 fd 重定向(注意不要数字和 > 留空格:错误写法1 > ,正确写法1>

指定重定向 fd = 1 的文件:即重定向标准输入流文件

./mypipe 1> log.txt

cout 打印的重定向到文件 log.txt 中了,cerr 打印的在显示器上:
在这里插入图片描述


指定重定向 fd = 2 的文件:即重定向标准错误流文件

./mypipe 2> log.txt

这样其实就是将标准错误流重定向了,cerr 打印的结果就会放到文件 log.txt

cerr 打印的重定向到文件 log.txt 中了,cout 打印的在显示器上:
在这里插入图片描述



实现 fd=1, fd=2 同时重定向

命令 ./mypipe 1> log.txt 2>&1

先将 fd=1 重定向为 log.txt,再 2>&1fd=2 的内容重定向到 fd=1

相当于有两个指针同时指向同一个文件 log.txt


这样则两中输出都写入了文件 log.txt

在这里插入图片描述



5、匿名管道的使用:代码层面


声明:

1、对应 C/C++ 库函数 和 系统调用混用的场景,建议在 系统调用 前加上双冒号 :: ,没有任何“副作用”,就是为了区分一下 库函数 和 系统调用,其本身的作用是标识为全局的意思

2、管道的两个 fd 的含义: fd[0] 对应读端, fd[1] 对应写端


通过子进程向管道文件中写入数据,父进程从管道中读出数据,实现了进程间通信的效果

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;

int main()
{
    int fds[2] = {0};
    int ret = pipe(fds);
    if(ret == 0)
    {
        cout << "管道创建成功: \n";
        cout << "fd[0] : " << fds[0] << '\n'; 
        cout << "fd[1] : " << fds[1] << '\n'; 
    }

    // 创建子进程
    pid_t pid = ::fork();
    if(pid < 0) {
        cerr << "pid error!" << '\n';
        exit(1);
    }

    // 实现:子进程写,父进程读

    // 子进程
    if(pid == 0)
    {  
        // 前置工作
        ::close(fds[0]); // 关闭读端

        // 写数据
        int cnt = 0;
        while(1)
        {
            cnt++;

            const char* message = "h";
            // ssize_t write(int fd, const void *buf, size_t count);
            ssize_t n = ::write(fds[1], message, strlen(message));
            if(n < 0) {
                cerr << "write error !" << '\n';
                exit(1);
            }
            sleep(1);
        }



        // 结束工作
        ::close(fds[1]);
        exit(0);
    }
    // 父进程
    else if(pid > 0)
    {
        // 前置工作
        ::close(fds[1]); // 关闭写端

        // 读数据
        char buff[1024];
        int cnt = 0;
        
        while(1)
        {
            cnt++;

            // ssize_t read(int fd, void *buf, size_t count);
            ssize_t n = read(fds[0], buff, 1024);
            if(n < 0) {
                cerr << "read error !" << '\n';
                exit(1);
            }
            else if(n > 0)
            {
                buff[n] = '\0';
                cout << "父进程第 " << cnt << " 轮读取: " <<  buff << '\n';
            }
        
        }
        

        // 结束工作
        ::close(fds[0]);
        int status = 0;
        int ret = ::waitpid(pid, &status, 0);
        if(ret < 0){
            cerr << "wait error !" << '\n';
            exit(1);
        }
    }

    return 0;
}


代码运行效果:

在这里插入图片描述


6、管道的四种情况

你在上面父子进程通信的代码中,给子进程的循环加上 sleep(5);,表示让子进程慢一点输入内容

再次运行该代码,会发现,父进程迟迟不打印一次:这是因为父进程在阻塞!


首先,IPC本质是先让不同的进程,看到同一份资源!

而这份资源是共享资源,既然共享,就可能出现同时操作该共享资源的情况

当写数据的进程写到一半,读数据的进程就直接读操作,这是否合理?

不行,这造成了数据不一致问题,在线程角度就是线程安全问题。

这种共享资源,也叫做临界资源,需要保护起来,避免多方进程不按顺序操作导致数据不一致问题

我们需要保护该资源等待一个操作对象操作完成后,另一个操作对象才能操作,而这种资源由谁保护?这就是管道中保护临界资源的保护机制!

管道保护机制:当管道内有数据时,父进程(读方)直接读取数据,当管道内没有数据时,就会将父进程链入管道文件 struct file 的等待队列中,一直到有数据才可以读,没数据就阻塞等待,这样不就保证了管道数据的安全吗!


下面同时管道的 4 种情况,进一步理解管道通信的机制:

(1)管道为空&&管道正常,read 会阻塞

注:read 是一个系统调用

我们再实验一下:

子进程的写数据该成一个长一点的字符串:

const char* message = "hello_World_Linux!";

父进程一次读 3 个字符(改小一点)

ssize_t n = read(fds[0], buff, 3);

运行结果如下: 注意下面的图是动图,只不过需要等待几秒

你可以发现,程序一旦运行起来,父进程就立马读了好多次,然后需要等待几秒父进程才会再次读取

在这里插入图片描述


这里想说的是:

当缓冲区中有数据时,父进程会不停的将数据全部读出来

当缓冲区内没有数据时,父进程就会阻塞等待子进程写入数据,只有当缓冲区中重新有数据,父进程才会继续读取



(2)管道为满&&管道正常,write会阻塞

注:write 是一个系统调用

我们再实验一下:

子进程修改:

1、写数据改成一个字符 “h”

const char* message = "h";

2、加上写入数据的记录:统计子进程当前的写入次数

cout << "写入数据第 " << cnt << " 次" << '\n';

3、将 sleep 语句去掉:表示子进程不停的快速写入内容

父进程修改:

加上长时间 sleep(1000):目的是“阻塞住”父进程,先别读,等子进程写数据写一会


运行结果如下:

在这里插入图片描述


可以发现,程序一打开,子进程就不断写入数据,最后在写入数据第 65536 次停下了

解释原因:

因为一次写入 1 字节(一个字符 h),因此说明本次一共写入 65536 字节数据

计算一下,65536 字节 / 1024 字节 = 64 KB

可知写入 64KB 的数据,这里证明了我们当前的 Ubuntu 系统下,管道文件缓冲区大小为 64KB !!

表示管道是有上限的!!



面向字节流

我们再实验一下:

子进程保持数据不变,父进程修改:

1、sleep 改小点:sleep(1)

2、一次读数据的大小多点:

ssize_t n = read(fds[0], buff, 1024);

运行结果如下:

在这里插入图片描述


可以发现,子进程“拼命”写几万次,父进程一次就读一堆

这里可以看出:父进程读数据时,根本不用关系管道文件写入何种数据、被写入多少次,只关心我要多少,我自己进程的需求多少

这个现象叫做:面向字节流!



(3)管道写端关闭&&读端继续,读端读到0,表示读到文件结尾

如果管道的写端,它不光不写了它还退了,那么读端,它再读就没有意义了,因为管道只有一端写,一端读,写端已经关了,你还读啥呢?

如果写端关闭,读端读完管道内部的数据,在读取的时候,就会读取到返回值0,表示对端关闭,也表示读到文件结尾

因此,read 的返回值 n==0 时,表示没有数据写入,读到文件结尾了,可以直接退出管道通信

if(n==0)
{
    //...
    break;
}

在这里插入图片描述



(4)管道写端正常&&读端关闭,OS会直接杀掉写入的进程

OS 会给目标进程发信号:13 号信号 SIGPIPE

我们主动关闭父进程的 读端,并退出循环,则进入等待子进程程序中

子进程此时就会被信号 13 杀掉

在这里插入图片描述

#include<iostream>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;

int main()
{
    int fds[2] = {0};
    int ret = pipe(fds);
    if(ret == 0)
    {
        cout << "管道创建成功: \n";
        cout << "fd[0] : " << fds[0] << '\n'; 
        cout << "fd[1] : " << fds[1] << '\n'; 
    }

    // 创建子进程
    pid_t pid = ::fork();
    if(pid < 0) {
        cerr << "pid error!" << '\n';
        exit(1);
    }

    // 实现:子进程写,父进程读

    // 子进程
    if(pid == 0)
    {  
        // 前置工作
        ::close(fds[0]); // 关闭读端

        // 写数据
        int cnt = 0;
        while(1)
        {
            cnt++;

            const char* message = "h";
            // ssize_t write(int fd, const void *buf, size_t count);
            ssize_t n = ::write(fds[1], message, strlen(message));
            if(n < 0) {
                cerr << "write error !" << '\n';
                exit(1);
            }
            cout << "子进程写入数据第 " << cnt << " 次" << '\n';
            sleep(1);
        }



        // 结束工作
        ::close(fds[1]);
        exit(0);
    }
    // 父进程
    else if(pid > 0)
    {
        // 前置工作
        ::close(fds[1]); // 关闭写端

        // 读数据
        char buff[1024];
        int cnt = 0;
        
        while(1)
        {
            cnt++;

            // ssize_t read(int fd, void *buf, size_t count);
            ssize_t n = read(fds[0], buff, 1024);
            if(n < 0) {
                cerr << "read error !" << '\n';
                exit(1);
            }
            else if(n > 0)
            {
                buff[n] = '\0';
                cout << "父进程第 " << cnt << " 轮读取: " <<  buff << '\n';
            }
            else if(n == 0)
            {
                cout << "n: " << n << '\n';
                cout << "write quit!  so I need to quit, too!" << '\n';
                break; 
            }
            cout << "————————" << '\n';
            cout << "关闭父进程的读端!" << '\n'; 
            ::close(fds[0]);
            break;
        }
        

        // 结束工作
        
        int status = 0;
        int ret = ::waitpid(pid, &status, 0);
        if(ret < 0){
            cerr << "wait error !" << '\n';
            exit(1);
        }
        cout << "father wait child success, child pid : " << pid << '\n';
        cout << "退出码为: " << WEXITSTATUS(status) << '\n';   // 退出码是status的后八位,也可以位运算得出:((status<<8)&0xFF)
        cout << "信号为: " << WTERMSIG(status) << '\n';        // 信号是status的前八位,也可以位运算得出:(status&0x7F)
        
    }

    return 0;
}


运行结果如下:

果然就是信号 13 干的

在这里插入图片描述



7、匿名管道的五大特性


这几个特性是根据上面讲解的管道的四种情况总结的来:

1、面向字节流:和管道的读写次数、读写数据类型等信息无关,我只关心我读或写的需求

2、用于具有血缘关系的进程,进行IPC,常用于父子:

只有在有血缘关系的情况下,才能继承到管道的读写端的连接,共享资源管道的读写端才能确定并运用起来

3、文件的生命周期,随进程!管道也是!

当父子进程都关闭时,管道文件也就无需存在了,即关闭

4、管道只能用于单向数据通信

因为多进程同时拥有读和写权限,则容易导致读写数据不一致问题

5、管道自带同步互斥等保护机制

当管道内没有数据时,read 阻塞,等待数据写入

当管道内数据满了,write 阻塞,等待数据被读出

这些现象的背后就证明了,管道具有同步互斥等保护管道操作顺序机制!

通俗理解互斥:去自助ATM机存取钱,就会进入一个房间,一个人进去就反锁,外面的人想要进去操作就必须等这个人结束,这就是互斥


http://www.kler.cn/a/519497.html

相关文章:

  • Linux C openssl aes-128-cbc demo
  • Batch Normalization学习笔记
  • 77,【1】.[CISCN2019 华东南赛区]Web4
  • Java数据结构 (链表反转(LinkedList----Leetcode206))
  • Qt网络通信(TCP/UDP)
  • 运维实战---多种方式在Linux中部署并初始化MySQL
  • DeepSeek_R1论文翻译稿
  • RV1126画面质量五:Profile和编码等级讲解
  • 【北京大学 凸优化】Lec1 凸优化问题定义
  • Linux Futex学习笔记
  • 第 10 课 Python 内置函数
  • 在 Ubuntu22.04 上安装 Splunk
  • 2025年1月22日(什么是扫频)
  • vue router路由复用及刷新问题研究
  • 从 VJ 拥塞控制到 BBR:ACK 自时钟和 pacing
  • 《Kotlin核心编程》上篇
  • 【动态规划】杨表
  • YOLOv11改进,YOLOv11检测头融合DSConv(动态蛇形卷积),并添加小目标检测层(四头检测),适合目标检测、分割等任务
  • SQL注入漏洞之SQL注入基础知识点 如何检测是否含有sql注入漏洞
  • 【leetcode100】二叉树的层序遍历