Linux | 进程间通信
目录
前言
一、进程间通信的基本概念
二、管道
1、管道的基本概念
2、匿名管道
(1)原理
(2)测试代码
(3)读写控制相关问题
a、读端关闭
b、写端关闭
c、读快写慢
d、读慢些快
(4)进程池代码拓展
3、命名管道
(1)原理
(2)测试代码
三、共享内存
1、共享内存的原理
2、测试代码
3、共享内存的特性
前言
进程间通信的方式有很多,如管道、共享内存、消息队列、信号量、socket套接字等等;本文主要讲解其中管道和共享内存两种方式;
一、进程间通信的基本概念
我们要理解进程间通信呢首先得知道以下三个问题;
进程间通信是什么?
我们为什么要进程间通信?
怎么进行进程间通信?
对于所有知识,我们拥有上述三个问题得答案我们就可彻底弄清进程间通信得本质了;
进程间通信是什么?
进程间通信就是让两个进程,看到同一块空间(内存),以达到我们通过这块共同空间来进程交互得过程;
我们都知道,我们可以通过fork创建子进程,fork创建后得子进程与父进程共享同一块代码;那么我们是否可以通过创建一个创建变量的资源缓冲区来使这两个父子进程看到同一块空间(内存)呢?很不幸,由于我们进程间的独立性,所以我们无法通过全局变量来让两个进程看到同一块空间,并且使用这个空间进行通信,准确来说是可以看到,但是不能进行通信,因为当我们父进程或子进程对这块空间内容进行修改时会发生写时拷贝;这也是保证进程间独立性而产生的机制;也正是由于进程具有独立性,所以我们的进程间通信便没有那么容易;
为什么要进行进程间通信?
在实际开发中,我们可能会有并发编程的需求,而我们的单进程是不具备并发能力的,而有时我们的并发之间的多进程需要一起协同配合,既然需要协同配合就可能需要进行通信,因此我们需要进程间通信;
怎么进行进程间通信?
关于如何进行进程间通信,我们主要有以下几种策略,分别来自于不同的方;
Linux原生方案:管道(匿名管道、命名管道)
System V方案:共享内存、消息队列(不常用)、信号量;
POSIX 方案:socket套接字
上述为一些主流方案,本文主要讲解 管道方案 与 System V 提供的共享内存方案;
二、管道
1、管道的基本概念
首先管道我们在前面学习指令的时候就已经接触过了,只不过我们对其了解并不深刻;当时我们只知道我们可以通过管道将一个指令(进程)的输出结果传输给另一个指令(进程);

1、管道是一个信息传输的媒介!如上图所示,进程A将信息写入管道,而进程B从管道中读取数据;
2、管道是一个文件;我们之前提过,Linux下,一切皆文件的理念,那么我们的管道也应该是一个文件,只不过这个文件是属于内存级文件,不会将数据刷新到缓冲区中,也没有必要刷新到缓冲区中做持久化的动作;
3、管道的文件的通信方式为半双工通信;关于这我们需要补充以下几个概念;
单工通信:只有一个方向的通信,且只有固定的一端作为接收端,一端作为输入端;
半双工通信:通信的双方都可以作为接收端也可以作为输入端,当某一时刻,只能有一个端发送,一端接收;就好像两个人聊天,你说一句,我接收后,再回复一句,你再接收;
全双工通信:通信的双方既可以作为接收端也可以作为输入端,某一个时刻,既可以输入也可以接收;就好像两个人吵架,每个人都可以挺别人说话的同时对别人说话;
2、匿名管道
(1)原理
首先,我们来介绍匿名管道实现进程间通信的原理,再使用匿名管道作为进程间通信的方法之前,我们得确保通信双方进程为父子关系;这是使用匿名管道实现进程间通信得基本前提;
前面我们讲过进程的相关内核数据,与我们创建一个进程会发生什么?以及当我们调用fork会发生什么?有了上述知识铺垫,我们不难想到,当我们使用fork创建子进程时,我们的子进程会创建自己的内核数据,如PCB控制块,虚拟地址空间、页表、文件描述符结构体等内核数据,其中我们还讲过子进程会拷贝父进程内核数据中的某些数据,当我们对这些数据需要进行修改时会发生写时拷贝的现象;那么问题来了,我们的维护当前进程打开的文件的结构体 files_struct 是否也会发生拷贝呢?当然,这是肯定的?因此,我们使用fork时,应该如下图所示;

我们匿名管道实现进程间通信就是基于这一特性---- “子进程会继承父进程的文件描述符数组”;因此我们不妨首先创建一个管道文件,然后父进程分别以读和写的方式打开这个管道文件,接着我们创建子进程,子进程必然会对文件描述符数组的内容进行拷贝,子进程也拥有对该文件读和写这两个文件描述符,此时若我们想让父进程写,子进程读,我们只需要将父进程的读文件描述符关闭,子进程写文件描述符关闭,然后我们再调用系统调用read和write,分别向对应文件描述符读和写即可!这就是我们使用匿名管道的方式实现进程间通信的过程;

(2)测试代码
在正是实现代码之前,我们首先介绍几个与管道相关的接口;
pipe:创建匿名管道

参数:该函数只有一个参数,是一个整型数组,当我们调用该函数时,该函数会为我们创建一个匿名管道文件,并打开这个管道文件其中数组的 0 号下标放的是以读的方式打开该文件的文件描述符,1 号下标方式的以写的方式打开该管道文件的文件描述符;(记忆:0想象成嘴巴,代表读,1想象成笔,代表写);
返回值:若函数调用成功,返回0,调用失败,返回-1,错误码被设置;
有了上面函数的学习,我们就可以写一段简单的基于匿名管道的进程间通信代码了;如下所示;
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{// 1、创建管道int pipefd[2] = {0};int n = pipe(pipefd);if(n == -1){perror("pipe");exit(1);}// 2、创建子进程pid_t id = fork();if(id == 0){// 子进程(读)// 3、建立单向信道// 关闭子进程写端文件描述符close(pipefd[1]);// 4、进行通信char buf[1024]; // 接收缓冲区while(true){// 读取来自父进程的信息ssize_t sz = read(pipefd[0], buf, sizeof(buf));// 这里就不对read进行差错检验了buf[sz] = '\0'; // 我们想要读的是字符串,因此在语言层面得加 \0if(sz > 0){std::cout << "读数据前\n";std::cout << "child[" << getpid() << "], father: " << buf << std::endl;}else if(sz == 0){std::cout << "father stop write, me quit" << std::endl;break;}sleep(1);}exit(0);}// 父进程(写)// 3、建立单向信道close(pipefd[0]); // 关闭读端// 4、进行通信const char* msg = "这是发给子进程的消息 ";char tmp[1024] = {0};int count = 0;while(true){memset(tmp, 0, sizeof(tmp));snprintf(tmp, sizeof(tmp), "%s[%d], %d\n", msg, getpid(), count++);ssize_t sz = write(pipefd[1], tmp, strlen(tmp));sleep(1);}// 5、回收子进程waitpid(id, nullptr, 0);// 6、关闭文件描述符(可关可不关,因为程序快运行结束,OS会自动释放)close(pipefd[1]);return 0;
上述代码是实现了一个父进程写,子进程不断的读的功能;
(3)读写控制相关问题
基于上述代码,我们还要进行更深层次的研究;我们分别测试以下几种情况下会发生什么?
a、读端关闭
我们给上述代码读端设置一个计数器,设置成5秒后,读端退出;观察会发生什么;

我们输入命令行监视脚本指令;如下所示;
while :; do ps -axj | head -1 && ps -axj | grep ./main | grep -v grep; sleep 1; echo "--------------------";done

我们发现,当我们的读端关闭时,写端进程被终止了!也就是被杀掉了!由于上述我们让子进程关闭读端文件描述符后休眠5秒,因此我们的子进程没有退出;我们不难得出结论;
结论:读端关闭时,写端进程被操作系统杀死;
b、写端关闭
同样的道理,我们使用计数器的方式,使写端到一定的时间后关闭,我们观察读端会如何;

运行结果如下所示;

仔细观察,打印了我们在read返回值为0的输出内容;
结论:当我们写端关闭时,读端会读到文件的末尾,返回0;
c、读快写慢
我们将上述代码更改一下;将我们的读端设置为一秒读一次,将我们的写端设置为三秒写一次;再运行代码,看一看会发生什么;

我们会发现,我们的进程三秒才会打印一次;我们的读进程不是一秒读一次吗?那我们的读进程在干嘛呢?
结论:当读快写慢时,读进程会阻塞等待写进程进行写入;
d、读慢些快
我们将上述代码改一下,读进程三秒读一次,而写进程一秒写一次;又会发生什么呢?

我们发现第一次立刻打印了,后面三秒打印一次,且每次都打印了三条内容;这时结果也显而易见了;
结论:读慢写快时,我们的写进程会一直往管道文件里写,直至写满,写满后,写进程会阻塞,直至下一次读进程读取管道文件时,写进程才会被唤醒;
总结:
综上所述,我们使用匿名管道进行进程间通信的本质是我们通过子进程会继承父进程文件描述符数组的特性,使我们的父进程和子进程看到同一个文件,它们可以通过该文件进行通信;在我看来进程间通信的本质是让两个进程看到同一块空间的方式!而通信是根据上层定义的;
匿名管道提供了访问控制,所谓访问控制,就是我们上述讨论a、b、c、d四种不同的方式;并不是所有进程间通信具有访问控制,如我们后面讲的共享内存;
匿名管道的生命周期是随着进程的,一旦进程结束,我们的匿名管道文件也随之销毁;
(4)进程池代码拓展
通过上述知识,我们可以实现一个简单的进程池代码;

具体代码如下所示;
// task.h文件,主要封装任务和任务管理器
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <functional>
#include <mutex>// 定义函数对象类型
using func_t = std::function<void()>;void webRequest()
{std::cout << "正在执行网络请求任务" << std::endl;
}void readMySQL()
{std::cout << "正在执行读取数据库任务" << std::endl;
}void otherTask()
{std::cout << "正在执行其他任务" << std::endl;
}// 设计一个单例类,管理所有任务
class ManageTask
{
public:static ManageTask* getInstance(){// 双检查加锁if(_pm == nullptr) // 提高效率{_m.lock();if(_pm == nullptr) // 保证线程安全{_pm = new ManageTask();}_m.unlock();}return _pm;}// 加载类内成员void load(){// 增加任务描述与命令号的映射_dict.insert({_tasks.size(), "web请求"});// 增加命令_tasks.push_back(webRequest);_dict.insert({_tasks.size(), "读取数据库"});_tasks.push_back(readMySQL);_dict.insert({_tasks.size(), "其它任务"});_tasks.push_back(otherTask);}// 自定义新增任务void add(func_t& cb, std::string& str){_dict.insert({_tasks.size(), str});_tasks.push_back(cb);}// 自定义删除任务(任务号)void del(int command){_dict.erase(command);_tasks.erase(_tasks.begin() + command);}// 展示当前任务void show(){for(auto e : _dict){std::cout << e.first << ": " << e.second << std::endl;}}// 获取任务func_t get_task(int command){return _tasks[command];}// 获取任务个数size_t get_size(){return _tasks.size();}// 获取命令描述std::string get_command(int command){return _dict[command];}
private:ManageTask(){}ManageTask(ManageTask& m){}ManageTask operator=(ManageTask& m){}
private:static ManageTask* _pm;static std::mutex _m;std::vector<func_t> _tasks;std::map<int, std::string> _dict;
};// 初始化单例对象指针和锁
ManageTask* ManageTask::_pm = nullptr;
std::mutex ManageTask::_m;
// main.cc文件,主要实现线程池,以及派发任务等逻辑
#include <iostream>
#include <vector>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "task.hpp"#define NUM 6int main()
{srand(time(nullptr)); // 种下随机数种子ManageTask::getInstance()->load(); // 初始化任务表// 1、创建进程池std::vector<std::pair<pid_t, int>> desc; // 子进程pid与写端fdfor(int i = 0; i < NUM; i++){// 1.1 创建管道文件int pipefd[2] = {0};int n = pipe(pipefd);if(n == -1) {std::cerr << "create pipe fail" << std::endl;exit(1);}// 1.2 创建子进程int id = fork();if(id == 0){usleep(100);// 子进程(读)// 1.3 关闭管道写端close(pipefd[1]);// 1.4 进行读取数据int command = 0;while(true){ssize_t sz = read(pipefd[0], &command, sizeof(command));if(sz != sizeof(int) || command < 0 || command >= ManageTask::getInstance()->get_size()){std::cerr << "读取无效命令" << std::endl;continue;}// 1.5执行命令对应任务ManageTask::getInstance()->get_task(command)();}exit(0);}// 父进程(写)// 1.3 关闭对应读端,并将写端保存起来close(pipefd[0]);desc.push_back(std::pair<pid_t, int>(id, pipefd[1]));}// 2、使用进程池// 2.1 获取菜单while(true){int select = 0;int command = 0;std::cout << "******************************" << std::endl;std::cout << "****** 1、showTask ******" << std::endl;std::cout << "****** 2、execute ******" << std::endl;std::cout << "******************************" << std::endl;// 2.2 从用户获取选择std::cout << "Enter select> ";std::cin >> select;// 2.3 从用户获取命令选项if(select == 1){ManageTask::getInstance()->show();}else if(select == 2){// 2.4 获取命令std::cout << "Enter command> ";std::cin >> command;// 2.5 选择一个子进程执行(随机数实现负载均衡)int proc = rand() % ManageTask::getInstance()->get_size();// 2.6 向指定进程发送命令write(desc[proc].second, &command, sizeof(command));std::cout << "已经成功给" << desc[proc].first << "进程,对应命令:" << ManageTask::getInstance()->get_command(command) << std::endl;}else{std::cerr << "选择有误,请重新选择" << std::endl;continue;}usleep(500);}// 3、关闭进程池// 关闭写端fdfor(int i = 0; i < desc.size(); i++){close(desc[i].second);}// 回收子进程for(int i = 0; i < desc.size(); i++){waitpid(desc[i].first, nullptr, 0);}return 0;
}
3、命名管道
(1)原理
命名管道的原理与匿名管道不同,命名管道则是通过自己创建一个管道文件,然后双方通过打开这个文件实现看到同一块内存资源的功能;命名管道的最大优势在于命名管道可以使不具有血缘关系的两个进程进行通信;原理非常简单,这里就不做过多解释,可通过后面代码来学习命名管道;
(2)测试代码
在正是学习命名管道之前,我们同样先补充一批接口的使用;
首先我们学习一个命令行指令,mkfifo,该指令加上文件名即可创建一个指定名字的管道文件;具体看如下演示;

mkfifo:创建一个管道文件(这是一个函数,与我们上述命令行指令重名而已)

参数一:这个参数为我们要创建管道文件的文件名(默认在当前目录下创建文件)
参数二:这个参数为我们创建管道文件的权限,这个权限会与我们默认权限掩码进行计算最终权限,计算规则(我们设置权限 & (~默认权限));
返回值:若调用成功则返回0,失败则返回-1,且错误码被设置;
unlink:删除一个文件,与我们命令行下的rm指令功能一致;

参数:要删除文件的路径
返回值:若调用成功,则返回0,失败则返回-1,错误码被设置;
有了上述的知识铺垫,我们可以很容易的写出一个命名管道通信的程序,如下所示;
// comm.hpp文件主要存放公共代码,如管道文件名
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>// 管道文件名
#define PipeName "FIFO.ipc"
// pipeServer.cc文件主要作为接收端,该端要创建管道文件,运行结束后要删除管道文件
#include "comm.hpp"int main()
{umask(0); // 设置创建文件临时的权限掩码// 1、创建管道文件int n = mkfifo(PipeName, 0666);if(n == -1){perror("mkfifo");exit(1);}// 2、打开管道文件int fd = open(PipeName, O_RDONLY, 0666);if(fd == -1){perror("open");exit(2);}// 3、通信char buf[1024];while(true){ssize_t sz = read(fd, buf, sizeof(buf) - 1);buf[sz] = '\0'; // 去掉默认换行if(strcmp(buf, "quit") == 0){std::cout << "client quit, me too" << std::endl;break;}std::cout << "client# " << buf << std::endl;}// 4、关闭管道文件close(fd);// 5、删除管道文件unlink(PipeName);return 0;
}
// pipeClient.cc文件作为通信的发送端实现,主要向服务端发送请求
#include "comm.hpp"int main()
{// 1、打开管道文件int fd = open(PipeName, O_WRONLY, 0666);if(fd == -1){perror("open");exit(2);}// 2、通信std::string buf;while(true){std::getline(std::cin, buf);write(fd, buf.c_str(), buf.size());}// 3、关闭管道文件close(fd);return 0;
}
我们发现,只要我们将管道文件创建好,其他的就像我们操作普通文件一样简单;命名管道的使用比匿名管道会容易很多;
注意:这里有一个细节,在我们使用管道文件时,若我们服务端使用open打开管道文件时,此时若我们的客户端没有调用open函数打开管道文件,我们的服务端会一只卡在open函数内,直至我们客户端也使用open函数打开管道文件,这时我们的服务端open函数才会返回文件描述符;这个细节在我们后面共享内存的一份代码中有一定重要的作用;
三、共享内存
1、共享内存的原理
之前在讲解进程地址空间时,我们曾经讲过,堆栈中间有一块共享区,我们之前的动态库就是会被映射到这块共享区中;而今天我们的主角,共享内存的原理也与这块空间有关;进程间通信的本质就是看到同一块内存空间,而我们共享内存实现进程间通信的方案就是我们首先在内存中申请一块空间,然后将我们需要进程间通信的进程与这块物理空间进行关联,映射到自己进程地址空间的共享区中;这样便可以实现进程间通信了;如下图所示;

这样进程间通信就只需要往自己的进程地址空间的某个位置写入读取即可;
2、测试代码
在使用共享内存实现进程间通信前,我们需要学习下面几个函数接口;
shmget:申请一块共享内存空间并返回对应的shmid / 获取一块共享内存空间的shmid;

参数一:通过key值获取或者申请一块共享内存空间,每块共享内存空间的key值不同;
参数二:申请 / 获取共享内存空间的大小
参数三:标志位,通常是由 IPC_CREAT 或 IPC_EXCL 这两个宏加上权限组成;
返回值:若函数调用成功则返回 shmid,这个也就是共享内存的句柄,与fd类似;若调用失败,则返回-1,错误码被设置;
注意:
1、关于参数三,我们若想获取某个key对应的shmid,我们直接填0即可;若某个key对应的共享内存空间不存在,我们需要创建,则我们一般会填 IPC_CREAT | IPC_EXCL | 0666;其中0666为共享内存空间的访问权限;
IPC_CREAT:若共享内存空间不存在,则创建之;
IPC_EXCL:通常配合上面IPC_CREAT使用,若共享空间存在,则报错返回-1;因此这两个选项配合使用可以保证获得到的共享内存是一个新创建的共享内存!
2、关于上述的参数一key,我们可以通过下面这个函数来获取;
ftok:通过路径和项目id生成唯一key值;

参数一:项目路径,这里可以随便填写一个;
参数二:项目id,这里也可以自己设置一个;
返回值:若函数调用成功,则返回key值,若失败,则返回-1,错误码被设置;
shmat:是共享内存空间与当前虚拟地址空间进行相关联,建立映射关系;

参数一:shmid值,之前我们在shmget中获取的 id;
参数二:我们想要与哪一块虚拟地址空间绑定,建立映射关系;这里通常填NULL,表示让OS系统随机分配一块空间建立映射关系;
参数三:一般设置为0,可不关心;
返回值:若调用成功,则返回与共享内存建立映射的虚拟地址,若调用使用,则返回-1,错误码被设置,注意这里的-1被强制装换成了 void* 类型;
shmdt:将共享内存与我们的共享内存去关联,与我们的shmat相对应;

参数一:要去关联的共享内存的地址,也就是shmat的返回值;
返回值:若函数调用成功,则返回0,若失败,则返回-1,错误码被设置;
shmctrl:共享内存的控制

参数一:shmid值;
参数二:这里有三个宏来控制这个函数的行为,我们一般选择IPC_RMID,表示我们要删除这块共享内存空间;
参数三:填NULL即可;
返回值:若调用成功则返回0,若失败则返回-1,错误码被设置;
学习上述的四个函数我们就可以完成进程间通信了,上面四个函数基本囊括了使用共享内存进行进程间通信的整个过程,首先调用ftok获取key值,然后再通过shmget 创建共享内存 / 获取shmid,然后通过shmat 与当前进程的虚拟地址空间进行关联,接着就可以开始进行通信了,通信完毕后,我们使用 shmdt 将虚拟地址与共享内存去关联;最后我们使用shmctl删掉申请的共享内存空间;
接着我们需要学习一些命令行来获取共享内存相关信息,我们可以通过 ipcs -m 来获取当前机器的共享内存申请使用情况;使用 ipcrm -m shmid 来删除指定的共享内存;下面为我们使用共享内存进行进程间通信的代码;
// comm.hpp 文件用于保存一些共享代码#include <iostream>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm"
// 形成key所需项目id
#define proj_id 0x13
// 共享内存大小
#define SIZE 4096
// shmServer.cc 文件实现服务端接收客户端发送信息,其中服务端承担创建共享内存,删除共享内存的任务#include "comm.hpp"int main()
{// 1、生成唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success\n";// 2、创建新的共享空间int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 3、与共享空间相关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout << "shmat success\n";// 4、通信(读)while(true){printf("%s\n", shmaddr);if(shmaddr[0] == 'z')break;sleep(1);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout << "shm rm success\n";return 0;
}
// shmClient.cc 文件用于客户端发送信息给服务端,其中仅需对共享内存关联,通信、去关联等操作即可#include "comm.hpp"int main()
{// 1、获取唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}// 2、获取共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("shmget");exit(2);}// 3、关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");exit(3);}// 4、通信(写)char ch = 'a';while(ch <= 'z'){shmaddr[0] = ch;ch++;sleep(1);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}return 0;
}
我们编译运行服务端代码,如下所示;


确实,我们的服务端一直读取数据,可是读取到的数据为空,此时我们并没有运行客户端,且客户端也不可能进行输出发送;可我们依旧可以读取数据,只不过数据为空罢了,这就与我们的管道通信有了本质的区别,共享内存的进程间通信并没有访问控制!
接着我们通过ipcs -m 查找我们创建的共享内存,确实存在,其shmid为25,key就是我们通过 ftok 生成的key,owner就是共享内存的拥有者,也就是当前用户,perms就是这块共享内存的访问权限,我们设置成了 666,4096就是我们申请这块共享内存的大小,nattach就是关联到这块共享内存空间的进程数,status就是这块共享内存的状态;
我们接着启动客户端;如下所示;

我们发现服务端已经收到客户端发来的信息,并且我们的nattach的数量也由1变成了2,随后当客户端发送完26个英文字母后退出,此时服务端也读取到了字符z,两个进行相继推出,nattach也由2变成了0,共享内存被删除;
前面的实验,我们也不难发现,我们的共享内存不具备访问控制,那我们要是想使用共享内存实现类似管道的访问控制是否可以做到呢?其实也不难,我们可以通过管道来实现,如下面的代码;
// comm.hpp 文件#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/stat.h>
#include <sys/shm.h>
#include <unistd.h>
#include <fcntl.h>// 形成key所需路径
#define pathName "/home/zsw/linuxCode/shm/shm_pipe"
// 形成key所需项目id
#define proj_id 0x14
// 共享内存大小
#define SIZE 4096
// 管道文件名
#define pipeName "./fifo.ipc"// 负责创建与销毁管道文件
class Init
{
public:Init(){umask(0);int n = mkfifo(pipeName, 0666);if(n == -1){perror("mkfifo");exit(5);}}~Init(){unlink(pipeName);}
};// 通过read阻塞来形成等待效果
void wait(int fd)
{int tmp = 1;ssize_t sz = read(fd, &tmp, sizeof(int));if(sz != sizeof(tmp)){std::cerr << "fd: " << fd << " 等待错误\n";printf("sz:%d, sizeof(tmp):%d\n", sz, sizeof(tmp));perror("wait");exit(6);}
}// 通过write唤醒等待进程
void signal(int fd)
{int tmp = 1;ssize_t sz = write(fd, &tmp, sizeof(tmp));if(sz != sizeof(int)){std::cerr << "write err\n";exit(7);}
}
// shmServer.cc#include "comm.hpp"int main()
{Init init; // 创建管道文件// 打开管道文件int fd = open(pipeName, O_RDONLY, 0666);// 1、生成唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success, k:" << k << std::endl;// 2、创建新的共享空间int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666); // 创建一个新的共享内存,k存在,则创建失败if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 3、与共享空间相关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");// 退出前释放共享空间shmctl(shmid, IPC_RMID, nullptr);exit(3);}std::cout << "shmat success\n";sleep(5);// 打开管道文件int fd = open(pipeName, O_RDONLY, 0666);// 4、通信(读)while(true){wait(fd);printf("%s\n", shmaddr);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";// 6、释放共享空间shmctl(shmid, IPC_RMID, nullptr);std::cout << "shm rm success\n";return 0;
}
// shmClient.cc文件#include "comm.hpp"int main()
{// 小坑:不能在这打开管道文件,因为打开管道文件有一个特性,如果对端不打开,这里将会一直阻塞// 然后在后续过程中,若客户端进程先执行,下面shmget函数可能出现文件未创建的错误,因为客户端// 执行的比服务端快,服务端还未创建共享内存,而客户端就想访问了// int fd = open(pipeName, O_WRONLY, 0666);// 1、获取唯一keyint k = ftok(pathName, proj_id);if(k == -1){perror("ftok");exit(1);}std::cout << "ftok success, k:" << k << std::endl;// 2、获取共享内存int shmid = shmget(k, SIZE, 0);if(shmid == -1){perror("shmget");exit(2);}std::cout << "shmget success\n";// 打开管道文件int fd = open(pipeName, O_WRONLY, 0666);// 3、关联char* shmaddr = (char*)shmat(shmid, nullptr, 0);if(*(int*)shmaddr == -1){perror("shmat");exit(3);}std::cout << "shmat success\n";// 4、通信(写)std::string msg;while(true){std::cout << "Enter message> ";getline(std::cin, msg);strcpy(shmaddr, msg.c_str());signal(fd);}// 5、去关联int n = shmdt(shmaddr);if(n == -1){perror("shmdt");exit(4);}std::cout << "shmdt success\n";return 0;
}
这段代码就有我们前面所说管道的一个性质,并且,这里有一个小坑;正常情况下,我们一般先运行服务端代码,因为我要保证客户端代码在获取 shmid 时,已经被创建了,因此在测试代码一(不加管道代码)时,我们总是先运行服务端代码;

如上图所示,若我们将打开管道文件的代码放到最上面,也就是最先执行,此时,当我们的服务端打开管道文件时,由于对端未打开管道文件,因此,我们会阻塞住,此时我们接着运行客户端代码,由于客户端打开管道文件时,由于对端已经在等待了,因此可以直接返回管道文件对应文件描述符,此时若我们的客户端继续运行,当调用shmget获取shmid时,由于key所对应的共享内存并未创建,因此我们的客户端会直接运行失败;
3、共享内存的特性
1、共享内存仅需内存级读写即可,与管道不同,使用管道需要调用系统调用read、write等;而共享内存仅需往指定虚拟地址空间写入、读取即可;
2、共享内存是最快的通信方式,因为拷贝次数最少;写入最少拷贝仅需从键盘文件对应缓冲区拷贝到共享内存中即可,写入仅需从共性内存中拷贝到要写入的缓冲区即可;
相关文章:
Linux | 进程间通信
目录 前言 一、进程间通信的基本概念 二、管道 1、管道的基本概念 2、匿名管道 (1)原理 (2)测试代码 (3)读写控制相关问题 a、读端关闭 b、写端关闭 c、读快写慢 d、读慢些快 (4&a…...
Vue.js正式环境中配置多个请求的URL
在Vue.js中,你可以在正式环境中配置多个请求的URL,通常使用一些配置文件或者环境变量的方式。下面是一种常见的配置方式: 1. 创建配置文件:在项目的根目录下,创建一个配置文件,比如可以是config.js&#x…...
简单的 UDP 网络程序
文章目录: 简单的UDP网络程序服务端创建套接字服务端绑定启动服务器udp客户端本地测试INADDR_ANY 地址转换函数关于 inet_ntoa 简单的UDP网络程序 服务端创建套接字 我们将服务端封装为一个类,当定义一个服务器对象之后,需要立即进行初始化…...
人工智能-深度学习之文本预处理
文本预处理 对于序列数据处理问题, 这样的数据存在许多种形式,文本是最常见例子之一。 例如,一篇文章可以被简单地看作一串单词序列,甚至是一串字符序列。 本节中,我们将解析文本的常见预处理步骤。 这些步骤通常包括…...
【Java 进阶篇】插上翅膀:JQuery 插件机制详解
在前端开发中,JQuery 作为一个广泛应用的 JavaScript 库,为开发者提供了丰富的工具和方法,简化了 DOM 操作、事件处理等繁琐的任务。而在这个庞大的生态系统中,插件机制是 JQuery 的一项重要特性,使得开发者能够轻松地…...
手动编译GDB
手动编译GDB 起因在于使用Clang-14编译C文件并生成调试信息,使用gdb调试时报DWARF相关错误。经检查原因在于虚拟机为Ubuntu 20.04,使用apt下载时官方提供gdb版本为9.2,不支持DWARF5,而Clang-14生成的调试信息是DWARF5版本的。为解决该问题,手…...
竞赛选题 深度学习花卉识别 - python 机器视觉 opencv
文章目录 0 前言1 项目背景2 花卉识别的基本原理3 算法实现3.1 预处理3.2 特征提取和选择3.3 分类器设计和决策3.4 卷积神经网络基本原理 4 算法实现4.1 花卉图像数据4.2 模块组成 5 项目执行结果6 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 &a…...
替换SlowFast中Detectron2为Yolov8
一 需求 FaceBookReserch中SlowFast源码中检测框是用Detectron2进行目标检测,本文想实现用yolov8替换detectron2二 实施方案 首先,yolov8 支持有自定义库ultralytics(仅支持yolov8),安装对应库 pip install ultraly…...
轻量化网络--MobileNet V1
文章目录 depth-wise separable convolutions普通卷积depthwise conconvolutionspointwise convolutions网络结构进一步分析网络训练方式两个重要的超参数Width Multiplier: Thinner ModelsResolution Multiplier: Reduced Representation实验结果消融实验细粒度,高分辨率识别…...
gittee启动器
前言 很多小伙伴反馈不是使用gitee,不会寻找好的项目,在拿到一个项目不知道从哪里入手。 鼠鼠我呀就是宠粉,中嘞,老乡。整!!! git的基本指令 在使用gitee的时候呢,我们只需要记住…...
Spark数据倾斜_产生原因及定位处理办法_生产环境
在最近的项目中,历史和实时数据进行关联平滑时出现了数据倾斜,产生了笛卡尔积,具体现象如下:运行内存175GB,核数64,运行代码时,查看SparkUI界面的active jobs ,数据输入是1G…...
2023OceanBase年度发布会后,有感
很荣幸收到了OceanBase邀请,于本周四(11月16日)参加了OceanBase年度发布会并参加了DBA老友会,按照理论应该我昨天(星期五)就回到成都了,最迟今天白天就该把文章写出来了,奈何媳妇儿买…...
ubuntu18.04中代码迁移到20.04报错
一、 PCL库,Eigen库报错,如: /usr/include/pcl-1.10/pcl/point_types.h:903:29: error: ‘enable_if_t’ in namespace ‘std’ does not name a template type; did you mean ‘enable_if’?/usr/include/pcl-1.10/pcl/point_types.h:698:…...
QQ五毛项目记
问题与挑战:某公司为了实现某马总造福全人类,红旗插遍全球的宏伟目标,为应对后续用户激增的问题。特别安排了一次针对全体用户的秒杀活动:于XXXX年XX月XX日XX时XX分XX秒开始的秒杀五毛钱一百个QQ币的活动。每个账户仅限一次&#…...
小程序实现登录持久化
小程序实现登录持久化需要使用到小程序的缓存API,例如wx.getStorageSync()和wx.setStorageSync()等方法。以下是一个简单的代码实现: // App.js App({ // 在全局的App.js中定义全局变量userInfo,用于存放用户信息 globalData: { userInfo: …...
2023年亚太杯数学建模思路 - 案例:ID3-决策树分类算法
文章目录 0 赛题思路1 算法介绍2 FP树表示法3 构建FP树4 实现代码 建模资料 0 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 1 算法介绍 FP-Tree算法全称是FrequentPattern Tree算法,就是频繁模…...
C复习-输入输出函数+流
参考: 里科《C和指针》 perror 定义在stdio.h中。当一个库函数失败时,库函数会在一个外部整型变量errno(在errno.h中定义)中保存错误代码,然后传递给用户程序,此时使用perror,会在打印msg后再打…...
duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113
duplicate复制数据库单个数据文件复制失败报错rman-03009 ora-03113 搭建dg过程中,发现有一个数据文件在复制过程中没有复制过来,在备库数据文件目录找不到这个数据文件 处理方法: 第一步:主库备份86#数据文件 C:\Users\Admi…...
golang 解析oracle 数据文件头
package mainimport ("encoding/binary""fmt""io""os" ) // Powered by 黄林杰 15658655447 // Usered for parser oracle datafile header block 1 .... // oracle 数据文件头块解析 // KCBlockStruct represents the structure of t…...
van-popup滑动卡顿并且在有时候在ios上经常性滑动卡顿的情况
解决”pc端页面可以滚动,移动端手势无法滚动“问题的一次经历 - 掘金 <van-popup v-model"studentclassShow" :lock-scroll"false" position"bottom" style"z-index: 3000" :style"{ height: 55% }"><d…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
测试微信模版消息推送
进入“开发接口管理”--“公众平台测试账号”,无需申请公众账号、可在测试账号中体验并测试微信公众平台所有高级接口。 获取access_token: 自定义模版消息: 关注测试号:扫二维码关注测试号。 发送模版消息: import requests da…...
React 第五十五节 Router 中 useAsyncError的使用详解
前言 useAsyncError 是 React Router v6.4 引入的一个钩子,用于处理异步操作(如数据加载)中的错误。下面我将详细解释其用途并提供代码示例。 一、useAsyncError 用途 处理异步错误:捕获在 loader 或 action 中发生的异步错误替…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
七、数据库的完整性
七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
