6. 共享内存¶
共享内存是System-V IPC家族的核心组件,也是效率最高的进程间通信方式, 主打大批量数据高速传输,本身无同步互斥机制,需配合System-V信号量使用。
6.1. 共享内存基本概念¶
共享内存是内核开辟的物理内存区域,允许多个进程将同一块物理内存,映射到自身的虚拟地址空间中, 实现进程间直接内存访问。相较于管道、消息队列需内核中转拷贝数据,共享内存跳过内核拷贝环节, 进程直接读写内存,通信效率大幅提升。
共享内存的思想非常简单,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的,但是可以通过某些方式, 使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样的效果就相当于多个进程的虚拟内存空间部分重叠在一起, 如下图所示:
当进程1向共享内存写入数据后,共享内存的数据就变化了,那么进程2就能立即读取到变化了的数据, 而这中间并未经过内核的拷贝,因此效率极高。
6.1.1. 核心特性¶
高效性:无需内核中转拷贝,是Linux最快的IPC方式,适配大批量数据传输;
无同步互斥:内核不提供同步机制,多进程并发读写易引发数据践踏,必须配合信号量/互斥锁使用;
全进程适配:支持任意亲缘/非亲缘进程通信,无进程关系限制;
内核持续性:生命周期独立于进程,需手动删除,否则直至系统重启才释放;
字节流操作:无固定数据格式,由进程自行定义数据结构,灵活性高。
共享内存的优缺点:
维度 |
描述 |
|---|---|
优点 |
传输速度极快,无内核拷贝开销 |
优点 |
接口简单,内存操作与malloc一致 |
优点 |
支持任意进程间通信 |
缺点 |
无内置同步互斥,需额外组件(信号量/互斥锁)配合 |
缺点 |
数据无格式约束,易发生内存越界问题 |
缺点 |
内核资源需手动释放,若遗漏易造成资源泄漏 |
6.2. 共享内存应用场景¶
大批量数据传输:图像、视频、日志等大数据流的进程间高速传递;
多进程数据共享:配置共享、状态共享,避免重复数据拷贝;
高性能进程协作:后台服务与数据处理进程的实时数据交互;
嵌入式系统IPC:资源受限场景下,轻量化高速进程通信。
6.3. 共享内存函数说明¶
System-V共享内存依托key标识、内核管理,配套ftok、shmget、shmat、shmdt、shmctl五大接口, 操作逻辑与消息队列、信号量完全统一。
6.3.1. ftok函数¶
ftok用于通过文件路径+项目ID生成唯一key,避免手动指定key的冲突问题,是消息队列开发的规范前置操作。
1 2 3 4 5 6 7 8 9 | #include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
// 参数:
// pathname:存在的文件路径
// proj_id:项目ID,8位非0值
// 返回值:成功返回key_t类型键值,失败返回-1
|
路径的作用:提供一个 “稳定的唯一标识源”——路径对应的文件/目录在文件系统中有唯一的inode号;
proj_id的作用:同一文件可以通过不同proj_id生成不同key,比如一个文件对应多个IPC资源。
6.3.2. shmget函数¶
shmget函数用于创建新共享内存或获取已有共享内存,返回shmid(共享内存标识符),是操作入口。函数原型如下:
1 2 3 4 | #include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
|
参数说明:
key:标识共享内存的键值,可以有以下取值:
0 或 IPC_PRIVATE。当key的取值为IPC_PRIVATE,则函数shmget()创建一块新的共享内存; 如果key的取值为0,而参数shmflg中设置了IPC_PRIVATE这个标志,则同样将创建一块新的共享内存。
大于0的32位整数:视参数shmflg来确定操作。
size:要创建共享内存的大小,所有的内存分配操作都是以页为单位的,所以即使只申请只有一个字节的内存, 内存也会分配整整一页。
shmflg:表示创建的共享内存的模式标志参数,在真正使用时需要与IPC对象存取权限mode(如0600)进行“|”运算来确定共享内存的存取权限。 msgflg有多种情况:
IPC_CREAT:如果内核中不存在关键字与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存,返回此共享内存的标识符。
IPC_EXCL:如果内核中不存在键值与key相等的共享内存,则新建一个共享内存;如果存在这样的共享内存则报错。
SHM_HUGETLB:使用“大页面”来分配共享内存,所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认尺寸(4KB)更大的分页,以减少缺页中断。Linux内核支持以2MB作为物理页面分页的基本单位。
SHM_NORESERVE:不在交换分区中为这块共享内存保留空间。
返回值:shmget()函数的返回值是共享内存的ID。
当调用shmget()函数失败时将产生错误代码,有如下取值:
EACCES:key指定的共享内存已存在,但调用进程没有权限访问它
EEXIST:key指定的共享内存已存在,而msgflg中同时指定IPC_CREAT和IPC_EXCL标志
EINVAL:创建共享内存时参数size小于SHMMIN或大于SHMMAX。
ENFILE:已达到系统范围内打开文件总数的限制。
ENOENT:给定的key不存在任何共享内存,并且未指定IPC_CREAT。
ENOMEM:内存不足,无法为共享内存分配内存。
6.3.3. shmat函数¶
shmat函数用于将内核共享内存映射到当前进程的虚拟地址空间,映射成功后可像普通内存一样读写。函数原型如下:
1 2 3 4 | #include <sys/ipc.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
|
参数说明:
shmid:共享内存ID,通常是由shmget()函数返回的。
shmaddr:如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域, 如果为NULL,则系统会自动选择一个合适的虚拟内存空间地址去映射共享内存。
shmflg:操作共享内存的方式:
SHM_RDONLY:以只读方式映射共享内存。
SHM_REMAP:重新映射,此时shmaddr不能为NULL。
NULLSHM:自动选择比shmaddr小的最大页对齐地址。
shmat()函数调用成功后返回共享内存的起始地址,这样我们就能操作这个共享内存了。
共享内存的映射有以下注意的要点:
共享内存只能以只读或者可读写方式映射,无法以只写方式映射。
shmat()第二个参数shmaddr一般都设为NULL,让系统自动找寻合适的地址。但当其确实不为空时, 那么要求SHM_RND在shmflg必须被设置,这样的话系统将会选择比shmaddr小而又最大的页对齐地址(即为SHMLBA的整数倍)作为共享内存区域的起始地址。 如果没有设置SHM_RND,那么shmaddr必须是严格的页对齐地址。
6.3.4. shmdt函数¶
shmdt函数与shmat函数相反,是用来解除进程与共享内存之间的映射的,在解除映射后, 该进程不能再访问这个共享内存。函数原型如下:
1 2 3 4 | #include <sys/ipc.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
|
参数说明:
shmaddr:映射的共享内存的起始地址。
shmdt()函数调用成功返回0,如果出错则返回-1,并且将错误原因存于errno中。
虽然shmdt()函数很简单,但是还是有注意要点的:该函数并不删除所指定的共享内存区, 而只是将先前用shmat()函数映射好的共享内存脱离当前进程,共享内存还是存在于物理内存中。
6.3.5. shmctl函数¶
shmctl函数用于获取属性、设置参数、删除共享内存,是释放内核资源的核心接口,必须调用避免资源泄漏。函数原型:
1 2 3 4 | #include <sys/ipc.h>
#include <sys/shm.h>
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:共享内存属性信息结构体指针,设置或者获取信息都通过该结构体,shmid_ds结构如下:
注意:选项SHM_LOCK不是锁定读写权限,而是锁定SHM能否与swap分区发生交换。 一个SHM被交换至swap分区后如果被设置了SHM_LOCK,那么任何访问这个SHM的进程都将会遇到页错误。 进程可以通过IPC_STAT后得到的mode来检测SHM_LOCKED信息。
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的进程个数 */
...
};
其中权限信息结构体如下:
struct ipc_perm {
key_t __key; /* 该共享内存的键值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; /* 序列号 */
};
6.4. 共享内存示例¶
使用共享内存的一般步骤是:
创建或获取共享内存ID。
将共享内存映射至本进程虚拟内存空间的某个区域。
当不再使用时,解除映射关系。
当没有进程再需要这块共享内存时,删除它。
共享内存由于其特性,与进程中的其他内存段在使用习惯上有些不同。一般进程对栈空间分配可以自动回收, 而堆空间通过malloc申请,free回收,这些内存在回收之后就可以认为是不存在了。但是共享内存不同, 用shmdt()函数解除映射后,实际上其占用的内存还在,并仍然可以使用shmat映射使用。 如果不使用shmctl()函数删除这个共享内存的话,那么它将一直保留直到系统被关闭,除此之外, 我们应该配合信号量去使用共享内存,避免多进程间的随意使用造成数据踩踏。
整个实验的思路是:首先创建system V信号量用于控制临界区,然后实现两个进程, 分别为共享内存写进程,共享内存读进程,在写进程中实现写数据,在读进程中将数据读取,并且打印出来,代码如下:
6.4.1. 共享内存写进程¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | #include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
// 定义ftok的公共路径和项目ID
#define FTOK_PATH "/opt" // 定义ftok的路径,确保存在
#define FTOK_SHM_PROJ_ID 10 // 自定义共享内存proj_id
#define FTOK_SEM_PROJ_ID 66 // 自定义信号量专属proj_id
#define SHM_SIZE 4096 // 共享内存大小
int main()
{
int running = 1;
void *shm = NULL;
char buffer[BUFSIZ + 1]; // 用于保存输入的文本
int shmid;
int semid; // 信号量标识符
key_t shm_key, sem_key; // 共享内存/信号量的ftok键值
// ========== 1. 生成共享内存的IPC键值 ==========
if ((shm_key = ftok(FTOK_PATH, FTOK_SHM_PROJ_ID)) == -1) {
fprintf(stderr, "ftok for shm failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Generate shm key via ftok: 0x%x\n", shm_key);
// 创建共享内存
shmid = shmget(shm_key, SHM_SIZE, 0644 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// 将共享内存连接到当前进程的地址空间
shm = shmat(shmid, (void*)0, 0);
if (shm == (void*)-1) {
fprintf(stderr, "shmat failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Memory attached at %p\n", shm);
// ========== 2. 生成信号量的IPC键值 ==========
if ((sem_key = ftok(FTOK_PATH, FTOK_SEM_PROJ_ID)) == -1) {
fprintf(stderr, "ftok for sem failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Generate sem key via ftok: 0x%x\n", sem_key);
// 打开/创建信号量
semid = semget(sem_key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
fprintf(stderr, "semget failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// ========== 3. 向共享内存写数据 ==========
while (running) {
// 读取用户输入
printf("Enter some text: ");
if (fgets(buffer, BUFSIZ, stdin) == NULL) {
fprintf(stderr, "fgets failed\n");
running = 0;
continue;
}
// 直接拷贝数据到共享内存
// 注意:strncpy第三个参数是“最多拷贝的字节数”,减1留位置给字符串结束符
strncpy((char*)shm, buffer, SHM_SIZE - 1);
// 强制添加字符串结束符,避免内存中无终止符导致读端乱码
((char*)shm)[SHM_SIZE - 1] = '\0';
sem_v(semid); // 释放信号量,通知读端读取
// 输入了end,退出循环
if (strncmp(buffer, "end", 3) == 0) {
running = 0;
}
}
// ========== 4. 资源释放 ==========
// 把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
sleep(2);
exit(EXIT_SUCCESS);
}
|
代码说明如下:
第38行,调用shmget()创建或获取一个大小为4096的共享内存。
第45行,调用shmat()函数映射共享内存到当前进程,地址保存到shm指针。
第78行,使用strncpy函数把用户输入得到的字符拷贝至共享内存shm中。
代码中写入到共享内存后,通过释放信号量操作告知其它进程有可获取的资源,这是常用的共享内存临界段保护方法。
6.4.2. 共享内存读进程¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | #include <sys/types.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/ipc.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include "sem.h"
// 定义ftok公共路径和不同项目ID
#define FTOK_PATH "/opt" // 定义ftok的路径,确保存在
#define FTOK_SHM_PROJ_ID 10 // 自定义共享内存proj_id
#define FTOK_SEM_PROJ_ID 66 // 自定义信号量专属proj_id
#define SHM_SIZE 4096 // 共享内存大小
int main(void)
{
int running = 1; // 程序是否继续运行的标志
char *shm = NULL; // 分配的共享内存的原始首地址
int shmid; // 共享内存标识符
int semid; // 信号量标识符
key_t shm_key, sem_key; // ftok生成的共享内存/信号量键值
// ========== 1. 生成共享内存IPC键值 ==========
if ((shm_key = ftok(FTOK_PATH, FTOK_SHM_PROJ_ID)) == -1) {
fprintf(stderr, "ftok for shm failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Generate shm key via ftok: 0x%x\n", shm_key);
// 创建/获取共享内存
shmid = shmget(shm_key, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
fprintf(stderr, "shmget failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// 将共享内存连接到当前进程的地址空间
shm = shmat(shmid, 0, 0);
if (shm == (void*)-1) {
fprintf(stderr, "shmat failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("\nMemory attached at %p\n", shm);
// ========== 2. 生成信号量IPC键值 ==========
if ((sem_key = ftok(FTOK_PATH, FTOK_SEM_PROJ_ID)) == -1) {
fprintf(stderr, "ftok for sem failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Generate sem key via ftok: 0x%x\n", sem_key);
// 打开/创建信号量
semid = semget(sem_key, 1, 0666 | IPC_CREAT);
if (semid == -1) {
fprintf(stderr, "semget failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
init_sem(semid, 0); // 初始化信号量
// ========== 3. 读取共享内存数据 ==========
while (running) {
// 等待信号量(P操作:写端释放后才读取)
if (sem_p(semid) == 0) {
printf("You wrote: %s", shm);
sleep(rand() % 3); // 模拟处理延迟
// 输入了end,退出循环
if (strncmp(shm, "end", 3) == 0) {
running = 0;
}
}
}
// ========== 4. 资源清理 ==========
del_sem(semid); /** 删除信号量 */
// 把共享内存从当前进程中分离
if (shmdt(shm) == -1) {
fprintf(stderr, "shmdt failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
// 删除共享内存
if (shmctl(shmid, IPC_RMID, 0) == -1) {
fprintf(stderr, "shmctl(IPC_RMID) failed: %s\n", strerror(errno));
exit(EXIT_FAILURE);
}
printf("Shared memory and semaphore deleted successfully.\n");
exit(EXIT_SUCCESS);
}
|
代码说明如下:
第37行,调用shmget()创建或获取一个大小为4096的共享内存。
第44行,调用shmat()函数映射共享内存到当前进程,地址保存到shm指针。
第68-79行,使用sem_p等待信号量,获取到信号量后,直接使用printf函数打印出共享内存shm的内容。
6.4.3. 实验操作¶
本示例代码在system_programing/shm_write和shm_read目录下, 分别编译并且运行即可,现象如下:
写进程:
在写进程中可以输入任何信息,当输入end表示结束,此时共享内存将被删除。
1 2 3 4 5 6 7 8 9 10 11 | # 以下操作在 system_programing/shm_write代码目录进行
make
# 运行
./build/shm_write_demo
# 信息输出如下
Generate shm key via ftok: 0xa020001
Memory attached at 0x7f00ecd96000
Generate sem key via ftok: 0x42020001
Enter some text:
|
读进程:
打开一个 新终端 ,切换至shm_read目录编译并运行:
1 2 3 4 5 6 7 8 9 10 | # 以下操作在 system_programing/shm_read代码目录进行
make
# 运行
./build/shm_read_demo
# 信息输出如下
Generate shm key via ftok: 0xa020001
Memory attached at 0x7f301853d000
Generate sem key via ftok: 0x42020001
|
在写进程的终端输入一些内容,然后再查看读进程的终端是否有信息打印,最后在写进程的终端输入end结束。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | # 在写进程的终端输入
Generate shm key via ftok: 0xa020001
Memory attached at 0x7f00ecd96000
Generate sem key via ftok: 0x42020001
Enter some text: 你好
Enter some text: test
Enter some text: hello world
Enter some text:
# 在读进程的终端输出信息如下
Generate shm key via ftok: 0xa020001
Memory attached at 0x7f301853d000
Generate sem key via ftok: 0x42020001
You wrote: 你好
You wrote: test
You wrote: hello world
# 在写进程的终端输入end结束
Enter some text: end
# 在读进程的终端输出信息如下
You wrote: end
Shared memory and semaphore deleted successfully.
|
小技巧
在本例子中,若发送进程不是通过end字符退出(如Ctrl+C或Ctrl+D),则不会触发读进程主动删除共享内存,
在这种情况下可通过 ipcs -m 命令查看到该共享内存依然存在,通过 ipcrm -m [共享内存shmid] 即可删除。
