systemV共享内存
共享内存区是最快的IPC形式。共享内存的大小一般是4KB的整数倍,因为系统分配共享内存是以4KB为单位的(Page)!4KB也是划分内存块的基本单位。
之前学的管道,是通过文件系统来实现让不同的进程看到同一份资源,那有没有不通过文件来实现进程间通信的呢?有!MMU这些
原理
PCB、进程地址空间、页表是每个进程各有一份的【这部分是进程】,文件系统是一个独立的,对进程来说只有一份。
操作步骤:
- 创建共享内存:为了实现进程间通信,用户通过系统调用接口在物理内存申请一块内存空间[物理内存块+共享内存相关属性struct_shm]
- 进程和共享内存挂接:系统将创建好的内存映射到进程的地址空间,然后交给对应进程的PCB-task_struct。
- 断开通信的操作:a.去关联:先取消映射关系;b.释放内存:释放该块物理内存。
malloc能申请物理内存的空间,也能为某进程创建单一的映射关系,为什么不用它呢?
malloc是做不到的!因为malloc出来的映射关系是单一的,不同的malloc之后返回的起始地址都是不一样的,无法共享。
要明确把握的概念
- 共享内存是一种进程间通信方式,不是内存!所有想通信的进程都可以采用。
- 操作系统在同一时间可以存在很多的共享内存。
- 共享内存是专门设计的用于IPC的,故有专门的接口。
概念
让不同的进程看到同一块内存空间。
查看ipc-systemV资源的指令
查看ipc资源ipcs -m
-m表示共享内存,-q消息队列
[yyq@VM-8-13-centos 2023_02_27_SharedMemory]$ ipcs -m------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
systemV版本的共享内存的生命周期是随OS的,不随进程!所以一定要记得及时释放资源。注意:消息队列等方式的共享内存就是随进程的。
命令行删除共享内存ipcrm -m shmid
while :; do ipcs -m; sleep 1; done
函数接口
ftok()
函数 得到key
#include <sys/types.h>
#include <sys/ipc.h>
原型key_t ftok(const char *pathname, char proj_id);//路径名的指针pathname,自己写的8bits的非0数字proj_id
返回值成功返回一个key值;失败返回-1
Today proj_id is an int, but still only 8 bits are used. Typical usage has an ASCII character proj_id, that is why the behavior is said to be undefined when proj_id is zero.
key保证进程看到同一块共享内存!能进行唯一性标识。key就是32位的int。
shmget()
函数 得到shmid
#include <sys/ipc.h>
#include <sys/shm.h>
功能:用来创建共享内存
原型int shmget(key_t key, size_t size, int shmflg);
参数key:ftok()函数返回值,是共享内存段名字,保证该块共享内存的唯一性size:共享内存大小shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的IPC_CREAT 如果共享内存不存在,就创建;存在,则返回地址IPC_EXCL 无法单独使用,要与IPC_CREATE一起用。如果共享内存不存在,就创建;如果存在,就出错返回-->如果创建成功,就一定是一个新的共享内存
返回值成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
IPC_CREAT、IPC_EXCL本质是宏,用二进制标志位的方式来表示选项。
需要创建共享内存的进程用IPC_CREAT | IPC_EXCL | 0600
,后续要和该进程通信的进程使用IPC_CREATE
【类似于父进程来创建匿名/命名管道,子进程直接用】。0600表示权限,可以看到perms
会随之变化,若perms
为0,挂接会失败。
**共享内存的大小一般要取页表大小(4096字节)的整数倍。**建议为4KB的整数倍,因为系统分配共享内存是以4KB为单位的!4KB也是划分内存块的基本单位。
shmat()
函数 进程和共享内存挂接
at-attach
功能:将共享内存段连接到进程地址空间
原型void *shmat(int shmid, const void *shmaddr, int shmflg);
参数shmid: 共享内存标识shmaddr:指定连接的地址==NULL:核心自动选择一个地址!=NULL且shmflg!=SHM_RND:以shmaddr为链接地址!=NULL且shmflg==SHM_RND:连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr-(shmaddr % SHMLBA)shmflg==SHM_RDONLY:连接操作用来只读共享内存shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY
返回值成功返回一个指针,指向共享内存第一个节;失败返回-1//假设void* ret为返回值,则判断条件为(long long)ret == -1L[强转为int的前提条件:系统为32为系统!但是linux是64位系统,指针是8个字节,而int是4字节,编译会报错]
理解key使共享内存具有唯一性,以及key和shmid间的关系和区别
ftok()函数返回值是key和shmget()函数返回值是shmid。
首先,共享内存=物理内存块+共享内存相关属性(即操作系统为管理共享内存建立的一个结构体)。
创建共享内存的时候如何保证共享内存在系统中的唯一性?通过key来实现,key类似于fd,通过shmge()
t函数将key设置为struct shm{ key_t k; }
k上,当共享内存属性的k一样时,就能进入同一块内核内存空间。
区别:shmid类似于fd,供用户使用,key类似于inode,用于标定系统的共享内存,是内核级的描述符;即key是系统层面用的,shmid是给用户用的,是为了解耦,fd和inode同理。
注意:成功挂接后,通过ipcs -m
命令查看该块共享内存的属性,可以看到nattch
的数字会++.
shmdt()函数 去关联
dt–detach
#include <sys/types.h>
#include <sys/shm.h>
功能:将共享内存段与当前进程脱离
原型int shmdt(const void *shmaddr);
参数shmaddr: 由shmat所返回的指针
返回值成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
shmctl()
函数 删除共享内存段/查看共享内存属性
#include <sys/ipc.h>
#include <sys/shm.h>功能:用于控制共享内存
原型int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数shmid:由shmget返回的共享内存标识码cmd:将要采取的动作(有三个可取值)IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值IPC_ET:在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值IPC_RMID:删除共享内存段buf:指向一个保存着共享内存的模式状态和访问权限的数据结构//当cmd为IPC_RMID时,传NULL;当使用其他cmd时,传一个struct shmid_ds{}数据结构的指针进去,就能获得共享内存的信息(所有者、权限、大小、创建时间等信息)
返回值成功返回0;失败返回-1
注意:有进程和该块共享内存挂接时,即nattch!=0
,也可以完成删除操作,但是最好是先将进程去关联后再删除该块共享内存。
linux下是64位平台,无法把指针强转为int类型–8自己无法转4字节,可以把指针转成long long类型,并且给-1加上L
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010309 3 yyq 600 4096 0 ------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010309 3 yyq 600 4096 1 //server先挂接上------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010309 3 yyq 600 4096 2 //server和client都挂接上了------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010309 3 yyq 600 4096 1 //client先去关联------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010309 3 yyq 600 4096 0 //server回收资源
共享内存的数据结构如下
struct shmid_ds {struct ipc_perm shm_perm; /* Ownership and permissions */ //centos用的名称是shm_permsize_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...};
struct ipc_perm { // struct shm_permkey_t __key; /* Key supplied to shmget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */unsigned short __seq; /* Sequence number */};
查看共享内存属性
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("%d-nattach:%d-key:%d\n", ds.shm_segsz, ds.shm_nattch, ds.shm_perm._key);
通信模块
用到的函数为
int snprintf(char *str, size_t size, const char *format, ...);
向特定的区域做格式化输入,共享内存没有输入输出接口,第一种方法:和以前一样定义一个缓冲区,然后把缓冲区的memcpy过去即可;第二种方法:共享内存和进程挂接上以后,shmat函数会直接给我们返回该段共享内存映射到进程地址空间的void*
指针,直接把它强转为char*
即可使用。
//方式1
const char* msg = "hello server, I'm client";
pid_t pid = getpid();
int count = 1;
char buffer[1024];
snprintf(buffer,sizeof(buffer), "%s[pid:%d][消息编号:%d]", msg, pid, count);
memcpy(start, buffer, strlen(buffer) + 1);
//方式2--挂接成功的返回值可以强转为char*,字符数组就可以直接用,不用缓冲区
const char* msg = "hello server, I'm client";
pid_t pid = getpid();
int count = 1;
snprintf(start, SHM_SIZE, "%s[pid:%d][消息编号:%d]", msg, pid, count++);
总结
共享内存段的优点
所有进程间通信方式中最快的一个方式,有效减少拷贝次数。==>不用缓冲区和memcpy
共享内存段的缺点
发消息的慢5s,读消息的快1s:读消息会一直读上一条的,等发过来了再读新的。即共享内存不会进行同步和互斥操作,没有对数据进行任何保护。类比管道,写端不写,读端就会阻塞等待;读端不读,写端写到缓冲区满了就不写了。需要使用信号量来对数据进行保护。
面试题:管道和共享内存
比较一下管道和共享内存(场景包括键盘输入和显示器输出),同样的代码,如果用管道/共享内存来实现,需要经过几次拷贝?
- 管道:4次+2次
- 共享内存:2次+2次
优化:共享内存的数据保护
需要实现类似于管道的功能,client写完了再通知server,进行读取,如果client没有写入或者没通知,server就不读取。
思路:同时使用匿名管道和共享内存,用匿名管道进行通知,共享内存传递消息。