【Linux 0.12】内存管理

内存管理

Linux 内核采用分页管理方式,利用页目录和页表结构处理内存的申请和释放操作。内存管理以页面为单位进行的。一个内存页面指的是连续的 4KB 物理内存。

总体功能

80x86 架构下,程序在寻址过程中使用 段地址+段偏移 实现。而该地址并不能直接用于寻找物理地址,因此被称为虚拟地址。

虚拟地址首先通过段管理机制编程一种中间地址形式——32位的线性地址,然后使用分页管理机制将此线性地址映射到物理地址。

内存分页管理机制

intel x86 系统中,内存分页管理是通过页目录表页表组成二级表进行的。

img

每个表的表项都是 4B,当指定了一个页目录表项和对应的页表项之后,我们就能唯一地确定所对应的物理内存页。

1024 * 1024 * (1024 * 4)= 4 GB,因此所有页表最多可寻址 4G 的内存空间。

在 Linux 0.12 内核中,所有进程都使用一个页目录表(位于物理地址 0x0 处),而每个进程都有自己的页表。内核代码段和数据段的长度是 16M,使用了 4 个页表(占用 4 个页目录项)。而这四个页表直接位于页目录表的后面,参见 head.s 程序。

内核使用的 4 个页表项直接映射到对应的 16MB 物理地址上。也就是说对于内核段来说,线性地址就是物理地址(仅针对 0.12 内核而言)。

用户态程序在申请物理内存时使用的是线性地址。线性地址映射到页目录、页表再到物理地址的方法如下图:

img

img

一个系统中可以存在多个页目录表,而在某个时刻,只有一个页目录表可用。当前使用的页目录表是由 CPU 寄存器 CR3 来决定的,它保存了当前页目录表的物理内存地址。而 Linux 内核只使用了一个页目录表

页目录和页表项结构如下:

img

Linux 内核对线性地址空间的使用和分配

因为使用内核版本 4.x 的 linux 系统时,每个进程所使用的线性地址空间(或者虚拟地址空间)为 4G,这使得我在阅读本章内容时产生了困扰。然而事实上,在 Linux 0.12 系统下,每个进程占用的逻辑空间为 64M。

每个进程在线性地址空间的地址都是从 nr * 64MB 的位置开始的(nr 是任务号),占用的地址空间范围是 64MB。

页面出错异常处理

在开启了分页机制的状态下,CPU 在执行线性地址到物理地址的过程中,如果如果遇到以下条件,就会引起页出错异常中断 int 14:

  1. 地址变换中用到的页目录项或者页表项的存在位 P 为 0 (代表该表项无效)
  2. 当前程序没有足够的权限访问指定页面

此时,CPU 将向页出错异常程序提供两个信息来协助诊断和纠正错误:

  1. 栈中的出错码:一个 32 bit 的长字,但仅最低 3 bit 有用,分别是 P, W/R,和 U/S 位(和页表项的后 3 bit 相同)
  2. 寄存器 CR2 中的线性地址。CPU 会将引起异常的访问使用的线性地址存放在 CR2 寄存器中。页出错异常处理程序可以使用这个地址来定位相关的页目录和页表项。

其中,page.s 程序就利用上述信息来区分是缺页异常还是写保护异常,从而决定调用 memory.c 中的 do_no_page() 还是 do_wp_page() 函数。

memory.c 程序

本程序进行内存分页的管理。

内核使用一个字节数组 mem_map[] 表示物理内存页面的状态,每个字节描述一个物理内存页的占用状态,其值表示被占用的次数(?),0 表示对应的物理内存空闲。

在内存初始化的过程中,系统首先计算 1MB 以上的内存区对应的内存页面数量(PAGING_PAGES)(1MB 以上的内存区域才会被分页管理),并将 memm_map[] 内表示高速缓存区和虚拟磁盘的页面置为 100(表示占用);其余物理页面对应的值置为 0(可以被内存管理程序分配使用)。

对于虚拟地址(逻辑地址)的管理,内核使用了页目录表和页表实现。

get_free_page() 函数和 free_page() 函数用于管理主内存区中物理内存的占用和空闲情况,与每个进程的线性地址无关。

  • get_free_page() :在主内存区申请一页空闲的内存页,并返回物理内存页的起始地址

    寻找 mem_map[] 中值为 0 的项。如果找到,则将值置为 1。

  • free_page() :用于释放指定地址处的一页物理内存。

    先判断内存地址是否小于 1MB,是则直接返回(因为 1MB 以内是内核专用的内存)。如果给定的内存地址大于实际内存的最高地址,则显示出错信息。然后根据指定地址换算出页面号(页面在 mem_map[] 中的索引),查看对应的 mem_map[] 项是否为 0。不为 0 则 减一。

    只是单纯地处理了 mem_map[] 的项,这样做有什么意义?

    需要和 free_page_taboes() 配合使用。free_page() 函数在 free_page_tables() 中被调用,当处理页表项,将页表项对应的索引置为 0 时,也需要将 mem_map[] 表示的页面被引用的次数的值减一。所以 free_page() 只是一个单纯的工具人函数,不适合被独立调用(个人理解)

free_page_tables()copy_page_tables() 以一个页表对应的物理内存块为单位(1024 * 4K = 4MB)。

  • free_page_tables() :释放指定线性地址和长度(页表个数)对应的物理内存页块。首先必须保证指定地址在 4MB 的边界上,然后判断地址是否为 0 (是则出错返回)。

    为什么要以 4MB 为边界?前面的 1MB 不是内核专用吗?

    因为页目录的划分。前 1MB 也被划分在页目录的第一项里了。所以并不影响。

  • copy_page_tables() :用于复制指定线性地址和长度(指页表的个数)对应的物理内存页。

    1
    int copy_page_tables(unsigned long from,unsigned long to,long size);

    本质上,from 和 to 代表的地址都对应某个页目录项。(所以它们是虚拟地址/逻辑地址/线性地址吗?)。然后将若干个页目录项(即对应页表)从 from 拷贝到 to。

    复制并不是对物理页面的内容进行复制,而是使两个页表的内容都指向了同一块物理内存,然后用写时复制处理。

虽然 Linux 0.12 使用了虚拟地址/线性地址,但是和近代内核不同,不同进程占用线性地址的不同区域。所以给出了一个线性地址,理论上是唯一的,仅被一个进程使用。

page.s 程序