【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之systemV共享内存方式

《Linux之systemV共享内存方式》:本文主要介绍Linux之systemV共享内存方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、工作原理二、系统调用接口1、申请共享内存(一)key的获取(二)共享内存的申请2、将共享内存段连接到进程地址空间3、将

快速修复一个Panic的Linux内核的技巧

《快速修复一个Panic的Linux内核的技巧》Linux系统中运行了不当的mkinitcpio操作导致内核文件不能正常工作,重启的时候,内核启动中止于Panic状态,该怎么解决这个问题呢?下面我们就... 感谢China编程(www.chinasem.cn)网友 鸢一雨音 的投稿写这篇文章是有原因的。为了配置完

Python如何精准判断某个进程是否在运行

《Python如何精准判断某个进程是否在运行》这篇文章主要为大家详细介绍了Python如何精准判断某个进程是否在运行,本文为大家整理了3种方法并进行了对比,有需要的小伙伴可以跟随小编一起学习一下... 目录一、为什么需要判断进程是否存在二、方法1:用psutil库(推荐)三、方法2:用os.system调用

Linux命令之firewalld的用法

《Linux命令之firewalld的用法》:本文主要介绍Linux命令之firewalld的用法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux命令之firewalld1、程序包2、启动firewalld3、配置文件4、firewalld规则定义的九大

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Linux之计划任务和调度命令at/cron详解

《Linux之计划任务和调度命令at/cron详解》:本文主要介绍Linux之计划任务和调度命令at/cron的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录linux计划任务和调度命令at/cron一、计划任务二、命令{at}介绍三、命令语法及功能 :at

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

kali linux 无法登录root的问题及解决方法

《kalilinux无法登录root的问题及解决方法》:本文主要介绍kalilinux无法登录root的问题及解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,... 目录kali linux 无法登录root1、问题描述1.1、本地登录root1.2、ssh远程登录root2、

Linux ls命令操作详解

《Linuxls命令操作详解》通过ls命令,我们可以查看指定目录下的文件和子目录,并结合不同的选项获取详细的文件信息,如权限、大小、修改时间等,:本文主要介绍Linuxls命令详解,需要的朋友可... 目录1. 命令简介2. 命令的基本语法和用法2.1 语法格式2.2 使用示例2.2.1 列出当前目录下的文