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

文章目录

  • 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…

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

跨网络的通信过程、路由的作用以及默认网关

如下网络拓扑图&#xff0c;交换机0所在的网段为192.168.1.0/24&#xff0c;交换机1所在网段为192.168.2.0/24&#xff0c;且各自有2台主机&#xff1a; 假设PC0&#xff08;192.168.1.10/32&#xff09;要跟PC4&#xff08;192.168.2.11/32&#xff09;通信&#xff0c;如何实…

基于 chinese-roberta-wwm-ext 微调训练 6 分类情感分析模型

一、模型和数据集介绍 1.1 预训练模型 chinese-roberta-wwm-ext 是基于 RoBERTa 架构下开发&#xff0c;其中 wwm 代表 Whole Word Masking&#xff0c;即对整个词进行掩码处理&#xff0c;通过这种方式&#xff0c;模型能够更好地理解上下文和语义关联&#xff0c;提高中文文…

DuiLib中的list控件以及ListContainerElement控件

文章目录 前言1、创建list控件2、创建 ListContainerElement 元素&#xff0c;并添加到 List 控件中,这里的ListContainerElement用xml来表示3、在 ListContainerElement 元素中添加子控件 1、List控件2、ListContainerElement控件 前言 在 Duilib 中&#xff0c;List 控件用于…

Python 集合 add()函数使用详解,集合添加元素

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 add函数使用详解 1、元素的顺序2、可以添加的元素类型3、添加重复的元素4、一次只…

Python爬虫学习笔记:1688商品详情API 开发API接口文档

1688API接口是阿里巴巴集团推出的一种开放平台&#xff0c;提供了丰富的数据接口、转换工具以及开发资源&#xff0c;为开发者提供了通用的应用接口及大量数据资源&#xff0c;支持开发者在1688上进行商品搜索、订单管理、交易报表及物流等方面的操作。 1688API接口主要包含以…

Unity游戏源码分享-单车骑行游戏

Unity游戏源码分享-单车骑行游戏 项目地址&#xff1a;https://download.csdn.net/download/Highning0007/88057717

MySQL之DML和DDL

1、显示所有职工的基本信息&#xff1a; 2、查询所有职工所属部门的部门号&#xff0c;不显示重复的部门号。 3、求出所有职工的人数。 4、列出最高工和最低工资。 5、列出职工的平均工资和总工资。 6、创建一个只有职工号、姓名和参加工作的新表&#xff0c;名为工作日期表。 …

react报错信息

报错信息 render函数里dom不能直接展示obj对象 取变量记得要有{} https://segmentfault.com/q/1010000009619339 这样在写的时候就已经执行方法了&#xff0c;所以此处用箭头函数&#xff08;&#xff09;》{}才会在点击时执行或者 遍历数据使用map来遍历&#xff0c;使用forea…

TCP和UDP的区别

连接&#xff1a;TCP 是面向连接的传输层协议&#xff0c;传输数据前先要建立连接&#xff1b;UDP 是不需要连接&#xff0c;即刻传输数据。首部开销&#xff1a;TCP 首部长度较长&#xff0c;首部在没有使用「选项」字段时是 20 个字节&#xff0c;如果使用了「选项」字段则会…

概率论的学习和整理17:EXCEL的各种期望,方差的公式

目录 1 总结 1.1 本文目标总结方法 1.2 总结一些中间关键函数 2 均值和期望 2.1 求均值的公式 2.2 求随机变量期望的公式 2.3 求随机变量期望的朴素公式 3 方差 3.1 确定数的方差 3.2 统计数的方差公式 3.3 随机变量的方差公式 3.4 EXCEL提供的直接计算方差的公式 …

CentOS目录详解

在centos中&#xff0c;最顶层的目录称作根目录&#xff0c; 用/表示。/目录下用户可以再创建目录&#xff0c;但是有一些目录随着系统创建就已经存在&#xff0c;接下来重点介绍几个常用目录。 /bin&#xff08;binary&#xff09;包含了许多所有用户都可以访问的可执行文件&a…

PostgreSQL MVCC的弊端优化方案

我们之前的博客文章“我们最讨厌的 PostgreSQL 部分”讨论了大家最喜欢的 DBMS 多版本并发控制 (MVCC) 实现所带来的问题。其中包括版本复制、表膨胀、索引维护和真空管理。本文将探讨针对每个问题优化 PostgreSQL 的方法。 尽管 PostgreSQL 的 MVCC 实现是 Oracle 和 MySQL 等…

如何在Appium中使用AI定位

当我们在写自动化测试脚本的时候&#xff0c;传统情况下一定要知道元素的属性&#xff0c;如id、name、class等。那么通过AI的方式定位元素可能就不需要知道元素的属性&#xff0c;评价人对元素的判断来定位&#xff0c;比如&#xff0c;看到一个搜索框&#xff0c;直接使用ai:…

【无标题】使用html2canvas和jspdf生成的pdf在不同大小的屏幕下文字大小不一样

问题&#xff1a;使用html2canvas和jspdf生成的pdf在不同大小的屏幕下文字大小不一样&#xff0c;在mac下&#xff0c;一切正常&#xff0c;看起来很舒服&#xff0c;但是当我把页面放在扩展屏幕下&#xff08;27寸&#xff09;&#xff0c;再生成一个pdf&#xff0c;虽然排版一…

【算法基础:数据结构】2.3 并查集

文章目录 并查集算法原理&#xff08;重要&#xff01;⭐&#xff09; 经典例题836. 合并集合&#xff08;重要&#xff01;模板&#xff01;⭐&#xff09;837. 连通块中点的数量&#xff08;维护连通块大小的并查集&#xff09;240. 食物链&#xff08;维护额外信息的并查集&…

【AI绘画】AI绘画乐趣:稳定增强扩散技术展现

目录 前言一、Stable Diffusion是什么&#xff1f;二、安装stable-diffusion-webui1. python安装2. 下载模型3. 开始安装&#xff1a;4. 汉化&#xff1a;5. 模型使用&#xff1a;6. 下载新模型&#xff1a;7. 基础玩法 三、总结 前言 本文将借助stable-diffusion-webui项目来…

【数据结构与算法】哈夫曼编码(最优二叉树)实现

哈夫曼编码 等长编码&#xff1a;占的位置一样 变长编码&#xff08;不等长编码&#xff09;&#xff1a;经常使用的编码比较短&#xff0c;不常用的比较短 最优&#xff1a;总长度最短 最优的要求&#xff1a;占用空间尽可能短&#xff0c;不占用多余空间&#xff0c;且不…

网络版计算器

本次我们实现一个服务器版本的简单的计算器&#xff0c;通过自己在应用层定制协议来将数据传输出去。 协议代码 此处我们为了解耦&#xff0c;采用了两个类&#xff0c;分别表示客户端的请求和服务端的响应。 Request class Request { public:Request(){}Request(int x, int…

Unity 任意数据在Scene窗口Debug

任意数据在Scene窗口Debug &#x1f354;效果&#x1f96a;食用方法 &#x1f354;效果 如下所示可以很方便的把需要Debug的数据绘制到Scene中&#xff08;普通的Editor脚本只能够对MonoBehaviour进行Debug&#xff09; &#x1f96a;食用方法 &#x1f4a1;. 新建脚本继承Z…

实例018 类似windows xp的程序界面

实例说明 在Windows XP环境下打开控制面板&#xff0c;会发现左侧的导航界面很实用。双击展开按钮&#xff0c;导航栏功能显示出来&#xff0c;双击收缩按钮&#xff0c;导航按钮收缩。下面通过实例介绍此种主窗体的设计方法。运行本例&#xff0c;效果如图1.18所示。 ​编辑…