【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号手册中的介绍。

NAMEpipe, pipe2 - create pipeSYNOPSIS#include <unistd.h>int pipe(int pipefd[2]);
RETURN VALUEOn 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.关闭不要的fdclose(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.关闭不要的fdclose(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 #设置管道的权限
    

    系统调用:

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

    NAMEunlink - delete a name and possibly the file it refers toSYNOPSIS#include <unistd.h>int unlink(const char *pathname);
    RETURN VALUEOn 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容器,用以组织ChildProcvector<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.关闭不要的fdclose(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,用以存放父进程创建一个子进程时,已经拥有的写端fdvector<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存入wfdswfds.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

    NAMEshmget - allocates a System V shared memory segmentSYNOPSIS#include <sys/ipc.h>#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);RETURN VALUEOn 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 VALUEOn 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 VALUEOn success shmdt() returns 0; on error -1 is returned, and errno is set to indicate the cause of the error.
    

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

  5. 删除共享内存

    NAMEshmctl - System V shared memory controlSYNOPSIS#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 1class 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.mzph.cn/news/2341.shtml

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

相关文章

详细解析张雪峰老师对计算机专业的评价“进可攻,退可守”--【职场篇】

文章目录 张雪峰的评价计算机行业类的总结性指示就业面宽本科也不太卷的方向进可攻&#xff0c;退可守另一个就业出口--培训高校&#xff0c;大专&#xff0c;高职&#xff0c;科研机构&#xff0c;中小学计算机老师等等 就业总结导图持续学习&#xff0c;技术过人 总结 张雪峰…

你认为大数据的特点是什么?_光点科技

随着信息技术的迅猛发展&#xff0c;大数据已成为当今社会不可忽视的重要资源。它是指规模庞大且快速增长的数据集合&#xff0c;其中包含着宝贵的信息和见解。大数据的特点是多样而复杂的&#xff0c;它们塑造了我们的世界并深刻地影响着各行各业。 巨大的规模&#xff1a;大数…

消息重试框架 Spring-Retry 和 Guava-Retry

一 重试框架之Spring-Retry 1.Spring-Retry的普通使用方式 2.Spring-Retry的注解使用方式 二 重试框架之Guava-Retry 总结 图片 一 重试框架之Spring-Retry Spring Retry 为 Spring 应用程序提供了声明性重试支持。它用于Spring批处理、Spring集成、Apache Hadoop(等等)。…

Maven -- <dependencyManagement>管理子项目版本

背景&#xff1a; 一个旧项目&#xff0c;想使用mybatis-plus&#xff0c;想着这是比较基础的依赖包&#xff0c;就在父项目中添加对应依赖&#xff0c;如下: <!-- 依赖声明 --><dependencyManagement><dependencies><!-- mybatis-plus 依赖配置 -->&l…

Java Arrays类

Arrays类 介绍 用于管理或操作数组(比如排序和搜索) 常用方法 1、Arrays.toString(ints)&#xff1a;返回数组的字符串形式 int[] ints {1, 2, 3, 4, 5}; System.out.println(Arrays.toString(ints));2、sort排序(自然排序和定制排序) import java.util.Arrays; import …

python实现接口压力测试

python实现接口压力测试 直接上代码&#xff1a; # -*- coding: utf-8 -*-import json import requests import logginglogging.basicConfig(levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__)restime …

LayUI之增删改查

目录 一、前言 1.1 前言 1.2 前端代码(数据表格组件) 1.3 封装JS 二、LayUI增删改查的后台代码 2.1 编写Dao方法 2.1 增加 2.2 删除 2.3 修改 三、LayUI增删改查的前端代码 3.1 增加 一、前言 1.1 前言 上一篇文章我们一起做了LayUI的动态添加选项卡&#xff0c;这一篇…

IP库新增多种颜色转换空间IP

颜色空间转换是图像及视频中常用的解决方案&#xff0c;涉及hsv-rgb、rgb-ycrcb等一些常见的颜色空间互相转换&#xff0c;今天带来几种常见的颜色空间转换IP&#xff0c;主要如下&#xff1a; IP库简介 一直想做一个可以供大家学习、使用的开源IP库&#xff0c;类似OpenCores&…

基于单片机快递柜的设计与实现

功能介绍 以51单片机作为主控系统&#xff1b;液晶显示当前信息&#xff0c;最多可存储几十个&#xff1b;按下存储按键液晶显示当前快递柜剩余数量&#xff1b;继电器打开&#xff0c;表示用来放物品&#xff1b;正次按下存储按键将取消存快递&#xff0c;继电器关闭快递柜可用…

“探索图像处理的奥秘:使用Python和OpenCV进行图像和视频处理“

1、上传图片移除背景后下载。在线抠图软件_图片去除背景 | remove.bg – remove.bg 2、对下载的图片放大2倍。ClipDrop - Image upscaler 3、对放大后的下载照片进行编辑。 4、使用deepfacelive进行换脸。 1&#xff09;将第三步的照片复制到指定文件夹。C:\myApp\deepfakeliv…

Proxmox VE 为 Windows 虚拟机添加硬盘遇到的问题

环境&#xff1a;PVE 8.x、Windows 11/Windows Server 2019 &#x1f449;问题一&#xff1a; 为 windows 虚拟机添加磁盘&#xff0c;重启虚拟机后&#xff08;在 windows 系统中重启&#xff09;磁盘未能生效&#xff0c;并显示为橘色。 ❗橘色 意味需要重启VM才能生效&…

如何应对客户报价要求过低的情况?这些方案帮你化解危机!

有个客户在寄样品之前让报价&#xff0c;并且要求承诺价格必须低于15美金&#xff0c;业务员同意了&#xff0c;让客户把样板安排寄到中国&#xff0c;但是收到样品后发现客户的样品在侧面还有一块突出的部分&#xff0c;所以15美金太低了&#xff0c;无论如何也得18美金&#…

【数据挖掘】如何为可视化准备数据

一、说明 想要开始您的下一个数据可视化项目吗&#xff1f;首先与数据清理友好。数据清理是任何数据管道中的重要步骤&#xff0c;可将原始的“脏”数据输入转换为更可靠、相关和简洁的数据输入。诸如Tableau Prep或Alteryx之类的数据准备工具就是为此目的而创建的&#xff0c;…

网络虚拟化相关的Linux接口介绍

Linux拥有丰富的网络虚拟化功能&#xff0c;能被虚拟机&#xff0c;容器还有云网络使用。在这篇文章中&#xff0c;我会给出所有通用网络虚拟化接口的简要介绍。没有代码分析&#xff0c;只有简短的接口介绍和在Linux上的使用操作。这系列接口都可以使用ip link命令实现。 这篇…

微信原生实现一个简易的图片上传功能

一、实现原理 wx.showActionSheet()&#xff1a;显示操作菜单&#xff0c;选择是从相册选择还是相机拍摄照片wx.chooseImage()&#xff1a;从本地相册选择图片或使用相机拍照。wx.uploadFile()&#xff1a;将本地资源上传到服务器。客户端发起一个 HTTPS POST 请求&#xff0c…

项目实战Qt网盘系统

背景&#xff1a;随着时代的发展&#xff0c;业务数据量的剧增及移动办公需求&#xff0c;人们对内存的需求越来越强&#xff0c;传统的存储产品&#xff0c;在容量及携带型日益不能满足人工的工作需求&#xff0c;网盘再此背景下应运而生。网盘是能够提供文件同步&#xff0c;…

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数)

回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数) 文章目录 回归预测 | MATLAB实现GRU(门控循环单元)多输入单输出(不调用工具箱函数)预测效果基本介绍程序设计参考资料 预测效果 基本介绍 GRU神经网络是LSTM神经网络的一种变体&#xff0c;LSTM 神经网 …

Ubuntu 考虑采用新的 “统一默认安装 (unified default install)”

导读Ubuntu安装程序中的 “最小化安装” (Minimal installation) 是该发行版多年来最受欢迎的功能之一。 当用户选择 Ubuntu 的 “最小化安装” 选项时&#xff0c;可以在安装更少的预装应用程序情况下&#xff0c;获得完整、功能齐全的 Ubuntu 系统。 但这个功能可能要被砍掉…

个人博客系统(二)

该博客系统共有八个页面,即注册页面、登录页面、添加文章页面、修改文章页面、我的博客列表页面、主页、查看文章详情页面、个人中心页面。 1 注册页面 该页面如图所示: 首先,要先判断注册的用户名、密码、确认密码以及验证码是否为空,若有一个为空,点击提交,则会提醒 …

文献阅读:MathPrompter: Mathematical Reasoning using Large Language Models

文献阅读&#xff1a;MathPrompter: Mathematical Reasoning using Large Language Models 1. 内容简介2. 方法细节3. 实验内容4. 结论&思考 文献链接&#xff1a;https://arxiv.org/abs/2303.05398 1. 内容简介 这篇文章是今年3月份的时候微软提出的一篇工作&#xff0…