清茗小记

生活随笔,心灵漫游

0%

Linux进程间通信——共享内存(Posix共享内存区)

进程间共享内存通信有 POSIX 和 SystemV 两种实现方法,其中,SystemV API 广泛应用与 X windows 系统及其扩展版本中。

本文章简单介绍 Linux 进程间通信——共享内存Posix 共享内存区

共享内存区是可用 IPC 形式中最快的。

一旦这样的内存区映射到共享它的进程的地址空间,这些进程间数据的传递就不再涉及内核。这里说的“不再涉及内核”的含义是:进程不再通过执行任何进入内核的系统调用来彼此传递数据。显然,内核必须建立允许各个进程共享该内存区的内存映射关系,然后一直管理该内存区(处理页面故障等)。

mmap、munmap 和 msync 函数

mmap 函数

mmap 函数把一个文件或一个 Posix 共享内存区对象映射到调用进程的地址空间。使用该函数有三个目的:

  • 使用普通文件以提供内存映射 I/O
  • 使用特殊文件以提供匿名内存映射
  • 使用 shm_open 以提供无亲缘关系进程间的 Posix 共享内存区
1
2
3
4
5
6
#include <sys/mman.h>
void *mmap(void *addr,size_t len,int prot,int flags,int fd,off_t offset);
// 返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED
// 其中addr可以指定描述符fd应被映射到的进程内空间的起始地址。
// 它通常被指定为一个空指针,这样告诉内核自己去选择起始地址。
// 无论哪种情况下,该函数的返回值都是描述符fd所映射到内存区的起始地址。

内存映射区的保护由 prot 参数指定。该参数的常见值是代表读写访问的 PROT_READ | PROT_WRITE。

prot 说明
PROT_READ 数据可读
PROT_WRITE 数据可写
PROT_EXEC 数据可执行
PROT_NONE 数据不可访问
Flags 说明
MAP_SHARED 如果参数 start 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标
MAP_PRIVATE 变动是私有的
MAP_FIXED 准确地解释 addr 参数
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)
MAP_DENYWRITE 只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝
MAP_ANONYMOUS 建立匿名映射。此时会忽略参数 fd,不涉及文件,而且映射区域无法和其他进程共享。

从移植性上考虑,MAP_FIXED 不应该指定。如果没有指定该标志,但是 addr 不是一个空指针,那么 addr 如何处置取决于实现。不为空的 addr 值通常被当作有关该内存区应如何具体定位的线索。可移植的代码应把 addr 指定成一个空指针,并且不指定 MAP_FIXED。

munmap 函数

为从某个进程的地址空间删除一个映射关系,我们调用 munmap。

1
2
3
4
5
6
#include <sys/mman.h>
int munmap(void *addr,size_t len);
// 返回:若成功则为0,若出错则为−1
// 其中addr参数是由mmap返回的地址,len是映射区的大小。
// 再次访问这些地址将导致向调用进程产生一个SIGSEGV信号
// (当然这里假设以后的mmap调用并不重用这部分地址空间)。

如果被映射区是使用 MAP_PRIVATE 标志映射的,那么调用进程对它所作的变动都会被丢弃掉。

msync 函数

如果我们修改了处于内存映射到某个文件的内存区中某个位置的内容,那么内核将在稍后某个时刻相应地更新文件。然而有时候我们希望确信硬盘上的文件内容与内存映射区中的内容一致,于是调用 msync 来执行这种同步。

1
2
3
4
#include <sys/mman.h>
int msync(void *addr,size_t len,int flags);
// 返回:若成功则为0,若出错则为−1
// 其中addr和len参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。
常值 说明
MS_ASYNC 执行异步写
MS_SYNC 执行同步写
MS_INVALIDATE 使高速缓存地数据失效
为什么使用 mmap

mmap 的描述间接说明了内存映射文件:open 它之后调用 mmap 把它映射到调用进程地址空间的某个文件。使用内存映射文件所得到的奇妙特性是,所有的 I/O 都在内核的掩盖下完成,只需编写存取内存映射区中各个值的代码。 决不调用 read、write 或 lseek。这么一来往往可以简化代码。
mmap 的另一个用途是在无亲缘关系的进程间提供共享的内存区。

Posix 共享内存区

Posix 共享内存区涉及两个步骤要求:

  1. 指定一个名字参数调用 shm_open,以创建一个新的共享内存区对象或打开一个已经存在的共享内存区对象;
  2. 调用 mmap 把这个共享内存区映射到调用进程的地址空间。

shm_unlink 函数删除一个共享内存区对象的名字。跟所有其他 unlink 函数(删除文件系统中一个路径名的 unlink,删除一个 Posix 消息队列的 mq_unlink,以及删除一个 Posix 有名信号量的 sem_unlink)一样,删除一个名字不会影响对于其底层支撑对象的现有引用,直到对于该对象的引用全部关闭为止。删除一个名字仅仅防止后续的 open、mq_open 或 sem_open 调用取得成功。

对于一个普通文件:如果该文件的大小大于 length 参数,额外的数据就被丢弃掉。如果该文件的大小小于 length,那么该文件是否修改以及其大小是否增长是未加说明的。实际上对于一个普通文件,把它的大小扩展到 length 字节的可移植方法是:先 lseek 到偏移为 length−1 处,然后 write 1 个字节的数据。所幸的是几乎所有 Unix 实现都支持使用 ftruncate 扩展一个文件。对于一个共享内存区对象:ftruncate 把该对象的大小设置成 length 字节。我们调用 ftruncate 来指定新创建的共享内存区对象的大小,或者修改已存在的对象的大小。当打开一个已存在的共享内存区对象时,我们可调用 fstat 来获取有关该对象的信息。

1
2
#include <unistd.h>
int ftruncate(int fd,off_t length);
1
2
3
#include <sys/types.h>
#include <sys/stat.h>
int fstat(int fd,struct stat *buf);

简单示例

写共享内存示例代码
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
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <error.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <string.h>

#define SHM_NAME "/shm0"
#define SHM_SIZE 4096

int main(int argc, char** argv)
{
if (argc < 2)
{
printf("Usage: ./shm_write msg\n");
return -1;
}

// 创建或打开已存在的共享内存
int shm_fd = shm_open(SHM_NAME, O_RDWR|O_CREAT, 0);
if (shm_fd == -1)
{
perror("shm_open error");
return -1;
}

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

// 映射共享内存
char* buffer = (char *)mmap(NULL, SHM_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, shm_fd, 0);
if (buffer == (char *)MAP_FAILED)
{
perror("mmap error");
return -1;
}

// 关闭共享内存文件描述符,并不影响映射
close(shm_fd);

// 写共享内存
strncpy(buffer, argv[1], strlen(argv[1]));

// 删除共享内存
//shm_unlink(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
50
51
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
#include <error.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#define SHM_NAME "/shm0"
#define SHM_SIZE 4096

int main()
{
// 创建或打开共享内存
int shm_fd = shm_open(SHM_NAME, O_RDWR|O_CREAT, 0);
if (shm_fd == -1)
{
perror("shm_open error");
return -1;
}

// 获取共享内存大小,读取的时候要用
struct stat stat={0};
if (fstat(shm_fd, &stat))
{
perror("fstat error");
return -1;
}

// 映射共享内存,大小为上一步获取的大小
char* buffer = (char*)mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, shm_fd, 0);
if (buffer == (char *)MAP_FAILED)
{
perror("mmap error");
return -1;
}
// 关闭文件描述符
close(shm_fd);

// 读取共享内存
printf("Read Msg:%s\n", buffer);

// 删除共享内存
shm_unlink(SHM_NAME);
return 0;
}