Linux内存管理

一、为什么要提出逻辑地址、线性地址和逻辑地址

原因:

物理地址、线性地址(虚拟地址)和逻辑地址;主要是为了阐述段式管理和页式管理基本概念;Linux操作系统内存管理和虚拟内存概念;为内核开发做一个基础铺垫。

1.物理地址:

物理地址是指出现在cpu外部的地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。

2.逻辑地址:

程序代码经过编译后在汇编程序中使用的地址。

3.线性地址:

又名虚拟地址,在32位cpu框架下,可以表示4G地址空间,用16进制表示就是0x00000000到0xffffffff。

4.地址转换:

cpu要将一个逻辑地址转换为物理地址,需要两步:首先cpu利用段式内存管理单元,将逻辑地址转换成线性地址,在利用页式内存管理单元,把线性地址最终转换为物理地址。


二、段式管理与页式管理

什么是段式管理?

16位cpu内部拥有20位的地址线,它的寻址范围就是2的20次方,也就是1M的内存空间。但是16位的菜谱用于存放地址的寄存器只有16位,因此只能访问65536个存储单元,64K。为了能够访问1M的内存空间,cpu就采用了内存分段的管理模式,并在cpu内部加入了段寄存器。16位cpu把1M内存空间分为若个逻辑段,每个逻辑段的要求如下:

1、 逻辑段的起始地址(段地址)必须是16的倍数,即最后4个二进制必须全为0.

2、 逻辑段的最大容量为64K

物理地址的形成方式:

由于段地址必须是16的倍数,所以值的一般形式为XXXX0H,即前16位二进制位是变化的,后4位是固定的0,鉴于段地址的这种特性,可以只保存前16位二进制位来保存这个段基地址,所以每次使用时要用段寄存器左移4个0(乘以16)来得到实际的段地址。在确定了某个存储单元所属的段后,只是知道了该存储单元所属的范围(段地址->段地址+65536),如果想确定该存储单元的具体位置,还必须知道该单元在段内的偏移。有了段地址和偏移量,就可以唯一的确定内存单元在存储器中具体位置。

逻辑地址=段内偏移量

由于逻辑地址得到物理地址的公式为:PA=段寄存器的值*16+逻辑地址

段寄存器是为了对内存进行分段管理而增加的,16位cpu有四个段寄存器,程序可同时反问四个不同含义的段。

1、CS+IP:用于代码段的访问,CS指向存放程序的段基址,IP指向下条要执行的指令在CS段的偏移量,用这两个寄存器就可以得到一个内存物理地址,该地址存放着一条要执行的指令。

2、SS+SP:用于堆栈段的访问,SS指向堆栈段的基地址,SP指向栈顶,可以通过SS和SP两个寄存器直接访问栈顶单元的内存物理位置。

3、DS+BX:用于数据段的访问。DS中的值左移四位得到数据段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。

4、ES+BX:用于附加段的访问。ES中的值左移四位得到附加段起始地址,再加上BX中的偏移量,得到一个存储单元的物理地址。

32位pc的内存管理能然采用“分段”的管理模式,逻辑地址同样由段地址和偏移量两部分组成,32位pc的内存管理和16位pc的内存管理有相同之处,也有不同之处,因为32位pc采用了两种不同的工作方式:实模式和保护模式。我们一般使用在保护模式的。

1、 实模式

在实模式下,32位的cpu的内存管理与16位的cpu是一致的。

2、 保护模式

段基地址长达32位,每个段的最大容量可达4G,段寄存器的值是段地址的“选择器”(Selector),用该“选择器”从内存中得到一个32位的段地址,存储单元的物理地址就是该段地址加上段内偏移量,这与32位cpu的物理地址计算方式完全不同。

32位cpu内有6个段寄存器,其值在不同的模式下具有不同的含义:

1、 在实模式下:

段寄存器的值*16就是段地址

2、 在保护模式下:

段寄存器的值就是一个选择器,间接指出一个32位的段地址


什么是页式管理?

从管理和效率的角度出发,线性地址被划分为固定长度的组,称为页(page),例如32位的 ,线性地址最大可为4G,如果用4KB为一个也来划分,这样的这个线性地址就被划分为2的20次方个页。

另一类“页”,也称之为“物理页”,或者是页框、页帧。分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与线性地址页是相同的。



1、 分页单元中,页目录的地址放在cpu的cr3寄存器中,是进行地址转换的开始点。

2、 每个进程,都有其独立的虚拟地址空间,运行一个进程,首先需要在它的页目录地址放到cr3寄存器中,将其他进程的保存下来。

3、 每个32位的线性地址被划分为三个部分:页目录索引(10位),页表索引(10位):偏移(12位即4k)。

依据一下步骤进行地址转换:

1、 装入进程的页目录地址(操作系统在调度进程时,把这个地址装入cr3)

2、 根据现行线性地址的前10位,在页目录中找对应的索引项,页目录中的项是一个页表

的地址。

3、 根据线性地址的中间10位,在页表中找到页的起始地址。

4、 将页的起始地址与线性地址的最后12位相加,得到物理地址。

这样的二级模式是可以覆盖4G的物理地址空间的,

页目录中共有:2^10项,也就是说有这么多个页表;页表对应有:2^10页;每个页可寻址:2^12个字节。即2^32=4GB。

3.Linux内存管理

Linux内核的设计并没有全部采用Intel所提供的段机制,仅仅是有限度地使用了分段机制。这不仅简化了linux内核的设计,而且为把linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。

在linux中,所有段的基地址均为0,由此可以得出,每个段的逻辑地址空间范围是0-4GB。因为每个段的基地址为0,因此逻辑地址与线性地址保持一致(即逻辑地址的偏移量字段的值与线性地址的值总是相同的),在linux中所提到的逻辑地址和线性地址(虚拟地址),可以认为是一致的。看来,linux巧妙地把段机制给绕过去了,而完全利用了分页机制。

前面介绍的是i386的二级管理架构,不过有的cpu使用的三级,甚至是四级架构,linux2.6.29内核为每种cpu提供统一的界面,采用了四级页面管理架构来兼容二级、三级、四级管理架构的cpu。

这四级分别为:

1、 页全局目录(Page Global Directory):即pgd,是多级页表的抽象最高层。

2、 页上级目录(Page Upper Directory):即pud。

3、 页中间目录(Page Middle Directory):即pmd,是页表的中间层。

4、 页表(Page Table Entry):即pte

 

4.虚拟内存

Linux操作系统采用虚拟内存管理技术,使得每个进程都有一个独立的进程地址空间,该空间是大小为3G,用户所看到和接触的都是虚拟地址,无法看到实际的物理地址。利用这种虚拟地址不但能起到保护操作系统的作用,而且更重要的是用户程序可使用比实际物理内存更大的地址空间。

Linux4G的虚拟地址空间划分为两个部分---用户空间和内核空间。用户空间从00xbfffffff,内核空间从3G4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间。例外情况是用户进程通过系统调用访问内核空间。

 

nLinux内存管理之二:Linux在X86上的虚拟内存管理

本文档来自网络,并稍有改动。

前言

  Linux支持很多硬件运行平台,常用的有:Intel X86AlphaSparc等。对于不能够通用的一些功能,Linux必须依据硬件平台的特点来具体实现。本文的目的是简要探讨LinuxX86保护模式上如何实现虚拟内存管理功能。为简化和方便叙述,本文做如下限定:X86处理器为80486和其后的处理器,X86工作在保护模式,不采用物理内存扩展(使用32bits物理地址),不使用扩展页(页大小为4K)。凡是与限定模式无关的内容,本文都尽量略过。Linux的虚拟内存管理中与硬件平台无关的内容在本文中也被略过。本文所援引的Linux内核源代码版本为Linux 2.2.5

X86的分段和分页机制

I. X86的分段机制和相应系统结构

  X86的分段机制就是将X86的线性地址空间分成许多小空间--段(segment),利用这些段来存储(记录)代码和数据,通过对段的保护来提供一种对数据或代码的保护。根据每个段的作用和存储内容的不同,X86将段分为三类进程段(代码段、数据段和堆栈段)和两类系统段:任务状态段(TSSTask-State Segment)和LDT段(由于GDT不是通过段描述符和段选择符来访问,所以X86没有认为存在一个GDT段;同理,也不存在IDT段)。

  在分段机制,X86使用了如下几种主要数据结构:

  · 全局描述符表(GDTGlobal Describtor Table):存放系统用的段描述符和各项任务共用的段描述符,可以是上述的任何一类段的段描述符,最大表长64KB

  · 局部描述符表(LDTLocal Describtor Table):存放某个任务专用的各段的段描述符,只能是三类进程段的段描述符和调用门描述符,最大表长4GB

  · 段描述符(Segment Describtor):64bits,用来描述一个段的基地址(该地址是线性地址),该段的类型,对该段操作的限制;

  · 门描述符(Gate Describtor):64bits,一种特殊的描述符,为处于不同特权级的系统调用或程序的调用或访问提供保护;分为四类:调用门描述符(Call Gate Describtor)、中断门描述符(Interrupt Gate Describtor)、陷阱门描述符(Trap Gate Describtor)、任务门描述符(Task Gate Describtor);

  · 段选择符(Segment Selector):16bits,用于在GDTLDT中索引相应的段描述符;

  · 中断描述表(IDTInterrupt Describer Table):存放门描述符,只能是中断门描述符,陷阱门描述符和任务门描述符,最大表长64KB

  同时,X86提供了如下几个用于支持分段机制的寄存器:

  · 全局描述符表寄存器(GDTRGDT Register):48bits32bitsGDT的基地址(线性地址),16bitsGDT的表长;GDTR的初始值为:基地址0,表长0xFFFF

  · 局部描述符表寄存器(LDTRLDT Register):80bits16bitsLDT段选择符,64bits为该LDT段的段描述符; 

  · 中断描述符表寄存器(IDTRIDT Register):48bits32bitsIDT的基地址(线性地址),16bitsIDT的表长;IDTR的初始值为:基地址0,表长0xFFFF

  · 任务寄存器(TRTask Register):80bits16bits为任务状态段选择符,64bits为该任务状态段的段描述符;

  · 六个段寄存器(Segment Register):分为可见部分和隐藏部分,可见部分为段选择符,隐藏部分为段描述符;六个段寄存器分别为CSSSDSESFSGS;关于这些段寄存器的作用参见[1]3.4.2 'Segment Register';

  86工作在保护模式时,进程使用的48bits逻辑地址(Logical address)。逻辑地址的高16bits为段选择符,低32bits是段内的偏移量。通过段选择符在GDTLDT中索引相应的段描述符(得到该段的基地址),再加上偏移量得到逻辑地址对应的线性地址(Linear Address)。如果没有采用分叶管理,线性地址是直接映射物理地址(Physical Address),于是可以直接用线性地址访问内存;否则,还要通过X86的分页转换,将线性地址转换为物理地址。

  以上是对X86分段相关内容的简要描述,对于各数据结构、寄存器的细节和逻辑地址转换为线性地址的细节,请查阅 [1]。 

 

II. X86的分页机制和相应系统结构

  32bits的线性地址空间可以直接映射到物理地址空间,也可以间接映射到许多小块的物理空间(磁盘存储空间)上。这种间接映射方式就是分页机制。X86可用页大小为4KB2MB4MB2MB4MB只能在PentiumPentium Pro处理器中使用,本文中限定采用4KB页)。

  在分页机制,X86使用了四种数据结构:

  · 页目录项(PDEPage Directory Entry):32bits结构,高20bits为页表基地址(物理地址),以4KB为递增单位,低12bits为页表属性,具体换算参见后面初始化部分;

  · 页目录(Page directory):存储页目录项,位于一页中,总共可容纳1024个页目录项;

  · 页表项(PTEPage Table Entry):32bits结构,高20bits为页基地址(物理地址),低12bits为页属性;

  · 页表(Page table):存储页表项,位于一页中,总共可容纳1024个页表项;

  · 页(Page):4KB的连续地址空间;

  为了实现分页机制和提高地址转换的效率,X86提供和使用了如下的硬件结构:

  · 页标志位(PGPage):该标志位为1,说明采用页机制;实际就是控制寄存器CR0的第31bit

  · 页缓存/快表(TLBsTranslation Lookaside Buffers):存储最近使用的PDEPTE,以提高地址转换的效率;

  · 页目录基地址寄存器(PDBRPage Directory Base Register):用于存储页目录的基地址(物理地址),实际就是控制寄存器CR3

  为了实现将线性地址映射到物理地址,X8632bits线性地址解释为三部分:第31bit到第22bit为页目录中的偏移,用于索引页目录项(得到对应页表的基地址);第21bit到第12bit为页表中的偏移,用于索引页表项(得到对应页的基地址);第11bit到第0bit为页中的偏移。这样,通过两级索引和页中的偏移量,最后能正确得到线性地址对应的物理地址。

  关于分页机制的详细描述和作用,请查阅参考文档[1]

 

LINUX的分段策略

 

  LinuxX86上采用最低限度的分段机制,其目的是为了避开复杂的分段机制,提高Linux在其他不支持分段机制的硬件平台的可移植性,同时又充分利用X86的分段机制来隔离用户代码和内核代码。因此,在Linux上,逻辑地址和线性地址具有相同的值。

  由于X86GDT最大表长为64KB,每个段描述符为8B,所以GDT最多能够容纳8192个段描述符。每产生一个进程,Linux为该进程在GDT中创建两个描述符:LDT段描述符和TSS描述符,除去LinuxGDT中保留的前12项,GDT实际最多能容纳4090个进程。Linux的内核自身有独立的代码段和数据段,其对应的段描述符分别存储在GDT中的第2项和第3项。每个进程也有独立的代码段和数据段,对应的段描述符存储在它自己的LDT中。有关LinuxGDT表项和DLT表项分布情况参见附表1,附表2所示。

  在Linux中,每个用户进程都可以访问4GB的线性地址空间。其中0x0~0xBFFFFFFF3GB空间为用户态空间,用户态进程可以直接访问。从0xC0000000~0x3FFFFFFF1GB空间为内核态空间,存放内核访问的代码和数据,用户态进程不能直接访问。当用户进程通过中断或系统调用访问内核态空间时,会触发X86的特权级转换(从特权级3切换到特权级0),即从用户态切换到内核态。

 

LINUX的分页策略

 

  标准Linux的分页是三级页表结构,除了X86支持的页目录和页,还有一级被称为中间页目录。因此,线性地址在转换为物理地址的过程中,线性地址就被解释为四个部分(不是X86所认识的三个部分),增加了页中间目录中的索引。当运行在X86平台上时,Linux通过将中间页目录最大的页目录项个数定义为1,并提供一组相关的宏(这些宏将中间页目录用页目录来替换)将三级页面结构分解过程完美的转换为X86使用的二级页面分解。这样,无需改动内核中页面解释的主要代码(这些代码都是认为线性地址由四个部分组成)。关于这些宏定义参见Linux源码"/include/asm/pgtable.h""/include/asm/page.h"

  内核态虚拟空间从3GB3GB+4MB的一段(对应进程页目录第768项指引的页表),被映射到物理地址0x0~0x3FFFFF4MB)。因此,进程处于内核态时,只要通过访问3GB3GB+4MB就可访问物理内存的低4MB空间。所有进程从3GB4GB的线性空间都是一样的,由同样的页目录项,同样的页表,映射到相同的物理内存段。Linux以这种方式让内核态进程共享代码和数据。

 

Linux分段分页初始化

  无论Linux系统如何被引导,经过zImage(参见arch/i386/boot/bootsect.s)或经过LILO,最后都会跳转执行arch/i386/boot/setup.s(被装载到SETUPSEG,物理地址 0x90200),setup.sBIOS中获取计算机系统的硬件参数(如硬盘参数),放到内存参数区(临时寄放),同时做一些初步的状态检查,为进入保护模式做准备。关于引导过程和setup.s的具体执行参见[2]

  保护模式下的内核初始化模块从物理地址0x100000开始执行,该地址开始的代码和数据结构都对应在arch/i386/kernel/head.s中,参见附表3。初始化模块主要功能是对相关寄存器IDTGDT,页目录及页表等进行初始化。下面,忽略head.s执行流程的细节,概要阐述head.s主要的初始化功能。

  1. 部分寄存器的初始化:将段寄存器DSESGSFS__KERNEL_DS0x18include/asm-i386/segment.h)来初始化(通过前面对段寄存器的描述和段选择符的介绍可知道,其作用是将定位到GDT中的第三项(内核数据段),并设置对该段的操作特限级为0);置位CR0PG位,并根据CPU的型号选择置位AM, WP, NE 和 MP;用0x101000初始化CR3(页目录swapper_pg_dir的地址);置ESP32bits__KERNEL_DS0x18),低32bitsinit_user_stack+8192LDTR初始化为0

  2. 有关IDT的初始化:这只是临时初始化IDT,进一步的操作在start_kernel中进行;用于表示IDT的变量(idt_table[ ])在arch/i386/kenel/traps.c中定义,变量类型(desc_struct)定义在include/asm-i386/desc.hIDT共有IDT_ENTRIES256)个中断描述符,属性字均为0x8E00,每个中断描述符都指向同一个中断服务程序ignore_initIgnore_int的功能仅仅是输出消息int_msg"unknown interrupt")。而IDTR的值为通过命令lidt idt_descr实现。通过在head.s中查看idt_descr的值可以计算得知,IDT的基地址为idt_table的地址,表长IDT_ENTRIES*8-10x7FF)。

  3. 有关GDT的初始化:GDT共有GDT_ENTRIES个段描述符。GDT_ENTRIES的计算公式为:12+2*NR_TASKS。其中12表示前面提到的LinuxGDT中保留的12项,NR_TASKS512)指系统设定容纳的进程数,定义在include/linux/tasks.hGDThead.s直接分配存储单元(标号为gdt_table)。初始化后的GDT如附表1所示。GDTR的值通过命令lgdt gdt_descr实现。通过在head.s中查看gdt_descr的值可以计算得知,GDT的基地址为gdt_table的地址,表长GDT_ENTRIES*8-10x205F)。

  4. 页目录的初始化:页目录由变量swapper_pg_dir表示,共有1024个页目录项。其第0项和第768项均指向pg0(第0页),初始化值为0x00102007(根据其高20bits的值0x102换算:0x102*4KB=0x102000,第0页紧跟页目录后,物理地址为0x102000),由此可知,Linux 4GB空间中的虚拟地址0x00xBFFFFFFF3GB)均由pg0映射(物理地址0x0~0x3FFFFF4MB));其他页目录项初始值为0x0

  5. pg0的初始化:第n项对应第n页,属性为0x007;即第n项的初始化值的高20bits值为n,底12bits值为0x007;由此可见pg0映射了物理空间的低4MB空间;

  6. 初始化empty_zero_page:该页的前2KB空间用来存储setup.s保存在内存参数区的来自BIOS的系统硬件参数;后2KB空间作为命令行缓冲区;

  head.s进行完初始化后调用start_kernelinit/main.c)继续各方面的初始化,主要是调用各方面函数初始化内核的数据结构,下面对与X86系统相关的调用函数简述其(与本文相关的)功能。

  1. setup_arch() (arch/i386/kernel/setup.c);设置内核可用物理地址范围(memory_start~memory_end);设置init_task.mm的范围;调用request_regionkernel/resource.c)申请I/O空间,参见附表4

  2. paging_init() (arch/i386/mm/init.c);取消虚拟地址0x0对物理地址的低端4MB空间的映射;根据物理地址的实际大小初始化所有的页表。

  3. trap_init() (arch/i386/kernel/traps.c);在IDT中设置各种入口地址,如异常事件处理程序入口,系统调用入口,调用门等。其中,trap0~trap17为各种错误入口(溢出,0除,页错误等,错误处理函数定义在arch/i386/kernel/entry.s);trap18~trap47保留;设置系统调用(INT 0x80)的入口为system_callarch/i386/kernel/entry.s);在GDT中设置0号进程的TSS段描述符和LDT段描述符。

  4. init_IRQ() (arch/i386/kernel/irq.c);初始化IDT 0x20~0xff项。

  5. time_init() (arch/i386/kernel/time.c);读取实时时间,重新设置时钟中断irq0的中断服务程序入口。

  6. mem_init() (arch/i386/mm/init.c);初始化empty_zero_page;标记已被占用的页。

 

Linux进程和分段分页

  每当启动一个新的进程,Linux都为其创建一个进程控制块(task_structinclude/linux/sched.h)。task_struct中最重要的与存储有关的成员为mmmm_struct* mminclude/linux/sched.h)和tssthread_struct tssinclude/asm-i386/processor.h)。在创建过程中,系统所涉及的(与分段分页相关)功能包括:

  1. 每个进程(根据需要)建立新页目录(mm成员pgd_t * pgd),并将其地址置入寄存器CR3中;相关代码:

new_page_tablesmm/memory.c);//创建和初始化新页目录

SET_PAGE_DIRinclude/asm-i386/pgtable.h);//设置页目录基地址寄存器

  2. GDT中添加进程对应的TSS项和LDT项,其占用的GDT项号分别记录在tss成员trunsigned long tr)和ldtunsigned long ldt)中;相关代码:

  _LDT / _TSSinclude/asm-i386/desc.h);//换算LDT / TSS对应的GDT项号

  set_ldt_desc / set_tss_desc arch/i386/kernel/traps.c);//GDT中添加LDT / TSS描述符

  3. 创建该进程的LDTmm成员void * segments);相关代码:

  copy_segmentsarch/i386/kernel/process.c);//创建进程的LDT并初始化LDT 

  Linux采用"按需调页"的原则来分配内存页面,从而避免页表过多占用存储空间。创建一个进程时页面分配的情况大致是这样的:进程控制块(1页);内存态堆栈(1页);页目录(1页);页表(需要的n页)。在进程以后执行的执行中,再根据需要逐渐分配更多的内存页面。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值