【Linux】进程间通信——管道/共享内存

文章目录

1. 进程间通信

进程间通信(Inter-Process Communication,简称IPC)是指不同进程之间进行数据交换和共享信息的机制和技术。在操作系统中,每个进程都是独立运行的,有自己的地址空间和数据,因此进程之间需要一种机制来进行通信,以便彼此协调工作、共享数据或者进行同步操作。

进程间通信的前提,也是重中之重,是让不同的进程看到同一份资源。 由于进程的独立性,只有先让不同进程看到同一份资源,有了通信的平台,才能实现通信。本文重点在于如何搭建进程间通信的平台,使得不同进程看到同一份资源。

2. 管道

管道,是一种传统的进程间通信方法。管道的本质是一个特殊文件,一个进程作为写入端,一个进程作为读取段,通过写入和读取管道实现通信。

💭管道分为匿名管道命名管道,它们的使用场景不同。

匿名管道

💭匿名管道(pipe)应用于有亲缘关系的进程之间通信(如:父子进程、兄弟进程)。以父子进程为例,原理:

  1. 父进程创建管道,并分别以写方式和读方式打开管道,此时父进程就拥有了两个新的文件描述符,以写方式打开管道的文件描述符称为写端fd ,以读方式打开管道的文件描述符称为读端fd

  2. 接着创建子进程,子进程继承了父进程的文件描述符表,二者有了相同的写端fd和读端fd。

  3. 然后根据需求关闭不要的文件描述符,如:父进程写数据给子进程,即父进程作为写入端,子进程作为读取端,那就关闭父进程的读端fd和子进程的写端fd。

  4. 此时父子进程已经能看到同一份资源了,通信开始,父进程调用write写入管道,子进程调用read读取管道,和文件操作相同。

在这个过程中创建的管道,称之为匿名管道。之所以是匿名管道,是因为整个过程中用户都无法获知管道的名称等具体信息,该管道由OS维护。

上述过程的逻辑演绎如下:

💡补充

  • 管道是一种特殊的文件,它在内存中以缓冲区的形式存在。因此打开管道就和打开文件一样,OS也会在内存中创建一个打开文件句柄来维护管道。通过打开文件句柄,我们可以引用到管道的缓冲区,从而对其进行读写操作。

  • 匿名管道的生命周期随进程。当引用该管道的所有进程退出,OS自动关闭并删除匿名管道。(打开文件句柄和inode的引用计数问题)

  • 因为管道是一种临时的通信机制,不像普通文件具有持久性的存储需求,所以管道是没有磁盘文件的。那么管道是否像文件一样拥有一个inode呢?是的。管道文件的inode主要用于标识和管理管道,记录与管道相关的元数据信息,并跟踪管道的引用计数。管道文件的inode并不链接实际数据,数据是通过内核的缓冲区进行传递和管理的。

  • 管道是一种半双工的通信方式,即一端写一端读,单向数据流动。

  • 下面是代码分析。

💬首先是创建匿名管道的接口

c++ 复制代码
int pipe(int pipefd[2]);

pipe是一个系统调用接口。当前进程创建匿名管道,传入参数pipefd是一个能够存放2个元素的整型数组,调用成功后,管道的写端fd和读端fd存入pipefd中,pipefd[0]是读端fd,pipefd[1]是写端fd。

下面是pipe在2号手册中的介绍。

c 复制代码
NAME
       pipe, pipe2 - create pipe

SYNOPSIS
       #include <unistd.h>

       int pipe(int pipefd[2]);
RETURN VALUE
       On success, zero is returned.  On error, -1 is returned, and errno is  set appropriately.

下面是使用匿名管道实现进程间通信的一段代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;
const int NUM = 1024;

// 先创建管道,进而创建子进程,父子进程使用管道进行通信
// 父进程向管道当中写"i am father",
// 子进程从管道当中读出内容, 并且打印到标准输出

int main()
{
    // 1.创建管道
    int pipefd[2] = {0};
    int ret = pipe(pipefd);
    if (ret < 0)
    {
        cerr << errno << ":" << strerror(errno) << endl;
        return 1;
    }

    // 2.创建子进程
    pid_t id = fork();
    assert(id >= 0);

    if (id == 0)
    {
        // 子进程读
        // 3.关闭不要的fd
        close(pipefd[1]);

        // 4.通信
        char buf[NUM] = {0};
        int n = read(pipefd[0], buf, sizeof(buf) - 1);
        if (n > 0)
        {
            buf[n] = '\0';
            cout << buf << endl;
        }
        else if (n == 0)
        {
            cout << "读取到文件末尾" << endl;
        }
        else
        {
            exit(1);
        }
        close(pipefd[0]);
        exit(0);
    }

    // 父进程写
    // 3.关闭不要的fd
    close(pipefd[0]);

    // 4.通信
    const char *msg = "I am father";
    write(pipefd[1], msg, strlen(msg));

    close(pipefd[1]);

    // 5.等待子进程退出
    int n = waitpid(id, nullptr, 0);
    if (n == -1)
    {
        cerr << errno << ":" << strerror(errno) << endl;
        return 1;
    }

    return 0;
}

⭕执行结果

bash 复制代码
[ckf@VM-8-3-centos Testpipe]$ ./a.out 
I am father #子进程成功读取并输出父进程发送的信息

命名管道

💭命名管道(named pipe)应用于无亲缘关系的进程之间通信。无亲缘关系的两个进程,无法通过继承文件描述符表来获得同一个匿名管道,因此就需要命名管道。命名管道有特定的文件名,多个进程可以通过相同的文件名找到相同的管道,进而实现通信。使用命名管道的步骤如下:

  1. 创建命名管道

    创建命名管道的方式有两种,通过指令或系统调用。

    指令:

    bash 复制代码
    mkfifo [选项] [name]
    OPTION:
    	-m MODE #设置管道的权限

    系统调用:

    cpp 复制代码
    NAME
           mkfifo - make a FIFO special file (a named pipe)
    
    SYNOPSIS
           #include <sys/types.h>
           #include <sys/stat.h>
    
           int mkfifo(const char *pathname, mode_t mode);
    RETURN VALUE
           On success mkfifo() returns 0.  In the case of an error, -1 is returned (in which case, errno is set appropriately).
  2. 进程打开命名管道

    进程可以调用open接口,以读或写方式打开命名管道,此时必须保证命名管道是存在的。注意:进程要有命名管道对应的权限才能正确地读取或写入数据,权限在创建管道时设定。

  3. 通信

  4. 关闭管道,删除管道

    进程调用close关闭管道,退出程序。命名管道的生命周期不随进程,进程退出命名管道依旧存在。因此需要用户自行删除,可以通过指令rm删除命名管道文件,也可以在进程中调用unlink接口。

    cpp 复制代码
    NAME
           unlink - delete a name and possibly the file it refers to
    
    SYNOPSIS
           #include <unistd.h>
    
           int unlink(const char *pathname);
    RETURN VALUE
           On success, zero is returned.  On error, -1 is returned, and errno is set appropriately.

💬下面是两个进程使用命名管道实现进程间通信,client是写进程,负责创建namedpipe和删除namedpipe,并向server发送数据,数据由用户交互传递。server是读进程,只负责读取client发送的数据。

注意: 对于打开命名管道的写端,调用open时,若此时该命名管道没有读端,则写端会阻塞等待至少一个读端打开该管道,写端才会打开。同理,若想打开读端但是没有写端,也会阻塞等待。

cpp 复制代码
//client
#include "common.hpp"

int main()
{
    // 1.创建命名管道
    umask(0);
    int ret = mkfifo(pipename.c_str(), 0666);
    if (ret < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }
    
    // 2.以写方式打开命名管道
    int wfd = open(pipename.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }

    //3.向管道中写入数据
    char buf[NUM] = {0};
    std::cout << "请输入您想要发送给服务端的信息: " << std::endl;
    while (true)
    {
        char *str = fgets(buf, sizeof(buf), stdin);
        assert(str);
        (void)str;

        int n = strlen(buf);
        buf[n - 1] = '\0'; // 消除'\n'

        if (strcasecmp(buf, "quit") == 0)
            break;

        int ret = write(wfd, buf, sizeof(buf));
        assert(ret > 0);
        (void)ret;
    }

    // 4.退出,关闭写端
    close(wfd);
    unlink(pipename.c_str());

    return 0;
}
cpp 复制代码
//server
#include "common.hpp"

int main()
{
    // 1.以读方式打开命名管道
    int rfd = open(pipename.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }

    //2.读取管道中的数据
    char buf[NUM] = {0};
    while (true)
    {
        int cnt = read(rfd, buf, sizeof(buf));
        if (cnt > 0)
        {
            buf[cnt] = '\0';
            std::cout << "message from client: " << buf << std::endl;
        }
        else if (cnt == 0)
        {
            std::cout << "通信结束" << std::endl;
            break;
        }
        else
        {
            return 1;
        }
    }

    // 3.关闭读端
    close(rfd);

    return 0;
}
c++ 复制代码
//common.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>	
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <cassert>

const std::string pipename = "fifo";
const int NUM = 1024;

⭕实操演示


管道的特性

💭作为特殊的文件,管道具有一些特性(匿名管道和命名管道同时具备)

  1. 当管道为空时或读进程读完数据时,读进程再次读取时会阻塞等待写进程写入数据后才开始读取。
  2. 当管道为满时,读进程没有读取数据,写进程会阻塞等待读进程读取出一些数据后再写入数据,否则未被读取的数据可能会被覆盖。
  3. 若所有写进程被关闭,读进程仍在读取,此时读进程调用的read函数会返回0,表示读取到文件末尾,即读取结束
  4. 若所有读进程被关闭,写进程再写入数据就无意义了,因此OS会发送信号SIGPIPE,终止写进程

🔎这种特性也被称为"管道的阻塞机制"。管道的阻塞机制确保了数据在写进程和读进程之间的可靠传递和同步处理,提高了数据处理的准确性和效率,为进程之间的通信和数据交换提供了便利和可靠性。


管道的应用:简易的进程池

使用匿名管道制作一个简易的进程池,大概思路:先创建一个父进程,然后让这个父进程创建多个子进程,通过用户交互的模式,让父进程下发指定的任务给不同的子进程。其中,"下发任务"这个过程,就是利用管道来实现,父进程对于每个子进程都有唯一一个管道用以传输"任务"数据。

  1. 管理子进程

    一个父进程对多个子进程,且每个子进程对应一个管道,那么肯定要先将多个子进程管理起来。根据"先描述,再组织"的管理思想,我的设计如下:先将子进程描述为一个结构体,该结构体中包含子进程pid、子进程对应管道在父进程中的写端fd、以及一个子进程名称(自定义格式,为了后续方便调试观察)。然后在父进程中定义一个容器,用以组织这些创建出来的子进程结构体,方便后续管理。

    cpp 复制代码
    //描述子进程结构体
    struct ChildProc
    {
        ChildProc(int pid, int write_fd) : _pid(pid), _write_fd(write_fd)
        {
            _proc_name = "proc->" + to_string(_pid) + ":" + to_string(_write_fd);
        }
    
        int _pid;
        int _write_fd;
        string _proc_name;
    };
    cpp 复制代码
    //父进程主函数,即整个进程池的框架
    int main()
    {
        //定义一个vector容器,用以组织ChildProc
        vector<ChildProc> child_processes;
    
        // 1.创建子进程
        CreatProcess(child_processes);
    
        // 2.父进程下发命令(用户交互式)
        OrderProcess(child_processes);
    
        // 3.进程退出
        WaitProcess(child_processes);
        cout << "子进程已全部成功退出,并被回收!" << endl;
    
        return 0;
    }
  2. 创建子进程

    父进程循环创建子进程。每次子进程创建完毕后,由于父进程尚且没有向管道写入数据,当前子进程read阻塞等待,父进程继续创建下一个子进程。父进程每次fork创建完一个子进程,要将其描述为ChildProc结构体,再插入管理的容器中。

    cpp 复制代码
    const int child_process_num = 3;
    
    void CreatProcess(vector<ChildProc> &cps)
    {
        for (int i = 0; i < child_process_num; i++)
        {
            // 1.创建管道
            int pipefd[2] = {0};
            int ret = pipe(pipefd);
            if (ret < 0)
            {
                perror("The following error happen:");
            }
            
            // 父进程写,子进程读(父进程向子进程发送命令)
            
            // 2.创建子进程,一个子进程在父进程中对应一个写端
            int id = fork();
            assert(id >= 0);
            
            // 子进程
            if (id == 0)
            {
                // 3.关闭不要的fd
                close(pipefd[1]);
                
                // 子进程接收并执行命令
                while (true)
                {
                    int n = 0;
                    // 此时管道为空时,子进程read阻塞等待父进程下发命令
                    int cnt = read(pipefd[0], &n, sizeof(int));
                    if (cnt > 0)
                    {
                        //FuncArray在Tasks.hpp中实现
                        FuncArray[n]();
                        cout << endl;
                    }         
                    else if (cnt == 0)
                    {
                        //父进程退出,即写端关闭,read返回值为0,子进程也随之退出
                        cout << "读取结束,子进程退出"
                             << " pid: " << getpid() << endl;
                        break;
                    }
                    else
                    {
                        exit(1);
                    }
                }
                close(pipefd[0]);
                exit(0);
            }
    
            // 父进程
            // 将子进程(子进程pid和写端fd)管理起来,父进程才方便下发命令
            cps.push_back(ChildProc(id, pipefd[1]));
            close(pipefd[0]);
        }
    }

    在common.hpp头文件中,简单写几个子进程可执行的任务,这里没有定义实际任务,只是打印语句以表示任务成功执行。后续这块可完善。

    cpp 复制代码
    #pragma once
    #include <iostream>
    #include <functional>
    using namespace std;
    
    void TaskWeChat()
    {
        cout << "wechat is running..." << endl;
    }
    
    void TaskChrome()
    {
        cout << "chrome is running..." << endl;
    }
    
    void TaskSteam()
    {
        cout << "steam is running.." << endl;
    }
    
    const function<void()> FuncArray[] = {TaskWeChat,TaskChrome,TaskSteam};
  3. 父进程下发命令给子进程

    cpp 复制代码
    int SelectBoard()
    {
        //用户选择面板
        cout << "#########################" << endl;
        cout << "# 0.wechat     1.chrome #" << endl;
        cout << "# 2.steam      3.quit   #" << endl;
        cout << "#########################" << endl;
        cout << "请选择你将下发的命令: ";
    
        int command = 0;
        cin >> command;
        return command;
    }
    
    void OrderProcess(vector<ChildProc> &cps)
    {
        int num = -1;
        while (true)
        {
            // 用户交互, 下发命令
            int command = SelectBoard();
            if (command == 3)
                break;
            if (command < 0 || command > 2)
                continue;
    
            // 轮询调用子进程
            num = (num + 1) % cps.size();
            printf("调用了子进程%d号, ", num);
            cout << cps[num]._proc_name << endl;
            
            // 将命令写入对应子进程的管道中
            write(cps[num]._write_fd, &command, sizeof(command));
            sleep(1);
        }
    }
  4. 等待子进程进程退出并回收

    cpp 复制代码
    void WaitProcess(vector<ChildProc> &cps)
    {
        // 先关闭父进程的所有写端,根据管道的特性(关闭管道所有写端,读端退出),关闭写端让对应的子进程退出
        // 随后,父进程要回收所有的子进程
    
        for (auto &cp : cps)
        {
            close(cp._write_fd);
            waitpid(cp._pid, nullptr, 0);
        }
    }

⭕运行程序,并进行测试。发现让父进程发送0、1、2命令都正常,可当发送3号退出命令,让父进程等待并回收子进程时,程序卡住了。

这里有一个隐藏的bug。匿名管道,我们运用了子进程继承父进程文件描述符表的机制,但在进程池中,由于利用了这个继承机制,又会产生bug。父进程创建0号子进程时是没问题的,如我们预期。当创建1号子进程时,由于此时父进程文件描述符表有了0号子进程的写端fd,被1号子进程继承了,所以此时0号子进程的管道有了两个写端fd,这并不符合我们的预期,我们的设计是让父进程和每个子进程之间有一个独立的管道。若创建三个子进程,最后进程池的结构如下:

再看看我们刚才写的WaitProcess函数。造成阻塞的原因是:close关闭第一个子进程管道的写端时,并没有关闭全部写端,因此该子进程并没有退出,waitpid阻塞等待。

cpp 复制代码
void WaitProcess(vector<ChildProc> &cps)
{
    for (auto &cp : cps)
    {
        close(cp._write_fd);
        waitpid(cp._pid, nullptr, 0);
    }
}

💡解决方法:

  1. 因为最后一个子进程只有父进程一个写端,因此可以先关闭最后一个子进程的写端fd,此时该子进程成功退出,OS自动关闭其所有文件描述符,因此它由于bug链接到其它子进程的管道上的写端fd会被关闭。如此逆向close即可完成。

  2. 这种进程池结构并不是我们想要的,因此直接在创建子进程时关闭对应管道错误的写端fd,形成我们期望的进程池结构,才是上策。修改代码如下:

    cpp 复制代码
    void CreatProcess(vector<ChildProc> &cps)
    {
        //创建一个容器wfds,用以存放父进程创建一个子进程时,已经拥有的写端fd
        vector<int> wfds;
        for (int i = 0; i < child_process_num; i++)
        {
            int pipefd[2] = {0};
            int ret = pipe(pipefd);
            if (ret < 0)
            {
                perror("The following error happen:");
            }
    
            // 每次创建管道后,将写端fd存入wfds
            wfds.push_back(pipefd[1]);
            
            int id = fork();
            assert(id >= 0);
    
            if (id == 0)
            {
                // 子进程关闭从父进程继承的所有写端(包括子进程自己管道的和其它管道的写端fd)!!         
                for (auto &wfd : wfds)
                {
                    close(wfd);
                }
                
                // 错误写法,在当前子进程push写端fd,其它子进程看不到!!!写时拷贝问题
                // wfds.push_back(pipefd[1]);
                // for (auto& wfd : wfds)
                // {
                //     close(wfd);
                //     cout << "关闭fd: " << wfd << endl;
                // }
    
    
                while (true)
                {
                    int n = 0;
                    int cnt = read(pipefd[0], &n, sizeof(int));
                    if (cnt > 0)
                    {              
                        FuncArray[n]();
                        cout << endl;
                    }
                    else if (cnt == 0)
                    {
                        cout << "读取结束,子进程退出"
                             << " pid: " << getpid() << endl;
                        break;
                    }
                    else
                    {
                        exit(1);
                    }
                }
                close(pipefd[0]);
                exit(0);
            }
            
            cps.push_back(ChildProc(id, pipefd[1]));
            close(pipefd[0]);
        }
    }

    此时再次发送quit指令,观察到子进程成功退出并被父进程回收。


3. System V共享内存

另一种进程间通信的方式是共享内存。共享内存是最快的进程间通信(IPC)形式。因为其通信过程中,传输数据时,不再需要经过内核的"中转",而是直接通过地址的映射获得共享资源。

共享内存的概念

💭在进程间通信(IPC )中,共享内存是一种特殊的通信机制,允许多个进程共享同一块物理内存区域,从而实现高效的数据交换和共享。与其他IPC方式相比,共享内存的主要优势是数据直接存储在内存中,避免了数据在进程之间的复制,从而提高了通信的速度和效率。缺点是无法保证数据的安全性。

共享内存的结构

共享内存(Shared Memory Segment,简称shm),是一段由多个进程共享的物理内存空间,各个进程将其通过页表映射到自己的地址空间共享区中。使得多个进程可以访问相同的空间,实现交换数据,完成IPC。图中,struct_shm(在真正的内核中并非这个名字)是内核中用于管理共享内存的一个结构体,每个共享内存对应一个该结构体,该结构体中包含了共享内存区的各种属性和元数据,如共享内存的大小、权限、关联进程等信息,这些结构体也会被OS组织并管理起来。

共享内存 = 管理共享内存信息的数据结构 + 真正的共享内存空间

共享内存的使用

💭以下假设使用共享内存通信的只有两个进程,实际上一个共享内存可以连接多个进程。

  1. 共享内存的获取

    通信双方,必须先能看到同一份共享资源,才能进行通信。获取的方式是,一方负责创建共享内存,另一方查找对方创建的共享内存,用到的接口是shmget

    cpp 复制代码
    NAME
           shmget - allocates a System V shared memory segment
    
    SYNOPSIS
           #include <sys/ipc.h>
           #include <sys/shm.h>
    
           int shmget(key_t key, size_t size, int shmflg);
    
    RETURN VALUE
           On success, a valid shared memory identifier is returned.  
           On error, -1 is returned, and errno is set to indicate the error.

    📌参数

    • key

      用于标识唯一的一个共享内存段。多个进程约定同一个key,可获取同一份共享内存。key是一个整型,可以通过ftok函数获取

      cpp 复制代码
      key_t ftok(const char *pathname, int proj_id);

      ftok的参数是一个路径字符串pathname和一个整型值项目idproj_id。内含特定的算法,通过这两个参数生成一个重复率较低的key值,并作为返回值。只要参数相同,生成的key值就相同。

    • size

      共享内存的大小,单位是字节byte

    • shmflg

      标记位。主要的标记有IPC_CREATIPC_EXCL,若shmflg==IPC_CREAT,表示若以key为键值的共享内存不存在,创建之。若存在,用之即可。若shmflg==IPC_CREAT|IPC_EXCL,表示若以key为键值的共享内存不存在,创建之。若存在,报错。(IPC_EXCL不能单独使用,只与IPC_CREAT一起使用)。另外,标记位还包含mode_flags,它用于定义共享内存的权限,格式与open的参数mode相同 ,指明onwer、group、world(运行进程者)对于共享内存的权限。

    📌返回值

    ​ 共享内存描述符(shared memory identifier,简称shmid),用于标识唯一的一段共享内存。

    🔎参数key和返回值shmid的区别?

    key 在函数调用时使用,意味着共享内存可能尚未存在。key的作用是在进程获取共享内存之前(此时共享内存可能还没创建),唯一标识一个共享内存段,使通信双方能够约定同一个共享内存段。这样,一个进程创建以key为键值的shm,另一个进程查找以key为键值的shm,并获取相同的shmid。shmid用于进程获取共享内存后,唯一标识一个共享内存段,这个标识符可以用于后续的共享内存操作 。

    二者作用大致相同,但作用的时间节点不同。

  2. 进程与共享内存建立联系

    上一步做的事,只是让通信双方获知了用哪一块共享内存(获取相同的shmid),但并没有真正与共享内存建立联系。那么现在就要把进程和共享内存链接起来,即在各自的地址空间中映射共享内存段。需要用到的接口是shmat。(shm attach)

    cpp 复制代码
    SYNOPSIS
           #include <sys/types.h>
           #include <sys/shm.h>
    
           void *shmat(int shmid, const void *shmaddr, int shmflg);
    RETURN VALUE
           On success shmat() returns the address of the attached shared memory segment; on error (void *) -1 is returned, and errno is set to indicate  the  cause  of  the error.

    📌参数

    • shmid

      就是第一步中获得的shmid。

    • shmaddr

      指定共享内存映射到当前进程的地址。一般设置为NULL,由OS自动选择映射的地址,较为安全可靠。

    • shmflg

      指明链接共享内存的读写模式。设置SHM_RDONLY为只读, 否则是即读又写(一般设置为0 )。没有只写的选项。注意,进程必须有对应权限才能设定对应的shmflg,如:设置SHM_RDONLY,进程对该共享内存必须有读权限。设置为0,进程对该共享内存必须有读权限和写权限。权限在shmget函数中设定。

    📌返回值

    ​ 一个void*类型的指针,指向当前进程地址空间中映射共享内存段的起始地址,后续该地址为shmaddr

  3. 开始通信,交换数据

    不像管道需要调用系统接口写入和读取数据,共享内存只需要在映射的地址空间中读写数据,这段空间的起始地址在第二步已经获得,直接当成数组的起始地址用就行。注意,获得的指针shmaddr是void*类型,不同场景下可能需要强转成其它类型来使用。

  4. 进程与共享内存解除联系

    通信结束后,通信双方无需再引用共享内存,即可先解除与共享内存的联系。因为一个共享内存可能会被多对进程引用,而不止一个,所以只有当引用该共享内存的进程数量为0时,才会删除这个共享内存。解除进程与共享内存的联系,用到接口shmdtshm detach

    cpp 复制代码
    SYNOPSIS
           #include <sys/types.h>
           #include <sys/shm.h>
    
           int shmdt(const void *shmaddr);
    RETURN VALUE
           On success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.

    传入shmaddr即可,返回值无意义,只是用作判断函数调用成功与否。

  5. 删除共享内存

    cpp 复制代码
    NAME
           shmctl - System V shared memory control
    
    SYNOPSIS
           #include <sys/ipc.h>
           #include <sys/shm.h>
    
           int shmctl(int shmid, int cmd, struct shmid_ds *buf);

    📌参数

    • shmid

      要删除的共享内存描述符

    • cmd

      控制指令。删除的指令是IPC_RMID

    • buf

      用于接收其它指令的返回值。删除时传入NULL即可。

注意:进程间通信时,创建和删除共享内存的工作最好由一个进程来完成,其它进程只是与已创建的共享内存进行连接和断连即可。

除了系统调用,还有一些关于共享内存的指令:

bash 复制代码
ipcs -m #查看共享内存信息
bash 复制代码
ipcrm [OPTION] [...] #删除共享内存
OPTION:
  -M 按key删除
  -m 按shmid删除

代码实现

由于利用共享内存实现IPC时,总是有相似的前置工作(创建和连接)和后置工作(断连和删除),因此可以将其封装在一个类中,将前置工作封装在类的构造函数中,后置工作封装在类的析构函数中,实现共享内存自动化搭建和销毁。如下代码:

cpp 复制代码
//头文件common.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <stdlib.h>
#include <cassert>

const std::string pathname = ".";
const int proj_id = 666;
const int shm_size = 4096;

#define CREATER 0
#define USER 1

class smart_init
{
public:
    smart_init(int type)
    {
        // 获取共享内存
        assert(type == CREATER || type == USER);
        if (type == CREATER)
            _shmid = creatShm(getKey());
        else if (type == USER)
            _shmid = searchShm(getKey());

        _type = type;

        // 与共享内存建立联系
        _shm_addr = attachShm(_shmid);
    }

    ~smart_init()
    {
        // 与共享内存断开联系
        detachShm(_shm_addr);

        if (_type == CREATER)
        {
            remoteShm(_shmid);
        }
    }

    void *get_shmaddr()
    {
        return _shm_addr;
    }

private:
    key_t getKey();
    int creatShm(key_t k);
    int searchShm(key_t k);
    int getShm(key_t k, int flag);
    void *attachShm(int shmid);
    void detachShm(const void *shmaddr);
    void remoteShm(int shmid);

private:
    int _type;
    int _shmid;
    void *_shm_addr;
};

std::string toHex(int n)
{
    char buf[64] = {0};
    snprintf(buf, sizeof(buf), "0x%x", n);
    return std::string(buf);
}

key_t smart_init::getKey()
{
    key_t k = ftok(pathname.c_str(), proj_id);
    if (k == -1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(1);
    }
    return k;
}

int smart_init::getShm(key_t k, int flag)
{
    int shmid = shmget(k, shm_size, flag);
    if (shmid == -1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(2);
    }
    return shmid;
}

int smart_init::creatShm(key_t k)
{
    umask(0);
    return getShm(k, IPC_CREAT | IPC_EXCL | 0666);
}

int smart_init::searchShm(key_t k)    
{
    umask(0);
    return getShm(k, 0666);
}

void *smart_init::attachShm(int shmid)
{
    void *shm_ptr = shmat(shmid, nullptr, 0);
    if (shm_ptr == (void *)-1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(3);
    }
    return shm_ptr;
}

void smart_init::detachShm(const void *shmaddr)
{
    int ret = shmdt(shmaddr);
    if (ret == -1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(4);
    }
}

void smart_init::remoteShm(int shmid)
{
    int ret = shmctl(shmid, IPC_RMID, nullptr);
    if (ret == -1)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        exit(5);
    }
}
cpp 复制代码
//进程A
#include "common.hpp"

int main()
{
    smart_init si(CREATER);
    char* shm_ptr = (char*)si.get_shmaddr();
    
    //通信
    int cnt = 0;
    const char* msg = "i am process A";
    strcpy(shm_ptr,msg);
    sleep(10);

    return 0;
}
cpp 复制代码
//进程B
#include "common.hpp"

int main()
{
    smart_init si(USER);

    //通信
    char* shm_ptr = (char*)si.get_shmaddr();
    printf("message from A: %s\n",shm_ptr);

    return 0;
}

ENDING...

相关推荐
7yewh12 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
Arenaschi15 分钟前
在Tomcat中部署应用时,如何通过域名访问而不加端口号
运维·服务器
小张认为的测试16 分钟前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
waicsdn_haha23 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
打鱼又晒网24 分钟前
linux网络套接字 | 深度解析守护进程 | 实现tcp服务守护进程化
linux·网络协议·计算机网络·tcp
良许Linux38 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
蜜獾云1 小时前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
小屁不止是运维1 小时前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构
Hacker_Oldv1 小时前
WPS 认证机制
运维·服务器·wps
bitcsljl1 小时前
Linux 命令行快捷键
linux·运维·服务器