【Linux学习】进程间通信的方式(匿名管道、命名管道、共享内存)1

简介: 【Linux学习】进程间通信的方式(匿名管道、命名管道、共享内存)

一、进程间通信

1.1 进程间通信的概念

进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息


1.2 进程间通信的本质

通俗的来讲,进程间通信其实就是为了让不同的进程看到同一份资源。

各个运行的进程之间都具有独立性,这个独立性主要体现在数据层面,而逻辑代码层面可以实现共有(例如子进程和父进程),因此实现各个进程之间的通信非常困难。若要想实现进程间通信,必须借助第三方资源。这些进程通过向第三方资源的写入或者读取数据,进而实现通信,第三方资源其实就是操作系统提供的一段内存区域。

1.3 进程间通信的目的

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

1.4 进程间通信的分类

  1. 管道
  • 匿名管道
  • 命名管道
  1. System V IPC
  • System V 共享内存
  • System V 消息队列
  • System V 信号量
  1. POSIX IPC
  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁


二、管道

2.1 管道的概念

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个 “管道”。管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。


例如,统计我们当前使用云服务器上的登录用户个数。

who | wc -l

其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据传输到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。


2.2 匿名管道

2.2.1 匿名管道的原理

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

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


注意:

  • 这里父子进程看到的同一份文件资源是由操作系统来维护的,所以当父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
  • 管道虽然是一种文件,但是程序对管道写入的数据不会刷新进磁盘中,因为将数据刷新进磁盘会降低进程间通信的效率。

2.2.2 pipe函数

pipe系统函数的功能就是创建一个匿名管道。

函数原型:

#include <unistd.h>
int pipe(int fildes[2]);

参数:

fildes: pipe函数的参数是一个输出型参数,数组fildes用于返回两个指向管道读端和写端的文件描述符,其中fidles[0]表示读端,fildes[1]表示写端。

返回值:

创建成功返回0,失败返回错误码。

2.2.3 匿名管道的创建与使用

1、首先父进程调用pipe系统调用创造匿名管道。

2、父进程调用fork函数创建子进程。

3、 父进程关闭写端,子进程关闭读端。

此时就可以实现父子间的进程通信了,即子进程向管道中写入数据,父进程从管道中读取数据。

【注意事项】

  1. 管道只能够进行单向通信,因此当父进程创建完子进程后,需要确认父子进程谁读谁写,然后关闭相应的读写端。
  2. 从管道写端写入的数据会被内核缓冲,直到从管道的读端被读取。即只有当子进程写完了数据,父进程才能读取。

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

站在内核角度看待管道

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

以下是父子进程间通信的简单代码:

#include <iostream>
#include <unistd.h>
#include <cstring>
#include <ctime>
#include <string>
#include <sys/wait.h>
#include <cassert>
using namespace std;
int main()
{
    //创建匿名管道
    int pipefd[2] = {0};
    if(pipe(pipefd) != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }
    //创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        cerr << "fork error" << endl;
        return 2;
    }
    else if(id == 0)
    {
        //子进程进行读操作,应该关闭写
        close(pipefd[1]);
        #define NUM 1024
        char buffer[NUM];
        while(true)
        {
            cout << "时间戳:" <<(uint64_t)time(nullptr) << endl;
            memset(buffer, 0, sizeof(buffer));
            ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if(s > 0)
            {
                buffer[s] = '\0';
                cout << "子进程收到消息,内容是:" << buffer << endl;
            }
            else if(s == 0)
            {
                cout << "父进程写完了,子进程退出" << endl;
                break;
            }
            else
            {
                //do nothing
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    else
    {
        //父进程进行写操作,应该关闭读
        close(pipefd[0]);
        const char* msg = "你好啊,子进程!我是父进程。这次发送信息的编号:";
        int cnt = 0;
        while(cnt < 5)
        {
            char sendBuffer[1024];
            sprintf(sendBuffer, "%s : %d", msg, cnt);
            sleep(1);
            write(pipefd[1], sendBuffer, strlen(msg));
            ++cnt;
            cout << "cnt:" << cnt <<endl;
        }
        close(pipefd[1]);
        cout << "父进程写完了" <<endl;
    }
}

2.2.4 匿名管道读写规则

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

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

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

  1. 当没有数据可读时:
  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  1. 当管道满时:
  • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据。
  • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN。



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

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

5.当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

6.当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。


2.2.5 匿名管道的特点

1、只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信

通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

2、内核会对管道操作进行同步与互斥

在一段时间内只允许一个进程访问的资源,又称独占资源。管道在同一时刻只允许一个进程对其进行写入或者读取操作,因此管道也属于临界资源。临界资源需要被保护,否则可能会出现同一个时刻有多个进程对同一个管道进行写入或读取的操作,导致同时读写,交叉读写等情况以至于最后读取的数据没能达到预期的结果。


为了避免这些问题,操作系统对管道操作进行同步与互斥:


  • 同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
  • 互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。

同步是一种更为复杂的互斥,而互斥是一种特殊的同步。也就是说互斥是两个任务之间不可以同时运行,他们会相互排斥,必须等待一个线程运行完毕,另一个才能运行,而同步也是不能同时运行,但他是必须要按照某种次序来运行相应的线程(也是一种互斥)!因此互斥具有唯一性和排它性,但互斥并不限制任务的运行顺序,即任务是无序的,而同步的任务之间则有顺序关系。


3、管道的生命周期随进程

管道本质上就是文件,它依赖于文件系统,即当所有打开管道的进程都退出后,该文件也会被释放掉,所以管道的生命周期随进程。


4、管道是半双工的,数据只能向一个方向流动


在数据通信中,数据在线路上的传送方式可以分为以下三种:


  1. 单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。
  2. 半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。
  3. 全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。

管道是半双工的,数据只能向一个方向传输,若需要双向通信,则需要创建两个管道。

5、管道提供流式服务

一个进程向管道中写入数据,另一个进程每次从管道读取的数据的多少是任意的,这种被称为流式服务,与之相对应的是数据报服务。

  • 流式服务: 数据没有明确的分割,不分一定的报文段。
  • 数据报服务: 数据有明确的分割,读数据按报文段读取。


2.3. 命名管道

2.3.1 命名管道的概述

匿名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO 文件。

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中。这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信。因此,通过 FIFO 不相关的进程也能交换数据。

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

命名管道与匿名管道的特点部分相同,不同之处在于:

  • 匿名管道由pipe函数创建并打开,而命名管道有mkfifo函数创建,使用open函数打开。
  • 命名管道在文件系统中作为一个特殊的文件而存在,但命名管道中的内容却存放在内存中,向命名管道中写入的数据不会刷新到磁盘中。
  • 当使用命名管道的进程退出后,命名管道文件将继续保存在文件系统中以便以后使用。

2.3.3 命名管道打开的规则

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

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_NONBLOCK enable:立刻返回成功

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

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

2.3.4 命名管道的创建

使用mkfifo命令创建匿名管道

mkfifo fifo

使用这个命名管道文件,就能实现两个进程之间的通信了。我们在一个进程中用shell脚本每秒向命名管道写入一个字符串,在另一个进程中用cat命令从命名管道当中进行读取。现象就是当第一个进程启动后,另一个进程会每秒从命名管道中读取一个字符串打印到显示器上。这就证明了这两个毫不相关的进程可以通过命名管道进行数据传输,即通信。

当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉,在这里就可以很好的得到验证:当我们终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以此时bash就会被操作系统杀掉,我们的云服务器也就退出了。


使用mkfifo函数创建管道

在程序中使用mkfifo函数创建命名管道,函数原型如下:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);

参数:

  1. pathname: 表示要创建的命名管道文件。
    若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下。
  2. mode: 表示创建命名管道文件的默认权限。

返回值:

  • 命名管道创建成功,返回0。
  • 命名管道创建失败,返回-1。

例如,使用下面的代码创建命名管道:

#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#define FILE_NAME "myfifo"
int main()
{
    umask(0); //将权限掩码设置为0
    if(mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfile");
        return 1;
    }
    //创建命名管道成功...
    return 0;
}


编译运行产生结果如下:

2.3.5 命名管道实现Server与Client进程间通信

实现服务端和客户端的通信,需要先启动服务端并让其创建一个命名管道文件,任何再以读的方式打开该命名管道,之后就能读取到来自客户端写入的消息。

服务端代码:

#include "comm.h"
using namespace std;
#define NUM 1024
int main()
{
    //创建命名管道文件
    umask(0);
    if (mkfifo(IPC_PATH, 0600) != 0)
    {
        cerr << "mkfifo error" << endl;
        return 1;
    }
    //打开命名管道文件
    int pipeFd = open(IPC_PATH, O_RDONLY);
    if (pipeFd < 0)
    {
        cerr << "open error" << endl;
        return 2;
    }
    //向命名管道中读取数据
    char buffer[NUM];
    while (true)
    {
        ssize_t s = read(pipeFd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = '\0'; 
            cout << "客户端 -> 服务端# " << buffer << endl;
        }
        else if (s == 0)
        {
            cout << "客户端退出了,服务端也退出!" << endl;
            break;
        }
        else
        {
            //读取数据错误
            cout << "read: " << strerror(errno) << endl;
            break;
        }
    }
    close(pipeFd);
    cout << "服务端退出" << endl;
    unlink(IPC_PATH);
    return 0;
}

对于客户端来说,因为命名管道已经由服务端创建好了,所以不需要再次创建,客户端只需要以写的方式打开服务端创建的命名管道并写入数据即可,从而实现客户端与服务端的进程间通信。

客户端代码:

#include "comm.h"
using namespace std;
#define NUM 1024
int main()
{
    //打开命名管道
    int pipeFd = open(IPC_PATH, O_WRONLY);
    if(pipeFd < 0)
    {
        cerr << "open error" << endl;
        return 2; 
    }
    //向命名管道中写入数据
    char line[NUM];
    while(true)
    {
        printf("请输入你的消息# ");
        fflush(stdout);
        memset(line, 0, sizeof(line));
        //fgets得到C风格字符串,会在末尾自动添加 '\0'
        if(fgets(line, sizeof(line), stdin) != nullptr)
        {
            line[strlen(line) - 1] = '\0'; //处理回车  例如输入 abcd/n/0 
            write(pipeFd, line, strlen(line));
        }
        else
        {
            break;
        }
    }
    close(pipeFd);
    cout << "客户端退出" << endl;
    return 0;
}

为了让客户端和服务端使用同一个命名管道文件,这里让客户端和服务端都共同包含一个头文件,该头文件当中提供这个共用的命名管道文件的文件名,这样客户端和服务端就可以通过这个文件名,打开同一个命名管道文件,进而进行通信了。

【Linux学习】进程间通信的方式(匿名管道、命名管道、共享内存)2:https://developer.aliyun.com/article/1383938

目录
相关文章
|
2天前
|
消息中间件 存储 Linux
|
25天前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
22 0
Linux c/c++之IPC进程间通信
|
25天前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
17 0
Linux c/c++进程间通信(1)
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
2月前
|
SQL 网络协议 数据库连接
已解决:连接SqlServer出现 provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程【C#连接SqlServer踩坑记录】
本文介绍了解决连接SqlServer时出现“provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程”错误的步骤,包括更改服务器验证模式、修改sa用户设置、启用TCP/IP协议,以及检查数据库连接语句中的实例名是否正确。此外,还解释了实例名mssqlserver和sqlserver之间的区别,包括它们在默认设置、功能和用途上的差异。
|
3月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
66 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
317 0
|
8天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
18 1
|
13天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
17天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。