共享内存
共享内存是属于SystemV系列的IPC方式,System V是OS特地设计的通信方式,也就是说OS会负责管理这块。而对于管道通信,管道是基于文件实现的,所以管道并不算OS特地设计的。
System V系列的所申请的资源要手动删除,它并不会自动清除(除非重启),System V IPC资源的生命周期随内核。
共享内存的原理
共享内存的原理:让不同的进程,看到同一份资源。
首先,现在物理内存中开辟一段空间,这段空间就是共享内存。
然后,通过修改映射关系 (修改页表),使得虚拟地址空间中也开辟一块空间。
实际上,这一过程就是将在物理内存中申请的共享内存,映射到进程地址空间中,对于进程而言,它访问它的共享区的时候实际上就是在访问共享内存了。
共享内存的查看及删除
ipcs #查看System V系列的所有资源
ipcs -m #查看共享内存
ipcrm -m [shmid] #删除shmid对应的共享内存
ipcrm -a #删除进程间通信的所有资源
共享内存建立的过程
申请共享内存
· shmget函数
int shmget(key_t key, size_t size, int shmflg); //包含于<sys/shm.h>
功能:
用来创建共享内存 / 获取共享内存(但是不创建)
参数:
key:共享内存段的名字,一般由ftok函数生成,而ftok函数是通过PATHNAME和PROJ_ID创建的key值 在下面细说
size:共享内存的大小。最好是4096字节的整数倍。
shmflg:由9个权限标志构成,它们的用法和open文件时所使用的mode模式的标志是一样的。 #下面会细说
返回值:
成功:返回一个非负整数,该整数就是shmid,也就是共享内存的标识码。
失败:返回-1
<shmget参数详细说明>
key:给ftok函数提供pathname和proj_id就可以创建出key值。我们通过key值就可以开辟共享内存了。共享内存的本质就是:让不同的进程通过同一个key值,看到同一份资源!
size:这里的size最好是4096字节的整数倍!4096 bytes是一页的大小。解释一下“页”:在内存与外设进行交互的时候(IO的时候),内存是以4KB (4096 bytes) 为单位进行划分的,也是以4KB为单位与外设进行交互的。所以这里的size如果不是4096的整数倍,会造成浪费。比如:4097,那么OS就会为你开辟2页的空间 (2 * 4096),然后再把其中一页的4095个字节不让你使用,这样就是4097字节了,浪费还是很大的。
shmflg:shmflg一般我们只用其中的2个,IPC_CREAT和IPC_EXCL。
携带IPC_CREAT:根据key值去创建共享内存,如果已经有该key值对应的共享内存了,不会去创建新的共享内存,而是去获取这个已经存在的共享内存。
携带IPC_EXCL:一般是和CREAT一起使用,确保一定能够开辟一个新的共享内存!
注意:shmflg这个参数的位置,也可以附带上权限,如下图。如果不附带权限的设置的话,无法get到共享内存,因为没有权限。
ftok函数
key_t ftok(const char* pathname, int proj_id); // 包含于<sys/types.h> <sys/ipc.h>
功能:
用来创建key值,可以把这个key值传给shmget函数。
参数:
pathname:路径名
proj_id:自己随便给一个就行
返回值:
成功:返回key值 #只要由pathname和proj_id相同,key值就是一样的!
失败:返回-1
关于PATHNAME和PROJ_ID的定义
//PATHNAME就是pwd的结果
//PROJ_ID就是随便写的
将共享内存挂接到进程地址空间中
· shmat函数
void* shmat(int shmid, const void* shmaddr, int shmflg); //包含于<sys/shm.h>
功能:
挂接共享内存到进程地址空间中
参数:
shmid:共享内存标识
shmaddr:指定连接的地址 #一般写NULL
shmflg:它的2个可能取值是 SHM_RND 和 SHM_RDONLY #一般写0
返回值:
成功:返回一个指针,指向共享内存的起始地址 #类似与malloc的返回值,返回值的用法也是类似
失败:返回-1
实际用法
'去’关联共享内存
· shmdt函数
int shmdt(const void* shmaddr); //包含于<sys/shm.h>
功能:
去关联共享内存与当前进程
参数:
shmaddr:shmat所返回的指针
返回值:
成功返回0,失败返回-1
释放共享内存
· shmctl函数
int shmctl(int shmid, int cmd, struct shmid_ds* buf); //包含于<sys/shm.h>
功能:
增/删/改 共享内存,一般用来删除
参数:
shmid:shmget函数的返回值
cmd:删除共享内存填 IPC_RMID 就行
buf:这里指定共享内存中的某一块地址。一般填NULL就行
返回值:
成功:返回0
失败:返回-1,并设置错误码
如何使用共享内存
共享内存的使用方法类似于“对malloc所申请的空间的操作”。 我们拿指针去接收共享内存的起始地址,然后通过指针去访问共享内存。 #其实很好理解,共享内存这块内存就相当于2个进程的公共部分,我们只要有指向这个地方的指针,就可以轻而易举的访问了。
问题:server端在创建共享内存之后,client该怎么找到?
通过相同的key值去找到共享内存。
PATHNAME + PROJ_ID ----> key
有了相同的key值,通过向shmget中传key值就可以得到相同的shmid了,也就是同一块共享内存。
问题:为什么共享内存是速度最快的IPC方法?
原因:①:共享内存的拷贝次数少
②:在使用共享内存时不涉及系统调用接口(也就是不会有内核态到用户态之间的转化,因为都是在用户层进行操作的)
③:不提供任何保护机制(没有同步与互斥)
问题:为什么共享内存的拷贝次数少?
共享内存的使用方法就和使用堆空间类似,直接向共享内存中写入,另一个进程直接就能看到。它与管道不同,管道还需要拷贝数据到管道,另一个进程再从管道中拷贝数据到自己当中。
信号量
(这里只是介绍一下概念,我会在多线程的博客当中主要介绍信号量)
信号量主要作用于同步和互斥,而不是存储进程间通信的数据。
信号量的本质是一个计数器
问题:那它是什么的计数器呢?
它是描述临界资源个数的计数器,临界资源就是多个进程/线程所共享的资源。
· 信号量分为:
二元信号量:信号量的值只能是0和1,它在同一时刻只能被一个线程获取
多元信号量:信号量的取值是整数,可以被多个线程同时获得,直到信号量的值变为0
问题:要申请一个资源,进程必须要直接占有这个资源吗?
不用。进程只要申请信号量成功了,就一定有属于它的资源。
· 二元信号量
二元信号量只能取0和1,因此它被用来实现互斥这一机制。
当信号量为1的时候,进程可以使用这块资源,在使用之前,让信号量变为0,代表该资源在被人使用。(这个操作也叫P操作)
当该资源的使用结束了,让信号量重新变为1。(这个操作叫V操作)
· 信号量相关接口
#include <sys/sem.h>
// 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
int semget(key_t key, int num_sems, int sem_flags);
// 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
int semop(int semid, struct sembuf semoparray[], size_t numops);
// 控制信号量的相关信息
int semctl(int semid, int sem_num, int cmd, ...);
备注:关于信号量更详细的使用我会在后面的多线程博客中具体说明。并且我们比较常用的信号量接口时POSIX标准的。