Linux 进程间通信之System V 共享内存
序言:
上个博客我们介绍了命名管道的原理和实现,这篇博客我们将介绍System V标准的共享内存的实现原理,系统调用 ,和其他几个进程间通信的区别和优缺点。
一、共享内存原理
我们在之前的博客说过想实现进程间通信有一个前提:使两个进程看见同一份资源,我们介绍过了匿名管道和命名管道的实现原理,那么共享内存的实现原理是什么?下面我们来开始介绍:
1、如何解决使两个进程看见同一份资源这个前提?
答案是通过在物理内存中开辟一块空间再去把这块空间映射到不同进程的虚拟地址中,从而使得两个进程看见同一份资源。
2、它映射到虚拟地址空间的什么位置呢?
答案是在堆栈之间的共享区。
下面我们来看一副图来理解一下:
另外我想在这里去说明一下一些别的问题:
1、什么叫标准?
标准是由行业内的权威机构或者是组织等,进行制定的它里面规定了协议,技术,规范等的统一规定,确保不同系统、组件或软件之间的兼容性和互操作性。
2、操作系统如何管理共享内存的?
在内核中可能不止一个共享内存,这些资源需要管理如何去管理?答案还是 “ 先描述,再组织 ”,用数据结构去描述想要去管理的资源再用对应的数据结构去组织这些管理信息。
struct shmid_ds {struct ipc_perm shm_perm; /* operation perms */size_t shm_segsz; /* size of segment (bytes) */__kernel_time_t shm_atime; /* last attach time */__kernel_time_t shm_dtime; /* last detach time */__kernel_time_t shm_ctime; /* last change time */__kernel_pid_t shm_cpid; /* pid of creator */__kernel_pid_t shm_lpid; /* pid of last operator */unsigned long shm_nattch; /* no. of current attaches */unsigned long __unused1;unsigned long __unused2;
};
这也也证明了进程间通信的资源要属于操作系统,那么它的生命周期是随内核的也就是说如果不主动删除和关机他就一直在内核中存在。
3、如何去唯一去区分共享内存呢?
前面的介绍我们解决了两个进程看见同一份资源的这个问题,那么我们怎么让两个进程能够找到这个共享内存资源呢?这需要我们在用户层面去约定我们要设定一个值可以去找到这个资源,这个值就叫做key,在内核中可以用key来区分共享内存的唯一性(key是一个非负整数)。
现在我们又有一个问题那就是如何保证key是唯一的,下面我们将介绍一个系统调用来解决这个问题:
ftok:
ftok可以通过传入的pathname和projid通过一种算法产生一个唯一值键值(key)返回回去。
pathname:一个字符串,尽量保证是一个路径。
projid:一个整数。
返回值:如果成功会返回一个唯一的key值,如果失败会返回 -1 。
注:
共享内存在设计的时候也参考了文件系统的设计,但它们两是弱相关的并不是强相关,操作系统还是为了共享内存的实现单独设计了一套内核结构。
二、共享内存相关指令
1、ipcs
ipcs指令可以查看所有的System V标准的进程间通信:
ipcs -m 可以查看共享内存:
shmid是单调递增的不会像文件描述符是选择当前最小的文件描述符分配给struct file。
2、ipcrm
ipcrm加上对应的选项和 id 可以删除对应的资源:
3、表头的意义
查出来的共享内存表的表头分别是:键值,标识符,拥有者,权限,大小,挂载数(当一个进程调用shmat成功了以后nattch会++,如果nattch == 2 说明两个进程都把共享内存挂载到了自己的地址空间),状态。
三、共享内存的系统调用
1、shmget
shmget:可以获取到共享内存资源
key:通过ftok获得的key值来做唯一性的区分。
size:想要的共享内存的大小(以物理空间4KB的整数倍开辟,但在虚拟地址空间上会以size的大小开辟)。
shmflg:
IPC_CREAT | IPC_EXCL:无论共享内存存不存在都会创建并返回,如果共享内存已经存在就会出错返回(要加上想要的共享内存的权限为0666)。
IPC_CREAT:如果共享内存存在的话会返回共享内存的shmid,如果共享内存不存在的话就会创造并返回它的shmid。
返回值:如果成功会返回共享内存的标识符(shmid) ,如果失败会返回 -1 。
2、shmat
shmat:将共享内存段挂载到进程的地址空间
shmid:共享内存的标识符(shmget的返回值)。
shmaddr:指定挂载的地址,它的可以指定一个地址使共享内存挂载到指定的地址。可以直接设置为nullptr,意思是由操作系统决定挂载的地址防止出错。
shmflg:想要上面样的方式挂载到进程的地址空间中,可以直接设置成 0 使用默认权限(这里的默认权限是shmget时shmflg的权限)。
返回值:如果成功会返回在进程中挂载的开始地址,如果失败的话,返回失败。
3、shmdt
shmdt:将共享内存与当前进程进行去关联
shmaddr:为共享内存挂载在当前进程的开始地址(shmat的返回值)。
返回值:如果成功会返回 0 ,如果失败会返回 -1。
4、shmctl
shmctl:用于控制共享内存
shmid:共享内存的标识符(shmget的返回值)。
cmd:想要的操作(三个选项)
IPC_STAT:将
shmid
对应的共享内存段的元数据(如权限、大小、附加进程数等)拷贝到buf
指向的shmid_ds
结构体中。IPC_RMID:把共享内存删除掉。
IPC_SET:用
buf
指向的shmid_ds
结构体中的字段,更新共享内存段的属性(仅允许修改部分字段)。
buf:
IPC_STAT
:buf
是输出参数(内核填充数据)。
IPC_SET
:buf
是输入参数(用户填充要修改的属性)。
IPC_RMID
:buf
无意义,通常设为NULL
。
四、共享内存的特点
1、共享内存的大小
上面我们简单说明了一下共享内存申请的规则:申请的共享内存是4KB的整数倍
下面我们看图来理解一下:
我们在申请一块内存的时候无论多大如果没有都会申请4KB的整数倍,这是因为物理内存以页为基本单位,页的大小就是4KB,但在我们使用的时候可以按直接使用(具体的原因需要到线程那里再谈地址空间才可以真正理解)。
2、共享内存的优点
1、向共享内存中写入不需要系统调用
因为共享内存会映射到对应的进程的地址空间中,进程访问直接的地址空间不需要任何的系统调用直接通过printf就可以读取数据,对于想要向共享内存中写入数据我们可以参考malloc的使用,可以把返回的shmaddr强转成我们想要的不同类型的地址然后再去写入。
为什么管道文件要使用系统调用读取和写入?
因为管道文件都需要文件缓冲区去作为共享的资源,文件缓冲区并没有映射到进程的地址空间。
2、共享内存是最快的进程间通信方案
因为进程不需要调用系统调用,数据写入直接可以被对方看见所以它的速度是最快的。
3、共享内存的缺点
1、没有同步机制
使用共享内存通信,通信双方没有同步机制,我们先来看有同步机制的进程间通信,管道通信,我们在前面的博客说过管道通信的四种特殊情况,当管道内没有数据的时候,读端会阻塞住等写端向管道中写入数据以后,读端才解除阻塞去读取数据。我们再来看共享内存,读端可以任何时候任何速度去读取数据,换一句话来说读进程和写进程是不相干的,它们两自己干着自己的事情不会考虑到另一方。
注:
什么叫做同步机制?
现在有两个进程,这两个进程想要去访问同一块资源为了保证顺序性,这两个进程排好队等一个访问结束了以后另一个进程才去访问,这种方式就叫做同步机制。
什么叫做互斥机制?
还是这个例子两个进程访问同一份资源,它们在访问之前先要去竞争锁,获得锁的进程才有资格去访问资源,另一个进程只要等锁打开了以后再去竞争获取了以后才可以去访问资源,这就叫做互斥机制。
2、读取不具有原子性
我们先来介绍一下什么是原子性?
简单来说,原子性只有两态:写完和没有写完,不存在写了一半的情况。
现在如果写端想要向共享内存中写入“hello world”它可能写到“hello”读端就进来读取数据,但我们的数据还没有写完数据是不完成的出现了写一半的情况,所以共享内存是不具有原子性的。
五、共享内容,消息队列和信号量的关系
System V标准中除了共享内存还有消息队列和信号量,它们的内核数据结构中都有strcut ipc_perm这个结构体,这三个进程间通信的结构体的地址都被放在了struct ipc_perm* perms[]这个数组中。
为什么三种不同的数据结构可以被放在同一个类型的数组中?
因为它们的第一个结构体成员都是struct ipc_perm,虽然三个的类型不同它们的地址都指向了第一个 成员的开始也就是struct ipc_perm这个结构体,通过强转的方式再变回它们各自不同的数据结构类型。
struct ipc_perm
{__kernel_key_t key;__kernel_uid_t uid;__kernel_gid_t gid;__kernel_uid_t cuid;__kernel_gid_t cgid;__kernel_mode_t mode; unsigned short seq;
};
六、代码实操
现在有两个进程一个是服务器进程,另一个是客户端进程,客户端向共享内存中写入‘A’~‘Z’字母,服务器需要创造共享进程资源和读取数据。
comm.hpp
#pragma once
#include <sys/types.h>
#include <sys/ipc.h>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <sys/shm.h>
#include <iostream>#define SIZE 1024
#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)
#define PATHNAME "."
#define PROJID 10
#define CREATER "CREATER"
#define USER "USER"class Shm
{
public:Shm(std::string pathname, int projid, std::string usertype): _pathname(pathname), _projid(projid), _usertype(usertype){_key = ftok(_pathname.c_str(), _projid);}private:void Creat(){_shmid = shmget(_key, SIZE, IPC_CREAT | IPC_EXCL | 0666);if (_shmid == -1){ERR_EXIT("shm cerat :");}std::cout << "共享内存已经创建" << std::endl;}void Get(){_shmid = shmget(_key, SIZE, IPC_CREAT );if (_shmid == -1){ERR_EXIT("shm get :");}std::cout << "共享内存已经获取" << std::endl;}void Dettach(){int ret = shmdt(_addr);if (ret == -1){ERR_EXIT("Dettach :");}std::cout << "共享内存已经去关联" << std::endl;}void Destroy(){int ret = shmctl(_shmid, IPC_RMID, nullptr);if (ret == -1){ERR_EXIT("creater delete :");}std::cout << "共享内存删除" << std::endl;}public:void GetShm(){if (_usertype == USER){Get();}else if (_usertype == CREATER){Creat();}}void* GetAddr(){return _addr;}void Attach(){_addr = shmat(_shmid, nullptr, 0);if ((long long)_addr == -1){ERR_EXIT("shm attach :");}std::cout << "共享内存已经挂载" << std::endl;}~Shm(){std::cout << "调用到析构函数" << std::endl;if (_usertype == "USER"){Dettach();}else{Dettach();Destroy();}}private:std::string _usertype;int _shmid;void *_addr;key_t _key;std::string _pathname;int _projid;
};
server.cpp
#include "Comm.hpp"int main()
{Shm shm(PATHNAME, PROJID, CREATER);shm.GetShm();shm.Attach();char *addr = (char *)shm.GetAddr();sleep(3);while (true){sleep(1);std::cout << addr << std::endl;}return 0;
}
client.cpp
#include "Comm.hpp"int main()
{Shm shm(PATHNAME, PROJID, USER);shm.GetShm();shm.Attach();char *addr = (char *)shm.GetAddr();int num = 0;for(char i = 'A' ; i <= 'Z' ; i++ ,num++){sleep(1);addr[num] = i;}return 0;
}
=============================================================================
本篇关于命名管道的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持和修正!!!