【Linux修行路】进程通信——共享内存

2024-08-31 13:12

本文主要是介绍【Linux修行路】进程通信——共享内存,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

⛳️推荐

一、直接原理

1.1 共享内存的的申请

1.2 共享内存的释放

二、代码演示

2.1 shmget

2.1.1 详谈key——ftok

2.2 创建共享内存样例代码

2.3 获取共享内存——进一步封装

2.4 共享内存挂接——shmat

2.5 共享内存去关联——shmdt

2.6 释放共享内存——shmctl

2.7 开始通信

2.7.1 processb 基础代码编写

2.7.2 通信代码编写

三、共享内存的特点

3.1 共享内存 VS 管道

四、拓展内容

4.1 查看共享内存的属性

4.2 借助管道实现共享内存的同步与互斥


⛳️推荐

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站【Linux修行路】动静态库详解点击跳转到网站

一、直接原理

1.1 共享内存的的申请

共享的原理和动态库的共享原理一致,共享内存的申请主要分为以下三步:

1.2 共享内存的释放

去关联,释放共享内存。申请、挂接、去关联、释放这些动作都是由操作系统来做的,进程不能自己去做,进程中可以通过 malloc 去申请空间,但是因为进程独立性的存在,一个进程自己 malloc 申请的空间,只属于当前进程,不能由多个进程共享。

  • 操作系统在物理内存上申请一块空间。

  • 将申请到的空间,通过页表挂接到进程地址空间的共享区。

  • 返回起始虚拟地址,供程序中使用。

系统中可能同时有多组进程 都需要通信,因此系统中可能存在多个共享内存,所以操作系统要把这多个共享内存管理起来。先描述,再组织,操作系统中一定有一个内核结构体是用来描述共享内存的。

二、代码演示

2.1 shmget

shmget 函数用来申请一块共享内存。

image-20240306100932350

  • key:一个数字,是几不重要,关键在于它必须在内核中具有唯一性,能够让不同的共享内存具有唯一性标识。
  • size:创建共享内存的大小,单位是字节。一般建议是4096的整数倍,如果传的是4097,操作系统实际上申请的空间大小是 4096*2,虽然操作系统多申请了,但是多的部分用户不能使用,用了会被判定为越界。
  • shmflg:标记位,常用选项有 IPC_CREAT :如果申请的共享内存不存在,就创建,存在,就获取并返回。IPC_EXCL :如果申请的共享内存存在,就出错返回。IPC_CREAT | IPC_EXCL :如果申请的共享内存不存在,就创建,存在就出错返回。这俩选项一起使用保证了,如果我们申请成功了一个共享内存,这个共享内存一定是一个新的。IPC_EXCL 不单独使用。其次,共享内存的权限也通过这个标志位进行传递。
  • 返回值:创建成功,返回共享内存标识符;创建失败,返回-1。

2.1.1 详谈key——ftok

无论是创建共享内存还是使用共享内存,都需要调用该函数。第一个进程可以通过 key 创建共享内存,第二个之后的进程,只需要拿着同一个 key 就可以和第一个进程看到同一个共享内存。key 在共享内存的描述对象中,第一次创建共享内存的时候,就必须有一个 key 了。使用 ftok 函数生成一个 key

image-20240306105116360

  • pathname:路径名。
  • proj_id:项目 ID。
  • 返回值:生成成功 key 被返回;生成失败 -1 被返回(路径名如果不存在的话是有可能生成失败的)。

只要这两个参数一样,那么两个进程就可以得到同一个 key 值。

**key值为什么是通过用户传参来生成的,而不是操作系统直接生成的?**因为操作系统不知道哪两个进程需要通信,假设 A 进程和 B 进程进行通信,在 A 进程中操作系统随机生成了一个 key 给 A 进程,此时 B 进程也需要知道 key 值,但是操作系统是不知道 A 进程要和 B 进程进行通信,所以操作系统没办法将这个 key 值交给 B 进程,只有程序员(写代码的人)才知道 A、B 进程之间需要通信。其实 ftok 函数相当于是两个通信进程之间的一种约定,只要它们约定好同一个 pathname 和 proj_id,那么这两个进程就能得到同一个 key

2.2 创建共享内存样例代码

#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <string>
#include "log.hpp"
#include <string.h>
#include <errno.h>
using namespace std;const int size = 4096;
const string path_name = "/home/wcy";
const int proj_id = 0x6666;
Log log;key_t GetKey() // 获取 key
{key_t k = ftok(path_name.c_str(), proj_id);if(k < 0){// 获取 key 失败log(Fatal, "ftok error: %s", strerror(errno));exit(1);}log(Info, "ftok sucess, key is: %d", k);return k;
}int GetShareMem() // 创建共享内存
{key_t key = GetKey();int shmid = shmget(key, size, IPC_CREAT|IPC_EXCL);if(shmid < 0){log(Fatal, "creat share memeory error: %s", strerror(errno));exit(2);}log(Info, "creat share memory success, shmid is: %d", shmid);return shmid;
}#endif

key 和 shmid:

key 是在操作系统内部来唯一标识一块共享内存,是给操作系统来使用的;shmid 是给进程使用的,用来表示资源的唯一性。虽然共享内存属于文件系统,但是 shmid 和文件描述符的兼容性做的并不好,共享内存给自己单独设置了一个类似于文件描述符表的东西。

**共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一致存在。**只有内核重启或者用户主动释放,共享内存才会被释放。

查看操作系统中所有的共享内存:ipcs -m

image-20240306113648742

  • perms:权限位
  • nattch:和当前共享内存关联的进程个数。

命令行中删除共享内存:ipcrm -m shmid。指令是由用户输入的的,在用户层统一使用 shmid

2.3 获取共享内存——进一步封装

int GetShareMemHelper(int flag)
{key_t key = GetKey();int shmid = shmget(key, size, flag);if(shmid < 0){log(Fatal, "creat share memeory error: %s", strerror(errno));exit(2);}log(Info, "creat share memory success, shmid is: %d", shmid);return shmid;
}// 创建共享内存
int CreatMem()
{return GetShareMemHelper(IPC_CREAT|IPC_EXCL|0666);
}// 获取共享内存
int GetMem()
{return GetShareMemHelper(IPC_CREAT); // 这里也可以传0
} 

2.4 共享内存挂接——shmat

shmat:将某个共享内存挂接到当前进程的地址空间中。

image-20240306130457459

  • shmid:共享内存的标识符。
  • shmaddr:指向挂接在地址空间的什么位置,因为我们也不知道挂接在什么位置,所以一般设为 nullptr 即可。
  • shmflg:设置当前进程对该共享内存的权限,一般设置成0,表示采用共享内存自身的权限。
  • **返回值:**共享内存挂接在地址空间中的地址。
// processa
#include "comm.hpp"
#include <unistd.h>int main()
{// 创建共享内存int shmid = CreatMem();log(Debug, "creat sharemem done...");sleep(5);// 挂接共享内存char *sharmem = (char*)shmat(shmid, nullptr, 0);log(Debug, "%d attch success", shmid);sleep(5);log(Debug, "processa quit...");return 0;
}

2.5 共享内存去关联——shmdt

shmdt:去掉某个共享内存与当前进程的关联。

image-20240306132053723

  • shmaddr:就是 shmat 函数返回的那个地址。
// processa
#include "comm.hpp"
#include <unistd.h>int main()
{// 创建共享内存int shmid = CreatMem();log(Debug, "creat sharemem done...");sleep(5);// 挂接共享内存char *shamem = (char*)shmat(shmid, nullptr, 0);log(Debug, "%d attch done, to 0x%x", shmid, shamem);sleep(5);// 去关联shmdt(shamem);log(Debug, "%d deattch done", shmid);log(Debug, "processa quit...");return 0;
}

2.6 释放共享内存——shmctl

shmctl:用来释放一个共享内存。

image-20240306133608142

  • cmd:操作选项。IPC_STAT:将内核中共享内存的属性拷贝到 buf 里面。IPC_RMID:删除共享内存。
  • struct shmid_ds *buf:语言层面用来描述一个共享内存的结构体,里面保存了共享内存的部分属性。
  • **返回值:**如果操作是 IPC_RMID,那么删除成功返回0,失败返回-1。
// processa
#include "comm.hpp"
#include <unistd.h>int main()
{// 创建共享内存int shmid = CreatMem();log(Debug, "creat sharemem done...");sleep(5);// 挂接共享内存char *shamem = (char*)shmat(shmid, nullptr, 0);log(Debug, "sharemem %d attch done, to 0x%x", shmid, shamem);sleep(5);// 去关联shmdt(shamem);log(Debug, "sharemem %d deattch done", shmid);sleep(5);// 释放共享内存int ret = shmctl(shmid, IPC_RMID, NULL);if(ret < 0)log(Debug, "sharemem delete error: %s", strerror(errno));elselog(Debug, "sharemem delete success...");sleep(5);log(Debug, "processa quit...");return 0;
}

2.7 开始通信

2.7.1 processb 基础代码编写
#include "comm.hpp"
#include <unistd.h>int main()
{// 获取共享内存int shmid = GetMem();log(Debug, "Get sharemem done...");sleep(5);// 挂接char *shmaddr = (char*)shmat(shmid, nullptr, 0);log(Debug, "sharemem %d attch done, to 0x%x", shmid, shmaddr);sleep(5);// 去关联shmdt(shmaddr);log(Debug, "sharemem %d deattch done", shmid);return 0;
}
2.7.2 通信代码编写

processa:

#include "comm.hpp"
#include <unistd.h>int main()
{// 创建共享内存int shmid = CreatMem();// 挂接共享内存char *shamem = (char*)shmat(shmid, nullptr, 0);// ipc-cod 通信代码while(true){cout << "client asy@ " << shamem << endl; // 直接访问共享内存sleep(1);}// 去关联shmdt(shamem);// 释放共享内存int ret = shmctl(shmid, IPC_RMID, NULL);return 0;
}

processb:

#include "comm.hpp"
#include <unistd.h>int main()
{// 获取共享内存int shmid = GetMem();// 挂接char *shmaddr = (char*)shmat(shmid, nullptr, 0);// ipc-code 通信代码while(true){cout << "Please enter:";fgets(shmaddr, size, stdin);}// 去关联shmdt(shmaddr);return 0;
}

一旦有了共享内存,并且挂接到当前进程的地址空间上了,在程序中就把它当做该进程自己的内存空间来使用即可,无需再调用系统调用。一旦有人把数据写入到共享内存,其实我们立马就能看到,不需要经过系统调用,就能直接看到数据。可以把共享内存就当做用户自己 malloc 出来的一块空间。

三、共享内存的特点

  • 共享内存没有同步与互斥之类的保护机制,即写端没有向共享内存中写入,读端可以正常读取。

  • 共享内存是所有的进程间通信中,速度最快的。原因在于数据拷贝次数少。

  • 共享内存中的数据,完全是由用户自己维护,操作系统不会帮我们做清空工作。

3.1 共享内存 VS 管道

管道通信中:数据要拷贝两次。主要原因在于,管道本质上是文件,我们不能通过键盘直接往文件里面进行写入,要想让键盘输入的内容写入的文件中,首先需要在程序中定义一个字符数组(或者 string 对象),暂时存储键盘的输入,然后在将这个数组中的内容写入到文件,这就涉及一次拷贝(将数组中的内容,拷贝到文件缓冲区),其次另一端在进行读取的时候,所有的文件读取操作,都要求定义一段空间,将读取到的内容存储起来,这个过程又会涉及一次拷贝,总体算下来,完成一次管道通信,需要进行两次拷贝。

共享内存通信:程序中可以把共享内存当做自己的内存空间来使用,因此对于写端,可以直接从键盘读取数据存储到共享内存中,无需创建字符数组(或者 string 对象)来暂时存储输入的内容;对于读端,可以直接从共享内存中进行读取,然后打印,在没有特殊要求的情况可以不用将共享内存中的数据存储起来。

四、拓展内容

4.1 查看共享内存的属性

通过 shmctl 函数区获取共享内存的属性。struct shmid_ds 结构体就是用户层面去描述一个共享内存的结构体。

image-20240306151122027

int main()
{// 创建共享内存int shmid = CreatMem();struct shmid_ds shmds; // 用来存储共享内存的属性// 挂接共享内存char *shamem = (char*)shmat(shmid, nullptr, 0);// ipc-cod 通信代码while(true){cout << "client asy@ " << shamem << endl; // 直接访问共享内存// 打印共享内存的属性shmctl(shmid, IPC_STAT, &shmds);// cout << "__key: " << shmds.shm_perm.__key << endl;printf("0x%x\n", shmds.shm_perm.__key);cout << "shm_atime: " << shmds.shm_atime << endl;cout << "shm_cpid: " << shmds.shm_cpid << endl;cout << "shm_nattch: " << shmds.shm_nattch << endl;sleep(1);}// 去关联shmdt(shamem);// 释放共享内存int ret = shmctl(shmid, IPC_RMID, NULL);return 0;
}

4.2 借助管道实现共享内存的同步与互斥

processa:

#include "comm.hpp"
#include <unistd.h>int main()
{// 创建共享内存int shmid = CreatMem();Init init; // 创建有名管道// 打开管道int fd = open(FIFO_FILE, O_RDONLY);if (fd < 0){perror("open");exit(FIFO_OPEN_ERR);}struct shmid_ds shmds;// 挂接共享内存char *shamem = (char *)shmat(shmid, nullptr, 0);// ipc-cod 通信代码while (true){char ch;int n = read(fd, &ch, sizeof(ch));if (n == 0)break;else if (n > 0){cout << "client asy@ " << shamem; //<< endl; // 直接访问共享内存}else{log(Fatal, "read error: %s\n", strerror(errno));exit(FIFO_READ_ERR);}}// 去关联shmdt(shamem);// 释放共享内存int ret = shmctl(shmid, IPC_RMID, NULL);// 关闭管道close(fd);return 0;
}

processb:

#include "comm.hpp"
#include <unistd.h>int main()
{// 获取共享内存int shmid = GetMem();// 打开管道int fd = open(FIFO_FILE, O_WRONLY);if(fd < 0){perror("open");exit(FIFO_OPEN_ERR);}// 挂接char *shmaddr = (char*)shmat(shmid, nullptr, 0);// ipc-code 通信代码while(true){cout << "Please enter:";fgets(shmaddr, size, stdin);write(fd, "c", 1);}// 去关联shmdt(shmaddr);// 关闭管道close(fd);return 0;
}

在没有管道的情况下,processa 进程一直在不间断的读取共享内存中的数据,现在创建一个管道,在 processa 进程读取共享内存之前,想让它从管道中读取,只有读到了特定的信号,才能去共享内存中进行读取。processb 进程在向共享内存中写入数据之后,向管道中写入一个字符,以此为信号,通知 processa 进程,现在共享内存中有数据了,你可以去读取了。在 processb 进程没有向共享内存中写入数据的时候,此时管道为空,读端就会阻塞,也就是 processa 进程就会被阻塞住,以此来实现同步与互斥。

🎁结语:

        今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是我前进的动力!

这篇关于【Linux修行路】进程通信——共享内存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1124002

相关文章

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍1、W25Q64简介2、硬件电路3、W25Q64框图4、Flash操作注意事项软件SPI读写W2