进程间通信方式—共享内存(System V IPC 和POSIX)

System V IPC共享内存

共享内存的基本原理

  • 共享内存是一种进程间通信(IPC)机制。当多个进程需要共享数据时,可以创建一块共享内存区域。通过shmget函数创建共享内存段后,这块内存区域就存在于系统的内存空间中。
  • 然后,各个进程可以通过shmat函数将这块共享内存连接到自己的进程地址空间。连接后,进程就可以像访问自己的本地内存一样访问共享内存中的数据。

共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输,这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件,因此,共享内存通常和其他进程间通信方式一起使用。

Linux共享内存的API都定义在sys/shm.h头文件中,包括4个系统调用shmgetshmatshmdtshmctl

1.shmget 系统调用

shmget 系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。

1
2
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
  1. 参数详解

    • key_t key
      • 这是一个键值,用于标识共享内存段。它可以是由ftok函数生成的值,也可以是IPC_PRIVATE(用于创建私有共享内存段,通常在父子进程间使用)。ftok函数根据一个文件路径和一个项目标识符生成一个唯一的key_t值。
    • size_t size
      • 这个参数指定了要创建或获取的共享内存段的大小,单位是字节。如果是创建新的共享内存段,这个大小必须大于 0。如果shmget函数用于获取已存在的共享内存段,这个参数通常可以被忽略(但仍需提供一个合理的值)。
    • int shmflg
      • 这个参数用于控制共享内存段的创建和访问权限,它由以下几种标志组成:
        • IPC_CREAT:如果共享内存段不存在,则创建它。如果和IPC_EXCL一起使用(IPC_CREAT | IPC_EXCL),则只有在共享内存段不存在时才创建,若已存在则shmget函数返回 - 1 并设置errnoEEXIST
        • IPC_EXCL:与IPC_CREAT一起使用,用于确保创建的是一个新的、唯一的共享内存段。
        • 权限标志:如0666等,用于指定共享内存段的访问权限,格式与文件权限相同(用户、组、其他用户的读、写、执行权限)。
        • SHM_HUGETLB标志:当在shmflg参数中使用SHM_HUGETLB标志时,表示请求使用大页(huge pages)来分配共享内存。大页是一种内存管理技术,它使用比标准内存页更大的页面大小(通常为 2MB 或 1GB,取决于系统配置)。
        • SHM_NORESERVE标志SHM_NORESERVE标志用于在创建共享内存时,不预留交换空间(swap space)。通常情况下,当创建共享内存段时,系统会为其预留相应的交换空间,以确保在内存不足时可以将部分数据交换到磁盘上。使用SHM_NORESERVE可以避免这种交换空间的预留。这样,当物理内存不足时,对该共享内存执行写操作将触发SIGESV信号。
  2. 返回值

    • 成功时,shmget函数返回一个非负整数,即共享内存段的标识符(shmid)。这个标识符可以用于后续的shmat(连接共享内存段到进程地址空间)、shmdt(从进程地址空间分离共享内存段)和shmctl(控制共享内存段的操作,如删除、获取状态等)操作。

    • 失败时,函数返回 - 1,并设置errno

      变量来指示错误原因。常见的错误原因包括:

      • EINVALsize小于SHMMIN(最小共享内存大小)或者key无效。
      • EEXIST:当使用IPC_CREAT | IPC_EXCL标志且共享内存段已经存在时。
      • ENOENT:当key对应的共享内存段不存在且没有使用IPC_CREAT标志时。

如果 shmget 用于创建共享内存,则这段共享内存的所有字节都被初始化为 0,与之关联的内核数据结构 shmid_ds 将被创建并初始化。shmid_ds 结构体的定义如下:

1
2
3
4
5
6
7
8
9
10
11
struct shmid_ds
{
struct ipc_perm shm_perm; // 共享内存的操作权限
size_t shm_segsz; // 共享内存容量大小,单位是字节
time_t shm_atime; // 对这段内存最后一次调用shmat的时间
time_t shm_dtime; // 对这段内存最后一次调用shmdt的时间
time_t shm_ctime; // 对这段内存最后一次调用shmctl的时间
_pid_t shm_cpid; // 创建者的PID
_pid_t shm_lpid; // 最后一次执行shmat或shmdt操作的进程的PID
shmat_t shm_nattach; // 目前关联到此共享内存的进程数量
};

shmget 对 shmid_ds 结构体的初始化包括:

  • 将 shm_perm.cuid 和 shm_perm.uid 设置为调用进程的有效用户 ID。
  • 将 shm_perm.cgid 和 shm_perm.gid 设置为调用进程的有效组 ID。
  • 将 shm_perm.mode 的最低 9 位设置为 shmflg 参数的最低 9 位。
  • 将 shm_segsz 设置为 size。
  • 将 shm_lpid、shm_nattach、shm_atime、shm_dtime 设置为 0。
  • 将 shm_ctime 设置为当前的时间。

2.shmatshmdt 系统调用

共享内存被创建/获取后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中,使用完共享内存后,我们也需要将它从进程地址空间中分离,这两项任务分别由以下两个系统调用实现:

1
2
3
#include <sys/shm.h>
void* shmat(int shm_id, const void* shm_addr, int shmflg);
int shmdt(const void* shm_addr);

1.shamt

1
void* shmat(int shm_id, const void* shm_addr, int shmflg);
  • 功能:

    • 将由shm_id标识的共享内存段连接到调用进程的地址空间。连接成功后,进程可以像访问普通内存一样访问共享内存中的数据。
  • 参数:

    • shm_id
      • 这是共享内存段的标识符,由shmget函数成功创建或获取共享内存段时返回。它唯一标识了一个共享内存段。
    • shm_addr
      • 用于指定连接共享内存段的地址。
      • 如果shm_addrNULL,则由系统选择合适的连接地址。
      • 如果shm_addr不为NULL,并且shmflg中没有设置SHM_RND标志,那么共享内存段将被连接到shm_addr所指定的地址。
      • 如果shm_addr不为NULL,并且shmflg中设置了SHM_RND标志,那么连接的地址将是shm_addr向下舍入到系统页面大小(page - size)的整数倍的地址。
    • shmflg
      • 包含一些控制连接操作的标志。
      • SHM_RND:如上述,用于对连接地址进行舍入操作。
      • SHM_RDONLY:如果设置了这个标志,那么共享内存段将以只读方式连接到进程地址空间。如果不设置此标志,共享内存段将以读写方式连接。
  • 返回值:

    • 成功时,返回指向连接后的共享内存段在进程地址空间中的起始地址的指针。并修改shmid_ds中的字段

      • 将 shm_nattach 加 1。
      • 将 shm_lpid 设置为调用进程的 PID。
      • 将 shm_atime 设置为当前时间。
    • 失败时,返回(void *)-1,并且会设置errno

      来指示错误原因。常见的错误原因包括:

      • EINVALshm_id无效,或者shm_addrshmflg的组合无效。
      • ENOMEM:没有足够的内存来连接共享内存段。

shm_id参数是由shmget函数返回的共享内存标识符。shm_addr参数指定将共享内存关联到进程的哪块地址空间,最终效果还受到shmflg参数的可选标志SHM_RND的影响。

  • 如果 shm_addr 为 NULL,则被关联的地址由操作系统选择。这是推荐的做法,以确保代码的可移植性。

  • 如果 shm_addr 非空,并且 SHM_RND 标志未被设置,则共享内存被关联到 addr 指定的地址处。

  • 如果 shm_addr 非空,并且设置了 SHM_RND 标志,则被关联的地址是 [shm_addr-(shm_addr % SHMLBA)]。SHMLBA 的含义是 “段低端边界地址倍数”(Segment Low Boundary Address Multiple),它必须是内存页面大小 (PAGE_SIZE) 的整数倍。现在的 Linux 内核中,它等于一个内存页大小。SHM_RND 的含义是圆整 (round),即将共享内存被关联的地址向下圆整到离 shm_addr 最近的 SHMLBA 的整数倍地址处。

除了 SHM_RND 标志外,shmflg 参数还支持如下标志:

  • SHM_RDONLY。进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同时对共享内存进行读写操作(当然,这需要在创建共享内存的时候指定其读写权限)。

  • SHM_REMAP。如果 shmaddr 已经被关联到一段共享内存上,则重新关联。

  • SHM_EXEC。它指定对共享内存段的执行权限。对于共享内存的执行权限,执行权限和读权限是一样的。

2. shmdt

1
int shmdt(const void* shm_addr);
  • 功能:

    • 将之前由shmat连接到进程地址空间的共享内存段分离。分离操作并不会删除共享内存段本身,只是将该共享内存段从调用进程的地址空间中移除。

    • shm_addr

      • 这是共享内存段在进程地址空间中的起始地址,即shmat函数成功连接共享内存段时返回的地址。
  • 返回值:

    成功时,返回0。失败时,返回-1,并且会设置errno来指示错误原因。

    成功调用会修改内核数据结构 shmid_ds 的部分字段,如下:

    • 将 shm_nattach 减 1。

    • 将 shm_lpid 设置为调用进程的 PID。

    • 将 shm_dtime 设置为当前时间。

    常见的错误原因包括:

    • EINVALshm_addr不是有效的共享内存段地址。

shmdt函数将关联到shm_addr参数处的共享内存从进程中分离,它成功时返回0,失败则返回-1并设置errno。

调用该函数使得它从进程空间分离的含义

  • 不删除本身:
    • 共享内存段在系统内存中是独立存在的。即使一个进程不再需要使用它,这个共享内存段本身并不会消失。这是因为其他进程可能还在使用这块共享内存进行数据交互。例如,有进程 A、B、C 都连接到了同一块共享内存。如果进程 A 调用shmdt,只是进程 A 不再能访问这块共享内存,但进程 B 和 C 仍然可以正常访问,共享内存段本身依然存在于系统内存中。
  • 从进程地址空间移除:
    • 在进程调用shmdt之前,共享内存段是映射到该进程的地址空间中的。这意味着进程可以直接通过指针访问共享内存中的数据。当调用shmdt后,这个映射关系就被解除了。就好像在进程的 “视野” 中,这块共享内存消失了。虽然共享内存还在系统中,但该进程已经无法再通过之前的指针去访问其中的数据了。这就是所谓的从进程地址空间移除。

3.shmctl 系统调用

shmctl系统调用控制共享内存的某些属性。

1
2
#include <sys/shm.h>
int shmctl(int shm_id, int command, struct shmid_ds* buf);
  1. 函数功能

    • shmctl函数用于对共享内存段进行控制操作,如获取共享内存段的状态信息、设置共享内存段的属性、删除共享内存段等。
  2. 参数详解

    • int shm_id

      • 这是共享内存段的标识符,由shmget函数成功创建或获取共享内存段时返回。它唯一标识了一个共享内存段,shmctl函数将对这个标识符所对应的共享内存段进行操作。
    • int command:(见下表)

      • 这是一个控制命令,用于指定对共享内存段进行何种操作,常见的命令有:
        • IPC_STAT:获取共享内存段的状态信息,并将其存储到buf所指向的struct shmid_ds结构体中。这个结构体包含了共享内存段的各种属性,如操作权限、大小、最后访问时间等。
        • IPC_SET:根据buf所指向的struct shmid_ds结构体中的信息来设置共享内存段的属性。例如,可以修改共享内存段的操作权限等。
        • IPC_RMID:删除由shm_id标识的共享内存段。这是一个非常重要且具有危险性的操作,一旦执行,共享内存段及其所包含的数据将被永久删除,所有关联到该共享内存段的进程将无法再访问它。
      • 除了上述常见命令外,还有一些其他命令,如SHM_LOCK(锁定共享内存段到物理内存,防止被交换到磁盘)和SHM_UNLOCK(解锁共享内存段,允许其被交换到磁盘)等,但这些命令的可用性可能取决于操作系统的支持。
    • struct shmid_ds* buf

      • 这是一个指向struct shmid_ds结构体的指针,其作用取决于command

        参数的值:

        • commandIPC_STAT时,buf用于存储获取到的共享内存段的状态信息。
        • commandIPC_SET时,buf指向的结构体中的信息将被用于设置共享内存段的属性。
        • 如果command不涉及IPC_STATIPC_SET操作(如IPC_RMID),buf通常可以设置为NULL
  3. 返回值(见下表)

    • 成功时:

      • 如果commandIPC_STATIPC_SET,返回0表示操作成功。
      • 如果commandIPC_RMID,返回0表示共享内存段已成功删除。
    • 失败时:

      • 返回-1,并设置errno

        变量来指示错误原因。常见的错误原因包括:

        • EINVALshm_id无效,或者command参数无效,或者buf指向的结构体无效(在IPC_STATIPC_SET操作时)。
        • EPERM:调用进程没有足够的权限来执行请求的操作(例如,没有权限删除共享内存段或修改其属性)。

image-20241220160445378

4.函数接口使用

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main() {
// 创建共享内存段
int shm_id = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget");
return 1;
}

// 连接共享内存段
void* shm_addr = shmat(shm_id, NULL, 0);
if (shm_addr == (void *)-1) {
perror("shmat");
return 1;
}

// 使用共享内存段,这里简单地将其初始化为0
for (int i = 0; i < 1024; i++) {
((char *)shm_addr)[i] = 0;
}

// 分离共享内存段
if (shmdt(shm_addr) == -1) {
perror("shmdt");
return 1;
}

// 删除共享内存段
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}

return 0;
}

5.共享内存实例–实现生产者消费者模型

使用 System V IPC 的共享内存实现生产者 - 消费者模型

  • 以下是使用 System V IPC 的shmgetshmatshmdtshmctl函数实现生产者 - 消费者模型的 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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <time.h>
#include <unistd.h>

#define BUFFER_SIZE 10

// 共享内存结构体
struct SharedMemory {
int buffer[BUFFER_SIZE];
int in;
int out;
};

// 信号量操作函数
void semaphore_op(int semid, int sem_num, int op) {
struct sembuf sem_b;
sem_b.sem_num = sem_num;
sem_b.sem_op = op;
sem_b.sem_flg = 0;
if (semop(semid, &sem_b, 1) == -1) {
perror("semop");
exit(1);
}
}

// 生产者函数
void producer(int shm_id, int semid) {
struct SharedMemory *shared_mem = (struct SharedMemory *)shmat(shm_id, NULL, 0);
if (shared_mem == (void *)-1) {
perror("shmat producer");
exit(1);
}

srand(time(NULL));
while (1) {
semaphore_op(semid, 0, -1); // 等待有空位(P操作)
int item = rand() % 100;
shared_mem->buffer[shared_mem->in] = item;
shared_mem->in = (shared_mem->in + 1) % BUFFER_SIZE;
printf("Producer produced: %d\n", item);
semaphore_op(semid, 1, 1); // 增加产品数量(V操作)
sleep(1);
}
shmdt(shared_mem);
}

// 消费者函数
void consumer(int shm_id, int semid) {
struct SharedMemory *shared_mem = (struct SharedMemory *)shmat(shm_id, NULL, 0);
if (shared_mem == (void *)-1) {
perror("shmat consumer");
exit(1);
}

while (1) {
semaphore_op(semid, 1, -1); // 等待有产品(P操作)
int item = shared_mem->buffer[shared_mem->out];
shared_mem->out = (shared_mem->out + 1) % BUFFER_SIZE;
printf("Consumer consumed: %d\n", item);
semaphore_op(semid, 0, 1); // 增加空位数量(V操作)
sleep(2);
}
shmdt(shared_mem);
}

int main() {
// 创建共享内存
int shm_id = shmget(IPC_PRIVATE, sizeof(struct SharedMemory), IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget");
return 1;
}

// 创建信号量集
int semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0666);
if (semid == -1) {
perror("semget");
return 1;
}
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
} arg;
arg.val = BUFFER_SIZE;
// 初始化空闲缓冲区信号量(初始有BUFFER_SIZE个空位可插入数据)
if (semctl(semid, 0, SETVAL, arg) == -1) {
perror("semctl");
return 1;
}
arg.val = 0;
// 初始化产品信号量(初始无产品可供消费)
if (semctl(semid, 1, SETVAL, arg) == -1) {
perror("semctl");
return 1;
}

// 创建生产者和消费者进程(这里简单使用父子进程模拟)
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程为消费者
consumer(shm_id, semid);
} else {
// 父进程为生产者
producer(shm_id, semid);
}

// 等待子进程结束(这里只是简单等待,实际可能需要更完善的机制)
wait(NULL);

// 删除共享内存和信号量集
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
perror("shmctl");
return 1;
}
if (semctl(semid, 0, IPC_RMID) == -1) {
perror("semctl");
return 1;
}

return 0;
}

整体代码解释:

  • 共享内存结构体定义:
    • 定义了SharedMemory结构体,包含一个整数数组作为缓冲区,以及inout索引,用于生产者和消费者操作。
  • 信号量操作函数:
    • semaphore_op函数用于对信号量进行操作,实现P操作(sem_op为负,获取资源)和V操作(sem_op为正,释放资源)。
  • 生产者函数:
    • 通过shmat将共享内存连接到进程地址空间。
    • 使用semaphore_op等待空闲缓冲区信号量(semid的第 0 个信号量),生产一个随机数放入缓冲区,更新in索引,然后使用semaphore_op释放产品信号量(semid的第 1 个信号量)。
  • 消费者函数:
    • 同样通过shmat连接共享内存。
    • 使用semaphore_op等待产品信号量,从缓冲区取出数据,更新out索引,然后使用semaphore_op释放空闲缓冲区信号量。
  • 主函数:
    • 使用shmget创建共享内存,并使用semget创建信号量集。
    • 初始化信号量,一个表示空闲缓冲区数量,一个表示产品数量。
    • 使用fork创建子进程,子进程作为消费者,父进程作为生产者。
    • 最后等待子进程结束,并使用shmctlsemctl删除共享内存和信号量集。

运行结果:

image-20241220184549568

6.无血缘关系的进程之间通信

  1. ftok函数的定义和功能
    • 函数原型key_t ftok(const char *pathname, int proj_id);
    • 功能ftok函数用于生成一个唯一的key(键值),这个key通常用于 System V IPC(进程间通信)机制中,如创建共享内存、消息队列和信号量集等。它将一个文件路径名(pathname)和一个项目标识符(proj_id)组合起来,生成一个适合作为 System V IPC 资源标识符的key值。
  2. 参数解释
    • const char *pathname
      • 这是一个指向文件路径名的指针。这个文件路径必须是一个已经存在的文件的有效路径,通常使用当前目录(.")或者一个程序相关的配置文件路径等。ftok函数会使用文件的inode(索引节点)信息作为生成key的一部分。
      • 注意,如果文件被删除然后重新创建,即使文件名相同,inode可能会改变,这会导致ftok生成不同的key值。
    • int proj_id
      • 这是一个0 - 255之间的整数,作为项目标识符。它和文件路径的inode信息一起组合生成key。不同的项目可以使用不同的proj_id来区分,这样即使基于同一个文件路径,不同的项目也能生成不同的key值用于各自的 IPC 资源。例如,一个程序中有两个不同的模块需要使用消息队列进行通信,它们可以使用相同的文件路径但不同的proj_id来生成不同的key,以创建两个独立的消息队列。
  3. 返回值
    • 成功时,ftok函数返回一个key_t类型的非负整数,这个整数可以作为shmgetmsggetsemget等 System V IPC 函数的key参数来创建或获取对应的 IPC 资源。
    • 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
      • EACCESS:没有权限访问pathname指定的文件。
      • ENOENTpathname指定的文件不存在。

发送端

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define MEMORY_SIZE 100

int main() {
// 通过ftok生成key
key_t key = ftok(".", 'b');
if (key == -1) {
perror("ftok");
return 1;
}

// 创建共享内存段
int shm_id = shmget(key, MEMORY_SIZE, IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget");
return 1;
}

// 映射共享内存到进程地址空间
void *shm_ptr = shmat(shm_id, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat");
return 1;
}

// 向共享内存写入数据(这里简单写入字符串)
strcpy((char *)shm_ptr, "Hello from sender!");

// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
return 1;
}
sleep(10);
return 0;
}

接收端

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define MEMORY_SIZE 100

int main() {
sleep(3);
// 通过ftok生成key
key_t key = ftok(".", 'b');
if (key == -1) {
perror("ftok");
return 1;
}

// 获取共享内存段
int shm_id = shmget(key, MEMORY_SIZE, 0666);
if (shm_id == -1) {
perror("shmget");
return 1;
}

// 映射共享内存到进程地址空间
void *shm_ptr = shmat(shm_id, NULL, 0);
if (shm_ptr == (void *)-1) {
perror("shmat");
return 1;
}

// 从共享内存读取数据并打印
printf("Receiver read from shared memory: %s\n", (char *)shm_ptr);

// 分离共享内存
if (shmdt(shm_ptr) == -1) {
perror("shmdt");
return 1;
}

// 删除共享内存段
if (shmctl(shm_id, IPC_RMID, NULL) == -1) {
perror("shmctl for delete");
return 1;
}

return 0;
}

运行结果:

image-20241220190008467

共享内存的 POSIX 方法

mmap函数和munmap函数利用mmap函数的MAP_ANONYMOUS标志可以实现父、子进程间的匿名内存共享。通过打开同一个文件,mmap也可以实现无关进程之间的内存共享。

Linux提供了另一种在无关进程间共享内存的方式,这种方式无须任何文件的支持,但它需要先用shm_open函数来创建或打开一个POSIX共享内存对象。

1.shm_open

1
2
3
4
5
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int shm_open ( const char* name, int oflag, mode_t mode );

shm_open函数的使用方法与open系统调用完全相同。

  • 功能:
    • 用于创建或打开一个共享内存对象。这个函数是基于 POSIX 标准的,提供了一种可移植的方式来操作共享内存。
  • 参数:
    • const char* name
      • 这是共享内存对象的名字。从可移植性的角度考虑,名字应该遵循特定格式:以/开始,后面跟着多个非/字符,并且以\0结尾,长度通常不超过NAME_MAX(一般为 255)。例如"/my_shared_memory"
    • int oflag
      • 用于指定打开共享内存对象的方式,它是以下标志的按位或组合:
        • O_RDONLY:以只读方式打开共享内存对象。如果只指定了这个标志,那么进程只能从共享内存中读取数据。
        • O_RDWR:以可读可写方式打开共享内存对象,这是最常用的方式之一,允许进程对共享内存进行读写操作。
        • O_CREAT:如果共享内存对象不存在,则创建它。当使用这个标志时,mode参数用于指定新创建的共享内存对象的访问权限(类似文件权限)。
        • O_EXCL:通常与O_CREAT一起使用。如果name指定的共享内存对象已经存在,shm_open调用将返回错误(-1);如果不存在,则创建一个新的共享内存对象。这可以用于确保创建的是一个全新的、未被使用过的共享内存对象。
        • O_TRUNC:如果共享内存对象已经存在,将其截断(长度变为 0)。这个操作会丢失共享内存对象中原先存储的数据。
    • mode_t mode
      • oflag中包含O_CREAT标志时,mode参数用于指定共享内存对象的访问权限。它类似于文件权限,例如0666表示用户、组和其他用户都有读写权限。权限的低 9 位用于设置实际的权限。

返回值:

  • 返回值:
    • 成功时,返回一个非负整数,即共享内存对象的文件描述符。这个文件描述符可以用于后续的操作,如mmap(将共享内存映射到进程地址空间)等操作。
    • 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
      • EEXIST:当O_CREATO_EXCL一起使用且共享内存对象已经存在时。
      • EINVALname参数不符合格式要求,或者oflag参数无效。
      • EACCES:没有足够的权限按照oflagmode指定的方式创建或打开共享内存对象。

和打开的文件最后需要关闭一样,由shm_open函数创建的共享内存对象用完后也需要删除,可通过shm_unlink函数实现。

1
2
3
4
5
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

int shm_unlink ( const char* name );
  • 功能:

    • 用于删除一个共享内存对象。当一个进程调用shm_unlink后,共享内存对象会被标记为删除,但实际的删除操作会在所有进程都关闭了对该共享内存对象的引用(通过shm_open打开得到的文件描述符都被关闭)后才会执行。
  • 参数:

shm_unlink函数将name参数指定的共享内存对象标记为等待删除,当所有使用该共享内存对象的进程都使用munmap函数将它从进程中分离后,系统将销毁这个共享内存对象所占据的资源。

  • 返回值:

    • 成功时,返回0

    • 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:

      • EINVALname参数不符合格式要求。
      • ENOENT:指定的共享内存对象不存在。

如果代码中使用了以上POSIX共享内存函数,则编译时需要指定链接选项-lrt

3.使用案例

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
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#define MEMORY_SIZE 4096

int main() {
// 创建或打开共享内存对象
int fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
if (fd == -1) {
perror("shm_open");
return 1;
}

// 设置共享内存对象大小
if (ftruncate(fd, MEMORY_SIZE) == -1) {
perror("ftruncate");
return 1;
}

// 映射共享内存到进程地址空间
void* shared_memory = mmap(NULL, MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared_memory == MAP_FAILED) {
perror("mmap");
return 1;
}

// 使用共享内存,这里简单地写入数据
char* message = "Hello, shared memory!";
for (int i = 0; i < sizeof(message); i++) {
((char*)shared_memory)[i] = message[i];
}

// 解除映射
if (munmap(shared_memory, MEMORY_SIZE) == -1) {
perror("munmap");
return 1;
}

// 关闭共享内存对象
if (close(fd) == -1) {
perror("close");
return 1;
}

// 删除共享内存对象
if (shm_unlink("/my_shared_memory") == -1) {
perror("shm_unlink");
return 1;
}

return 0;
}

代码解释

  • 首先,使用shm_open创建或打开一个名为/my_shared_memory的共享内存对象,权限为0666,并以读写方式打开。
  • 然后,使用ftruncate设置共享内存对象的大小为MEMORY_SIZE(这里定义为 4096 字节)。
  • 接着,使用mmap将共享内存映射到进程的地址空间,以便可以像访问普通内存一样访问共享内存。
  • 之后,向共享内存中写入了一个字符串Hello, shared memory!
  • 再然后,使用munmap解除共享内存的映射。
  • 接着,使用close关闭共享内存对象的文件描述符。
  • 最后,使用shm_unlink删除共享内存对象。

4.实现生产者消费者模型

使用共享内存实现生产者 - 消费者模型(基于shm_openshm_unlink函数)

  • 以下是使用shm_openshm_unlink函数实现的生产者 - 消费者模型的 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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>
#include <semaphore.h>
#include <cstdlib>
#include <ctime>
#include <thread>

#define MEMORY_SIZE 4096
#define BUFFER_SIZE 10

// 共享内存结构体
struct SharedMemory {
int buffer[BUFFER_SIZE];
int in;
int out;
};

// 生产者函数
void producer(int shm_fd, sem_t* empty_sem, sem_t* full_sem) {
void* shm_ptr = mmap(NULL, MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
std::cerr << "mmap failed in producer" << std::endl;
return;
}
SharedMemory* shared_mem = (SharedMemory*)shm_ptr;

srand(static_cast<unsigned int>(time(nullptr)));
while (true) {
sem_wait(empty_sem);
int item = rand() % 100;
shared_mem->buffer[shared_mem->in] = item;
shared_mem->in = (shared_mem->in + 1) % BUFFER_SIZE;
std::cout << "Producer produced: " << item << std::endl;
sem_post(full_sem);
// 模拟生产时间
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
munmap(shm_ptr, MEMORY_SIZE);
}

// 消费者函数
void consumer(int shm_fd, sem_t* empty_sem, sem_t* full_sem) {
void* shm_ptr = mmap(NULL, MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
std::cerr << "mmap failed in consumer" << std::endl;
return;
}
SharedMemory* shared_mem = (SharedMemory*)shm_ptr;

while (true) {
sem_wait(full_sem);
int item = shared_mem->buffer[shared_mem->out];
shared_mem->out = (shared_mem->out + 1) % BUFFER_SIZE;
std::cout << "Consumer consumed: " << item << std::endl;
sem_post(empty_sem);
// 模拟消费时间
std::this_thread::sleep_for(std::chrono::milliseconds(800));
}
munmap(shm_ptr, MEMORY_SIZE);
}

int main() {
// 创建共享内存对象
int shm_fd = shm_open("/my_shared_memory", O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
std::cerr << "shm_open failed" << std::endl;
return 1;
}
// 设置共享内存大小
if (ftruncate(shm_fd, MEMORY_SIZE) == -1) {
std::cerr << "ftruncate failed" << std::endl;
return 1;
}

// 创建信号量
sem_t* empty_sem = sem_open("/empty_sem", O_CREAT, 0666, BUFFER_SIZE);
sem_t* full_sem = sem_open("/full_sem", O_CREAT, 0666, 0);
if (empty_sem == SEM_FAILED || full_sem == SEM_FAILED) {
std::cerr << "sem_open failed" << std::endl;
return 1;
}

// 映射共享内存到进程空间
void* shm_ptr = mmap(NULL, MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (shm_ptr == MAP_FAILED) {
std::cerr << "mmap failed" << std::endl;
return 1;
}
SharedMemory* shared_mem = (SharedMemory*)shm_ptr;
shared_mem->in = 0;
shared_mem->out = 0;

// 创建生产者和消费者线程
std::thread producer_thread(producer, shm_fd, empty_sem, full_sem);
std::thread consumer_thread(consumer, shm_fd, empty_sem, full_sem);

producer_thread.join();
consumer_thread.join();

// 关闭并删除信号量
sem_close(empty_sem);
sem_close(full_sem);
sem_unlink("/empty_sem");
sem_unlink("/full_sem");

// 解除映射并删除共享内存
munmap(shm_ptr, MEMORY_SIZE);
close(shm_fd);
shm_unlink("/my_shared_memory");

return 0;
}

整体代码解释:

  • 共享内存结构体定义:
    • 定义了SharedMemory结构体,包含一个整数数组作为缓冲区,以及inout索引,用于生产者和消费者操作。
  • 生产者函数:
    • 通过mmap将共享内存映射到进程地址空间。
    • 使用sem_wait等待empty_sem信号量(表示缓冲区有空闲位置),生产一个随机数放入缓冲区,更新in索引,然后使用sem_post释放full_sem信号量(表示缓冲区有数据可供消费)。
  • 消费者函数:
    • 同样通过mmap映射共享内存。
    • 使用sem_wait等待full_sem信号量,从缓冲区取出数据,更新out索引,然后使用sem_post释放empty_sem信号量。
  • 主函数:
    • 使用shm_open创建共享内存对象,并设置大小。
    • 创建empty_semfull_sem两个信号量,分别用于控制缓冲区的空闲和满状态。
    • 映射共享内存后初始化inout索引。
    • 创建生产者和消费者线程并等待它们结束。
    • 最后关闭并删除信号量,解除共享内存映射,关闭并删除共享内存对象。

运行结果:

image-20241220184837946

5.无血缘关系的进程之间通信

写入端

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#define SHM_NAME "/my_shared_memory"
#define SHM_SIZE 100

int main() {
int shm_fd;
void *ptr;

// 创建共享内存对象
shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
return 1;
}

// 设置共享内存大小
if (ftruncate(shm_fd, SHM_SIZE) == -1) {
perror("ftruncate");
return 1;
}

// 将共享内存映射到进程地址空间
ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
return 1;
}

// 向共享内存写入数据
strcpy((char *)ptr, "Hello from writer process!");

// 解除映射
if (munmap(ptr, SHM_SIZE) == -1) {
perror("munmap");
return 1;
}

// 关闭共享内存对象描述符
close(shm_fd);

return 0;
}

读出端

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SHM_NAME "/my_shared_memory"
#define SHM_SIZE 100

int main() {
int shm_fd;
void *ptr;

// 打开共享内存对象
shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
if (shm_fd == -1) {
perror("shm_open");
return 1;
}

// 将共享内存映射到进程地址空间
ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
return 1;
}

// 从共享内存读取数据并打印
printf("Read from shared memory: %s\n", (char *)ptr);

// 解除映射
if (munmap(ptr, SHM_SIZE) == -1) {
perror("munmap");
return 1;
}

// 关闭共享内存对象描述符
close(shm_fd);

// 删除共享内存对象
if (shm_unlink(SHM_NAME) == -1) {
perror("shm_unlink");
return 1;
}

return 0;
}

运行结果:

image-20241220190749596