system V是一套标准,专门设计出来为了通信。
之前谈到匿名管道和命名管道都涉及文件操作。共享内存不涉及到文件,无需借助内存交互,是IPC最快的形式。
system V共享内存原理
利用共享内存实现通信,同样需要再内存中,让不同进程A 和进程B看到同一份资源。
在进程A创建的时候,会通过在进程地址空间确定位置,形成缺页中断,在进程实际需要访问时,会在物理内存申请空间。通过页表映射起来。如果能让进程A和进程B在申请内存的时候都申请在同一个位置上,都能实现看到同一份资源,实现进程通信。
因此在物理内存上,有一块属于"大家的内存"就是共享内存。
共享内存是在堆栈之间。俩个的部分代码进程会分布在物理内存的同一块空间,将物理内存通过页表映射到mm_struct的共享内存区,并返回起始地址给用户,就能实现对一块资源的读写。
不止一对进程能在共享内存中,可以有多对同时参与。管理共享内存的方式就是先描述再组织。
共享内存=共享内存结构体shm+实际内存
共享内存的函数
shmget创建共享内存
函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 作用:向内存申请一块共享内存
- 返回值:
申请成功返回用户级标志符
申请失败返回 -1 错误码被设置
- 参数:
- key :需要创建的内存在系统的唯一标志
- size:需要创建的内存大小 一般为n*4096
- shmflg:创建方式
是一个宏,位图的操作,与文件open选项类似
有俩种选项:
1)IPC_CREAT 创建一个共享内存,不存在就创建,存在就返回已创建的地址2)IPC_CREAT | IPC_EXCL 创建一个共享内存,已存在就失败返回
换句话说 IPC_CREAT 无法确保内存是当前调用创建
IPC_CREAT | IPC_EXCL 确保内存是新建的
shmid是用户标识,key是内核级的标志
就类似于fd 和inode的关系
为什么key需要用户提供?
为了让用户在这俩个通信的进程之间知道。假设server中输入key,那么只有通过唯一的标识符才能找到共享内存。client和server是俩个独立的进程,因此在通信之前必须用户提供key。
获取唯一的key
ftok函数
是一个算法函数,根据路径名和项目id创建一个key,它的冲突概率及低(有点hash算法的感觉了)
创建key的意义
利用标识符创建标记唯一的共享内存,提供给后续的进程寻找到这块共享内存
举例:创建一个key
创建出来的key会很大,转成十六进制,更有利于观察
利用snprintf函数转化
创建共享内存
利用shmget函数
因为共享内存有俩种情况
1.还没有被创建,创建共享内存。
2. 已经被创建,用于查找,返回内存地址
由于这俩种情况的差别就在于flag上,利用子函数调用减少代码的冗余。
int CreateShmgetHelp(const key_t &key, int flag)
{
int shmid = shmget(key, size, flag);
if(shmid<0)
{
std::cerr<<"cerr: "<<errno<<",errstring: "<<strerror(errno)<<std::endl;
return 1;
}
return shmid;
}
int Creatshmid(const key_t &key)
{
return CreateShmgetHelp(key,IPC_CREAT|IPC_EXCL);
}
int Getshmid(const key_t &key)
{
return CreateShmgetHelp(key,IPC_CREAT);
}
IPC资源
共享内存的生命周期不随进程,而是跟谁OS,这是system V的特性
下面介绍查看IPC资源的指令
perms表示权限 bytes内存大小 nattch表示有几个进程和内存关联
- ipcs -m 查看共享内存的资源
- ipcs -q 查看消息队列的资源
- ipcs -s 查看信号量
- ipcs 三个一起查看
挂起 shmat
将地址空间和内存关联
man shmat
函数有三个参数
- shmid为用户要关联的 id
- shmaddr为用户要放置在内存区哪一块位置,nullptr表示让系统帮忙选择
- shmflag 可以设置为SHM_RDONLY 表示权限只读 一般为0表示只读
返回值为内存起始地址
起始地址为void* 一般都需要强制转化为需要的类型
去关联shmdt
与shmat相反,将页表的映射取消掉,就相当于物理内存还是,地址空间的使用被取消
参数是void* 从shmat接收到的返回值
删除共享内存
因为是指令删除 是用户层 故删除的时候需要记住 shmid
ipcrm -m shmid 就能ipc资源
ipcrm -m 7
系统调用shmctl
共享内存是需要管理的,在内存的起始地址,是存放描述的结构体,里面会包含属性信息
shmctl用于操作共享内存
参数介绍:
shmid
:控制共享内存的标识符。cmd
:控制的种类,主要用的是IPC_RMID(立即移除共享内存)。buf
:控制共享内存的数据结构,设置为空即可。返回值
:0表示返回成功,-1表示失败。
效果展示
如何显示同步?
但是共享内存是不提供同步机制的
为了实现同步--写端每次输入一个,读端就会等待。
利用命名管道的辅助实现同步。
共享内存的效率
是所有通信中,速度最快的一种通信方式
和管道比起来,至少减少几次拷贝?
凡是数据迁移都需要拷贝!
带硬件
假设从数据键盘文件缓冲区 1次 ,数据到调用writr写到文件缓冲区需要1次
从文件缓冲区通过read调用读到缓冲区需要1次 缓冲区到显示器1次
不考虑C语言接口的缓冲区 2 次
那么一共就有4次。
共享内存是通过映射,对于内存的同一个位置。假设从键盘读到内存 1次。从内存读到写到显示器1次。 共俩次。
所以至少减少了2次拷贝。
内核如何看待IPC资源
IPC资源是通过数组管理起来的 IPC_id_arrary
通过下标就能找到消息队列、共享内存、信号量等等
每一种资源都是被结构体stmid_ds描述起来,它们结构体的第一个内容是ipc_perm
struct Ipc_Perm
{
//key
//权限
}
OS通过ipc_ids管理这些字段,ipc_ids里面有一个柔性数组,柔性数组的内容是存放指针
指针指向ipc_perm 。当我们想申请贡献内存的时候,OS会创建shmid_ds结构体对象,并且添加ipc_id _arrary的指针。柔性数组的好处我们可以在使用时,再考虑申请多大的内存
通过指针数组的强转结构体的第一个段内容,就是多态的思想!
通过这样的方式就能统一管理数据!
信号量
本质是一个计数器,用来保护资源。
当资源被使用,计数器就会--,但资源被释放,计数器++。
当信号量为1时,表现为具有互斥性,我们称之二元信号量,信号量表现只有0和1,是锁的体现
如何理解信号量
信号量是标记资源是否被不同进程使用,那么势必要让进程看到信号量,那么信号量也是资源,也需要计数器保护,这不是矛盾吗?
信号量必须是原子的!就是要么完成,要么没做。
信号量的操作
semop -- (申请资源) P操作
semop ++ (释放资源)V操作
信号量的接口与共享内存相近,不作介绍。
共享内存,是映射到物理内存中。获取shmget(),key size flag都是有讲究,返回的shmid是需要被记录。创建后就需要挂起shmat 返回地址空间的起始地址,取消关联shmdt,在物理内存取消共享空间shmctl(id,IPC_RMID,nullptr)
共享内存是可以提供大容量的,不支持同步机制,是效率速度最快的通信方式。