Linux——进程间通信(匿名管道、命名管道、共享内存)

进程间通信的概念

进程间通信(InterProcess Communication,简称IPC)是指两个或多个不同进程之间传递信息或共享资源的过程。在现代操作系统中,进程间通信是非常常见的,因为它允许不同的进程在运行时交互和协作。

进程间通信目的

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

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

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

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

进程间通信的本质

我们知道进程具有独立性,进程独立性主要体现在数据层面上,而进程的执行逻辑可以共有也可以私有(例如父子进程的写时拷贝),也就是说一个进程不能直接从另一个进程中获取数据。这时就需要借助第三方资源,通信进程通过向第三方资源写入数据或读取数据,从而实现进程通信,所谓的第三方资源就是由操作系统提供的一段内存区域。

进程通信示意图:

Linux——进程间通信(匿名管道、命名管道、共享内存)_第1张图片

 进程A通过对第三方资源写入(或读取)数据,进程B通过对第三方资源读取(或写入)数据,进程通过从第三方资源获取另一个进程的数据。所以进程通信的本质就是:让不同的进程看到同一份资源(内存,文件内核缓冲等)。资源由操作系统的不同模块提供,不同模块提供的资源对应不同的通信机制。

常见的进程间通信机制

1. 管道(Pipe):管道是一种单向通信机制,在父进程和子进程之间创建,通过读取和写入文件描述符来完成通信。

2. 静态共享内存(Shared Memory):多个进程共享同一块内存,可以实现高效的数据传输和共享。

3. 动态共享内存(Message Queue):进程通过一个消息队列来进行通信,消息队列在进程间进行消息传递。

4. 信号(Signal):进程发送信号给另一个进程来实现通信,常用于进程间的通信和同步。

5. 套接字(Socket):套接字是一种在网络上进行进程间通信的机制,它允许不同的进程在不同的机器上交互和协作。

6. 远程过程调用(Remote Procedure Call, RPC):RPC允许在不同的计算机上运行的进程之间进行通信和调用。

不同的进程间通信机制具有不同的特点和适用场景,选择正确的机制可以提高程序的性能和可靠性。

 管道通信

管道是Unix中最古老的进程间通信形式,也是以种最基本的IPC机制,只能单向通信,通信的数据流向是单向的,一个进程向管道中写入数据,另一个进程则从管道中读取数据。,由pipe函数创建,pipe()原型:

Linux——进程间通信(匿名管道、命名管道、共享内存)_第2张图片

例如,我们要统计登陆用户个数

 命令who和命令 wc -l在执行时是两个不同的进程,进程who通过标准输出将数据写入管道中,进程wc通过标准输入从管道中读取数据,进而完成对数据的处理。也就是说进程who和进程wc通过第三方资源“管道”完成了通信。

管道通信示意图:

Linux——进程间通信(匿名管道、命名管道、共享内存)_第3张图片

 匿名管道:主要用于父进程和子进程之间的通信。

进程间通信的本质是,不同进程看到同一份资源,那么如何使用匿名管道实现父子进程间的通信呢?

通过前面的学习我们知道父进程在没有创建子进程前打开的文件,创建子进程后子进程也打开同一文件。也就是子进程会继承父进程打开的文件,也会创建相同的files_struc管理打开的文件。

Linux——进程间通信(匿名管道、命名管道、共享内存)_第4张图片

 注意:

父子进程使用的管道文件资源由操作系统提供并维护,该管道文件是内存级文件,父子进程对管道文件写入数据并不会发生写时拷贝。操作系统不会把管道文件中的数据刷新到磁盘,因为通信数据刷新到磁盘会降低通信效率,而且没有必要。

匿名管道通信过程

pipe函数


#include
int pipe(int pipefd[2])

参数pipe[2]是输出型参数,用于返回两个指向管道读端和管道写端的文件描述符。

———————————————————————————————————————————————————————————————————————————————————————————
数组元素                                      含义
———————————————————————————————————————————————————————————————————————————————————————————
pipefd[0]                                    管道文件描述符的读端
———————————————————————————————————————————————————————————————————————————————————————————
pipefd[1]                                    管道文件描述符的写端
———————————————————————————————————————————————————————————————————————————————————————————

pipe函数调用成功时返回0,调用失败时返回-1。



匿名管道通信步骤


在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:

1、父进程调用pipe函数创建管道

   // 创建管道
    int fd[2] = {0};
    if (pipe(fd) < 0)
    {
        perror("pipe");
        return 1;
    }

Linux——进程间通信(匿名管道、命名管道、共享内存)_第5张图片

2、父进程调用fork( )创建子进程 

 子进程会继承父进程打开的文件(这里注意管道文件)。

 pid_t id = fork();

fork()后父子进程共享管道原理

Linux——进程间通信(匿名管道、命名管道、共享内存)_第6张图片

3、关闭对应的读写端。

Linux——进程间通信(匿名管道、命名管道、共享内存)_第7张图片

 4、开始通信

5、关闭管道,结束进程。

#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
  // 1、创建管道
  
    int fd[2] = {0}; 
 {
    if (pipe(fd) < 0)  
        perror("pipe");
        return 1;
    }
//2、创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程// 关闭子进程的读端
        close(fd[0]); 

        const char *msg = "你好父进程,我是子进程... ... ";
        int count = 10;
        while (count--)
        {
            write(fd[1], msg, strlen(msg));//子进程向管道文件写入数据
            sleep(1);
        }
        close(fd[1]);//写入完成关闭写端
        exit(0);
    }

    // 父进程
    close(fd[1]);// 关闭父进程的写端
    char buff[64];
    while(1)
    {
        ssize_t s=read(fd[0],buff,sizeof(buff));//父进程从管道中读数据
        if(s>0)
        {
            buff[s]='\0';
            printf("子进程发送到父进程:%s\n",buff);
        }
        else if(s==0)
        {
            printf("读取结束\n");
            break;
        }

        else
        {
            printf("读取出错\n");
            break;
        }
    }
    close (fd[0]);//读取结束关闭读端
    waitpid(id,NULL,0);

    return 0;
}

Linux——进程间通信(匿名管道、命名管道、共享内存)_第8张图片

对管道文件的读写规则

1、当没有数据可读时

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

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

 2、当管道满的时候

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将不再保证写入的原子性。

管道的特点

1、一般而言,内核会对管道操作进行同步与互斥。

管道文件是一种同一时刻只能被一个进程读取、写入操作的资源,我们将同一时刻只允许一个进程对其操作的资源称为临界资源。临界资源是要被保护的,对临界资源保护就可以避免出现同一时刻由多个进程对同一个管道文件进行操作,导致同时读写、交叉读写以及读取数据不一致的现象出现。

进程之间对管道的操作关系主要有两种,同步与互斥。

同步概念:是指散布在不同进程之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。

在管道文件通信中,同步指的是在读进程读取数据之前,必须确保写进程已经往管道中写入了数据。

当写进程往管道中写入数据时,如果读进程已经打开了管道文件,则写进程的写操作将会被阻塞,直到读进程从管道中读取了数据。同样的,当读进程从管道中读取数据时,如果写进程没有往管道中写入数据,则读操作也会被阻塞,直到写进程往管道中写入了数据。

因此,在管道文件通信中,同步是由操作系统的管道机制来保证的,即写进程和读进程之间的数据流向是同步的。写进程的数据必须被读进程读取后才能继续写入数据,而读进程必须等待写进程往管道中写入数据才能读取数据。这种同步保证了数据的完整和正确性,防止了数据的丢失和错乱。

互斥概念:是指散布在不同进程之间的若干程序片断,当某个进程运行其中一个程序片段时,其它进程就不能运行这个程序片段,只能等到该进程运行完这个程序片段后才可以运行。

在管道文件通信中,互斥指的是保证进程对同一管道的访问是有序的,一个进程在写入数据到管道时,其他进程只能等待其写入完成后才能进行读取操作,或者相反,一个进程在读取管道中的数据时,其他进程只能等待其读取完成后才能进行写入操作。

实现互斥的方式通常是使用锁来控制管道的访问。在访问管道时,进程需要先获得管道的锁,如果锁已经被其他进程持有,则进程需要等待锁的释放。在管道的访问结束后,进程需要释放锁,使得其他进程可以获得锁并进行管道访问操作。

需要注意的是,如果管道的访问没有互斥保证,可能会出现数据不一致的情况,导致程序出现错误。因此,在使用管道进行进程间通信时,一定要保证互斥的正确性。

如以上程序所示,子进程在对管道文件写入数据时,父进程不能管道进行读取操作,只有在子进程写入完成后,父进程才能读取数据。


总结:

        互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

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

3、管道提供流式服务。

4、一般而言,进程退出,管道释放,所以管道的生命周期随进程 。

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

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

单工通信(Simplex Communication):单工模式的数据传输是单向的。通信双方中,一方固定为发送端,另一方固定为接收端。

半双工通信(Half Duplex):半双工数据传输指数据可以在一个信号载体的两个方向上传输,但是不能同时传输。

全双工通信(Full Duplex):全双工通信允许数据在两个方向上同时传输,它的能力相当于两个单工通信方式的结合。全双工可以同时(瞬时)进行信号的双向传输。
 

Linux——进程间通信(匿名管道、命名管道、共享内存)_第9张图片

管道通信中,有四种特殊情况需要特别注意:

  1. 阻塞情况:a、读取的进程不读:对管道写入的进程写入数据时,如果管道缓冲区已满,则写入的进程将被阻塞,直到读取的进程开始读取数据并释放管道缓冲区空间。b、写入的进程不写,读取进程一直读,那么此时会因为管道里面没有数据可读,对应的读取的进程会被挂起,直到管道里面有数据后,读的进程才会被唤醒。

  2. 管道关闭:当一个进程关闭一个管道时,写入者将收到SIGPIPE信号。此时,写入的进程必须要处理该信号并采取相应的操作。

  3. 写入的进程提前结束:当一个写入的进程结束时,管道读取者仍能够读取数据,在读取的进程将管道当中的数据读完后,就会继续执行该进程之后的代码逻辑,而不会被挂起。但是读取的进程如果后续再读取者管道缓冲区时将被阻塞,直到有另一个写入者进程开始写入数据。

  4. 读取者提前结束:当一个读取者进程结束时,管道写入者仍能够写入数据。但是,如果管道缓冲区满时,写入者将被阻塞,直到有另一个读取者进程开始读取数据。而写入的进程还在一直向管道写入数据,那么操作系统会将写端进程杀掉。

阻塞情况就能够很好的说明,管道是自带同步与互斥机制的,读取的进程和写入的进程是有一个步调协调的过程的,不会说当管道没有数据了读端还在读取,而当管道已经满了写端还在写入。读端进程读取数据的条件是管道里面有数据,写入的进程写入数据的条件是管道当中还有空间,若是条件不满足,则相应的进程就会被挂起,直到条件满足后才会被再次唤醒 。

当一个进程关闭一个管道时,写入者将收到SIGPIPE信号。此时,写入的进程必须要处理该信号并采取相应的操作。

代码示例:

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
int main()
{
    // 1、创建匿名管道
    int pipefd[2] = {0};
    pipe(pipefd);

    // 2、创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        close(pipefd[0]); // 关闭管道的读端
        const char *s1 = "父进程,我是子进程......";
        int count = 10;
        while (count--)
        {
            write(pipefd[1], s1, strlen(s1));
            sleep(1);
        }
        close(pipefd[1]); // 写入完成,关闭写端
        exit(0);
    }
    // 父进程
    // 关闭管道的写端
    close(pipefd[1]);
    close(pipefd[0]);
    int status = 0; // 关闭父进程的读端(子进程写入失败,被操作系统杀掉)

    waitpid(id, &status, 0);
    // cout << "子进程退出码信号:"<

Linux——进程间通信(匿名管道、命名管道、共享内存)_第10张图片

 管道缓冲区的大小

如果一个进程一直向管道中写入数据,会将管道的缓冲区占满,那么 管道缓冲区有多大?

Linux——进程间通信(匿名管道、命名管道、共享内存)_第11张图片

测试管道大小代码示例:

#include 
#include 
#include 
#include 
int main()
{
    int pipefd[2] = {0};
    if (pipe(pipefd) < 0)
    {
        perror("pipe");
        exit(1);
    }
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        close(pipefd[0]);
        int count = 0;
        char c = 'a';
        while (1)
        {
            write(pipefd[1], &c, 1);
            count++;
            printf("%d\n", count); // 统计写入的字节数
        }
        close(pipefd[1]);
        exit(0);
    }
    // 父进程
    close(pipefd[1]);
L    close(pipefd[0]);

    return 0;
}

Linux——进程间通信(匿名管道、命名管道、共享内存)_第12张图片

基于匿名管道实现父进程与多个子进程之间通信

ctrlProcess.cc

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "task.hpp"
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:
    static int number;

public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
        char namebuff[64];
        snprintf(namebuff, sizeof(namebuff), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = number;
    }

    const string name() const
    {
        return processname;
    }

    ~EndPoint()
    {
    }

public:
    pid_t _child_id;
    int _write_fd;
    string processname;
};
int EndPoint::number = 0;
void WaitCommand()
{
    while (true)
    {
        int command;
        int n = read(0, &command, sizeof(int));
        if (n == sizeof(int))
        {
            t.Exceute(command);
        }
        else if (n == 0)
        {
            cout << "父进程让" << getpid()<<"退出" << endl;
            exit(12);
        }
        else
        {
            break;
        }
    }
}
void creatProcess(vector *end_points)
{
    vector 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)
        {
            cout<push_back(EndPoint(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}
int ShowBoard()
{
    cout << "***********************************************************" << endl;
    cout << "0.日志任务                                       1.数据库任务" << endl;
    cout << "2.网络任务                                       3.退出     " << endl;
    cout << "***********************************************************" << endl;
    cout << "请选择#" << endl;
    int command = 0;
    cin >> command;
    return command;
}

void ctrlProcess(vector end_points)
{
    int num = 0;
    int cnt = 0;
    while (true)
    {
        // 1.选择任务

        // 随机式:int command=rand()%3;
        // 交互式
        int command = ShowBoard();
        if (command == 3)
            break;
        if (command < 0 && command > 2)
            continue;

        // 2.选择进程
        // int index=rand()%t.funcs.size();
        int index = cnt++;
        cnt %= end_points.size();
        cout << "选择了进程:" << end_points[index].name() << "|处理任务" << command << endl;

        // 3.下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));
        sleep(2);
    }
}
void waitProcess(const vector end_points)
{

    // 1全部让子进程退出
    // 父进程关闭写端
    // for (const auto &ep : end_points)
    // for(int end=end_points.size()-1;end>=0;end--)
    // {
    //     cout << "全部让子进程退出:"<= 0; end--)
    {
        cout << "全部让子进程退出:" << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);
        waitpid(end_points[end]._child_id, nullptr, 0);
        cout << "父进程回收全部子进程 " << end_points[end]._child_id << endl;
    }
    sleep(8);

    // 2.父进程回收子进程

    // for (const auto &ep : end_points)
    // {
    //     waitpid(ep._child_id, nullptr, 0);
    //     cout << "父进程回收全部子进程 " << endl;
    // }
}

int main()
{
    // 构建控制结构,父进程写入,子进程读取
    vector end_points;
    creatProcess(&end_points);

    // 得到了文件描述符的写端和子进程id
    ctrlProcess(end_points);

    // 3.处理退出问题。
    waitProcess(end_points);

    return 0;
}

task.hpp

#pragma once
#include 
#include 
#include

using namespace std;
typedef void (*func_t)(); // 函数指针

void PrintLog()
{
    cout <<"pid:"<=0&&command funcs;
};

命名管道

 命名管道是一种特殊类型的文件,在Linux系统中用于进程间通信。它允许两个或多个进程通过共享一个特殊的文件来通信。命名管道的名称与文件名类似,具有唯一的路径名,并且可以通过在文件系统中进行查找来访问。它是一种半双工通信机制,这意味着它只能在一个方向上进行通信。一个进程可以向管道写入数据,而另一个进程则可以从管道中读取数据。命名管道是UNIX社区提供的最古老的IPC方式之一,也是现今UNIX/Linux环境下非常流行的一种IPC方式。

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

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

创建命名管道

通过指令

$mkfifo filename

Linux——进程间通信(匿名管道、命名管道、共享内存)_第13张图片

我们可以通过指令mkfifo+管道文件的命名从而创建一个命名管道文件 。

如:mkfifo  filemake

命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,但这个映像的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中。


 通过函数

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

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

如果pathname以路径的方式给出,则将命名管道文件创建在pathname路径下。

如果若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下(代码文件的路径)。

mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。例如,将mode设置为0666,则命名管道文件创建出来的权限如下:
 

 但实际上创建出来文件的权限值还会受到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。umask的默认值一般为0002,当我们设置mode值为0666时实际创建出来文件的权限为0664。

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

 umask(0);

mkfifo()的返回值。

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

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

mkfifo创建管道文件示例:

在当前路径下创建命名管道文件

#include 
#include 
#include 
#define MYTESTFILENAME "mytestfilename"
int main()
{
umask(0);
if(mkfifo(MYTESTFILENAME,0666)<0)
{
    perror("mkfifo");
    exit(1);

}
return 0;

}

Linux——进程间通信(匿名管道、命名管道、共享内存)_第14张图片

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

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

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

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

命名管道的打开规则

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

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

O_NONBLOCK enable:立刻返回成功。

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

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

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

命名管道使用实例

使用命名管道实现server服务端和client客户端之间的通信,进程通信间的本质是要让不同的进程看到同一份资源,要让server服务端和client客户端之间使用命名管道进行通信。

1、通过mkfifo()创建一个命名管道文件。

2、server服务端和client客户端看到同一份命名管道文件,让server服务端和client客户端以对应的读或写方式打开命名管道文件。

3、数据通信,让server服务端和client客户端向命名管道文件写入或读取数据。

4、通信结束关闭管道文件。

服务端代码:

#include "comm.hpp"

int main()
{
    // 调用mkfifo()创建命名管道文件
    umask(0); // 将文件默认掩码设置为0
    if (mkfifo(FILE_NAME, 0666) < 0)
    {
        perror("mkfifo");
        exit(1);
    }

    //打开管道文件  以读的方式打开命名管道文件
    int fd = open(FILE_NAME, O_RDONLY); 
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    
    //开始通信
    char msg[1024];
    while (1)
    {
        msg[0] = '\0'; // 清空msg 保证读到的信息为最新

        // 服务端从命名管道当中读取信息
        ssize_t s1 = read(fd, msg, sizeof(msg) - 1);
        if (s1 > 0)
        {
            msg[s1] = '\0';              // 设置'\0',避免输出
            printf("接收到# %s\n", msg); // 输出客户端发来的信息
        }
        else if (s1 == 0)
        {
            printf("client quit!\n");
            break;
        }
        else
        {
            printf("read error!\n");
            break;
        }
    }
    close(fd); // 通信结束,关闭命名管道文件
    return 0;
}

客户端代码:

#include "comm.hpp"

int main()
{
    //命名管道文件服务端已经创建好了 客户端以写的方式打开命名管道文件
    int fd = open(FILE_NAME, O_WRONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }
    char msg[1024];
    while (1)
    {
        msg[0] = '\0';            // 每次读之前将msg清空
        printf("请输入信息# "); // 提示客户端输入
        fflush(stdout);
        // 从客户端的标准输入流读取信息
        ssize_t s = read(0, msg, sizeof(msg) - 1);
        if (s > 0)
        {
            msg[s - 1] = '\0';
            // 将信息写入命名管道
            write(fd, msg, strlen(msg));
        }
    }
    close(fd); // 通信完毕,关闭命名管道文件
    return 0;
}

comm.hpp代码: 

#pragma once

#include 
#include 
#include 
#include 
#include 
 #include 
#include 

#define FILE_NAME "myfifo"

执行结果:

Linux——进程间通信(匿名管道、命名管道、共享内存)_第15张图片

Linux——进程间通信(匿名管道、命名管道、共享内存)_第16张图片

共享内存

共享内存的基本原理:

共享内存是一种进程间通信的方式,它允许多个进程共享同一段物理内存,从而实现数据的共享和交换。其基本原理如下:

1. 通过系统调用shmget创建一个共享内存段,并指定该段的大小和访问模式。

2. 通过系统调用shmat将共享内存段与当前进程的虚拟地址空间进行映射,使得进程可以访问共享内存。

3. 不同进程可以通过相同的key值和shmget函数创建同一个共享内存段,并通过shmat函数将其映射到各自的虚拟地址空间中。

4. 进程可以通过共享内存段中的指针访问共享数据。

5. 当进程不再需要访问共享内存段时,可以通过shmdt函数将其与虚拟地址空间的映射解除。

6. 当所有进程都不再需要使用共享内存段时,可以通过shmctl函数删除共享内存段,并释放占用的资源。

需要注意的是,由于多个进程可以同时访问共享内存段,因此需要对共享数据进行同步和互斥,以避免数据的不一致和冲突。

Linux——进程间通信(匿名管道、命名管道、共享内存)_第17张图片

共享内存的创建

创建共享内存需要调用系统调用shmget( ),函数原型:

  int shmget(key_t key, size_t size, int shmflg);

shmget( )头文件:

 #include 
 #include 

shmget( )参数说明:

第一个参数key,表示待创建共享内存在系统当中的唯一标识。
第二个参数size,表示待创建共享内存的大小。
第三个参数shmflg,表示创建共享内存的方式。

第一个参数key:在操作系统中可能同时有多个进程使用不同的共享内存进行通信,不同的进程通过key查找到相应的共享内存实现通信。

Linux——进程间通信(匿名管道、命名管道、共享内存)_第18张图片

 shmget函数的返回值说明:

调用成功:返回一个有效的共享内存标识符(用户层标识符)。
调用失败:返回-1。

fork( ):将一个已存在的路径名pathname和一个整数标识符proj_id通过一定算法转换成一个key值,称IPC键值。IPC键值是一个可以被系统中所有进程访问的并唯一标识IPC通信对象的值。ftok( )生成的IPC键值使用路径名和proj_id作为其生成的依据。具有相同路径名和proj_id的ftok()调用返回相同的IPC键值。

fork( )原型:

key_t ftok(const char *pathname, int proj_id);

函数头文件:

#include 
#include 

 注意:

1.使用ftok( )生成key值可能会产生冲突,此时可以对传入ftok( )的参数进行修改。

2.需要进行通信的各个进程,在使用ftok函数获取key值时,都需要采用同样的路径名和和整数标识符,进而生成同一种key值,然后才能找到同一个共享资源。

第三个参数shmflg:

IPC_CREAT:使用key值创建(标识)一个共享内存,如果已经存在key值标识的共享内存,则获取已经存在的共享内存并返回,不存在就创建。
IPC_CREAT | IPC_EXCL:创建一个共享内存,如果共享内存不存在就创建;如果已经存在则立马出错返回;如果创建成功共享内存一定是最新的,IPC_EXCL不能单独使用,要配合IPC_CREAT。

创建共享内存示例:

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

#define PATHNAME "."    // 路径名
#define PROJID 0x6666   // 项目id
const int gsize = 4096; // 共享内存大小

int main()
{

    key_t key = ftok(PATHNAME, PROJID); // IPC键值
    if (key < 0)
    {
        perror("ftok");
        exit(1);
    }

    int shm = shmget(key, gsize, IPC_CREAT | IPC_EXCL);
    if (shm < 0)
    {
        perror("shmget");

        exit(2);
    }
    printf("key:%x\n", key);
    printf("shm:%d\n", shm);

    return 0;
}

编译运行后就创建了一个共享内存。

Linux——进程间通信(匿名管道、命名管道、共享内存)_第19张图片

 共享内存的关联

shmat函数用于将共享内存附加到进程的地址空间中,让进程可以访问这块共享内存。它的函数原型为:

void *shmat(int shmid, const void *shmaddr, int shmflg);

参数说明:

shmid:共享内存标识符,在shmget函数创建共享内存时获得。
shmaddr:指定将共享内存映射到进程空间中的地址,通常设为NULL,由系统自动分配地址。
shmflg:指定附加共享内存的方式,包括SHM_RDONLY等。

函数返回值:

成功时,返回共享内存段的第一个字节的地址;
 失败时,返回-1,并设置errno错误码。

注意事项:

附加共享内存时应该确保进程已经获得了对共享内存的操作权限,否则会导致附加失败;
 进程应该在使用完共享内存后,及时将其分离(调用shmdt函数),防止资源泄漏。

共享内存的释放

共享内存的释放需要执行以下步骤:

1. 在使用共享内存的所有进程中,先调用shmdt()函数将共享内存从进程的地址空间中分离出来,断开与共享内存的联系。

2. 然后,一个进程调用shmctl()函数,使用IPC_RMID命令来删除共享内存。

3. 如果所有进程都已经分离了共享内存,并且最后一个进程删除了共享内存,那么该共享内存段就会被释放,可以被重新使用。

需要注意的是,在使用完共享内存后一定要记得释放,否则会造成系统资源的浪费。同时,应该注意确保所有进程都已经分离了共享内存,否则会出现错误。

注意:

共享内存的释放必须在所有使用该共享内存的进程都结束使用该内存后才能进行,否则可能会导致数据丢失、进程异常等问题。

共享内存的释放可以通过指令:ipcrm -m shmid 来释放指定的共享内存资源。

还可以通过系统调用shmctl( )进行共享内存的删除

函数原型:

 int shmctl(int shmid, int cmd, struct shmid_ds *buf);

 shmctl函数的参数说明:

第一个参数shmid,表示所控制共享内存的用户级标识符。

第二个参数cmd,表示具体的控制动作。

第三个参数buf,用于获取或设置所控制共享内存的数据结构。

shmctl函数的返回值说明:

shmctl调用成功,返回0。

shmctl调用失败,返回-1。

shmctl函数是一个用于控制共享内存的系统调用函数。shmctl函数的参数包括共享内存标识符、CMD命令和结构体shmid_ds指针。

shmctl函数有三个主要的CMD命令:

  • IPC_STAT:用于获取共享内存的状态信息,并将其存储在shmid_ds结构体中。
  • IPC_RMID:用于删除共享内存。
  • IPC_SET:用于设置共享内存的状态信息。

在使用shmctl函数时,需要先获取共享内存标识符。可以使用shmget函数获取共享内存标识符。然后再使用shmctl函数进行控制。

代码示例:


#include 
#include 
#include 
#include 
#include 


#define PATHNAME "."    // 路径名
#define PROJID 0x6666   // 项目id
const int gsize = 4096; // 共享内存大小

int main()
{

    key_t key = ftok(PATHNAME, PROJID); // IPC键值
    if (key < 0)
    {
        perror("ftok");
        exit(1);
    }

    int shmid = shmget(key, gsize, IPC_CREAT | IPC_EXCL);
    if (shmid < 0)
    {
        perror("shmget");

        exit(2);
    }
    printf("key:%x\n", key);
    printf("shm:%d\n", shmid);
    
    sleep(2);
    shmctl(shmid,IPC_RMID,NULL);
    sleep(2);

    return 0;
}

共享内存的关联

在进程间通信前我们需要先将进程与对应的共享内存关联,通过系统调用shmat( )将共享内存段与当前进程的虚拟地址空间进行映射,使得进程可以访问共享内存。

你可能感兴趣的:(算法,linux,运维)