
目录
2.3 shmat()函数——将共享内存段附加到进程的地址空间
3.1 shm_open()函数——创建或打开一个共享内存对
3.3 truncate()和ftruncate()——将文件缩放到指定大小
3.4 mmap()函数——将一组设备或者文件映射到内存地址
1. 概述
共享内存是嵌入式 Linux 系统中一种非常重要且高效的进程间通信机制。它允许多个不相关的进程访问同一块物理内存区域,从而实现大规模数据的快速共享。
其核心思想是,在物理内存中开辟一块区域,映射到多个进程各自的虚拟地址空间。这样,一个进程写入的数据,另一个进程立刻就能看到,无需任何形式的数据拷贝。
共享内存是存在于内核级别的一种资源,在shell中可以使用ipcs命令来查看当前系统 IPC 中的状态,在文件系统/proc 目录下有对其描述的相应文件:

共享内存的实现方式有两种标准:System V IPC 和 POSIX IPC。
2. System V IPC
这是比较传统但广泛使用的方法。关键步骤:
- ftok:生成一个唯一的键值。
- shmget:根据键值创建或获取共享内存段。可以指定大小和权限。
- shmat:将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。
- 读写操作:通过 shm_addr 指针像操作普通内存一样进行读写。
- shmdt:进程分离共享内存段。
- shmctl:用于控制共享内存段,例如删除它(IPC_RMID)。
2.1 shmget()函数——创建或打开共享内存
创建或打开一块共享内存区。
表头文件:
#include<sys/shm.h>
函数定义:
/* Get shared memory segment. */
extern int shmget (key_t __key, size_t __size, int __shmflg) __THROW;
- key:用于唯一标识共享内存段的键值;其有三种创建方式:
①使用 IPC_PRIVATE(推荐用于亲缘进程)
②使用 ftok() 生成键值(推荐用于无亲缘进程)
③使用固定整数值 - size:请求的共享内存段大小(字节),如果是创建新段,必须指定 size > 0,如果是获取已存在的段,size 被忽略(但必须 ≤ 已存在段的大小),实际分配的大小会被向上取整到系统页大小的整数倍;
- shmflg:控制共享内存的创建和访问权限,通过位或
|组合,其可选标志位如下。
| 标志位 | 值(十六进制) | 说明 |
|---|---|---|
| IPC_CREAT | 0x200 | 创建新段或获取已存在段 |
| IPC_EXCL | 0x400 | 与 IPC_CREAT 配合,确保创建新段 |
| SHM_HUGETLB | 0x4000 | 使用大页内存 |
| SHM_NORESERVE | 0x1000 | 不预留交换空间 |
| SHM_HUGE_2MB | (21 << 26) | 2MB 大页 |
| SHM_HUGE_1GB | (30 << 26) | 1GB 大页 |
| S_IRUSR | 0x100 | 用户读权限 |
| S_IWUSR | 0x080 | 用户写权限 |
| S_IRGRP | 0x020 | 组读权限 |
| S_IWGRP | 0x010 | 组写权限 |
| S_IROTH | 0x004 | 其他用户读权限 |
| S_IWOTH | 0x002 | 其他用户写权限 |
函数的返回值,如果成功则返回内存IP,如果失败则返回-1。
下面我们来使用shmget()函数来创建一块共享内存,这里我们使用IPC_PRIVATE进行创建:
#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#define BUFF 4096
int main(int argc, char const *argv[])
{
system("ipcs -m");//查看当前共享内存
//共享内存标识符
int shm_id;
shm_id = shmget(IPC_PRIVATE,BUFF,IPC_CREAT | 0666);
if(shm_id < 0)//创建共享内存失败
{
perror("shmget");
exit(1);
}
printf("成功创建共享内存:%d\n",shm_id);
system("ipcs -m");//查看当前共享内存
return 0;
}
编写Makefile:
cc := gcc
shmget_test : shmget_test.c
-$(cc) -o $@ $^
-./$@
-rm ./$@
可以看到创建了一个4096的共享内存:

对于使用ftok()生成键值,部分代码如下:
#include <sys/ipc.h>
key_t key = ftok("/some/existing/file", 'A');
shm_id = shmget(key, BUFF, IPC_CREAT | 0666);
对于使用固定数值(需要确保选择的键值不会与其他应用冲突):
#define MY_SHM_KEY 0x1234
// 或
#define MY_SHM_KEY 5678
shm_id = shmget(MY_SHM_KEY, BUFF, IPC_CREAT | 0666);
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| IPC_PRIVATE | 永不冲突,简单安全 | 只能亲缘进程使用 | 父子进程通信 |
| ftok() | 标准方法,无亲缘关系可用 | 依赖文件存在性 | 任意进程通信 |
| 固定值 | 完全控制,性能好 | 可能冲突,需要协调 | 内部系统,测试 |
2.2 shmctl()函数——删除共享内存
共享内存与消息队列以及信号量相同,在使用完毕后都应该进行释放,另外,当调用fork(函数创建子进程时,子进程会继承父进程已绑定的共享内存;当调用exec函数更改子进程功能以及调用exit()函数时,子进程中都会解除与共享内存的映射关系,因此在必要时仍应使用shmctl0函数对共享内存进行删除。
表头文件:
#include <sys/shm.h>
函数定义:
/* The following System V style IPC functions implement a shared memory
facility. The definition is found in XPG4.2. */
/* Shared memory control operation. */
#ifndef __USE_TIME_BITS64
extern int shmctl (int __shmid, int __cmd, struct shmid_ds *__buf) __THROW;
#else
- shmid:共享内存标识符,指定要操作的共享内存段,其来源由 shmget() 函数返回的正整数;
- cmd:控制命令,指定要执行的操作类型;
- buf:缓冲区指针,根据不同的 cmd,此参数有不同的用途。
其中对于cmd的一些类型:
| 命令 | 参数类型 | 作用 | 返回值 |
|---|---|---|---|
| IPC_STAT | struct shmid_ds * | 获取段状态 | 0/-1 |
| IPC_SET | struct shmid_ds * | 设置段参数 | 0/-1 |
| IPC_RMID | NULL | 标记删除段 | 0/-1 |
| IPC_INFO | struct shminfo * | 获取系统限制 | 最大索引/-1 |
| SHM_INFO | struct shm_info * | 获取使用统计 | 最大索引/-1 |
| SHM_STAT | struct shmid_ds * | 通过索引获取信息 | shmid/-1 |
| SHM_LOCK | NULL | 锁定内存 | 0/-1 |
| SHM_UNLOCK | NULL | 解除锁定 | 0/-1 |
我们创建在删除:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
int main()
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
printf("创建共享内存: %d\n", shmid);
system("ipcs -m");
// 删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
printf("已标记删除共享内存\n");
system("ipcs -m");
return 0;
}
可以看到上面创建了32829,下面给删除了:

我们也可以直接输入相应的键值进行删除,如我们想要删除上方的32811,直接输进去即可:
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
int main()
{
system("ipcs -m");
// 删除共享内存
if (shmctl(32811, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
printf("已标记删除共享内存\n");
system("ipcs -m");
return 0;
}

2.3 shmat()函数——将共享内存段附加到进程的地址空间
将共享内存段“附加”到当前进程的地址空间,返回其虚拟地址。
表头文件:
#include <sys/shm.h>
函数定义:
/* Attach shared memory segment. */
extern void *shmat (int __shmid, const void *__shmaddr, int __shmflg)
- shmid:共享内存段标识符,必须是已存在的共享内存段 ID,由 shmget() 函数返回的有效标识符;
- shmaddr:指定期望的附加地址,NULL(推荐)让内核自动选择可用地址,或者尝试在指定地址附加;
- shmflg:控制附加行为的标志位,常用标志位如下。
| 标志 | 值 | 说明 |
|---|---|---|
0 | 0 | 默认,读写方式附加 |
SHM_RDONLY | 010000 | 只读方式附加 |
SHM_RND | 020000 | 自动对齐 shmaddr |
SHM_REMAP | 040000 | 强制重新映射(Linux 扩展) |
SHM_EXEC | 0100000 | 内存可执行(Linux 扩展) |
其返回值,若是成功则返回实际引入的地址,如果失败,则返回-1。
编写代码:
#include<sys/shm.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
int main()
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0666);
printf("创建共享内存: %d\n", shmid);
system("ipcs -m");
char* shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
printf("共享内存附加成功 at: %p\n", shm_addr);
system("ipcs -m");
// 删除共享内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
printf("已标记删除共享内存\n");
system("ipcs -m");
return 0;
}
我们首先成功创建 shmid=65554,其连接数为0,状态正常,而后成功附加到地址0x7dd226f2d000,此时的连接数从0变为1,当我们删除65554后,其状态变为"目标",连接数仍为1(因为进程还未分离),当整个工程结束,我们通过 ipcs 查看可以发现已经删除过了:

2.4 shmdt()函数——进程分离共享内存段
头文件:
#include <sys/shm.h>
函数原型:
/* Detach shared memory segment. */
extern int shmdt (const void *__shmaddr) __THROW;
shmaddr:要分离的共享内存地址指针,必须是 shmat() 函数返回的有效地址,必须是当前进程已附加的共享内存地址。
成功返回 0,失败返回-1。
基础用法,往共享内存中写入数据:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/shm.h>
int main() {
int shmid;
char *shm_addr;
// 创建共享内存
shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
printf("创建共享内存成功: shmid=%d\n", shmid);
// 附加共享内存
shm_addr = shmat(shmid, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat");
exit(EXIT_FAILURE);
}
printf("共享内存附加成功 at: %p\n", shm_addr);
// 使用共享内存
strcpy(shm_addr, "Hello, Shared Memory!");
printf("写入的数据: %s\n", shm_addr);
// 分离共享内存 - 核心调用
if (shmdt(shm_addr) == -1) {
perror("shmdt");
exit(EXIT_FAILURE);
}
printf("共享内存分离成功\n");
// 删除共享内存段
shmctl(shmid, IPC_RMID, NULL);
return 0;
}

3. POSIX IPC
这是更现代、符合 POSIX 标准的方法,推荐在新项目中使用,关键步骤:
- shm_open:使用一个名字(如 /my_shm)来创建或打开一个共享内存对象。它返回一个文件描述符。
- ftruncate:设置共享内存对象的大小。
- mmap:将共享内存对象映射到进程的地址空间。
- 读写操作:通过 shm_addr 指针进行。
- munmap:解除内存映射。
- close:关闭文件描述符。
- shm_unlink:删除共享内存对象(当所有进程都关闭后,内核会释放资源)。
3.1 shm_open()函数——创建或打开一个共享内存对
头文件:
#include <sys/mman.h>
函数原型:
/* Open shared memory segment. */
extern int shm_open (const char *__name, int __oflag, mode_t __mode);
- name:共享内存对象的名字,其名字长度有限制(通常最多 255 字符),格式应该以 / 开头,如 "/my_shm";
- oflag:打开标志,控制创建和打开行为;
O_RDONLY // 只读
O_RDWR // 读写
O_CREAT // 如果不存在则创建
O_EXCL // 与 O_CREAT 一起使用,如果已存在则失败
O_TRUNC // 如果已存在,将其截断为0长度
- mode:权限模式(当创建新对象时使用),八进制权限,类似文件权限。
3.2 shm_unlink()函数——删除共享内存
删除一个先前由 shm_open() 创建的命名共享内存对象。尽管这个函数被称为“unlink”,但它并没有真正删除共享内存段本身,而是移除了与共享内存对象关联的名称,使得通过该名称无法再打开共享内存。当所有已打开该共享内存段的进程关闭它们的描述符后,系统才会真正释放共享内存资源。
头文件:
#include <sys/mman.h>
函数原型:
/* Remove shared memory segment. */
extern int shm_unlink (const char *__name);
- name: 要删除的共享内存对象名称
3.3 truncate()和ftruncate()——将文件缩放到指定大小
truncate和ftruncate都可以将文件缩放到指定大小,二者的行为类似:如果文件被缩小,截断部分的数据丢失,如果文件空间被放大,扩展的部分均为\0字符。缩放前后文件的偏移量不会更改。缩放成功返回0,失败返回-1。
不同的是,前者需要指定路径,而后者需要提供文件描述符;ftruncate缩放的文件描述符可以是通过shm_open()开启的内存对象,而truncate缩放的文件必须是文件系统已存在文件,若文件不存在或没有权限则会失败。
#include <unistd.h>
#include <sys/types.h>
/**
* 将指定文件扩展或截取到指定大小
*
* char *path: 文件名 指定存在的文件即可 不需要打开
* off_t length: 指定长度 单位字节
* return: int 成功 0
* 失败 -1
*/
int truncate(const char *path, off_t length);
/**
* 将指定文件描述符扩展或截取到指定大小
*
* int fd: 文件描述符 需要打开并且有写权限
* off_t length: 指定长度 单位字节
* return: int 成功 0
* 失败 -1
*/
int ftruncate(int fd, off_t length);
3.4 mmap()函数——将一组设备或者文件映射到内存地址
mmap系统调用可以将一组设备或者文件映射到内存地址,我们在内存中寻址就相当于在读取这个文件指定地址的数据。父进程在创建一个内存共享对象并将其映射到内存区后,子进程可以正常读写该内存区,并且父进程也能看到更改。使用man 2 mmap查看该系统调用声明:
#include <sys/mman.h>
/**
* 将文件映射到内存区域,进程可以直接对内存区域进行读写操作,就像操作普通内存一样,但实际上是对文件或设备进行读写,从而实现高效的 I/O 操作
*
* void *addr: 指向期望映射的内存起始地址的指针,通常设为 NULL,让系统选择合适的地址
* size_t length: 要映射的内存区域的长度,以字节为单位
* int prot: 内存映射区域的保护标志,可以是以下标志的组合
* (1) PROT_READ: 允许读取映射区域
* (2) PROT_WRITE: 允许写入映射区域
* (3) PROT_EXEC: 允许执行映射区域
* (4) PROT_NONE: 页面不可访问
* int flags:映射选项标志
* (1) MAP_SHARED: 映射区域是共享的,对映射区域的修改会影响文件和其他映射到同一区域的进程(一般使用共享)
* (2) MAP_PRIVATE: 映射区域是私有的,对映射区域的修改不会影响原始文件,对文件的修改会被暂时保存在一个私有副本中
* (3) MAP_ANONYMOUS: 创建一个匿名映射,不与任何文件关联
* (4) MAP_FIXED: 强制映射到指定的地址,如果不允许映射,将返回错误
* int fd: 文件描述符,用于指定要映射的文件或设备,如果是匿名映射,则传入无效的文件描述符(例如-1)
* off_t offset: 从文件开头的偏移量,映射开始的位置
* return void*: (1) 成功时,返回映射区域的起始地址,可以像操作普通内存那样使用这个地址进行读写
* (2) 如果出错,返回 (void *) -1,并且设置 errno 变量来表示错误原因
*/
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
/**
* 用于取消之前通过 mmap() 函数建立的内存映射关系
*
* void *addr: 这是指向之前通过 mmap() 映射的内存区域的起始地址的指针,这个地址必须是有效的,并且必须是 mmap() 返回的有效映射地址
* size_t length: 这是要解除映射的内存区域的大小(以字节为单位),它必须与之前通过 mmap() 映射的大小一致
* return: int 成功 0
* 失败 -1
*/
int munmap(void *addr, size_t length);
3.5 总结使用
简单来说就是父进程读取子进程在共享内存写入的数据:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <string.h>
int main()
{
char *share;
pid_t pid;
char shmName[100] = {0};
sprintf(shmName, "/letter%d", getpid());
// 共享内存对象的文件标识符
int fd;
fd = shm_open(shmName, O_CREAT | O_RDWR, 0644);
if (fd < 0)
{
perror("共享内存对象开启失败!\n");
exit(EXIT_FAILURE);
}
// 将该区域扩充为100字节长度
ftruncate(fd, 100);
// 以读写方式映射该区域到内存,并开启父子共享标签 偏移量选择0从头开始
share = mmap(NULL, 100, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 注意:不是p == NULL 映射失败返回的是((void *) -1)
if (share == MAP_FAILED)
{
perror("共享内存对象映射到内存失败!\n");
exit(EXIT_FAILURE);
}
// 映射区建立完毕,关闭读取连接 注意不是删除
close(fd);
// 创建子进程
pid = fork();
if (pid == 0)
{
// 子进程写入数据作为回信
strcpy(share, "我收到信了!\n");
printf("子进程%d完成回信!\n", getpid());
}
else
{
// 等待回信
sleep(1);
printf("父进程%d看到子进程%d回信的内容: %s", getpid(), pid, share);
// 等到子进程运行结束
wait(NULL);
// 释放映射区
int ret = munmap(share, 100);
if (ret == -1)
{
perror("munmap");
exit(EXIT_FAILURE);
}
}
// 删除共享内存对象
shm_unlink(shmName);
return 0;
}




被折叠的 条评论
为什么被折叠?



