Linux0.11内核在X86下的内存管理(MMU)学习笔记
最近看了很多关于内存管理的资料,总结异同,记录下近期学习的⼼得,以后,可能没这么多时间
写⼼得了。
1 基本概念
1.1物理内存
在Linux0.11内核中,为了有效的使⽤机器中的物理内存,在系统初始化阶段内存被划分成为⼏个功
能区域,如下图:
其中,Linux内核程序占据在物理内存的开始部分,接下来是供硬盘或软盘等块设备使⽤的⾼速缓冲部分(其中要扣除显卡内存和ROM BIOS所占⽤的内存地址范围640K—1MB)。当⼀个进程需要读取块设备中的数据时,系统会⾸先把数据读到⾼速缓冲区中;当有数据需要⽤到块设备上去时,系统也是先将数据放到⾼速缓冲区中,然后由块设备驱动程序写到相应的设备上。内存的最后部分是供所以程序可以随时申请和使⽤的主内存区。内核程序在使⽤主内存区是,也同样⾸先要向内核内存管理模块提出申请,并在申请成功后⽅能使⽤。对于含有RAM虚拟盘的系统,主内存区头部还要划去⼀部分,供虚拟盘存放数
据。
1.2内存寻址
内存是指⼀组有序字节组成的数组,每个字节有唯⼀的内存地址。
内存地址则是指对存储在内存中的某个指定数据对象的地址进⾏定位。
数据对象是指存储在内存中的⼀个指定数据类型的数值或字符串。
80X86⽀持多种数据类型:1字节、2字节(1个字)、或4字节(双字或长字)的⽆符号整型数或带符号整型数,以及多字节字符串等。对于80X86CPU来说,其地址总线宽度为32位,因此可寻址的地址空间
范围是0—2^32(4GB)的物理内存,这是就产⽣⼀个冲突,我们实际上使⽤的物理内存⼀般没有4GB 这么⼤阿,怎么办,于是就引⼊了⼀个关键的技术:内存管理(MMU)。
1.3 地址转换过程中3个特殊地址的概念
虚拟地址(VA, Virtual Address)是指由程序产⽣的由段选择符和段内偏移地址两个部分组成的地址。因为这两部分组成的地址并没有直接⽤来访问物理内存,⽽是需要通过分段地址变换机制处理或映射后才对应到物理内存地址上,因此这种地址被称为虚拟地址。VA空间由GDT(Global Descriptor Table)映射的全局地址空间和由LDT(Local Descriptor Table)映射的局部地址空间组成。选择符的索引部分由13个bit
表⽰,加上区分GDT和LDT的1个⽐特位—TI,(低2个bit为RPL---Requestor’s Privilege Level),因此Intel 80X86 CPU 共可以索引2^14=16384个选择符。若每段的长度都取最⼤值4G,则最⼤虚拟地址空间范围
是16384*4G=64T。
逻辑地址(Logical Address)是指由程序产⽣的与段相关的偏移地址部分,即程序员编程所⽤的地址以
及CPU通过指令访问主存时所产⽣的地址。
⽤多道程序设计技术后,往往在主存储器中同时存放多个⽤户作业,⽽每个⽤户不能预先知道⾃⼰的作业将被放到主存储器中
的什么位置。这样,⽤户编制程序时就不能使⽤绝对地址。为了⽅便⽤户,每个⽤户都可认为⾃⼰作业的程序和数据存放在⼀组从“0”地址开始的连续空间中。把⽤户程序中使⽤的地址称“逻辑地址”,由逻辑地址对应的存储空间称“逻辑地址空间”。在Intel保护模式下即是指程序执⾏代码段限长内的偏移地址(假定代码段、数据段完全⼀样)。应⽤程序员仅需与逻辑地址打交道,⽽分段和分页机制对他来说是完全透明的,仅由系统编程⼈员涉及。不过有些资料并不区分逻辑地址和虚拟地址的概念,
⽽是将它们统称为逻辑地址。
线形地址(Linear Address)是虚拟地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(统称为线性地址空间)中的地址。程序代码会产⽣逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就⽣成了⼀个线性地址。如果启动了分页机制,那么线性地址可以再经过变换后产⽣⼀个物理地址。若没有分页机制,那么line address就直接是logic address。Intel80386的线性地址空间容量为4G。
物理地址(PA, Physical Address)是指出现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果地址。如果启⽤了分页机制,那么线性地址会使⽤页⽬录和页表中的项变换成物理地址。
如果没有启⽤分页机制,那么线性地址就直接成为了物理地址。
虚拟存储(或虚拟内存)(Virtual Memory)是指计算机呈现出要⽐实际拥有的内存⼤得多的内存量。
因此它允许程序员编制并运⾏⽐实际系统拥有的内存⼤得多的程序。这使得许多⼤型项⽬也能够在有限内存资源的系统上实现。⼀个很经典的⽐喻:你不需要很长的轨道就可以让⼀列⽕车从上海开到北京,你只需要⾜够长的铁轨(⽐如说3公⾥)就可以完成这个任务。采取的⽅法就是把后⾯的铁轨⽴刻铺到⽕车的前⾯,只要你的操作⾜够快就能满⾜要求,列车就能象在⼀条完整的轨道上运⾏。这也就是虚拟内存管理需要完成的任务。在Linux0.11内核中,给每个程序(进程)都划分了总容量为64MB的虚拟内存空间。
因此程序的逻辑地址范围是0x00000000—0x04000000。
1.4 内存地址的基本变换过程
任何完整的内存管理系统都包含两个关键部分:保护和地址变换。
提供保护措施是可以防⽌⼀个任务访问另⼀个任务或操作系统的内存区域。
地址变换能够让操作系统在给任务分配内存时具有灵活性,并且因为我们可以让某些物理地址不被任何逻辑地址所映射,所以在地址变换过程中同时也提供了内存保护功能。
80x86在从逻辑地址到物理地址变换过程中使⽤了分段和分页两种机制,如上图所⽰,第⼀阶段使⽤分段机制把程序的逻辑地址变换成处理器可寻址的内存空间(称为线性地址空间)中的地址,该阶段的分段机制总是使⽤(否则cpu如何寻址操作?)。第⼆阶段使⽤分页机制把线性地址转换为物理地址。该阶
段的分页机制选⽤,具体后续。
2 内存分段机制
分段机制可⽤于实现多种系统设计。这些设计范围从使⽤分段机制的最⼩功能来保护程序的平坦模型,
到使⽤分段机制创建⼀个可同时可靠的运⾏多个程序(或任务)的具有稳固操作环境的多段模型。
多段模型能够利⽤分段机制全部功能提⾼由硬件增强的代码、数据结构、程序和任务的保护措施。通常,每个程序(或任务)都使⽤⾃⼰的段描述符以及⾃⼰的段。对程序来说段能够完全是私有的,或者是程序之间共享的。对所有段以及系统上运⾏程序各⾃执⾏环境的访问都由硬件控制。
2.1 段的定义
分段机制就是把虚拟地址空间中的虚拟内存组织成⼀些长度可变的称为段的内存块单元。80386虚拟地址空间中的虚拟地址(逻辑地址)由⼀个段部分和⼀个偏移部分构成。段是虚拟地址到线性地址转换
机制的基础。每个段由下⾯三个参数定义:
(1)段基地址(Base address),指定段在线性地址空间中的开始地址。基地址时线性地址,对应于
段中偏移0处。
(2)段限长(limit),是虚拟地址空间中段内最⼤可⽤偏移位置,它定义了段的长度。在段描述符中
有定义,与颗粒度(G)配合使⽤,在上扩段和下扩段中有区别。
(3)段属性(Attributes),指定段的特性。如是否可读、可写或可作为⼀个程序执⾏;段的特权级等。
段描述符中有定义。
2.2实模式和保护模式的区别
2.2.1 实模式
是CPU启动的时候的模式,这时候就相当于⼀个速度超快的8086,不能使⽤多线程,不能实现权限分级;实模式下,虚地址到实地址转换,段寄存器左移四位与偏移相加,得到物理地址,寻址空间1M。
2.2.2 保护模式
操作系统接管CPU后,会使CPU进⼊保护模式,虚地址到实地址转换经过MMU,也就是分段和分页机制,寻址空间4G。另外,保护有两层含义,⼀是保护操作系统不被随意访问和破坏,另外,保护应⽤
程序在各⾃的地址空间不被随意破坏。
2.2.3 寻址⽅式的⽐较
在实模式下,寻址⼀个内存地址主要是使⽤段和偏移值,段值被存放在段寄存器中(如ds),并且段的
长度被固定为64KB。段内偏移地址存放在任意⼀个可⽤于寻址的寄存器中(如si)。根据段寄存器和偏移寄存器中的值,就可以算出实际指向的内存地址。
struct logical_address{
UINT16 seg_base;
UINT16 offset;
};
#define logical_to_phisical(logical) ((logical.seg_base4) + logical.offset)
这样做的缺点是不安全,程序员可以通过修改段寄存器的内容访问到任意⼀个内存单元,系统缺乏
保护,很脆弱。
80286尝试过渡到保护模式,加⼊了struct seg_descriptor这个概念。
在保护模式运⾏⽅式下,段寄存器中存放的不再是被寻址段的基地址,⽽是⼀个段描述符表(Segment Descriptor Table)中某⼀个描述符项在表中的索引值。索引值指定的段描述符项中含有需要寻址的内存
的基地址、段的长度和段的访问特权级等信息。如下:
struct logical_address{
struct seg_selector{
UINT16 index:13;
UINT16 tl:1;
UINT16 rpl:2;
};
UINT32 offset;
};
struct seg_descriptor{
UINT32 base:32; /* 基地址*/
UINT32 seg_limit:20; /* 段长度*/
UINT32 g:1; /* 粒度,表段的长度单位,0表⽰字节,1表⽰4KB */
UINT32 d_b:1; /* 存取⽅式,0=16位,1=32位*/
UINT32 unused:1; /* 固定设置成0 */
UINT32 avl:1; /* available,可供系统软件使⽤*/
UINT32 p:1; /* segment present,为0时表⽰该段的内容不在内存中*/
UINT32 dpl:2; /* Descriptor Privilege Level,访问本段所需权限*/
UINT32 s:1; /* 描述项类型,1表⽰系统,0表⽰代码或数据*/
UINT32 type:4; /* 段的类型,与上⾯的S标志位⼀起使⽤*/
};
linux字符串转数组
UINT32 logical_to_linear(struct logical_address logical_add){
static bool first = true;
UINT32 table_base;
struct seg_descriptor shadow;
if ( first ) {
if(logical_add.seg_selector.tl == 0)
table_base = gdtr; /* GDT */
else
table_base = ldtr; /* LDT */
shadow = (struct seg_descriptor) *(table_base + logical_add.seg_selector.index*8);
first = false;
}
if ( logical_add.seg_selector.tl > shadow.dpl ) /* 0: highest level; 3: lowest level */
thorow access_denied;
else if ( logical_add.offset > (shadow.limit(shadow.g?12:0)) )
thorow over_boundary;
else
return shadow.base + logical_add.offset;
}
寻址的内存位置是由该段描述符项中指定的段基地址与⼀个段内偏移值组合⽽成。段的长度可变,由描述符中的内容指定。
可见,和实模式下的寻址相⽐,段寄存器值换成了段描述符表中相应段描述符的索引值以及段表选择位和特权级,称为段选择符(Segment Selector),但偏移值还是使⽤段描述符表。这是由于在保护模式下
访问⼀个内存段需要的信息⽐较多,⽽⼀个16位的段寄存器放不下这么多内容。注意:如果你不在⼀个段描述符中定义⼀个内存线性地址空间区域,那么该地址区域就完全不能被寻址,CPU将拒绝访问该地址区
域。
2.3 保护模式下的分段机制
为了把逻辑地址转换成⼀个线性抵制,处理器会执⾏以下操作:
1)使⽤段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符。(仅当⼀个新
的段选择符加载到段寄存器中时才需要这⼀步)
2)利⽤段选择符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
3)把段描述符中取得的段基址加到偏移量上,最后形成⼀个线性地址。
如果没有开启分页,那么处理器直接把线性地址映射到物理地址(即线性地址被送到处理器地址总线上)。如果对线性地址空间进⾏了分页处理,那么就会使⽤⼆级地址转换吧线性地址转换成物理地址。
2.3.1段选择符
80x86为段部分提供了6个存放段选择符的段寄存器:CS,DS,ES,SS,FS和GS。其中CS总是⽤于寻址代码段,⽽堆栈段则专门使⽤SS段寄存器。在任何指定时刻由CS寻址的段称为当前代码段。此时EIP寄存器中包含了当前代码段内下⼀条要执⾏指令的段内偏移地址。因此要执⾏指令的地址可表⽰成CS:[EIP]。由段寄存器SS寻址的段称为当前堆栈段。栈顶由ESP寄存器内容指定。因此堆栈顶处地址是SS:[ESP]。另外4个寄存器是通⽤段寄存器,当指令中没有指定所操作数据的段时,那么DS 将是默认的
数据段寄存器。
保护模式下,CPU要寻址⼀个段时,就会使⽤16位的段寄存器的选择符(有些书上称为选择⼦)来
定位⼀个段描述符,格式如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。