Linux内存寻址机制

本篇主要介绍了Linux中的内存寻址机制,包括 内存地址的类型、分段、分页机制

参考资料《深入理解Linux内核 第三版》第二章

内存地址

80x86处理器区分以下三种不同的内存地址:

  • 逻辑地址

    包含在机器指令中用来指出一个数据或指令的地址。在分段机制中,要求把地址空间分成不同的段(如代码段数据段等),这样逻辑地址就由一个段(segment)和段内的便宜(offset)组成。

  • 线性地址(虚拟地址)

    逻辑地址经过分段单元处理后得到的即为线性地址,也叫虚拟地址,它对应的是分页机制,即把进程地址空间和物理内存都分成固定大小的页(常规为4kb大小),线性地址由页标号和页内偏移组成。由于在linux中,可以把虚拟地址等同于逻辑地址。

  • 物理地址

    内存芯片级用来寻址的地址,它对应于cpu发送到地址总线上的电信号,即为物理内存真实的地址。

内存控制单元(MMU)通过分段单元(segmentation unit)将逻辑地址转换为虚拟地址,通过分页单元(paging unit)将虚拟地址转换为物理地址。

在Linux中,所有段都从0x00000000开始,所以Linux下的逻辑地址和虚拟地址的值其实就是一样的。因此我这里主要关注了分页机制。

分段机制

下面粗略看看CPU在保护模式下的分段机制(关于实模式和保护模式参考https://zhuanlan.zhihu.com/p/42309472)

  • 段描述符

    每个段会有一个8字节的段描述符来描述,它记录了一个段的一些信息,比如段的起始线性地址、段的类型、存取权限、描述符特权级等。

    段描述符放在全局描述符表GDT或局部描述符表LDT中。GDT通常只有一个,而进程如果需要创建附加的段,可以创建自己的LDT,GDT在主存中的地址和大小存放在gdtr控制寄存器中,当前正在被使用的ldt地址和大小放在ldtr控制寄存器中。

  • 段选择符

    逻辑地址由16位段标识符和一个32位段内相对地址偏移组成。段标识符是一个16位长的字段,称为段选择符

    TI:Table Indicator,当TI=0表示查找GDT表,TI=1则查找LDT表

    INDEX:在GDT表或LDT表中的索引号,查找哪个段描述符

  • 段寄存器

    为了快速找到段选择符,处理器提供了段寄存器:cs、ds、ss、es、fs、gs,段寄存器用于存放段选择符。

    另外CS寄存器中还有两位用来指出CPU的当前特权级(Current Privilege Level)CPL,Linux只用两个级别分别是0和3,也分别称为内核态和用户态。

分页机制

刚才提到,Linux中对分段机制的使用非常有限,逻辑地址在数值上等同于线性地址(虚拟地址),而分页机制才是Linux主要使用的。

硬件中的分页

分页单元把线性地址分成一组一组的页,页内部连续的线性地址被映射到连续的物理地址中,也把物理地址(主存)按照同样的大小分成一组一组的页框(page frame),或者说是物理页。内核可以指定一个页的存取权限和物理地址,如果请求的访问类型和页的存取权限不匹配,会产生一个缺页异常。

cr0寄存器中的PG位用来指明是否启用分页机制,当PG=0时,线性地址就被解释为物理地址了。

把线性地址映射到物理地址的数据结构称为页表(page table),页表存放在主存中。

一个32位的线性地址被分为三个部分:

线性地址到物理地址的转换分两步进行,第一步是根据地址中的[目录]部分 到页目录中查询获得页表的地址,第二部则是根据地址中的[页表]部分到页表中查询页的地址,查询到页的地址后,加上地址中的[offset]即可得到线性地址对应的物理地址了。如下图:

每个活动进程都必须会分配得到一个页目录,但是页表则没有必要马上装进主存,当进程实际需要一个页表时才给该页表分配RAM,即,当真正访问一个物理地址的时候,才建立该页(线性地址)和物理页框(物理地址)的映射,这样会更有效率。

正在使用的页目录存放在cr3寄存器中,线性地址中的Directory字段指出目录表中的目录项,而目录项中存放有页表的地址;线性地址中的Table字段又指出页表中的表项,而表项中则含有页框的物理地址;最后,由Offset字段指定页内的偏移。由于offset字段为12位,因此一个页的大小就是2^12=4096字节(4KB)

页目录项和页表项是同样的结构:

如图所示,其中位31~12含有物理地址的高20位,用于定位物理地址空间中一个页面(也称为页帧)的物理基地址。表项的低12位含有页属性信息。

P–位0是存在(Present)标志,用于指明表项对地址转换是否有效。P=1表示有效;P=0表示无效。在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。如果P=0,那么除表示表项无效外,其余位可供程序自由使用,如图4-18b所示。例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。

R/W–位1是读/写(Read/Write)标志。如果等于1,表示页面可以被读、写或执行。如果为0,表示页面只读或可执行。当处理器运行在超级用户特权级(级别0、1或2)时,则R/W位不起作用。页目录项中的R/W位对其所映射的所有页面起作用。

U/S–位2是用户/超级用户(User/Supervisor)标志。如果为1,那么运行在任何特权级上的程序都可以访问该页面。如果为0,那么页面只能被运行在超级用户特权级(0、1或2)上的程序访问。页目录项中的U/S位对其所映射的所有页面起作用。

A–位5是已访问(Accessed)标志。当处理器访问页表项映射的页面时,页表表项的这个标志就会被置为1。当处理器访问页目录表项映射的任何页面时,页目录表项的这个标志就会被置为1。处理器只负责设置该标志,操作系统可通过定期地复位该标志来统计页面的使用情况。

D–位6是页面已被修改(Dirty)标志。当处理器对一个页面执行写操作时,就会设置对应页表表项的D标志。处理器并不会修改页目录项中的D标志。

AVL–该字段保留专供程序使用。处理器不会修改这几位,以后的升级处理器也不会。

Linux中的分页

如上述,两级的页表对于32位地址来说已经够用了,但64位则需要更多的分页级别来支持。

Linux采用了一种同时适用于32位和64位的分页模型:4级分页模型

  • 页全局目录
  • 页上级目录
  • 页中间目录
  • 页表

这样一个线性地址会被分为5个部分。对于没有启用物理地址扩展的32位系统,两级页表已经足够了,Linux通过使“页上级目录”位和“页中间目录”位均为0,从根本上消除了这俩字段,但页上级目录和页中间目录在指针序列中的位置仍保留,以便同样的代码在32位和64位系统下都能用。(内核为页上级目录和页中间目录保留了一个位置,这是通过把它们的页目录项数设为1,并把这两个目录项映射到全局目录的一个适当的目录项实现的)

在4级分页模型下,Linux的分页模式如下图:

0%