相信你一定已经多次看到下面这张图了
整个虚拟内存空间分为上下两部分, 最高地址是内核虚拟内存, 下面是进程的虚拟内存; 内核虚拟内存包含内核的代码和全局数据结构
Linux 将一组连续的虚拟页面(大小等同于 DRAM 的总理)映射到相应的一组连续的物理页面, 这就为内核提供了一种变量你的方法来访问物理内存中任何特定的位置. 这就为内核提供了一种便利的方法来访问物理内存中的特定位置; 换句话说, 在虚拟地址空间中如果要访问页表, 页表保存在内核虚拟内存中, 始终处于一个相对固定的虚拟地址位置
上图记录了 Linux 虚拟内存的相关数据结构, 内核为系统每个继承维护一个单独的任务结构 task_struct, 其中 task_struct 中的元素包含指向内核运行该进程所需的所有信息(比如 PID 指向用户栈的指针 可执行目标文件名 程序计数器等等)
task_struct 中的一个元素指向 mm_struct, 它描述了虚拟内存的当前状态, 其中比较重要的是 pgd mmap 字段, pgd 指向一级页表的基地址, 当内核运行这个进程的时候就将 pgd 放入 PTBR; mmap 指向一个 vm_area_structs 的链表, 每个 vm_area_structs 都描述了当前虚拟地址空间的一个区域, 你可以在上图中看到每一个 vm_area_struct 都指向虚拟内存空间中的一段地址, 其中
注意下面是低地址, 上面是高地址, 所以 vm_start 指向下面
相关的元素会在 mmap 函数中再次看到
当 MMU 试图翻译某个虚拟地址 A 但是发现缺页了, 这时候可能会有如上三种情况
这里的第一点要说明一下, 因为一个进程可以创建任意数量的新虚拟内存区域, 所以顺序搜索区域结构的链表花销会很大, 因此在实际中 Linux 在链表中构建一个一棵树并在这个树上进行查找
Linux 通过将一个虚拟内存区域于一个磁盘上的对象关联起来, 以初始化这个虚拟内存区域的内容, 这个过程被称为内存映射
内存映射的概念来源于一个聪明的发现, 如果虚拟内存系统可集成到传统文件系统当中, 那么就能提供一种简单而高效的把程序和数据加载到内存中的方法. 每个运行着 Linux shell程序的bash进程都有相同的代码区域, 每个C程序都需要来自标准C库的诸如 printf 这样的函数, 如果每个进程内都在物理内存中保存相同的代码副本, 那就是极端的浪费了. 幸运的是内存映射给我们提供了一种清晰的机制, 用来控制多个进程如何共享对象
内存映射的对象有两种
一旦一个虚拟页面被初始化了, 他就在一个由内核维护的专门交换文件之间换来换去, 交换文件也叫做交换空间(swap area). 在任何时刻, 交换空间都限制着当前运行中的进程能够分配的虚拟页面的总数
一个对象可以被映射到虚拟内存的一个区域, 要么作为 共享对象, 要么作为 私有对象
上图中假设进程 1 将一个共享对象映射到它的虚拟内存的一个区域中, CPU 引用这个页面之后会在物理内存中创建一份副本
当进程 2 页将同一个共享对象映射到虚拟内存的时候, 由于每个对象都有唯一的一个文件名, 内核可以迅速的判断进程 1 已经影射了这个对象, 并且可以使进程 2 的页表条目指向相应的物理页面. 关键点在于即使对象被映射到了多个共享区域, 物理内存中也只需要存放共享对象的一个副本
当进程 1 修改其虚拟页面的数据的时候, 这个修改会同步到物理内存, 磁盘, 物理内存的修改会影响到其他引用共享对象的进程
对于私有对象来说, 采用一种十分巧妙的写时复制(copy on write)的技术, 其生命周期的方式基本上与共享对象一样, 在物理内存中只保存私有对象的一份副本
对于每个映射私有对象的进程, 相应私有区域的页表条目都被标记为只读, 并且区域结构被标记为 私有的写时复制, 只有没有进程试图写自己的私有区域他们就可以继续共享物理内存中的对象的一份副本, 但是只要有一个进程试图写私有区域的某个页面, 那么这个写操作就会触发一个保护故障
当故障处理程序注意到保护异常是由于进程试图写 "私有的写时复制" 区域中的一个页面的副本, 他就会在物理内存中创建这个页面的一个新副本, 更新页表条目指向这个新的副本, 然后修改这个页面的可写权限, 如上图所示
故障处理程序返回的时候 CPU 重新执行写操作, 现在在新的页面进行写操作就可以进行正常执行了
我们可以看到通过延迟四有对象中的副本直到最后可能的时刻, 写时复制最充分的使用了稀有的物理内存
在 Linux 内核中,mm_users
和 mm_count
是 mm_struct
结构体的两个字段,它们都与内存管理单元(memory management unit)有关,但用途和含义有所不同.
mm_users
是一个原子计数器(atomic_t
),用于跟踪有多少个用户(包括进程和内核线程)正在使用这个 mm_struct
结构体.mm_users
的计数.mm_users
的计数降到 0 时,表示没有用户再使用这个 mm_struct
,此时可以释放与该地址空间相关的资源,但 mm_struct
本身可能还会被保留,因为它可能仍然被其他内核子系统(如文件系统)所引用.mm_users
通常与用户空间的进程关联,比如通过 mmget()
和 mmput()
函数来增加和减少计数.mm_count
也是一个原子计数器(atomic_t
),但它用于跟踪 mm_struct
结构体本身的引用次数.mm_struct
时,mm_count
会增加.mm_count
的计数降到 0,即没有任何进程或线程再引用这个 mm_struct
时,这个 mm_struct
将会被释放,相关的内存管理资源也会被清理.mm_count
与 mm_users
的主要区别在于,它不仅仅关注用户空间的进程,还关注内核线程和内核子系统可能对 mm_struct
的引用.简而言之,mm_users
跟踪的是使用这个地址空间的"用户"数量,而 mm_count
跟踪的是 mm_struct
结构体本身的引用次数.当一个进程退出并且它的 mm_users
计数降到 0 时,如果 mm_count
也为 0,那么 mm_struct
将会被释放.如果还有其他引用(mm_count
大于 0),则 mm_struct
会继续存在直到所有的引用都被释放.