1.共享内存的概念
共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种进程间通信机制需要内核介入的次数很少。
所有需要做的就是让一个进程将数据复制到共享内存中,并且这部分的数据对其他所有共享同一段的进程可用。
与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种IPC 技术的速度更快。
2.共享内存的使用步骤
(1)调用shmget()函数创建一个新的共享内存段或者取得一个已经存在的共享内存段的标识符(由其他进程创建的共享内存段)。这个函数将会返回后续调用中需要用到的共享内存标识符。
(2)使用shmat()函数将共享内存段成为调用进程的虚拟内存中的一部分。此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由shmat() 调用返回的addr 值,它是一个指向进程虚拟地址空间中该共享内存段的起点的指针。
(3)调用shmdt()函数来分离共享内存段,在这个调用之后,进程就无法再引用这块共享内存
了。这一步是可选的,并且在进程终止时会自动完成这一步。(该函数的作用和shmat函数相反)。
(4)调用shmctl()函数来删除共享内存段。只有当没有进程使用这个共享内存段的时候,这个共享内存段才会销毁。因此只有一个进程需要执行这一步。
3.共享内存操作函数介绍
与共享内存相关的函数通常保存在下面两个头文件中:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
通过man 2 shmget查看
shmget() returns the identifier of the System V shared memory segment associated with the value of the argument key. A new shared memory segment, with size equal to the value of size rounded up to a multiple of PAGE_SIZE, is created if key has the value IPC_PRIVATE or key isn't IPC_PRIVATE, no shared memory segment corresponding to key exists, and IPC_CREAT is specified in shmflg. If shmflg specifies both IPC_CREAT and IPC_EXCL and a shared memory segment already exists for key, then shmget() fails with errno set to EEXIST. (This is analogous to the effect of the combination O_CREAT | O_EXCL for open(2).)【shmget()返回与参数key值相关联的System V共享内存段的标识符。如果key的值为IPC_PRIVATE或key不是IPC_PRIVATE,不存在与key对应的共享内存段,并且在shmflg中指定了IPC_CREAT,则创建一个新的共享内存段,其大小等于size的值四舍五入到PAGE_SIZE的倍数。如果shmflg同时指定了ipc_create和IPC_EXCL,并且key已经存在一个共享内存段,那么shmget()会失败,errno设置为EEXIST。(这类似于O_CREAT | O_EXCL对open(2)的组合效果。)】
功能:
创建一个新的共享内存段,或者获取一个既有的共享内存段的标识符。新创建的内存段中的数据都会被初始化为0
参数:
key : key_t类型,其实是一个整形,通过这个找到或者创建一个共享内存段(与共享内存段相关联)。一般使用16进制表示,非0值。
size: 共享内存的大小,比如4096。
shmflg: 属性。可以设置访问权限,比如0664。 如果是创建共享内存段,那么设置为IPC_CREAT,创建一个新段。如果未使用此标志,则shmget()将查找与key相关联的段,并检查用户是否具有访问该段的权限。 IPC_CREAT | IPC_EXCL。 IPC_EXCL标志与IPC_CREAT一起使用,以确保此调用创建段。如果该段已经存在,则调用失败。
返回值:
失败:-1 并设置错误号
成功:>0 返回共享内存的标识符,后面操作共享内存都是通过这个值。
void *shmat(int shmid, const void *shmaddr, int shmflg);
功能:和当前的进程进行关联
参数:
shmid : 共享内存的标识符(ID),由shmget返回
shmaddr: 申请的共享内存的起始地址,指定NULL,由内核指定
shmflg : 对共享内存的操作。SHM_RDONLY, 必须要有读权限;设置为0表示读写权限都有。
返回值:
成功:返回共享内存的首(起始)地址。 失败返回(void *) -1
int shmdt(const void *shmaddr);
功能:解除当前进程和共享内存的关联
参数:shmaddr:共享内存的首地址(由shmat函数返回)
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
参数:
shmid: 共享内存的ID
cmd: 要做的操作
- IPC_STAT : 获取共享内存的当前的状态
- IPC_SET : 设置共享内存的状态
- IPC_RMID: 标记共享内存被销毁(与其他进程没有关联的时候才会删除)
buf:需要设置或者获取的共享内存的属性信息
- IPC_STAT : buf用来存储数据
- IPC_SET : buf中需要初始化数据,设置到内核中
- IPC_RMID : 没有用,设置为NULL
struct shmid_ds { struct ipc_perm shm_perm; /* Ownership and permissions */ size_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 */ ... };
key_t ftok(const char *pathname, int proj_id);
功能:根据指定的路径名,和int值,生成一个共享内存的key
参数:
pathname:指定一个存在的路径,例如 /home/nowcoder/Linux/a.txt
proj_id: int类型的值,但是系统调用只会使用其中的1个字节;
范围 : 0-255 一般指定一个字符 比如'a'
测试案例:通过共享内存来实现进程之间的通信。创建两个进程,一个用来写,一个用来读。
write_shm.c代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h> //memcpy
int main() {
// 1.创建一个共享内存,key随便给,关联共享内存
int shmid = shmget(100, 4096, IPC_CREAT|0664);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
char * str = "helloworld";
// 3.写数据
memcpy(ptr, str, strlen(str) + 1);
printf("按任意键继续\n");
getchar(); //没有的话,就继续向下执行,解除关联,然后删除共享内存,读进程还没有启动起来,内存就没了
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
read_shm.c代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
int main() {
// 1.获取一个共享内存、key是一个标识,标识同一个共享内存空间,101就不是这个了
int shmid = shmget(100, 0, IPC_CREAT);
printf("shmid : %d\n", shmid);
// 2.和当前进程进行关联
void * ptr = shmat(shmid, NULL, 0);
// 3.读数据
printf("%s\n", (char *)ptr);
printf("按任意键继续\n");
getchar();
// 4.解除关联
shmdt(ptr);
// 5.删除共享内存
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
4.其他问题
问题1:操作系统如何知道一块共享内存被多少个进程关联?
共享内存维护了一个结构体struct shmid_ds(通过shmctl函数可以获取查看), 这个结构体中有一个成员 shm_nattch,shm_nattach 记录了关联的进程个数。
问题2:可不可以对共享内存进行多次删除 shmctl
可以,因为shmctl 标记删除共享内存,不是直接删除;什么时候真正删除呢? 当和共享内存关联的进程数为0的时候,就真正被删除。当共享内存的key为0的时候,表示共享内存被标记删除了。如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存了。也不能再次进行关联。
5.共享内存和内存映射之间的区别
虽然共享内存和内存映射都是对内存进行操作,但是它们之间的区别如下:
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高(因为内存映射还需要操作磁盘文件)
3.共享内存, 所有的进程操作的是同一块共享内存。内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全:
- 进程突然退出:共享内存还存在,内存映射区消失
- 运行进程的电脑死机,宕机了:数据存在在共享内存中,没有了; 内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。
5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存: 如果一个进程退出,会自动和共享内存进行取消关联,即标记删除,当共享内存关联的进程数为0的时候,共享内存销毁。
6.共享内存操作指令
ipcs 用法
ipcs -a // 打印当前系统中所有的进程间通信方式的信息
ipcs -m // 打印出使用共享内存进行进程间通信的信息
ipcs -q // 打印出使用消息队列进行进程间通信的信息
ipcs -s // 打印出使用信号进行进程间通信的信息
◼ ipcrm 用法
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid // 移除用shmid标识的共享内存段
ipcrm -Q msgkey // 移除用msqkey创建的消息队列
ipcrm -q msqid // 移除用msqid标识的消息队列
ipcrm -S semkey // 移除用semkey创建的信号
ipcrm -s semid // 移除用semid标识的信号