内存管理与进程映射 、 虚拟内存 、 内存映射的建立和解除
1 进程的内存布局
1.1 问题
程序是保存在磁盘上的可执行文件。当程序被运行时,需要将可执行文件加载到内存,在内存中的可执行文件形成进程,一个程序(文件)可以同时存在多个进程(内存)。
1.2 方案
进程在内存空间中的布局形成进程映像,从低地址到高地址依次为
代码区(text):其中存放的是可执行指令、字面值常量、具有常属性且被初始化的全局变量和静态局部变量。
数据区(data):其中存放的是不具有常属性且被初始化的全局变量和静态局部变量。
BSS区(bss):其中存放的是未被初始化的全局变量和静态局部变量。
堆区(heap):其中存放的是动态分配内存。
栈区(stack):其中存放的是非静态局部变量。
参数和环境区:其中存放的是命令行参数和环境变量。
2 内存壁垒
2.1 问题
内存壁垒是指同一个虚拟内存地址,在不同的进程中,会被映射到完全不同的物理内存区域,因此在多个进程之间以交换虚拟内存地址的方式交换数据是不可能的。
2.2 方案
每个进程的用户空间地址都是在0~3G-1之间,但它们所对应的物理内存却是各自独立的,系统为每个进程的用户空间维护一张专属于该进程的内存映射表,记录虚拟内存到物理内存的对应关系,因此在不同进程之间交换虚拟内存地址是毫无意义的。用户空间的内存映射表会随着进程的切换而不断发生变化。
所有进程的内核空间地址都是在3G~4G-1之间,它们所对应的物理内存只有一份,系统为所有进程的内核空间维护一张内存映射表init_mm.pgd,记录虚拟内存到物理内存的对应关系,因此不同进程通过系统调用所访问的内核代码和数据是同一份。内核空间的内存映射表则无需随着进程的切换而发生变化。
一切对虚拟内存的越权访问,都将导致段错误。如:试图访问没有映射到物理内存的虚拟内存或试图以非法方式访问虚拟内存,如对只读内存做写操作等。
3 标准内存分配函数
3.1 问题
标准库提供了三个标准内存分配函数,它们是malloc、calloc、realloc。本案例讲解它们的实现原理。
3.2 方案
这三个函数在标准库内部维护一个线性链表,该链表用于管理堆中动态分配的内存。当使用其中一个函数分配内存时,会生成线性链表的一个结点,该结点首先包括附加若干(通常是12个)字节,用于存放控制信息,被称为内存控制块(MCB),该信息一旦被意外损坏,可能在后续操作中引发异常,然后包括申请分配的字节空间,用于返回给用户存放数据。线性链表的示意图,如图-1。

图-1
虚拟内存到物理内存之间存在映射。物理内存就是计算机主板上实际拥有的内存条,虚拟内存是进程运行时能访问到的所有内存空间的总和,即有可能有一部分不在物理内存中,而在磁盘上。计算机会对虚拟内存地址空间分页产生页(page),对物理内存地址空间分页产生页帧(page frame),这个页和页帧的大小是一样大的,4K(4096字节)。虚拟内存页的个数一般都大于物理内存页帧的个数。在计算机上有一个页表(page table),就是映射虚拟内存页到物理内存页的,更确切的说是页号到页帧号的映射,而且是一对一的映射。但问题是虚拟内存页的个数大于物理内存页帧的个数,这样会不会有些虚拟内存页的地址永远没有对应的物理内存地址空间呢?不会,操作系统是这样处理的。操作系统有个页面失效(page fault)功能,即找到一个最少使用的页帧,让他失效,并把它写入磁盘,随后把需要访问的页放到页帧中,并修改页表中的映射,这样就保证所有的页都有被调度的可能了。这就是虚拟内存地址到物理内存的映射。
4 内存映射的建立与解除
4.1 问题
所谓内存分配与释放,其本质就是建立或解除从虚拟内存到物理内存的映射,并在底层维护不同形式的数据结构,以把虚拟内存的占用与空闲情况记录下来。
4.2 步骤
实现此案例需要按照如下步骤进行。
步骤一:建立内存映射
代码如下所示:
- #include <stdio.h>
- #include <sys/mman.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char* p = (char*)mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
- if (p == MAP_FAILED)
- {
- perror ("mmap");
- exit (EXIT_FAILURE);
- }
- strcpy (p, "Hello, Memory !");
- printf ("%s\n", p);
- return 0;
- }
上述代码中,以下代码:
- char* p = (char*)mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
- if (p == MAP_FAILED)
- {
- perror ("mmap");
- exit (EXIT_FAILURE);
- }
使用mmap函数建立虚拟内存到物理内存或文件的映射。下面为函数形参的说明:
第一个形参为映射区内存起始地址,NULL系统自动选定后返回。
第二个形参为映射区字节长度,自动按页(4K)圆整。
第三个形参为映射区访问权限,可取以下值
PROT_READ - 映射区可读
PROT_WRITE - 映射区可写
PROT_EXEC - 映射区可执行
PROT_NONE - 映射区不可访问
第四个形参为映射标志,可取以下值
MAP_ANONYMOUS - 匿名映射,将虚拟内存映射到物理内存而非文件,忽略fd和offset参数
MAP_PRIVATE - 对映射区的写操作只反映到缓冲区中,并不会真正写入文件
MAP_SHARED - 对映射区的写操作直接反映到文件中
MAP_DENYWRITE - 拒绝其它对文件的写操作
MAP_FIXED - 若在start上无法创建映射,则失败(无此标志系统会自动调整)
MAP_LOCKED - 锁定映射区,保证其不被换出
第五个参数为文件描述符。
第六个参数为文件偏移量,自动按页(4K)对齐。
步骤二:解除内存映射
代码如下所示:
- #include <stdio.h>
- #include <sys/mman.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char* p = (char*)mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
- if (p == MAP_FAILED)
- {
- perror ("mmap");
- exit (EXIT_FAILURE);
- }
- strcpy (p, "Hello, Memory !");
- printf ("%s\n", p);
- if (munmap (p, 4096) == -1)
- {
- perror ("munmap");
- exit (EXIT_FAILURE);
- }
- strcpy (p += 4096, "Hello, Memory !");
- printf ("%s\n", p);
- if (munmap (p, 4096) == -1)
- {
- perror ("munmap");
- exit (EXIT_FAILURE);
- }
- return 0;
- }
上述代码中,以下代码:
- if (munmap (p, 4096) == -1)
- {
- perror ("munmap");
- exit (EXIT_FAILURE);
- }
使用munmap函数解除虚拟内存到物理内存或文件的映射。该函数的第一个形参为映射区内存起始地址,必须是页的首地址,第二个形参为映射区字节长度,自动按页(4K)圆整。munmap函数允许对映射区的一部分解映射,但必须按页。
4.3 完整代码
本案例的完整代码如下所示:
- #include <stdio.h>
- #include <sys/mman.h>
- #include <stdlib.h>
- #include <string.h>
- int main()
- {
- char* p = (char*)mmap (NULL, 8192, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, 0, 0);
- if (p == MAP_FAILED)
- {
- perror ("mmap");
- exit (EXIT_FAILURE);
- }
- strcpy (p, "Hello, Memory !");
- printf ("%s\n", p);
- if (munmap (p, 4096) == -1)
- {
- perror ("munmap");
- exit (EXIT_FAILURE);
- }
- strcpy (p += 4096, "Hello, Memory !");
- printf ("%s\n", p);
- if (munmap (p, 4096) == -1)
- {
- perror ("munmap");
- exit (EXIT_FAILURE);
- }
- return 0;
- }