共享内存(SHM)
1. 基本概念
Linux 系统中的共享内存是一种 IPC(进程间通信)机制,允许多个进程共享同一块内存区域。这种机制可以让多个进程可以直接在内存中读写数据,避免了数据在多个进程间来回拷贝的性能损失。
Linux 系统中的共享内存是基于内存映射文件(memory mapped file)实现的。这种机制允许将一个文件映射到进程的虚拟地址空间,然后在该虚拟地址空间中直接对文件进行读写操作。如果多个进程都将同一个文件映射到了它们的虚拟地址空间中,那么它们就可以直接在内存中读写共享的数据。
使用共享内存的流程如下:
- 创建一个内存映射文件。
- 使用
shmget()
函数创建一个共享内存段。 - 使用
shmat()
函数将该共享内存段映射到进程的虚拟地址空间中。 - 在该共享内存段中读写数据。
- 使用
shmdt()
函数取消映射。 - 使用
shmctl()
函数删除共享内存段。
2. 共享内存API
2.1 获取共享内存ID
shmget()
是 Linux 系统中的一个函数,用于创建或获取一个共享内存段。该函数的原型如下:
int shmget(key_t key, size_t size, int shmflg);
该函数接受三个参数:
key
:指定要创建或获取的共享内存段的键值。size
:指定要创建的共享内存段的大小(仅在创建共享内存段时有效)。shmflg
:控制创建共享内存段的方式的标志。
- IPC_CREAT:如果key对应的共享内存不存在,则创建。
- IPC_EXCL:如果该key对应的共享内存已存在,则报错。
- SHM_HUGETLB:使用“大页面”来分配共享内存。
- SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
- mode:共享内存的访问权限(八进制,如0644)。
如果函数执行成功,则返回共享内存段的标识符。否则,返回一个负值。
shmget()
函数用于创建或获取一个共享内存段。它能够根据指定的键值、大小和标志,在系统中创建一个新的共享内存段,或者获取已经存在的共享内存段。例如,可以使用 shmget()
函数创建一个大小为 1 MB 的共享内存段,并使用 shmat()
函数将该段映射到进程的虚拟地址空间中。然后,就可以在该段中读写数据了。
2.2 映射共享内存
shmat()
是 Linux 系统中的一个函数,用于将一个共享内存段映射到进程的虚拟地址空间中。该函数的原型如下:
void *shmat(int shmid, const void *shmaddr, int shmflg);
该函数接受三个参数:
shmid
:指定要映射的共享内存段的标识符。shmaddr
:指定要将共享内存段映射到哪个虚拟地址(可以为 NULL,表示让系统自动选择)。shmflg
:控制映射共享内存段的方式的标志。如果函数执行成功,则返回映射后的虚拟地址。否则,返回一个错误指针。
shmat()
函数用于将一个共享内存段映射到进程的虚拟地址空间中。它能够根据指定的标识符和标志,将一个共享内存段映射到指定的虚拟地址,从而允许进程直接在该地址处读写数据。例如,可以使用 shmget()
函数创建一个共享内存段,然后使用 shmat()
函数将该段映射到进程的虚拟地址空间中。在映射完成后,就可以在该段中读写数据了。
- 共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
- shmat()第二个参数shmaddr一般都设为NULL,让系统自动找寻合适的地址。但当其确实不为空时,那么要求SHM_RND在 shmflg必须被设置,这样的话系统将会选择比shmaddr小而又最大的页对齐地址(即为SHMLBA的整数倍)作为共享内 存区域的起始地址。如果没有设置 SHM_RND,那么shmaddr必须是严格的页对齐地址。总之,映射时将shmaddr 设置为NULL是更明智的做法,因为这样更简单,也更具移植性。
2.3 解除共享内存的映射
shmdt()
是 Linux 系统中的一个函数,用于取消共享内存段的映射。该函数的原型如下:
int shmdt(const void *shmaddr);
该函数接受一个参数:
shmaddr
:指向已经映射的共享内存段的虚拟地址。如果函数执行成功,则返回 0。否则,返回一个非 0 值。
shmdt()
函数用于取消共享内存段的映射。它能够根据指定的虚拟地址,将一个共享内存段从进程的虚拟地址空间中取消映射。在取消映射后,进程就不能再通过该虚拟地址读写数据了。例如,可以使用 shmat()
函数将一个共享内存段映射到进程的虚拟地址空间中,然后在该段中读写数据。当不再需要这个共享内存段时,可以使用 shmdt()
函数将该段取消映射。
注意:解除映射之后,进程不能再允许访问SHM。
2.4 设置或获取共享内存的属性
shmctl()
是 Linux 系统中的一个函数,用于控制共享内存段。该函数的原型如下:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
该函数接受三个参数:
shmid
:共享内存段的描述符。cmd
:控制共享内存段的命令。
- IPC_STAT:获取属性信息,放置到 buf 中。
- IPC_SET:设置属性信息为buf指向的内容。
- IPC_RMID:将共享内存标记为“即将被删除”状态。
- IPC_INFO:获得关于共享内存的系统限制值信息。
- SHM_INFO:获得系统为共享内存消耗的资源信息。
- SHM_STAT:同IPC_STAT,但 shmid为该SHM在内核中记录所有SHM信息的数组的下标,因此通过迭代所有的下标可以获得系统中所有SHM的相关信息。
- SHM_LOCK:禁止系统将该SHM交换至swap 分区。
- SHM_UNLOCK:允许系统将该SHM交换至swap分区。
buf
:指向共享内存段数据结构的指针。如果函数执行成功,则返回 0。否则,返回一个非 0 值。
shmctl()
函数用于控制共享内存段。它能够用来分配、挂载、取消挂载或销毁一个共享内存段。例如,可以使用 shmget()
函数获取共享内存段的描述符,然后使用 shmctl()
函数控制该共享内存段。
3. 共享内存相关结构体
3.1 属性信息结构体
IPC_STAT 获得的属性信息被存放在以下结构体中:
struct shmid_ds {
struct ipc_perm shm_perm; /* 权限相关信息 */
size_t shm_segsz; /* 共享内存尺寸(字节) */
time_t shm_atime; /* 最后一次映射时间 */
time_t shm_dtime; /* 最后一个解除映射时间 */
time_t shm_ctime; /* 最后一次状态修改时间 */
pid_t shm_cpid; /* 创建者PID */
pid_t shm_lpid; /* 最后一次映射或解除映射者PID */
shmatt_t shm_nattch; /* 映射该SHM的进程个数 */
};
3.2 权限信息结构体
struct ipc _perm {
key_t _key; /* 该SHM的键值 key */
uid_t uid; /* 所有者的有效UID */
gid_t gid; /* 所有者的有效GID */
uid_t cuid; /* 创建者的有效UID */
gid_t cgid; /* 创建者的有效GID */
unsigned short mode; /* 读写权限 + SHM_DEST + SHM_LOCKED标记 */
unsigned short _seq; /* 序列号 */
};
当使用IPC_RMID后,上述结构体struct ipc_perm中的成员mode将可以检测出SHM_DEST,但SHM并不会被真正删除,要等到shm_nattch等于0时才会被真正删除。IPC_RMID只是为删除做准备,而不是立即删除。
3.3 自定义结构体
- 当使用IPC_INFO时,需要定义一个如下结构体来获取系统关于共享内存的限制值信息,并且将这个结构体指针强制类型转化为第三个参数的类型。
struct shminfo {
unsigned long shmmax; /* 一块SHM的尺寸最大值 */
unsigned long shmmin; /* 一块SHM的尺寸最小值(永远为1) */
unsigned long shmmni; /* 系统中SHM对象个数最大值 */
unsigned long shmseg; /* 一个进程能映射的SHM个数最大值 */
unsigned long shmall; /* 系统中SHM使用的内存页数最大值 */
}
- 使用选项SHM_INFO时,必须保证宏_GNU_SOURCE有效。获得的相关信息被存放在如下结构体当中:
struct shm_info {
int used_ids; /* 当前存在的SHM个数 */
unsigned long shm_tot; /* 所有SHM占用的内存页总数 */
unsigned long shm_rss; /* 当前正在使用的SHM内存页个数 */
unsigned long shm_swp; /* 被置入交换分区的SHM个数 */
unsigned long swap_attempts; /* 已废弃 */
unsigned long swap_successes; /* 已废弃 */
};
3.4 注意
选项SHM_LOCK不是锁定读写权限,而是锁定SHM能否与swap分区发生交 换。一个 SHM被交换至swap分区后如果被设置了SHM_LOCK,那么任何访问这个SHM的进程都将会遇到页错误。进程可以通过IPC_STAT后得到的mode 来检测SHM_LOCKED信息。
4. 示例代码
下面的示例代码 a 可以向 b 发送消息。
- head.h
#ifndef _HEAD_H_
#define _HEAD_H_
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH "./"
#define PROJ_ID 1
#define HAVA_DATA 0x01
#define NO_DATA 0x02
#define SHMSIZE 1024 //共享内存的尺寸
#endif
- a.c
#include "head.h"
int main(int argc, char *argv[])
{
//获取一个IPC对象key
key_t key = ftok(PATH, PROJ_ID);
//获取一个共享内存的ID
int shmid = shmget(key, SHMSIZE, IPC_CREAT|0666);
if(shmid == -1) {
perror("shmget() fail");
exit(0);
}
//将共享内存映射到本进程空间
char *p = shmat(shmid, NULL, 0);
if(p == (void *)-1) {
perror("shmget() fail");
exit(0);
}
//初始无数据
*p = NO_DATA;
//给共享内存写入数据
while(1) {
fgets(p+1, SHMSIZE-1, stdin); //从标准输入获取最多不超过SHMSIZE-1大小的数据放入p+1所指向的内存空间
if( !strncmp(p+1, "quit", 4) )
break;
//当数据已经写完,改变当前内存的状态
*p = HAVA_DATA;
}
//释放共享内存
shmdt(p);
return 0;
}
- b.c
#include "head.h"
int main(int argc, char *argv[])
{
//获取一个IPC对象key
key_t key = ftok(PATH, PROJ_ID);
//获取一个共享内存的ID
int shmid = shmget(key, SHMSIZE, IPC_CREAT|0666);
if(shmid == -1) {
perror("shmget() fail");
exit(0);
}
//将共享内存映射到本进程空间
char *p = shmat(shmid, NULL, 0);
if(p == (void *)-1) {
perror("shmget() fail");
exit(0);
}
//从共享内存读取数据
while(1) {
while(*p != HAVA_DATA)/*empty*/; //如果没有数据,就阻塞等待数据被写入
printf("%s\n", p+1);
if( !strncmp(p+1, "quit", 4) )
break;
*p = NO_DATA;
}
//释放共享内存
shmdt(p);
return 0;
}