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. 共享内存示例

使用共享内存的一般步骤是:

  1. 创建或获取共享内存ID。

  2. 将共享内存映射至本进程虚拟内存空间的某个区域。

  3. 当不再使用时,解除映射关系。

  4. 当没有进程再需要这块共享内存时,删除它。

共享内存由于其特性,与进程中的其他内存段在使用习惯上有些不同。一般进程对栈空间分配可以自动回收, 而堆空间通过malloc申请,free回收,这些内存在回收之后就可以认为是不存在了。但是共享内存不同, 用shmdt()函数解除映射后,实际上其占用的内存还在,并仍然可以使用shmat映射使用。 如果不使用shmctl()函数删除这个共享内存的话,那么它将一直保留直到系统被关闭,除此之外, 我们应该配合信号量去使用共享内存,避免多进程间的随意使用造成数据踩踏。

整个实验的思路是:首先创建system V信号量用于控制临界区,然后实现两个进程, 分别为共享内存写进程,共享内存读进程,在写进程中实现写数据,在读进程中将数据读取,并且打印出来,代码如下:

6.4.1. 共享内存写进程

共享内存写进程(base_code/system_programing/shm_write/sources/shm_write.c文件)
 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. 共享内存读进程

共享内存读进程(base_code/system_programing/shm_read/sources/shm_read.c文件)
 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] 即可删除。