跳到主要内容

共享内存(SHM)

1. 基本概念

Linux 系统中的共享内存是一种 IPC(进程间通信)机制,允许多个进程共享同一块内存区域。这种机制可以让多个进程可以直接在内存中读写数据,避免了数据在多个进程间来回拷贝的性能损失。

Linux 系统中的共享内存是基于内存映射文件(memory mapped file)实现的。这种机制允许将一个文件映射到进程的虚拟地址空间,然后在该虚拟地址空间中直接对文件进行读写操作。如果多个进程都将同一个文件映射到了它们的虚拟地址空间中,那么它们就可以直接在内存中读写共享的数据。

使用共享内存的流程如下:

  1. 创建一个内存映射文件。
  2. 使用 shmget() 函数创建一个共享内存段。
  3. 使用 shmat() 函数将该共享内存段映射到进程的虚拟地址空间中。
  4. 在该共享内存段中读写数据。
  5. 使用 shmdt() 函数取消映射。
  6. 使用 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;
}