LV05-05-进程通信-04-共享内存

本文主要是进程通信——共享内存的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日
开发板 正点原子 i.MX6ULL Linux阿尔法开发板
uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03)
linux内核 linux-4.15(NXP官方提供)
STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
参考方向 参考原文
------
点击查看相关文件下载
--- ---

一、共享内存简介

共享内存就是映射一段能被其它进程所访问的内存,这段共享内存由一个进程创建,但其它的多个进程都可以访问,使得多个进程可以访问同一块内存空间。共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的,它往往与其它通信机制,例如结合信号量来使用,以实现进程间的同步和通信。

共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这一个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

image-20220604164114921

二、内存映射

1. 基本概念

内存映射就是使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像访问普通内存一样对文件进行访问,不必再调用readwrite等函数。

内存映射文件,是由一个文件到一块内存的映射。 共享内存(SharedMemory)实际就是文件映射的一种特殊情况。 使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

内存的映射有两种,一种是基于文件的映射(file-backed),称为文件映射,我看到有些地方也称之为有后备文件的映射,另一种是对没有文件关联的内容进行映射,也就是匿名映射

文件映射相对read()write()的对比:

  • 在大型文件的重复随机访问中,文件映射的优势突出,方便应用,代码简捷。
  • read()write()操作需要两次传输:一次是在文件和内核高速缓存之间,另一次是内核高速缓存和用户空间缓存之间。使用文件映射就无需第二次传输了。
  • 如果是顺序的访问文件,执行read()write()时使用的缓冲区足够大,以至于能够避免执行大量的I/O系统调用,但是mmap()带来的系统提升是非常有限,或者可以说没有提升。
  • 对于小数据量的I/O操作,mmap()的开销比read()write()还要大。

2. 共享内存创建

2.1 mmap() 

linux下可以使用man mmap命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/mman.h>

/* 函数声明 */
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

【函数说明】该函数将某个文件映射到物理内存中,也就是创建一个共享内存映射。

【函数参数】

  • addrvoid *类型,指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址,访问时可能需要强制类型转换。
  • lengthsize_t类型,必须大于0(否则就会报【Invalid argument】),表示映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
  • protint类型,指定共享的内存映射区的访问权限。
点击查看 prot 可取的值
ROT_READ映射区域可读
PROT_WRITE映射区域可写
PROT_EXEC映射区域可被执行
PROT_NONE映射区域不可访问

可以多选,中间用|连接。

  • flagint类型,映射区的特性标志位。
点击查看 flag 常用取值及说明
MAP_FIXED如果参数 addr 所指的地址无法成功建立映射时,则放弃映射,不对地址做修正,通常不建议使用此标志。
MAP_SHARED对应射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE对应射区域的写入操作会产生一个映射文件的复制,即私人的"写入时复制" (copy on write),对此区域作的任何修改都不会被写回原来的文件。
MAP_ANONYMOUS建立匿名映射,此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。一般用于血缘关系进程间通信。
MAP_DENYWRITE只允许对应射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED将映射区域锁定住,这表示该区域不会被置换(swap)。

【说明】MAP_SHARED , MAP_PRIVATE两个必选其一,而 MAP_FIXED 则不推荐使用。

  • fdint类型,表示代表要映射到内存的文件描述符,如果匿名映射则可以设置为-1
  • offsetoff_t类型,表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射,需要注意的是offset必须是系统分页大小的整数倍(Linux系统中系统分页的大小一般是4KB),如果不是的话,会报【Invalid argument】。

【返回值】void *类型,成功返回创建的映射区首地址,失败返回MAP_FAILED,其实就是(void *) -1 ,并设置errno值。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <sys/mman.h>

/* 至少应该有的语句 */
void *addr;
int fd = -1;
fd = open("/home/hk/shared.txt", O_RDWR); /* 注意文件大小不可为0,否则映射时会报总线错误 (核心已转储) */
addr = mmap(NULL, 100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

【注意事项】

(1)创建映射区的过程中,隐含着一次对映射文件的读操作,会将文件内容读取到映射区

(2)映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭,在flagMAP_SHARED时,即便是关闭了文件描述符,在修改共享内存数据的时候,部分数据也还是会写回物理磁盘上用于内存映射的文件的。

(3)使用文件映射时,用于映射的文件大小必须大于0,当映射文件大小为0时,若指定非0大小创建映射区,访问映射地址会报【总线错误 (核心已转储)】,指定0大小创建映射区,会报非法参数错误【Invalid argument】。

(4)mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

(5)当flagMAP_SHARED时,要求映射区的权限小于等于文件打开的权限(出于对映射区的保护),如果不满足的话(例如,open时权限为O_RDONLYmmapPROT_READ|PROT_WRITE)将会报非法参数【Invalid argument】错误。当flagMAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享

(6)文件偏移量必须为0或者4K(系统分页大小)的整数倍(不是会报非法参数IInvalid argument错误)。

2.2 使用实例

这里写一个测试的实例,用于测试上边几个的注意事项,会先使用后边的共享内存释放函数munmap

点击查看实例
test.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
/* 头文件 */
#include <stdio.h> /* perror */
#include <unistd.h> /* sleep fork */
#include <string.h> /* memcpy */

#include <sys/mman.h> /* munmap */
#include <sys/types.h>/* open */
#include <sys/stat.h> /* open */
#include <fcntl.h> /* open */

/* 主函数 */
int main(int argc, char *argv[])
{
void *addr;/* 共享内存映射的首地址 */
int fd; /* 接收文件描述符 */
/* 1. 打开共享文件 */
fd = open("/home/hk/shared.txt", O_RDWR);
if(fd < 0)
{
perror("open");
return -1;
}
/* 2. 创建共享内存映射 */
addr = mmap(NULL, 100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
/* 3. 关闭文件(只要映射建立成功,文件可以立即关闭) */
close(fd);
/* 4. 共享内存操作 */
printf("read=%s\n", (char*)(addr));
memcpy((addr),"99999999999999",15);
printf("read=%s\n", (char*)(addr));
while(1)
{
printf("read addr=%s\n", (char*)(addr));
sleep(1);
}
munmap(addr, 100);
return 0;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
read=99999999999999
read=99999999999999
read addr=99999999999999

第一个read读取的是原来的用于文件映射的文件中的内容,第二个read是对共享内存写入数据后读取的共享内存的内容,后边的都是对共享内存的读取。

3. 共享内存释放

3.1 munmap() 

linux下可以使用man munmap命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/mman.h>

/* 函数声明 */
int munmap(void *addr, size_t length);

【函数说明】该函数用来取消参数addr 所指的映射内存起始地址,也就是说通过该函数可以释放创建的内存映射。

【函数参数】

  • addrvoid *类型,指定要释放的内存映射的内存地址,也就是mmap()函数成功返回的映射区首地址,就是它的第一个参数。
  • lengthsize_t类型,表示内存映射地址空间的字节数,也就是mmap()函数的第二个参数。

【返回值】int类型,成功返回0,失败返回-1,并设置errno值。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
/* 需要包含的头文件 */
#include <sys/mman.h>

/* 至少应该有的语句 */
void *addr;
int fd = -1;
fd = open("/home/hk/shared.txt", O_RDWR); /* 注意文件大小不可为0,否则映射时会报总线错误 (核心已转储) */
addr = mmap(NULL, 100, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

munmap(addr, 100);

【注意事项】none

3.2 使用实例

暂无。

4. 同步映射区域到文件

4.1 msync() 

linux下可以使用man msync命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/mman.h>

/* 函数声明 */
int msync(void *addr, size_t length, int flags);

【函数说明】该函数用于同步映射区域内数据与文件数据。

【函数参数】

  • addrvoid *类型,指定需要同步的映射区域的内存地址。
  • lengthsize_t类型,表示需要同步的映射地址空间的字节数。
  • flagint类型,表示同步的模式。
点击查看 flag 取值
MS_SYNC 调用会阻塞,直到虚拟内存区域中所有被修改的分页被写入到真实文件为止。调用完后,虚拟内存与磁盘内容同步。
MS_ASYNC 虚拟内存区域与内核高速缓存区同步,并对立即执行read()操作的其他进程可见,但会在之后的某个时刻将修改内容写入磁盘。
MS_INVALIDATE使映射数据的缓存副本失效,当内存区域中所有被修改过的分页被同步到文件中之后,内存区域中所有与底层文件不一致的分页会被标记为无效。下次引用这些分页时会重新从文件的相应位置复制相应的分页内容。

【返回值】int类型,成功返回0,失败返回-1,并设置errno值。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include

/* 至少应该有的语句 */

【注意事项】none

4.2 使用实例

暂无。

5. mmap 映射原理与类型

5.1 虚拟内存映射过程

5.1.1  vm_area_struct 结构体

先来了解一个结构体vm_area_struct,该结构体定义在include/linux/mm_types.h文件中,可以使用如下命令来查找文件位置:

1
locate include/linux/mm_types.h

打开文件,找到这个接结构体,成员如下:

点击查看结构体成员定义
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
/*
* This struct describes a virtual memory area. There is one of these
* per VM-area/task. A VM area is any part of the process virtual memory
* space that has a special rule for the page-fault handlers (ie a shared
* library, the executable area etc).
*/
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */

unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;

struct rb_node vm_rb;

/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;

/* Second cache line starts here. */

struct mm_struct *vm_mm; /* The address space we belong to. */

/*
* Access permissions of this VMA.
* See vmf_insert_mixed_prot() for discussion.
*/
pgprot_t vm_page_prot;
unsigned long vm_flags; /* Flags, see mm.h. */

/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*/
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;

/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_lock &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units */
struct file * vm_file; /* File we map to (can be NULL). */
struct file *vm_prfile; /* shadow of vm_file */
void * vm_private_data; /* was vm_pte (shared mem) */

#ifdef CONFIG_SWAP
atomic_long_t swap_readahead_info;
#endif
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

分配的每个虚拟内存区域都由一个vm_area_struct 数据结构来管理,包括虚拟内存的起始和结束地址,以及内存的访问权限等。vm_area_struct是在mmap的时候创建的,vm_area_strcut代表了一段连续的虚拟地址,这些虚拟地址相应地映射到一个后备文件或者一个匿名文件的虚拟页。一个vm_area_struct映射到一组连续的页表项。页表项又指向物理内存page,这样就把一个文件和物理内存页相映射。

5.1.2 虚拟页的三种状态

当拿到一个虚拟地址时,系统会根据已有的vm_area_struct看这个虚拟地址是否属于某个vm_area_struct,然后会有以下两种情况:

  • 如果没有匹配到,就报段错误,访问了一个没有分配的虚拟地址。
  • 如果匹配到了vm_area_struct,根据虚拟地址和页表的映射关系,找到对应的页表项PTE,如果PTE没有分配,就报一个缺页异常,去加载相应的文件数据到物理内存,如果PTE分配,就去相应的物理页的偏移位置读取数据。

所以虚拟页会有以下三种状态:

未分配虚拟页指的是没有使用mmap建立vm_area_struct,所以也就没有对应到具体的页表项
已分配虚拟页未映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,但是页表项没有指向具体的物理页
已分配虚拟页已映射到物理页,指的是已经使用了mmap建立的vm_area_struct,可以映射到对应的页表项,并且页表项指向具体的物理页

5.1.3 mmap 映射原理

  • (一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域

(1)进程在用户空间调用库函数mmap

(2)在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址。

(3)为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行初始化。

(4)将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中。

  • (二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

(5)为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。

(6) 通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:

1
int mmap(struct file *filp, struct vm_area_struct *vma);/* 不同于用户空间库函数。 */

(7)内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。

(8)通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到内存中。

  • (三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

(9)进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。

(10)缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。

(11)调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到内存中。

(12)之后进程即可对这片内存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。

点击查看脏页面的概念

Linux内核由于存在page cache, 一般修改的文件数据并不会马上同步到磁盘,会缓存在内存的page cache中,我们把这种和磁盘数据不一致的页称为脏页,脏页会在合适的时机同步到磁盘。为了回写page cache中的脏页,需要标记页为脏(dirty)。

【注意事项】

(1)修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

(2)前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至物理内存。真正的文件读取是当进程发起读或写操作时。

5.2 mmap 的四种类型

mmap分为有后备文件的映射和匿名文件的映射,这两种映射又有私有映射和共享映射之分,所以mmap可以创建4种类型的映射:

后备文件的共享映射多个进程的vm_area_struct指向同一个物理内存区域,一个进程对文件内容的修改,会被其他进程可见。对文件内容的修改会被写回到后备文件。
后备文件的私有映射多个进程的vm_area_struct指向同一个物理内存区域,采用写时拷贝的方式,当一个进程对文件内容做修改,不会被其他进程看到。另外对文件内的修改也不会被写回到后备文件。当内存不够需要进行页回收时,私有映射的页被交换到交换区。一般用在加载共享代码库。
匿名文件的共享映射内核创建一个初始都是0的物理内存区域,然后多个进程的vm_area_struct指向这个共享的物理内存区域,对该区域内容的修改对所有进程可见。匿名文件在页回收时被交换到交换区。
匿名文件的私有映射内核创建一个初始都是0的物理内存区域,对该区域内容的修改只对创建者进程可见。匿名文件在页回收时被交换到交换区。malloc()底层是用了匿名文件的私有映射来分配大块内存。

6. 共享内存基本使用

内存映射可以用于进程的通信,在无血缘关系的进程间,或者是父子进程中,都可以使用内存映射的共享内存实现通信。

6.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
/* 头文件 */
#include <stdio.h> /* perror */
#include <unistd.h> /* sleep fork */
#include <string.h> /* memcpy */

#include <sys/mman.h> /* munmap */
#include <sys/types.h>/* open */
#include <sys/stat.h> /* open */
#include <fcntl.h> /* open */
/* 主函数 */
int main(int argc, char *argv[])
{
void *addr;/* 共享内存的首地址 */
int fd; /* 接收文件描述符 */
/* 1. 打开共享文件 */
fd = open("/home/hk/shared.txt", O_RDWR);/* 注意文件大小不可为0,否则映射时会报总线错误 (核心已转储) */
if(fd < 0)
{
perror("open");
return -1;
}
/* 2. 创建共享内存映射 */
addr = mmap(NULL, 2048, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
/* 3. 关闭文件(只要映射建立成功,文件可以立即关闭) */
close(fd);
/* 4. 打印读取数据 */
while(1)
{
printf("read process: read=%s\n", (char*)(addr));
sleep(1);
}

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
/* 头文件 */
#include <stdio.h> /* perror */
#include <unistd.h> /* sleep fork */
#include <string.h> /* memcpy */

#include <sys/mman.h> /* munmap */
#include <sys/types.h>/* open */
#include <sys/stat.h> /* open */
#include <fcntl.h> /* open */
/* 主函数 */
int main(int argc, char *argv[])
{
void *addr;/* 共享内存的首地址 */
int fd; /* 接收文件描述符 */
int i = 0;
/* 1. 打开共享文件 */
fd = open("/home/hk/shared.txt", O_RDWR);/* 注意文件大小不可为0,否则映射时会报总线错误 (核心已转储) */
if(fd < 0)
{
perror("open");
return -1;
}
/* 2. 创建共享内存映射 */
addr = mmap(NULL, 2048, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
/* 3. 关闭文件(只要映射建立成功,文件可以立即关闭) */
close(fd);
while(i < 2048)
{
memcpy((addr + i), "a", 1);
printf("i = %d\n", i++);
sleep(1);
}

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}

all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o
clean : clean_o
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

在终端执行以下命令编译程序:

1
2
3
make # 编译程序,将会生成两个可执行程序 
./mmap_write # 一个终端中共享内存区域写入数据
./mmap_read # 在另一个终端对共享内存进行读取操作

若之前用于文件映射的文件内容如下:

1
123456789

那么,两个终端显示的数据将是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 运行 ./mmap_read 的终端
hk@vm:~/2Sharedfiles/temp$ ./mmap_read
read process: read=123456789

read process: read=a23456789

read process: read=aa3456789
# ... ...

# 运行 ./mmap_write 的终端
hk@vm:/mnt/hgfs/Sharedfiles/temp$ ./mmap_write
i = 0
i = 1
i = 2
# ... ...

后边都是循环,运行 ./mmap_read 的终端显示的数据中间隔较大,这是因为文件映射到共享内存区后自带换行,这个符号还没有被覆盖,于是每一次都会被读出来,而每次循环打印的时候也会有一个换行,所以就会出现刚开始间隔大,后边间隔小的情况啦。

6.2 父子进程

我们前边学习mmap函数的时候,它是可以建立匿名映射的,而匿名映射主要就是用于父子进程之间的通信。

点击查看实例
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
/* 头文件 */
#include <stdio.h> /* perror */
#include <unistd.h> /* sleep fork */
#include <string.h> /* memcpy */

#include <sys/mman.h> /* mmap munmap */
#include <sys/types.h>/* open */
#include <sys/stat.h> /* open */
#include <fcntl.h> /* open */
#include <sys/wait.h> /* wait */


/* 主函数 */
int main(int argc, char *argv[])
{
void *addr;
pid_t pid;
/* 1. 创建共享内存映射 */
addr = mmap(NULL, 2048, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
/* 2. 创建父子进程 */
pid = fork();

if(pid < 0)/* 创建出错 */
{
perror("fork");
return -1;
}
else if(pid > 0)/* 父进程 */
{
memcpy(addr,"1234567890",10);

wait(NULL);
}
else /* 子进程 */
{
sleep(1);
printf("read father val=%s\n",(char *)addr);
}
/* 释放内存映射 */
munmap(addr,2048);
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

终端将会显示以下情况:

1
read father val=1234567890

7. 文件映射边界问题

7.1 正常文件范围的内存映射

image-20220606093647447

这种情况就是,我们要映射的区域大小并未超过文件大小,如上图,我们有一个9600 Bytes的文件,我们现在要映射一个5000 Bytes的共享内存区域,由于系统是按页来分配内存,在所用的系统中,一页是4KB,所以实际申请可访问的内存空间会被扩展,创建共享内存时映射语句为:

1
addr = mmap(NULL, 5000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

由于映射长度5000 Bytes并非系统分页大小的整数倍,所以会按照分页大小,自动向上取整,所以实际映射大小为8092 Bytes,即文件从起始位置有8092 Bytes映射到虚拟内存中。
文件中未被映射的区域是无法通过虚拟内存进行访问的,如果强制向虚拟内存末尾后面访问,则会访问到一片未定义的地址,产生SIGSEGV信号,默认该信号会终止进程,并打印core dump【注意事项】这一点其实我挺有疑问的,毕竟我尝试的时候似乎并没有什么特别的现象,代码也在正常跑,后便找到问题所在再补充吧。后来似乎明白了,原因在5.3一节的图中,当我使用访问4个页面外的内存时,出现了这个信号。
由于实际文件映射大小为8092 Bytes,且为共享映射,所以映射后虚拟内存的8092 Bytes可读可写,且修改的内容会同步到实际文件中。

7.2 超出文件范围的内存映射

image-20220606101734501

这种情况就是,我们要映射的区域大小超过文件大小,如上图,我们有一个2200 Bytes的文件,我们现在要映射一个6000 Bytes的共享内存区域,由于系统是按页来分配内存,在所用的系统中,一页是4KB,所以实际申请可访问的内存空间会被扩展,创建共享内存时映射语句为:

1
addr = mmap(NULL, 6000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

映射长度为6000 Bytes,自动向上对齐系统分页,所以实际映射大小为8092 Bytes。但文件的实际大小为2000 Bytes。所以长度为8092 Bytes的虚拟内存映射区域被分为三段。第一段,即实际真实文件映射大小2000 Bytes,该区域是可读、可写,且修改内容会同步到真实文件中;第二段,即第一个分页中除开首部2000 Bytes的剩余部分,大小为2096 Bytes,该部分可读可写,但由于没有文件映射与该内存相对应,所以写入内容无法同步到真实文件中;第三段,即第二个分页,该分页无文件与之对应,是为了映射长度的分页对齐而自动扩展的,如果访问该部分内容会产生SIGBUS信号。
访问未被映射的区域会产生SIGSEGV信号。【注意事项】这一点其实我挺有疑问的,毕竟我尝试的时候似乎并没有什么特别的现象,代码也在正常跑,后便找到问题所在再补充吧。后来似乎明白了,原因在5.3一节的图中,当我使用访问4个页面外的内存时,出现了这个信号。

7.3 有偏移的文件内存映射

image-20220606105047523

当我看到这张图的时候,上边两个疑问似乎都解决了,真正映射到进程的虚拟地址空间的起始是创建共享内存页面数量的2倍,区域之外的访问才会产生SIGSEGV信号。

三、System V 共享内存

前边我们已经知道了System V IPC包含三种通信机制,分别是消息队列、共享内存和信号量,这一部分就刚好是学习共享内存的笔记,就一起写在这里吧。

1. 创建或者打开共享内存

1.1 shmget() 

linux下可以使用man 2 shmget 命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/ipc.h>
#include <sys/shm.h>
/* 函数声明 */
int shmget(key_t key, size_t size, int shmflg);

【函数说明】该函数得到一个共享内存标识符ID或创建一个共享内存对象。

【函数参数】

  • keykey_t类型,通过ftok创建的key(键)值或者是IPC_PRIVATE(这样的话只能用于具有血缘关系的进程通信)。
  • sizesize_t类型,指定共享内存的大小,以字节为单位。所有的内存分配操作都是以页为单位的。所以如果一段进程只申请一块只有一个字节的内存,内存也会分配整整一页(在i386机器中一页的缺省大小PACE_SIZE=4096字节)这样,新创建的共享内存的大小实际上是从size这个参数调整而来的页面大小。即如果size14096,则实际申请到的共享内存大小为4K(一页);40978192,则实际申请到的共享内存大小为8K(两页),依此类推。
  • shmflgint类型,共享内存标志位,使用时需要与IPC对象存取权限(如0666)进行|运算来确定读写权限。一般是需要创建共享内存时为 IPC_CREAT|0666,若不需要创建,直接打开时,使用0666即可。
点击查看常用可取的值及含义
IPC_CREAT如果共享内存不存在,则创建一个共享内存,否则打开操作。
IPC_EXCL 只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误。
  • 如果单独使用IPC_CREATshmget()函数要么返回一个已经存在的共享内存的操作符,要么返回一个新建的共享内存的标识符。

  • 如果将IPC_CREATIPC_EXCL标志一起使用,shmget()将返回一个新建的共享内存的标识符;如果该共享内存已存在,就会返回错误。

【返回值】int类型,成功返回共享内存的标识符,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <sys/ipc.h>
#include <sys/shm.h>

/* 至少应该有的语句 */
int shmid;
key = ftok("./keytest.txt", 100);
shmid = shmget(key, 512, IPC_CREAT|0666);

【注意事项】共享内存的大小是有限制的,可以使用以下命令查看:

1
2
3
ipcs -l
# 或者
cat /proc/sys/kernel/shmmax

1.2 使用实例

暂无。

2. 映射共享内存

2.1 shmat() 

linux下可以使用man 2 shmat命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/shm.h>
/* 函数声明 */
void *shmat(int shmid, const void *shmaddr, int shmflg);

【函数说明】该函数将shmid标识的共享内存映射到当前进程的虚拟地址空间,随后可像本地空间一样访问。

【函数参数】

  • shmidint类型,共享内存IPC对象的ID
  • shmaddrvoid *类型,表示映射的方式,若为NULL,共享内存会被系统内核自动映射到一个合适的虚拟地址空间,建议使用NULL;若不为NULL,我自己没试过,可以参考函数帮助手册。
  • shmflgint类型,一些标志,一般是不指定的,写0就可以啦,表示可读可写。其他的可以查看帮助手册,若设置为SHM_RDONLY表示只读模式;不指定的话默认是读写权限;若设置为IPC_REMAP,表示替换位于shmaddr处的任意已有映射。

【返回值】void *类型,成功返回映射的共享内存段的地址,失败返回(void *) -1,并设置errno来指示错误的原因。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <sys/ipc.h>
#include <sys/shm.h>

/* 至少应该有的语句 */
char *buf;
buf = shmat(shmid, NULL, 0);

【注意事项】

(1)fork()后创建的子进程继承已连接的共享内存地址。

(2)exec执行时会执行其他进程,exec执行的进程将会与已连接的共享内存地址自动脱离(detach)。

(3)进程结束后,已连接的共享内存地址会自动脱离(detach).

2.2 使用实例

暂无。

3.撤销共享内存

3.1 shmdt() 

linux下可以使用man 2 shmdt命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/shm.h>
/* 函数声明 */
int shmdt(const void *shmaddr);

【函数说明】该函数是用来撤销共享内存映射,将会禁止本进程访问此片共享内存。

【函数参数】

  • shmaddrvoid *类型,表示要撤销的共享内存的首地址。

【返回值】int类型,成功0,失败返回-1,并设置errno来指示错误的原因。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/ipc.h>
#include <sys/shm.h>

/* 至少应该有的语句 */
char *buf;
buf = shmat(shmid, NULL, 0);

shmdt(buf);

【注意事项】撤销后,内存地址不可再访问,但是共享内存依然存在,共享内存占用的内存空间并未被释放。

3.2 使用实例

暂无。

4. 共享内存控制

4.1 shmctl() 

linux下可以使用man 2 shmctl命令查看该函数的帮助手册。

1
2
3
4
5
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/shm.h>
/* 函数声明 */
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

【函数说明】该函数是用来控制创建的共享内存,一般是用来删除创建的共享内存。

【函数参数】

  • shmidint类型,表示共享内存IPC对象的ID,由shmget函数生成,不同的key值对应不同的ID值。
  • cmdint类型,表示要执行的操作。
点击查看常见 cmd 取值
IPC_RMID 删除共享内存(释放内存空间)。公共的IPC选项(ipc.h中)
IPC_SET 设置ipc_perm参数,改变共享内存状态。公共的IPC选项(ipc.h中)
IPC_STAT 获取ipc_perm参数,获取共享内存状态。>公共的IPC选项(ipc.h中)
SHM_LOCK 锁定共享内存段,需要root权限。共享内存自己的选项(shm.h中)
SHM_UNLOCK解锁共享内存段,需要root权限。共享内存自己的选项(shm.h中)
  • bufstruct shmid_ds类型,保存或设置共享内存属性的地址。

【返回值】int类型,成功0,失败返回-1,并设置errno来指示错误的原因。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <sys/ipc.h>
#include <sys/shm.h>

/* 至少应该有的语句 */
shmctl(shmid, IPC_RMID, NULL);/* 释放共享内存空间 */

【注意事项】none

4.2 使用实例

暂无。

5. 使用实例

5.1 使用实例1

这个例子是使用ftok()函数创建key,以实现不同进程的通信。

点击查看实例
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
/* 头文件 */
#include <stdio.h> /* perror */
#include <sys/ipc.h> /* ftok shmget */
#include <sys/shm.h> /* shmget shmat */
#include <string.h> /* strcpy */
#include <unistd.h> /* sleep */
/* 主函数 */
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *buf;
/* 1. 生成key */
key = ftok("./keytest.txt", 100);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 打开共享内存(之前已创建) */
shmid = shmget(key, 512, 0666);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
buf = shmat(shmid, NULL, 0);
if(buf < 0)
{
perror("shmat");
return -1;
}
printf("buf=%p\n", buf);
/* 4. 读取数据 */
printf("share memory=%s\n", buf);

/* 5. 撤销共享内存(但并未释放内存空间) */
shmdt(buf);
/* 6. 释放共享内存空间 */
shmctl(shmid, IPC_RMID, NULL);
}
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
/* 头文件 */
#include <stdio.h> /* perror */
#include <sys/ipc.h> /* ftok shmget */
#include <sys/shm.h> /* shmget shmat */
#include <string.h> /* strcpy */

/* 主函数 */
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *buf;
/* 1. 生成key */
key = ftok("./keytest.txt", 100);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 创建或者打开共享内存 */
shmid = shmget(key, 512, IPC_CREAT|0666);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
buf = shmat(shmid, NULL, 0);
if(buf < 0)
{
perror("shmat");
return -1;
}
printf("buf=%p\n", buf);
/* 4. 写入数据 */
strcpy(buf, "Fanhua write!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
CC = gcc

DEBUG = -g -O2 -Wall
CFLAGS += $(DEBUG)

# 所有.c文件去掉后缀
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}

all : $(TARGET_LIST)

%.o : %.c
$(CC) $(CFLAGS) -c $< -o $@

.PHONY: all clean clean_o
clean : clean_o
@rm -vf $(TARGET_LIST)

clean_o :
@rm -vf *.o

在终端执行以下命令编译程序:

1
2
3
make # 生成可执行文件
./shm_wirte # 在一个终端执行共享内存写入程序
./shm_read # 在另一个终端执行读取共享内存的程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
9
10
11
12
# 执行 ./shm_write 的终端
hk@vm:~/2Sharedfiles/5temp$ ./shm_write
key=643d0b42
shmid=10
buf=0x7fb80b514000

# 执行 ./shm_read 的终端
hk@vm:~/2Sharedfiles/5temp$ ./shm_read
key=643d0b42
shmid=10
buf=0x7f24393f6000
share memory=Fanhua write!

5.2 使用实例2

这个例子是,使用IPC_PRIVATE创建IPC对象的key,这样的话只能是具有血缘关系的进程之间通信。

点击查看实例
test.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
#include <stdio.h>   /* perror */
#include <sys/ipc.h> /* shmget */
#include <unistd.h> /* fork */
#include <sys/shm.h> /* shmget shmat */
#include <string.h> /* strcpy */
#include <stdlib.h> /* exit */

int main()
{
int shmid;
char *buf = NULL;
pid_t pid;
/* 1.创建共享内存 */
if((shmid = shmget(IPC_PRIVATE, 512, 0666)) == -1)
{
perror("shmget");
exit(-1);
}
/* 2.创建父子进程 */
pid = fork();
if (pid < 0)/* 创建失败 */
{
perror("fork");
exit(-1);
}
else if (pid == 0)/* 子进程 */
{
printf("This is child process!\n");
/* 映射共享内存 */
if ((buf = shmat(shmid, NULL, 0)) == (void *)-1)
{
perror("shmat");
exit(-1);
}
/* 向共享内存写数据 */
strcpy(buf, "This is child process!\n");
system("ipcs -m");/* 显示IPC信息 */
/* 撤销共享内存 */
if (shmdt(buf) == -1)
{
perror("shmdt");
exit(-1);
}
system("ipcs -m");/* 显示IPC信息 */
printf("child process end!\n");
}
else /* 父进程 */
{
sleep(1);
printf("This is father process, please enter a char:");
getchar();
if ((buf = shmat(shmid, NULL, 0)) == (void *)-1)
{
perror("shmat");
exit(-1);
}
printf("share mem=%s\n", (char *)buf);
if (shmctl(shmid, IPC_RMID, NULL) == -1)
{
perror("RM");
exit(-1);
}
}
return 0;
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
This is child process!

------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x27f87e6c 9 hk 644 10 0
0x00000000 15 hk 666 512 1


------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x27f87e6c 9 hk 644 10 0
0x00000000 15 hk 666 512 0

child process end!
This is father process, please enter a char:
share mem=This is child process!

子进程先运行,父进程1s后运行,这时候子进程已经运行完毕,当我们输入任意字符,按下enter按键后,父进程读取共享内存数据。