《深入理解Linux内核3rd》学习笔记——第2章:内存寻址

  基于80x86微处理器的计算机中,内存寻址的转换过程是:逻辑地址→线性地址(虚拟地址)→物理地址。参与内存寻址的MMU(存储器管理单元)中有两个重要的部分——分段单元和分页单元,前者负责将逻辑地址转换为线性地址,后者负责将线性地址转换为实际的物理地址。

 

硬件分段机制 

  每个逻辑地址包含两个部分:一个段标识和一个段中偏移offset。这个段标识就是段选择子(Segment Selector),该数据结构中有3个域:index域、TI域、RPL域。TI=0表示该段保存在全局描述符表GDT中,TI=1表示该段保存在局部描述符表LDT中。一种寄存器叫段寄存器,保存了这个段选择子。
  每个段还有一个段描述符(Segment Descriptor)与之对应,该数据结构中保存了段的基本属性,比如访问权限和段长等。通过这个段描述符的Base域能够定位到这个段所对应的线性地址。

  在Linux系统中,有两种表,GDT和LDT,GDT是唯一的,这两个数据结构中保存了一些段的段描述符,通过它们可以在内存中找到某个段的段描述符。

  寻址的过程是这样的。从段寄存器中取出段选择子,段选择子的index域乘以8之后得到一个偏移,该偏移就是该段的段描述符在GDT或LDT中的索引,通过TI得到该段是在GDT还是LDT中,并从GDTR或LDTR寄存器中得到GDT或LDT的地址,然后与偏移相加,即得到该段的段描述符地址,然后与逻辑地址中的段中偏移相加,即得到了该逻辑地址对应的线性地址。即

  当TI=0时,linear addr = index * 8 + [GDTR] + offset,否则linear addr = index * 8 + [LDTR] + offset

  为了更快地进行逻辑地址到线性地址的转换,80x86提供了一个不可编程的寄存器,用来存放段描述符,这样,当一个段的段选择子被段寄存器加载的时候,不可编程寄存器就从内存加载该段的段描述符地址,这样,就可以通过不可编程寄存器得到段描述符的地址,不需要通过GDT或LDT了,加速了地址转换的过程。 

 

硬件分页机制

  分页单元是将线性地址转换为物理地址。

  一个线性地址被划分为固定长度的多个块,每块称之为一个页或页面(page),在一个页面上的连续的地址也被映射到连续的物理地址上。

  分页单元认为RAM是被划分为多个等长的页框(page frame)的,每个页框包含一个页面。

  常规的分页方法从80386开始,将32位线性地址划分为3部分:目录(高10位)、表(中间10位)、偏移(低12位)。这样,每个页就有2的12次方,即4KB的大小。同时,CPU有一个CR3寄存器,保存页目录的基地址,实际上CR3与进程关联,不同进程的CR3中的值不同。

  常规的分页采用了2级分页,其寻址过程是这样的,1、CR3中的值(页目录基地址)+线性地址的目录的值=页表基地址;2、页表基地址+线性地址中的表的值=页框物理地址,该页框物理地址包含了一个页面的数据;3、页框基地址+线性地址的偏移=物理地址。其中,页目录数据结构包含了一些页表的属性位,而页表数据结构包含了一些页面的属性。

  关于PAE(物理地址扩展)和64位地址的分页,其原理和常规的分页方式类似,且64位地址具有平台依赖性,本文不进行描述。

  为了降低CPU访存带来的负面的效率问题,在CPU和RAM中引入了高速缓存,其依据是“局部性原理”。

  80x86中还包含了称之为转换后援缓冲器(TLB)的硬件,以此来加速线性地址的转换,每个CPU都有一个自己的TLB,当一个线性地址被第一次使用时,计算其物理地址,并将物理地址存放在一个TLB表中,以后每次访问同一个线性地址都能够通过TLB快速地得到转换。当CPU的CR3寄存器被修改时,该CPU的TLB所有项都会无效。

 

Linux的分页机制

  从Linux 2.6.11开始,Linux根据不同的架构,将分页机制统一为4级分页机制,可以适应PAE和64位地址,其核心思想与常规分页相同,仅仅是中间引入了Page Upper Directory和Page Middle Directory这两个数据结构。

  Linux 2.6.11的内核源码中,定义了很多宏和函数来操作Page Table、Page Middle Directory、Page Upper Directory和Page Global Directory,本文不进行详细描述。

 

物理地址布局

  在Linux系统初始化的时候,内核必须建立一个物理地址映射,来指明哪些物理地址范围能够被内核使用。一般地,Linux内核被加载到物理地址为0x00100000的RAM上,前面空出了1MB的空间。这是因为一些空出的空间要被BIOS来使用。

  在系统引导的早期阶段,内核请求BIOS并获得物理地址的大小。在现代的计算机中,内核调用BIOS过程来建立一个物理地址范围表,以及其对应的存储类型。

  然后系统调用函数machine_specific_memory_setup(void)(include/asm-i386/mach-default/setup_arch_post.h),该函数创建物理地址映射,在该函数中,通过BIOS的E820表得到内存映射的信息。如果没法通过E820表来得到信息,该函数按照默认的方式来建立内存映射表:从0x9F到0x100之间的页框被标记为保留。

  setup_memory(void)函数在machine_specific_memory_setup之后被调用,用来分析物理地址区域并初始化一些数据来描述物理地址的布局。

进程页表和内核页表

  进程的线性地址分为两个部分:0x00000000~0xbfffffff(3GB)的用户态线性地址和0xc0000000~0xffffffff(1GB)的内核态线性地址。宏PAGE_OFFSET的值即为0xc0000000——内核空间的开始处。

  页全局目录表的前一部分映射的线性地址小于0xc000000(PAE未启动时是前768项,PAE启动后是前3项),其剩余的表项对所有进程来说都是一样的,等于主内核全局目录相应的表项。

  内核维护着自己使用的页表,称之为主内核页全局目录。当内核映像刚刚被装入内存后,CPU运行于实模式,此时分页功能未启用。内核初始化自己的页表的过程分为两个阶段。

  第1个阶段,内核创建一个有限的地址空间,包括内核代码段和内核数据段、初始页表和用于存放动态数据结构的128KB的空间,该空间仅够内核装入RAM盒对其初始化的核心数据结构。

  第2个阶段,内核充分利用剩余的RAM来建立页表(见“临时内核页表”)。

临时内核页表

  临时页全局目录是在内核编译过程中静态地初始化,而临时页表则由函数startup_32函数(arch/i386/kernel/head.S)初始化的,此时的Page Upper Directory和Page Middle Directory等同于页全局目录项。

  临时页全局目录存放在swapper_pg_dir变量中,临时页表在pg0变量处开始存放,紧接在内核为初始化的数据段之后。这里假设内核、临时页表和上文提到的128KB的空间能够容纳在RAM的前8MB空间里。为了映射8MB空间,内核需要用到2个页表项。

  分页的第一个阶段的目标是允许在实模式下和保护模式下都能很容易地对这8MB进行寻址,因此,内核创建一个映射,把0x00000000~0x007fffff(8M)和0xc0000000~0xc07fffff(8M)的线性地址映射到0x00000000~0x007fffff的物理地址。

  然后,内核把swapper_pg_dir的所有项都填充为0来创建期望的映射,除了第0、1、0x300(768)和0x301(769)这四个表项。这4项初始化过程如下:

  • 0 项和0x300项的地址字段设置为pg0的物理地址,而1项和0x301项的地址字段设置为紧随pg0后的页框的物理地址。
  • 这4项的Present、Read/Write和User/Supervisor标志置位。
  • 这4项的Accessed、Dirty、PCD、PWD和Page Size标志清零。

  同时,在startup_32函数中也启用了分页单元,即向cr3寄存器中写入swapper_pg_dir的地址,并设置cr0寄存器的PG标志。

posted on 2010-04-29 15:10  小虎无忧  阅读(2149)  评论(0编辑  收藏  举报

导航