[Linux]进程间通信-共享内存与消息队列

server/2025/1/7 18:25:43/

目录

一、共享内存

1.共享内存的原理

2.共享内存的接口

命令行

创建共享内存

共享内存的挂接

去掉挂接

共享内存的控制

3.共享内存的使用代码

Comm.hpp--封装了操作接口

客户端--写入端

 服务器--读取端

4.管道实现共享内存的同步机制

二、消息队列

1.底层原理

2.使用接口

创建消息队列

发送消息

接收消息 

消息队列的控制


一、共享内存

1.共享内存的原理

        共享内存是进程间通信中最快的一个形式了,对于管道来说是复用了文件系统的代码,而共享内存(System V)是操作系统单独设计的一个模块用来实现进程间通信。

        对于进程来说通信的前提条件就是不同的进程要看到同一份资源空间,共享内存的实现则是操作系统在内存中单独使用了一块空间用于进程之间的通信。因为是在物理内存中的空间,所以如果进程想要使用的话,还是要通过页表的映射到虚拟地址空间当中,映射的位置在栈区和堆区之间的共享区内。

        一个计算机中,不可能只有两个进程进行通信,也不可能之有两个进程使用共享内存进行通信,那么对于多个共享内存,每个共享内存是哪些进程在使用,权限是什么等等都需要操作系统直到,所以还是先组织在描述的方式,使用结构体将共享内存描述起来,并使用链表等数据结构把多个共享内存管理起来。使用引用计数计数,当没有进程使用共享内存的时候在释放共享内存。

        那么当一个进程创建了共享内存之后,如何保证其他想要通信的进程能够找到该共享区呢?就需要提供一个唯一的标识符key标志一个共享内存区域。两个进程在创建共享区的时候,会约定一个key,一个进程用接口创建好共享内存之后,把这个key放入操作系统管理共享内存的结构体中,另一个进程通过key向操作系统索要该共享内存的起始地址,操作系统通过遍历共享内存链表找到该key值的共享内存,然后将起始地址返回给进程。这样两个进程都获取到了该共享内存的起始地址,就访问到了一份资源,就实现了进程间的通信了。

2.共享内存的接口

命令行

ipcs -m     查看系统中的System V共享内存

ipcrm -m  XX(shmid) 用于释放共享内存

创建共享内存

头文件  <sys/ipc.h>  <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);

        该函数用来创建或获取共享内存的标识符。这里的key值就用于表示共享内存段,size则是该共享内存的大小,shmflg是用于设置共享内存段的权限和其他属性。成功返回标识符,错误返回-1同时错误码被设置。

        key字段:这个可以是用户自定义的非零整数,但是这样的话,很容易重复,我们最好是让系统帮我们生成一个唯一的不重复是最好的选择。系统为我们提供了ftok函数,会根据传递的路径名和项目标识符来生成一个唯一的键值key。

头文件  <sys/type.h>  <sys/ipc.h>

key_t ftok(const char* pathname, int proj_id);

        size字段:共享内存的大小尽量设置为4096的整数倍,因为底层是以4096byte为单位进行共享内存空间的申请,如果传递的是4097byte会为该共享内存申请两个4096byte大小的空间的。

        shmfig字段:当为IPC_CREAT时,表示如果共享内存段不存在就创建,如果存在就获取这个共享内存段的标识符,当为IPC_EXCL时不单独使用,通常配合IPC_CREAT,表示如果不存在就创建一个共享内存,该内存段已经存在了就返回-1,用于确保创建一个新的,唯一的共享内存段。而且可以设置共享内存的操作权限,和设置文件的权限类似,可以采用8进制传参。

共享内存的挂接

void *shmat(int shmid, const void* shmaddr, int shmflg);

        该函数时用于将共享内存段连接到进程的地址空间当中,使进程可以访问当这块区域。成功会返回一个指向共享内存段的指针,用于访问共享内存,但是返回的是void*类型,和malloc一样需要强转后再使用。错误返回(void*)-1,同时错误码被设置,如果错误码是EACCES表示权限不足,EINVAL表示参数无效。

        shmid是shmget函数返回的共享内存段标识符,用于确定要访问哪一个共享内存段。shmaddr指定共享内存连接到进程虚拟地址空间的起始地址,通常设置为nullptr让系统帮我们确定该起始地址,方位我们设置的地址有冲突。shmflg表示用于控制内存段的连接方式,可以设置读、写等权限,但是需要本身该共享内存就要有这种权限才可以,否则会出错,通常设置为0即可。

去掉挂接

int shmdt(const void* shmaddr); 

        用于将共享内存段和共享区之间的映射断开,传递的参数就是只需要是共享内存的起始地址就可以,因为系统直到共享内存的大小等各种字段,所以也就会根据大小,删除页表部分映射关系就实现了去掉映射的操作。

共享内存的控制

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

        这个函数就像是共享内存段的 “控制面板”,可以对共享内存段进行各种管理操作,如获取共享内存段的状态信息、修改其属性或者标记它为删除等。shmid还是共享内存唯一标识字段,cmd是一个命令码,用于指定对共享内存段要执行的操作。buf则是根据cmd的不同而设置不同的值。

        cmd与buf字段:当cmd为IPC_STAT时,表示获取共享内存段的当前状态信息,此时buf就必须传递一个struct shmid_ds结构体用来接收;当cmd为IPC_SET时用于设置共享内存段的属性,所以要将修改的属性设置到buf中,传递给系统,让操作系统去修改共享内存的属性;而当cmd为IPC_RMID的时候,则表示删除该共享内存,不是解除映射,而是真正的删除物理内存中的共享内存,所以不用传递buf了,设置为nullptr即可。而且共享内存的声明周期是跟随内核的,所以我们必须手动释放才可以。

3.共享内存的使用代码

Comm.hpp--封装了操作接口
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>//项目路径和工程id
const std::string pathname = "./Comm.hpp";
const int proj_id = 0x11223344;
//共享内存大小
const int size = 4096;
//管道文件名称
const std::string filename = "fifo_file";//创建key值
key_t GetKey()
{key_t key = ftok(pathname.c_str(), proj_id);if(key < 0){std::cout << "ftok craet key fail" << std::endl;exit(1);}return key;
}int ShmgetHelper(key_t key, int flag)
{int shmid = shmget(key, size, flag);if(shmid < 0){std::cout << "shmget create system v shared memory fail" << std::endl;exit(2);}return shmid;
}//创建共享内存
int CreateShm(key_t key)
{return ShmgetHelper(key, IPC_CREAT | IPC_EXCL | 0666);
}
//获取共享内存
int GetShm(key_t key)
{return ShmgetHelper(key, IPC_CREAT);
}
//创建管道文件
void Makefile()
{int n = mkfifo(filename.c_str(), 0666);if(n < 0){//已经存在了if(errno == EEXIST)return;//真的出错了std::cout << "mkfifo create pipe file fail" << std::endl;exit(3);}
}
客户端--写入端
#include "Comm.hpp"int main()
{//获取key值key_t key = GetKey();//获取共享内存的标识int shmid = GetShm(key);//和共享内存建立映射char* ptr = (char*)shmat(shmid, nullptr, 0);//以写的方式打开命名管道int fd = open(filename.c_str(), O_WRONLY);//写入数据for(char ch = 'a'; ch <= 'z'; ch++){ptr[ch -'a'] = ch;//该字段没有任何作用,只是用来唤醒服务端进行读取操作int code = 1;ssize_t n = write(fd, &code, sizeof(int));if(n< 0){std::cout << "write fail" << std::endl;break;}sleep(1);}//关闭共享内存的映射以及管道文件shmdt(ptr);close(fd);return 0;
}
 服务器--读取端
#include "Comm.hpp"int main()
{//创建命名管道Makefile();//获取key值key_t key = GetKey();//创建共享内存int shmid = CreateShm(key);//建立映射char* ptr = (char*)shmat(shmid, nullptr, 0);//以读的方式打开文件int fd = open(filename.c_str(), O_RDONLY);//进行通信while(true){int code = 0;ssize_t n = read(fd, &code, sizeof(code));if(n < 0){std::cout << "read fail" << std::endl;break;}else if(n == 0){std::cout << "client quit" << std::endl;break;}else{std::cout << ptr << std::endl;}}//关闭文件close(fd);//取消映射shmdt(ptr);//释放共享内存shmctl(shmid, IPC_RMID, nullptr);return 0;
}


4.管道实现共享内存的同步机制

        共享内存是没有任何的同步机制的,所以说读取端不知道写入端什么时候写入了数据,也不知道共享内存中有没有数据,所以为读写两端提供了命名管道的机制,当写入端向共享内存写入的同时会向管道文件写入一个无意义的数字,表示告诉服务器我写入了数据;服务器会一直阻塞式的读取管道文件,如果说客户端写入了,那么管道文件会有数据,read可以读取后,会表明有数据了,服务端就会去访问共享内存了。

二、消息队列

1.底层原理

        (System V)消息队列的底层原理和共享内存基本一致,只是消息队列是在物理内存中维护了一个队列这样的数据结构,而共享内存只是一块空间而已。

        因为是队列的数据结构,写入的数据会以数据块的形式作为队列中的一个一个的元素,而共享内存在读取的时候没办法将共享区中的数据进行分割,只能我们在应用层去分割数据。而且采用这种结构的话,是对数据的分割,就可以记录每个数据块是哪个进程发过来的了。

2.使用接口

        接口的使用和形式上基本上也没有什么太大的区别,这样的话也方便了使用者去记忆。

创建消息队列

        消息队列没有挂载的单独接口,当消息队列被创建出来之后,就会映射到虚拟地址空间当中,所以说该接口也是做了两个工作。

int msgget(key_t key, int msgflg);

发送消息

        因为写入的是结构化的数据,所以要提供接口去写入。

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); 

        msqid用来标识向哪个消息队列写入;msgp通常是用户自定义的一个结构体,结构体内部的成员为一个long类型的字段表示消息的类型和一个任意字段表示存放发送的消息;msgsz是传递消息的长度;msflg用于设置发送操作的属性,通常是0,也可以设置为IPC_NOWAIT就表示非阻塞式的发送数据,如果队列满了,会返回-1,同时错误码被设置为EAGAIN。

接收消息 

int msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

        msgtyp字段:用于指定接收消息的类型,当msgtyp大于0的时候,表示接收的消息类型为msgtyp的消息;msgtyp等于0的时候,表示接收消息队列中的第一个消息,不考虑类型;msgtyp小于0的时候则表示,接收的消息类型为小于或等于msgtyp绝对值的消息中类型值最小的消息。所以说发送消息传递的结构体中的类型并非是标志着int,double,而是用户在功能上自己定义区分了发送的消息。

消息队列的控制

        消息队列没有取消挂载(映射)的概念,只有释放的概念,和共享内存一样cmd为IPC_RMID的时候就是释放消息队列的操作,但是这里并不是真正的时候,而是引用计数减少,只有当最后一个使用该消息队列的进程释放了消息队列的时候,引用计数减少到0,才会真的释放该消息队列。

int msgctl(int msqid, int cmd, struct msqid_ds *buf);


http://www.ppmy.cn/server/156242.html

相关文章

前后端规约

文章目录 引言I 【强制】前后端交互的 API请求内容响应体响应码II 【推荐】MVC响应体III【参考】IV 其他引言 服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护…

安卓漏洞学习(十八):Android加固基本原理

APP加固技术发展历程 APK加固整体思路 加固整体思路&#xff1a;先解压apk文件&#xff0c;取出dex文件&#xff0c;对dex文件进行加密&#xff0c;然后组合壳中的dex文件&#xff08;Android类加载机制&#xff09;&#xff0c;结合之前的apk资源&#xff08;解压apk除dex以外…

el-table行列转换简单版,仅限单行数据

原始数据格式如下&#xff0c;如果不是此格式&#xff0c;请转换成以下格式在进行以下操作 [{ label: name, value: Tom },{ label: age, value: 25 },{ label: country, value: UK } ]代码如下 <template><el-table :data"tableData" style"width: …

探索光耦:光耦在风力发电中的应用——保障绿色能源的高效与安全

在全球能源结构加速向清洁、可再生方向转型的今天&#xff0c;风力发电作为一种绿色能源&#xff0c;已成为各国新能源发展的重要组成部分。然而&#xff0c;风力发电系统在复杂的环境中长时间运行&#xff0c;对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦&#xf…

ESP32学习--SPIFFS文件系统

文件系统SPIFFS学习 本次学习基于 storage/spiffsgen 例程 前言 在嵌入式系统的学习过程中&#xff0c;我们将越来越频繁的需要去和内存打交道&#xff0c;有的是外置存储&#xff0c;有的是内置的存储。当我们需要管理的内存越来越多的时候&#xff0c;再使用简单的地址读…

Spring Boot 3 配置大全系列 —— 如何配置用户的登录与认证?

学会这款 &#x1f525;全新设计的 Java 脚手架 &#xff0c;从此面试不再怕&#xff01; 升级 Spring Boot 3 配置讲解 —— 如何配置用户的登录与认证&#xff1f; 随着 Spring Boot 3 的发布&#xff0c;开发者迎来了许多新特性和改进&#xff0c;尤其是在安全性和用户认证…

题目解析与代码实现:You‘re Given a String

引言 本文将详细解读一道字符串处理题目 “You’re Given a String”&#xff0c;并用 Python 实现该题的解决方案&#xff0c;同时解析其核心算法逻辑。本文适合有一定基础的程序员&#xff0c;希望通过字符串算法提升能力的读者。 1. 题目描述 问题背景 题目给出了一个字符…

安卓11 SysteUI添加按钮以及下拉状态栏的色温调节按钮

最近客户想要做一个台灯产品&#xff0c;需要实现 串口调节台灯功能 &#xff0c;其中包括 亮度调节 色温调节 开关 三个功能 话不多说&#xff0c;贴代码 diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml old mode 100644 new …