Linux进程间通信——探索共享内存—— 剖析原理, 学习接口应用

**前言:**本节内容主要讲解进程间通信的, systemV版本下的共享内存。 共享内存,顾名思义, 其实就是一块内存, 它不同于管道是一个文件。 所以它的传输速度是很快的。 因为管道是文件,有缓冲区, 而共享内存只是一块内存, 可以直接与进程挂接, 直接拷贝。

ps:本节内容适合了解一些管道知识的友友们进行观看哦

目录

进程间通信的本质

共享内存的原理

共享内存相关接口

shmget------创建共享内存

shmflg

key

shmat------进程挂接

​编辑

shmdt------进程去关联

指令

[ipcs -m](#ipcs -m)

应用

准备文件

准备makefile

Com.hpp

GetKey------封装获取key值的函数

CreatShareMemory------获取共享内存

CreatShm和GetShm------获取共享内存

processa.cpp

processb.cpp

运行程序

key和shimd的区别

共享内存的生命周期


进程间通信的本质

进程间通信的本质就是------先让不同的进程, 看到同一份资源。(这是为了给通信奠定前提条件)

共享内存的原理

上面的这一张图, 其实就是叫做共享内存。 因为共享内存的本质, 就是两个进程能够看到同一份资源, 而上面, 就做到了两个进程看到同一份资源。 其中左边是一个进程的PCB, 然后右边是一个进程的PCB。 同时这两个PCB都有它们对应的虚拟地址空间然后物理内存中有一块共享资源, 两个进程都可以通过页表的映射看到这份共享资源。------ 这就是共享内存。

对于上面这一张图来说, 我们的第一步就是申请内存。 第二步然后就是将申请好的内存分别挂接到两个进程的进程地址空间, 然后返回我们的地址空间的地址即可。但是这是申请共享内存的流程。 如果我们要释放共享内存呢?

我们申请是由操作系统在内存中开辟一段地址空间, 然后把虚拟地址到物理地址之间的映射关系在页表中一填, 最后再挂接。 那么共享内存就创建好了。 ------而释放空间内存其实就是反过来, 一定是如何创建的, 那么就如何释放的。 ------所以第一步就是让进程和共享内存去关联 , 就是将页表之中虚拟地址和物理地址的映射关系去掉然后就释放共享内存

现在, 有一个问题就是------上面的操作是由进程直接做的吗? 答案是不是的, 进程没有资格直接访问共享内存的, 必须由操作系统来做

那么我们就能看到一个典型的现象。 ------我们的进程需要通信, 然后操作系统就帮进程建立信道。------这里进程就是需求方, 而操作系统就是执行方。 进程解决为什么我们要建立共享内存操作系统解决 的是如何建立共享内存 。 那么就意味着我们的操作系统是必须要给进程提供系统调用!!!

那么,我们的操作系统当中有着各种通信, 所以一定有着非常多的共享内存, 所以操作系统要不要将共享内存管理起来呢? ------答案是需要的, 而管理的方式就是先描述再组织。 也就是说, 一定会有内核数据结构来描述共享内存

共享内存相关接口

shmget------创建共享内存

上面的shmget函数,就是创建共享内存的接口。 使用它需要包含sys/ipc.h、sys/shm.h头文件。 同时三个参数, 一个返回值int 类型。 ------其中,size和返回值比较好说, size就是我们要创建的共享内存的大小, 返回值int就是共享内存标识符。 key和shmflg是没有办法一下子说清的, 下面我们来谈一谈这两个参数的相关问题:

shmflg

shmflag是创建的共享内存的模式, 这里说两种常用的使用选项:

IPC_CREAT(单独使用)如果你申请的共享内存, 不存在, 就创建, 存在就获取并返回。

IPC_CREAT | IPC_EXCL如果你申请的共享内存不存在, 就创建, 存在就出错返回。 ------这个选项就可以确保如果我们申请内存成功了, 这个内存一定是新的!!!(IPC_EXCL不单独使用!)

key

在谈key之前我们先思考一个问题, 就是对于一块共享内存, 我们如何知道他存不存在呢 ? 或者说我们怎么保证让不同的进程看到同一个共享内存呢?------带着这两个问题, 我们来谈key:

1、key是一个数字, 这个数字是几, 不重要, 关键是它必须在内核种具有唯一性, 能够让不同进程进行唯一性标识。

2、第一个进程可以根据key创建共享内存, 第二个之后的进程, 他们只要拿着同一个key, 就可以和第一个进程看到同一个共享内存了!!!

3、对于一个创建好的共享内存, key在哪?------这个可以直接挑明, 其实就在共享内存的描述对象中!!!

4、命名管道是通过路径 + 文件名来确定唯一的管道的!!!而我们的key, key类似于路径, 同样具有唯一性。共享内存就是通过key来确定唯一性!!!
现在有一个问题, 第一次创建共享内存的时候的key, 是如何有的呢?

这个函数, 就是用来在第一次创建共享内存时, 用来生成key的函数。 所以,key不是直接定义的!!!
上面的图中, key是如何创建出来的呢? 我们通过上面的蓝色框框其实可以看到, 这个函数是通过转化路径名和一个项目标识符来生成一个key

那么问题来了, 我们的key能不能随便定义一个出来呢? ------理论上****是可以的 , 因为我们两个进程看到同一个资源, 本质上就是需要拿着同一把钥匙 , 而key就是这一把钥匙。 ------所以我们不需要把ftok想的太复杂, 这个函数当生成key的时候不需要去系统中查找哪个key使用过, 哪个没有使用过。 而是类似于哈希函数, 使用了一套算法, pathname和proj_id进行数值计算即可!!

那么,为什么不能让操作系统自己去生成呢?为什么要让用户自己去设置呢? ------虽然操作系统做这些工作很简单, 但是如果操作系统给我们生成了这么一个key, 我们进行通信的时候, 我们怎么把这个key交给另一个进程呢? ------这里可不可以直接将返回的key交给另一个进程呢这个不可以的, 因为我们现在要解决的就是进程间通信的问题, 而想要将key交给另一个进程, 需要进程通信, 这就陷入了一个死循环。 所以, 我们的key, 就势必不能由操作系统自己决定。 ------这个也就说明, 我们的ftok, 与其说是用户自己指定的key, 不如说是用户之间约定的key!!!只要有两个用户, 他们同时使用进程, 同时使用进程, 使用同一个路径, 同一个id, 那么他们就能够进行通信!!!

shmat------进程挂接

什么是进程挂接?我们要访问一个共享内存, 虽然共享内存是操作系统的, 虽然我们使用shmid只是获得了共享内存的编号。 但是既然是内存, 那么如果我们操作系统提供一个系统接口, 通过shmid找到目标的共享内存, 然后让这个共享内存和进程的地址空间通过页表建立了联系。 那么我们的进程, 是不是就相当于能够访问这块共享内存了? 而这个过程, 就叫做挂接!!!

**而一个共享内存可以有多个进程和它挂接起来, 这个数量, 就是nattch, 我们如何观察到这个nattch, 可以使用ipcs -m指令。**ps:讲解在指令部分

shmdt------进程去关联

有进程挂接, 那么就有进程去关联, 也就是下面的shmdt, 传送的参数是shmat返回的地址空间的地址。

这里我们思考的是我们只有这个共享内存的起始地址, 这个函数是如何知道我们要取消关联的共享内存有多大呢? ------这个问题和realloc, free等函数类似, 他们都是只需要传送首地址, 不需要传送大小或者末尾地址, 这就说明了一定有我们在用户层看不到的东西, 在管理着我们的内存!!!

并且, 我们的去关联的流程就是------根据页表找到我们的物理内存, 把页表清掉, 根据inode属性, 让物理内存减减, 如果要释放, 就按照属性里面的大小进行释放即可!

指令

ipcs -m

ipcs -m可以查看一个共享内存的各个属性:

  • shimd:共享内存的编号。
  • perms:共享内存的权限。
  • bytes:共享内存的大小, 这个大小是以4096唯一个单位的, 也就是说, 虽然我们上面显示我们创建了4097个大小的共享内存, 但其实操作系统给我们开辟的是8k多字节。 从c语言的角度, 这部分多申请的空间叫做cookie。 ------实际上, 我们c语言在申请堆空间的时候,要不要管理所谓的堆空间呢?------答案是肯定要的, 所以就要先描述再组织, 所以我们c语言多申请的那部分空间, 也有对应的属性!!!
  • nattch:代表了该共享内存的挂接数量。

应用

准备文件

ps:Log.hpp我们在之前的文章已经实现过了, 所以这里直接拿来用了, 不会再实现一遍, 不会的友友可以去前面的文章看一看:linux进程间通信------学习与应用命名管道, 日志程序的使用与实现-CSDN博客

准备makefile

有两个cpp文件需要编译。所以需要.PHONY, 然后编译两个文件。 最后clean, 删除两个文件。

cpp 复制代码
.PHONY:all
all:processa.exe processb.exe 

processa.exe:processa.cpp
	g++ -o $@ $^ -std=c++11
processb.exe:processb.cpp 
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean: 
		rm -f processa.exe processb.exe

Com.hpp

GetKey------封装获取key值的函数

我们知道ftok函数的第一个参数是路径, 第二个参数是项目id。 我们就可以先定义两个常量------pathname和proj_id;然后, 我们又可以创建一个log函数, 让log函数来打印日志; ftok需要包含头文件sys/ipc.h以及sys/types.h。 同时其他的string, cstring, cerrno, 等等都包含进来需要用到。------然后其中的pathname其实就是传给ftok的路径函数, 其中的proj_id就是项目id。 通过这两个我们就可以获得一个key值。 用来创建共享内存。

cpp 复制代码
#pragma once

#include"Log.hpp"
#include<cerrno>
#include<cstring>
#include<string>
#include<iostream>

#include<sys/ipc.h>
#include<sys/types.h>

using namespace std;

const string pathname = "/home/_mian_yang";
const int proj_id = 0x88881; 

Log log;

//获取key
key_t GetKey()
{
	key_t k = ftok(pathname.c_str(), proj_id);   //ftok拿到k函数
	if (k == -1)  //如果k == -1, 那么就拿到k失败。
	{
		log(Fatal, "ftok error: %s", strerror(errno));  //打印错误信息
		exit(1);
	}
	log(Info, "ftok success! Key is: #d", k);  //创建k成功, 返回k
	return k; 

}

CreatShareMemory------获取共享内存

这是第二层封装, 封装了GetKey函数。 用来获取共享内存。 外层函数传递flag, 也就是贡献内存的打开方式------这个打开方式就是用来区分共享内存共享内存是已经创建好的还是第一次创建的。在本次实验中, 我们prcessa进程用来第一次创建共享内存, 而processb用来获取已经创建好的共享内存。 那么这两个进程内部调用CreatShareMemory的时候就要传递不同的flag: processa进程传递IPC_CREAT | IPC_EXCL | 权限, processb传递IPC_CREAT

另外, 因为shmget创建共享内存的函数需要传递一个size用来规定共享内存的大小, 所以, 我们可以在上面创建一个size常量。 另外使用shmget也需要包含一下sys/shm.h头文件

cpp 复制代码
//
#pragma once

#include"Log.hpp"
#include<cerrno>
#include<cstring>
#include<iostream>
#include<sys/ipc.h>
#include<sys/types.h>
#include<string>
#include<sys/shm.h>
using namespace std;

const int size = 4096;   //4kb
const string pathname = "/home/_mian_yang";
const int proj_id = 0x88881; 
Log log;


//获取key
key_t GetKey()
{
	key_t k = ftok(pathname.c_str(), proj_id);   //ftok拿到k函数
	if (k == -1)  //如果k == -1, 那么就拿到k失败。
	{
		log(Fatal, "ftok error: %s", strerror(errno));  //打印错误信息
		exit(1);
	}
	log(Info, "ftok success! Key is: #d", k);  //创建k成功, 返回k
	return k; 

}


int CreatShareMemory(int flag)
{
	key_t key = GetKey(); //获取key
	int shmid = shmget(key, size, flag);
	if (shmid == -1)
	{
		log(Fatal, "creat share memory error: %s", strerror(errno)); //创建共享内存失败, 打印错误消息
		exit(1);
	}
	log(Info, "creat share memory is success! shmget is: %d", shmid); //创建共享内存成功, 打印共享内存的的shmid
	return shmid;
}

CreatShm和GetShm------获取共享内存

cpp 复制代码
int GreatShm()
{
	return GreatShareMemory(IPC_CREAT | IPC_EXCL | 0666); //第一次创建需要判断是否存在
	
}

int GetShm()
{
	return GreatShareMemory(IPC_CREAT); //获取的时候不需要判断是否存在
}

processa.cpp

我们在本次实验中要实现a来打印, b来输入的实验效果。所以我们的a要从内存中读到数据。 在b中把数据写入内存。

另外, 我们设计让a来创建共享内存, 创建好共享内存后, 要知道, 我们平时向内存中读数据, 必须要获取这个内存的地址,但是我们现在只有共享内存的编号shmid, 那么如何获取这个地址? ------这就用到了挂接, 我们将共享内存挂接到当前进程的虚拟地址上, 那么进程访问虚拟地址就是在访问共享内存!!!(注:这里面使用了shmctl, 这个函数的使用难度很大, 这里博主使用了IPC_STAT选项, 目的是为了获取shmid对应共享内存中的数据, 那么获取到那里呢, 就需要提前创建一个描述共享内存的结构体, 博主定义的是shmds, 所以就有了下面的代码)

cpp 复制代码
#include"Com.hpp"
#include"Log.hpp"


extern Log log; //拿到Com.hpp里面创建的哪个全局的静态对象, 


int main()
{
    //先创建共享内存
    int shmid = GreatShm();//函数内部自动判断, 无需手动判断
    //挂接共享内存到到vm_area_struct
    char* shmaddr = (char*)shmat(shmid, nullptr, 0); //这个返回值, 就是
    //共享内存的虚拟地址, 我们利用虚拟地址, 就可以访问共享内存

    struct shmid_ds shmds; //创建一个结构体, 用来获取共享内存的数据

    while(true)
    {
        cout << "client say@ " << shmaddr << endl;
        sleep(1);
        shmctl(shmid, IPC_STAT, &shmds); //读取共享内存的各个属性
        //将各个数据打出来!
        cout << "shm size: " << shmds.shm_segsz << endl;
        cout << "shm nattch: " << shmds.shm_nattch << endl;
        cout << "shm key: " << shmds.shm_perm.__key << endl;
    }    
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, nullptr); //删除
   
    return 0;
}

processb.cpp

b进程我们用来读取数据, 同样需要挂接。 不同的是我们要创建一个缓冲区buffer, 来接收我们要打印的数据,然后向内存中将缓冲区的数据写进去。

cpp 复制代码
#include"Com.hpp"
#include"Log.hpp"

int main()
{
    int shmid = GetShm();//获取共享内存
    //向里面写东西, 写东西就需要获得这个共享内存的地址, 那么就需要挂接
    char* shmaddr = (char*)shmat(shmid, nullptr, 0); //挂接地址随机, 模式为0
    while (true)
    {
        //先创建一个缓冲区
        string str;
        cin >> str;  //向缓冲区种写入数据

        snprintf(shmaddr, str.size(), "%s", str.c_str()); //将缓冲区的数据写入
        //共享内存
    }
    return 0;
}

运行程序

如下图是我们甘冈打开两个进程:可以看到, 此时的挂接个数是2.

然后我们在b进程输入一个你好啊, 观察一下------

我们就会发现数据已经打印进去了。 然后我们如果退出进程b, 挂接数会减到1:

------以上就是共享内存接口的相关应用。

经过上面的实验, 我们可以很明显的看出来, 共享内存是没有同步机制的!!!!------而想要让共享内存拥有同步机制的效果, 可以使用管道!!!

key和shimd的区别

了解了应用后, 我们就可以深究一下内部的原理了。

首先我们谈的就是请问key和shmid有什么区别呢?我们知道, key是在操作系统标定唯一性的, shmid是在我们的进程里面的,用来标识资源的唯一性的。也就是说,key是操作系统层面的, 只有在创建共享内存的时候会用到key, 其他时候用不到。

共享内存的生命周期

我们第一次运行程序processa.exe的时候, key_t会创建成功, 并且共享内存被创建出来。但是当我们第二次运行processa.exe的时候, 我们会看到下面这种情况:

也就是说, 我们的共享内存并没有随进程的退出而退出。 这里我们可以使用ipcs -m查看当前系统中所有的共享资源

我们输入上面ipcs -m的指令可以看到我们刚刚创建的共享内存, 也就是说, 即便我们进程推出了, 但是我们的ipc资源, 还是存在的。 这说明如果我们不主动把共享内存关掉, 操作系统也不会给我们关。------这就是共享内存的特性。 ------也就是说, 贡献内存的生命周期是随着内核的!!!(管道文件的生命周期是随着进程的!!!)用户不主动关闭, 共享内存会一直存在, 除非内核重启!!!

释放的方式分为两种------一种是使用指令进行释放, 另一种就是上面程序中使用的进程内函数调用shmctl进行控制释放共享内存

  • 指令释放:ipcrm -m 共享资源的shmid
  • 内部调用:shmctl(shmid, IPC_RMID, nullptr);

------------------以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!

相关推荐
(⊙o⊙)~哦15 分钟前
linux 解压缩
linux·运维·服务器
拾光师1 小时前
spring获取当前request
java·后端·spring
牧小七1 小时前
Linux命令---查看端口是否被占用
linux
最新小梦1 小时前
Docker日志管理
运维·docker·容器
鸡鸭扣2 小时前
虚拟机:3、(待更)WSL2安装Ubuntu系统+实现GPU直通
linux·运维·ubuntu
Java小白笔记3 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
友友马3 小时前
『 Linux 』HTTP(一)
linux·运维·服务器·网络·c++·tcp/ip·http
千禧年@3 小时前
微服务以及注册中心
java·运维·微服务
重生之我在20年代敲代码3 小时前
HTML讲解(一)body部分
服务器·前端·html
清水白石0083 小时前
C++使用Socket编程实现一个简单的HTTP服务器
服务器·c++·http