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

news2024/11/24 18:40:34

文章目录

  • 1. 进程间通信
  • 2. 管道
    • 匿名管道
    • 命名管道
    • 管道的特性
    • 管道的应用:简易的进程池
  • 3. System V共享内存
    • 共享内存的概念
    • 共享内存的结构
    • 共享内存的使用
    • 代码实现

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并不链接实际数据,数据是通过内核的缓冲区进行传递和管理的。

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

在这里插入图片描述

  • 下面是代码分析。

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

int pipe(int pipefd[2]);

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

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

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.

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

#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;
}

⭕执行结果

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

命名管道

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

  1. 创建命名管道

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

    指令:

    mkfifo [选项] [name]
    OPTION:
    	-m MODE #设置管道的权限
    

    系统调用:

    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接口。

    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时,若此时该命名管道没有读端,则写端会阻塞等待至少一个读端打开该管道,写端才会打开。同理,若想打开读端但是没有写端,也会阻塞等待。

//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;
}
//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;
}
//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、以及一个子进程名称(自定义格式,为了后续方便调试观察)。然后在父进程中定义一个容器,用以组织这些创建出来的子进程结构体,方便后续管理。

    //描述子进程结构体
    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;
    };
    
    //父进程主函数,即整个进程池的框架
    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结构体,再插入管理的容器中。

    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头文件中,简单写几个子进程可执行的任务,这里没有定义实际任务,只是打印语句以表示任务成功执行。后续这块可完善。

    #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. 父进程下发命令给子进程

    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. 等待子进程进程退出并回收

    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阻塞等待。

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,形成我们期望的进程池结构,才是上策。修改代码如下:

    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

    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函数获取

      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)

    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

    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. 删除共享内存

    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即可。

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

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

ipcs -m #查看共享内存信息

在这里插入图片描述

ipcrm [OPTION] [...] #删除共享内存
OPTION:
  -M 按key删除
  -m 按shmid删除

代码实现

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

//头文件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);
    }
}
//进程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;
}
//进程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…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/770904.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

解锁医疗新时代!互联网医院系统源码助您开启智慧就医新体验

互联网医院系统软件开发之后具有许多优势&#xff0c;下面将介绍其中一些。   提供便利的就医方式&#xff1a;互联网医院系统软件可以让患者享受在线诊疗的便利。患者可以通过手机或电脑随时随地进行在线挂号、在线咨询、在线复诊等操作&#xff0c;不再受制于时间和地点的限…

供应链管理系统有哪些?

1万字干货分享&#xff0c;国内外 20款 供应链管理软件都给你讲的明明白白。如果你还不知道怎么选择&#xff0c;一定要翻到第三大段&#xff0c;这里我将会通过8年的软件产品选型经验告诉你&#xff0c;怎么样才能快速选到适合自己的软件工具。 &#xff08;为防后续找不到&a…

Python学习笔记-Windows下VirtualEnv+VSCode中虚拟环境配置

1 VirtualEnv简介 VirtualEnv是一个虚拟化环境&#xff0c;是独立开的开发环境&#xff0c;在一个文件夹中创建的独立虚拟环境&#xff0c;可以分隔开不同项目&#xff0c;开发互不影响。 优点如下&#xff1a; 使不同的应用开发环境独立&#xff0c;避免互相干扰环境升级不…

RabbitMq(一)

一、基本概念、常见工作模式以及简单使用 MQ全称Message Queue (消息队列)&#xff0c;是在消息的传输过程中保存消息的容器。多用于分布式系统之间进行通信。 小结 MQ消息队列&#xff0c;存储消息的中间件分布式系统通信两种方式:直接远程调用和借助第三方完成间接通信发…

【C语言初阶(18)】结构体

文章目录 前言Ⅰ结构体的声明Ⅱ 结构体的定义Ⅲ 结构体初始化Ⅳ 访问结构体成员⒈结构体变量访问结构体成员⒉结构体指针访问结构体成员 Ⅴ 结构体的嵌套Ⅵ 结构体传参 前言 C 语言提供了一些非常基本的数据类型&#xff0c;如 int、float、double、char 等&#xff0c;这些不同…

基于SpringBoot + EasyExcel + Vue + Blob实现导出Excel文件的前后端完整过程

首先前端发起HTTP请求之后&#xff0c;后端返回一个Excel输出流&#xff0c;然后前端用Blob类型接收数据&#xff0c;并且解析响应头数据以及提取源文件名&#xff0c;最后用a标签完成下载。 一、后端代码 &#xff08;1&#xff09;导入阿里巴巴的EasyExcel依赖&#xff08;…

【C++进阶之路】list的基本使用和模拟实现

文章目录 初步认识①定义②底层原理③迭代器的分类 一、基本使用1.插入结点元素2.删除结点元素3.合并两个有序链表4.将一条链表的某一部分转移到另一条链表5.对链表排序并去重6.vector与list排序的比较 二、模拟实现①要点说明②基本框架③迭代器构造函数- -*->list里的迭代…

HG20202-2014脱脂工程施工及验收规范

为提高脱脂工程施工技术水平,加强施工过程的质量控制,保证施工质量和安全,制定本规范。 本规范适用于化工建设工程中忌油工艺介质系统的设备、管道和管道组成件仪表和仪表组成件等的脱脂。 本规范不适用于下列情况的脱脂: 1、制造领域; 2、工厂停车检修。 设计文件或用户规…

2023云曦期末复现

目录 WEB sign SSTI serialize WEB sign 有10000个 进行bp爆破 能发现 410 和 414长度 还有 420 410 414存在16进制的字符 拼凑出来为 \x66\x6c\x61\x67\x7b\x61\x63\x63\x39\x39\x66\x39\x30\x34\x66\x30\x65\x61\x66\x61\x34\x31\x63\x30\x36\x34\x33\x36\x38\x31\x3…

行为型模式 - 策略模式

概述 先看下面的图片&#xff0c;我们去旅游选择出行模式有很多种&#xff0c;可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。 作为一个程序猿&#xff0c;开发需要选择一款开发工具&#xff0c;当然可以进行代码开发的工具有很多&#xff0c;可以选择Idea进行开发&…

WPF嵌入外部exe应用程序-使用Winfom控件承载外部程序

使用Winform控件承载外部程序 在WPF中使用Winfom控件添加winform相关的程序集在XAML头中加入对这两个程序集命名空间的引用使用Winform控件效果&#xff1a;问题 在Winfom控件中嵌入exe程序准备Winfrom控件更换父窗体的句柄完整实现代码&#xff1a;实现效果&#xff1a; 问题和…

王道计算机网络学习笔记(5)——传输层和应用层

前言 文章中的内容来自B站王道考研计算机网络课程&#xff0c;想要完整学习的可以到B站官方看完整版。 五&#xff1a;传输层 5.1&#xff1a;传输层基本概述 传输层的功能&#xff1a; 1传输层提供进程和进程之间的逻辑通信 2复用和分用 微信和QQ都使用传输层的协议进行发…

设计模式-外观模式在Java中的使用示例

场景 外观模式 外观模式是一种使用频率非常高的结构型设计模式&#xff0c;它通过引入一个外观角色来简化客户端与子系统 之间的交互&#xff0c;为复杂的子系统调用提供一个统一的入口&#xff0c;降低子系统与客户端的耦合度&#xff0c;且客户端调用非常方便。 示例 自…

【区块链+体育】“数智化”的杭州亚运会,中创助力区块链技术发展

“智能”&#xff0c;是杭州亚运会的办赛理念之一。除了数字藏品开亚运先河&#xff0c;杭州亚组委充分应用区块链、大数据、人工智能等前沿技术&#xff0c;为观众提供从购票、出行、观赛到住宿、美食和旅游等“一站式”服务。 本次亚运会将全程智能陆续落到了实处&#xff0…

10亿级用户,如何做 熔断降级架构?微信和hystrix的架构对比

说在前面 在40岁老架构师 尼恩的读者社区(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如极兔、有赞、希音、百度、网易、滴滴的面试资格&#xff0c;遇到一几个很重要的面试题&#xff1a; (1) 什么是熔断&#xff0c;降级&#xff1f;如何实现&#xff1f; (2) 服务熔…

测试用例(2)

项目管理工具 主要用tapd&#xff0c;jira少用 acp 敏捷项目管理证书 task:故事&#xff0c;一个故事有开始也有结束&#xff0c;那么在项目管理里面&#xff0c;会把每个任务按照一个task来看&#xff0c;那么这个task也可以叫story&#xff0c;具体指的就是任务有开始有结…

利用鸿鹄优化共享储能的SCADA 系统功能,赋能用户数据自助分析

摘要 本文主要介绍了共享储能的 SCADA 系统大数据架构&#xff0c;以及如何利用鸿鹄来更好的优化 SCADA 系统功能&#xff0c;如何为用户进行数据自助分析赋能。 1、共享储能介绍 说到共享储能&#xff0c;可能不少朋友比较陌生&#xff0c;下面我们简单介绍一下共享储能的价值…

数组的递归筛选

数组递归筛选 根据一个值筛选出来通过 includes 递归 const options [{name: "ikun",options: [{name: "YAY11",},],},{name: "YAY",}, ];function findValue(orgOptions,val) {let newArr1 []orgOptions.forEach(item>{if(item.options…

费尔法克斯水务通过使用 Liquid UI 移动化和定制 SAP PM 来提高收入和数据完整性

背景 费尔法克斯水务是北弗吉尼亚州地区领先的水县。它是华盛顿特区大都会区的三大供水商之一。它每天为近171万居民提供2.<>亿加仑的水。它渴望坚持其愿景&#xff0c;即保持以客户为中心&#xff0c;同时帮助维持该地区的高质量生活和经济状况。 挑战 由于桌面系统&…

Druid-排查conditionDoubleConstAllow配置问题(double const condition)

Druid-排查conditionDoubleConstAllow配置问题(double const condition) 报错信息 Caused by: java.sql.SQLException: sql injection violation, dbType postgresql, druid-version 1.2.18, double const condition : SELECT * FROM test where 11 AND TRUE AND TRUE关键词&…