文档章节

内存管理之3:linux的页式内存管理

我叫半桶水
 我叫半桶水
发布于 07/14 10:34
字数 3145
阅读 11
收藏 0

date: 2014-09-07 19:09

备注:本文中引用的内核代码的版本是3.14。

当地址的宽度是32位时,页面目录表+页面表的两级映射合情合理,但如果地址总线的宽度超过32位,比如64位时,两级映射就不合理了。linux要设计一套适用于所有地址宽度的页式内存管理机制,就不能不考虑这个问题。linux的页式管理将映射分成三层,在页面目录表和页面表之间加入一层“中间目录”。在代码中,页面目录称为PGD,中间目录称为PGM,页面表称为PT,页面表中项称为PTE(PT Entry),PGD、PGM和PT三者均为数组。相应的,把线性地址也分为四个位段,每个位段占若干个字节,线性地址到物理地址的转换示意如下:

linux页式管理模型

需要指出,linux三层页式管理只是一个通用的模型,最终还是要落到具体的CPU和MMU上。以i386来说,CPU实际上是按两层来管理的,将三层模型落到具体的两层映射上,需要跳过中间的PMD层次。另一方面,intel引入了物理地址扩充功能PAE,允许将地址宽度由32位拓宽到36位,因此便具备了实施三层映射的硬件基础。

也就是说i386支持两层地址映射也支持三层地址映射,至于最终选择哪一种,我们把选择权交给编译内核的人,在内核代码中只需要简单的根据编译选项来区分就可以了。

<arch/x86/include/asm/pgtable_32.h>
45 #ifdef CONFIG_X86_PAE
46 # include <asm/pgtable-3level.h>
47 #else
48 # include <asm/pgtable-2level.h>
49 #endif 

如果编译选项CONFIG_X86_PAE被设置,则选择三层映射,否则选择两层映射,这里我们只分析两层映射的代码。后面若无特殊说明,我们均只分析X86架构地址总线为32位的情形。

我们可以想象一下,将linux的三层模型落实到intel的两层映射上,有这样两个问题需要处理:

  • 问题1:linux三层模型中多出的PMD该如何处理
  • 问题2:在intel的段式内存管理基础上,怎么建立linux页式内存管理,具体地,全局段描述符表GDTR如何设置?段的数目需要固定吗?段寄存器的内容该设置为多少?

1 PMD该如何处理?

应该有下面这两种思路:

处理多余的PMD

这里内核采用了方案2。在pgtable-2level_types.h中没有定义PMD相关信息,取而代之,将SHARED_KERNEL_PMD定义为0,并且将PAGETABLE_LEVELS定义为2,表示两层映射。可以想象,因为内核的代码时针对三层映射而写,这里的两层映射相当于一个特例,在页面内存管理相关的代码中,这两个宏会被拿用来判断是否为两层映射。

<arch/x86/include/asm/pgtable-2level_types.h>
19 #define SHARED_KERNEL_PMD       0
20 #define PAGETABLE_LEVELS        2

为了便于后续情景分析的展开,这里有必要说一下2.4.0内核的实现。在2.4.0的内核中,采用了第一种方案,我们在下一篇文章中可以看到更多的细节。

2 段选择符与段描述符的设计

对于此问题,我们再补充些额外的信息。首先与段式内存管理相比,页式内存管理有很多好处,一种CPU既然支持页式管理就没必要再支持段式管理,但前面介绍了,i386比较特殊,它对地址一律先进行段式映射,再进行页式映射。我们知道,段式映射和页式映射都对内存访问进行了保护,这里有重复保护之嫌。其次,i386 CPU支持4种特权,而在linux中,只需要两种特权,用户特权(用户空间)和系统特权(系统空间即内核空间)。

介绍了这些信息之后,相信大家已经有想法了。首先,既然重头戏是页式映射,那么必须让段式映射过程“轻量级”,让段式映射“走走过场”;其次,具体到段选择符(段寄存器中的内容)和段描述符(GDTR指向的内容)的设计上,我们希望尽量不要进行段切换,而且段的特权只需要设置两种即可。 我们来看看内核是如何实现的。每当内核新建一个进程(task_struct),都要将其段寄存器设置好。代码如下:

<arch/x86/kernel/process_32.c>

201 void
202 start_thread(struct pt_regs *regs, unsigned long new_ip, unsigned long new_sp)
203 {
204         set_user_gs(regs, 0);
205         regs->fs                = 0;
206         regs->ds                = __USER_DS;
207         regs->es                = __USER_DS;
208         regs->ss                = __USER_DS;
209         regs->cs                = __USER_CS;
210         regs->ip                = new_ip;
211         regs->sp                = new_sp;
212         regs->flags             = X86_EFLAGS_IF;
213         /*
214          * force it to the iret return path by making it look as if there was
215          * some work pending.
216          */
217         set_thread_flag(TIF_NOTIFY_RESUME);
218 }

pt_regs是i386寄存器的“映像”(??在哪里被装入真正的寄存器)。第204-205行,设置附加段寄存器gs/fs的值为0,可见,linux中没有使用这两个段寄存器(??还存在疑问)。除了209行中将CS设置为__USER_CS外,ds/ss/es都设置为__USER_DS。可见,虽然intel意图将进程的映像分成代码段、数据段、堆栈段,但linux并不买账,linux中,数据段和堆栈段是不区分的。

段选择符__USER_DS与__USER_CS是如何定义的呢?

<arch/x86/include/asm/segment.h>
 26 #ifdef CONFIG_X86_32
 27 /*
 28  * The layout of the per-CPU GDT under Linux:
 29  *
 30  *   0 - null
 31  *   1 - reserved
 32  *   2 - reserved
 33  *   3 - reserved
 34  *
 35  *   4 - unused                 <==== new cacheline
 36  *   5 - unused
 37  *
 38  *  ------- start of TLS (Thread-Local Storage) segments:
 39  *
 40  *   6 - TLS segment #1                 [ glibc's TLS segment ]
 41  *   7 - TLS segment #2                 [ Wine's %fs Win32 segment ]
 42  *   8 - TLS segment #3
 43  *   9 - reserved
 44  *  10 - reserved
 45  *  11 - reserved
 46  *
 47  *  ------- start of kernel segments:
 48  *
 49  *  12 - kernel code segment            <==== new cacheline
 50  *  13 - kernel data segment
 51  *  14 - default user CS
 52  *  15 - default user DS
 53  *  16 - TSS
 54  *  17 - LDT
 55  *  18 - PNPBIOS support (16->32 gate)
 56  *  19 - PNPBIOS support
 57  *  20 - PNPBIOS support
 58  *  21 - PNPBIOS support
 59  *  22 - PNPBIOS support
 60  *  23 - APM BIOS support
 61  *  24 - APM BIOS support
 62  *  25 - APM BIOS support
 63  *
 64  *  26 - ESPFIX small SS
 65  *  27 - per-cpu                        [ offset to per-cpu data area ]
 66  *  28 - stack_canary-20                [ for stack protector ]
 67  *  29 - unused
 68  *  30 - unused
 69  *  31 - TSS for double fault handler
 70  */
 73
 74 #define GDT_ENTRY_DEFAULT_USER_CS       14
 75
 76 #define GDT_ENTRY_DEFAULT_USER_DS       15
 77
 78 #define GDT_ENTRY_KERNEL_BASE           (12)
 79
 80 #define GDT_ENTRY_KERNEL_CS             (GDT_ENTRY_KERNEL_BASE+0)
 81
 82 #define GDT_ENTRY_KERNEL_DS             (GDT_ENTRY_KERNEL_BASE+1)
......
109 /*
110  * The GDT has 32 entries
111  */
112 #define GDT_ENTRIES 32
 ......
185 #endif


187 #define __KERNEL_CS     (GDT_ENTRY_KERNEL_CS*8)
188 #define __KERNEL_DS     (GDT_ENTRY_KERNEL_DS*8)
189 #define __USER_DS       (GDT_ENTRY_DEFAULT_USER_DS*8+3)
190 #define __USER_CS       (GDT_ENTRY_DEFAULT_USER_CS*8+3)

第27-70行的注释给出了GDT的布局。可见GDT中并没有用全部的8192个段描述符,只用了32个段描述符,用宏GDT_ENTRIES表示。GDT中第12个段描述符是内核的CS段,第13个表项是内核的DS段,第14个表项是用户空间的CS段,第15个表项是用户空间的DS段。这里注意,虽然第17个表项指向LDT(局部段描述符表),但linux中基本没有用到LDT,只有在VM86模式中运行WINE,或在linux上模拟运行Windows软件才会用到。

对照前面段选择符的定义,16位的段选择符高13为用作索引,bit2用作TI,bit0~bit1用来定义特权级别。这里,四个段都是用GDT,所以TI应该为0,内核的特权级别是0级,而用户空间的特权级别是3级。由此,我们应该很容易得到段选择符的定义:

kernel_cs_段选择符定义

第187~190行的宏定义不难理解,* 8表示左移3位,+3表示设置特权级别为3级。

那么,对应的段描述符又是如何定义的呢?我们只关注上面提到的四个段。

 <arch/x86/kernel/cpu/common.c> 

  91 DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
  92 #ifdef CONFIG_X86_64
     ......
 107 #else
 108         [GDT_ENTRY_KERNEL_CS]           = GDT_ENTRY_INIT(0xc09a, 0, 0xfffff),
 109         [GDT_ENTRY_KERNEL_DS]           = GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
 110         [GDT_ENTRY_DEFAULT_USER_CS]     = GDT_ENTRY_INIT(0xc0fa, 0, 0xfffff),
 111         [GDT_ENTRY_DEFAULT_USER_DS]     = GDT_ENTRY_INIT(0xc0f2, 0, 0xfffff),
    ......
 141 #endif
 142 } };

GDT表是一个per cpu变量,即每个CPU都一个副本,per cpu变量的实现我们在后面还会讲到。结构体gdt_page是对GDT表的抽象,其成员gdt是一个段描述符数组,数组共有32个元素,8字节的段描述符对应的结构体为desc_struct。这段代码用到了gnu c中对结构体和数组初始化的特殊语法。第108~111行,分别对gdt数组中的第12~15个元素进行初始化,这几个元素为__KERNEL_CS/__KERNEL_DS/__USER_CS/__USER_DS对应的段描述符。

段描述符结构体desc_struct与GDT_ENTRY_INIT宏的定义如下:

<arch/x86/include/asm/desc_defs.h>

 21 /* 8 byte segment descriptor */
 22 struct desc_struct {
 23         union {
 24                 struct {
 25                         unsigned int a;
 26                         unsigned int b;
 27                 };
 28                 struct {
 29                         u16 limit0;
 30                         u16 base0;
 31                         unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
 32                         unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
 33                 };
 34         };
 35 } __attribute__((packed));
 36
 37 #define GDT_ENTRY_INIT(flags, base, limit) { { { \
 38             .a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \
 39             .b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \
 40                   ((limit) & 0xf0000) | ((base) & 0xff000000), \
 41         } } }

结构体desc_struct中只包含一个union成员,union中的第二个结构图完全按照8字节段描述符的布局来定义,参考下图,我们可以重温下段描述符的布局。

段描述符布局

union中第一个结构体将8字节的段描述符简化为两个32位无符号整形数a和b,这是为了方便我们来设置段描述符,GDT_ENTRY_INIT宏就是通过操作a和b来设置段描述符的。因为段描述的定义中有很多位段,特别是表示基址以及表示长度限制的位段分别不连续,一一去设置显然比较麻烦。

GDT_ENTRY_INIT宏的定义比较简单,对比段描述符的结构体,可知该宏设置段的基址为base,段的长度为limit,至于长度的单位以及其他的标志则根据flag来设置。

回过头来看看108~111的代码,可见__KERNEL_CS/__KERNEL_DS/__USER_CS/__USER_DS对应的段的基址都是0,段的长度限制都是0xfffff,只有flag不同,将flag展开来看:

kernel_cs_段描述符定义

  • 四个段的相同的部分有:

  • G位都为1,表示4个段的长度单位为4KB,

  • D位都为1,表示对4个段的访问都是32位指令

  • P位都为1,表示4个段都在内存中

  • S位都为1,表示代码段或者数据段

     可见,4个段的基址都为0,长度限制都为0xfffff,每个段都是从0到4GB整个虚存空间,linux采用前面所说的Flat地址,虚拟地址经过段式映射后的线性地址保持不变,因此在讨论linux页式映射时,可以直接将线性地址当作虚拟地址,二者一致。

  • 四个段不相同的部分只有DPL和type。

    DPLtype
    __KERNEL_CS0级代码段、可读、可执行、尚未受到访问
    __KERNEL_DS0级数据段、可读、可写、尚未受到访问
    __USER_CS3级代码段、可读、可执行、尚未受到访问
    __USER_DS3级数据段、可读、可写、尚未受到访问

i386的CPU在做段式映射时会检查这两个字段。比如__KERNEL_CS段描述符中的DPL为1,如果CS寄存器中的DPL为3,则说明CPU当前运行级别比想要访问的区段低,则不允许访问;或者__KERNEL_DS为数据段,而试图通过CS段寄存器来访问,这也不允许,因为type不匹配。这里所做的比对在页式映射过程中还会进行。

可见linux将intel的复杂的段式映射简单化了,只用GDT,而且只设置了32个段描述符,只用到4个段寄存器,而且基本上不会进行段切换,这里的段映射只是“走走过场”。linux这么“胆大妄为”的原因,是因为在页式映射中,它还会对地址访问进行严格保护。

由于必须建立在段式映射的基础上,linux这种的页式管理也被人称作“段页式”。虚拟地址的映射过程可以用下图示意(本图来自网络)。

段页式管理

© 著作权归作者所有

共有 人打赏支持
我叫半桶水
粉丝 0
博文 26
码字总数 71642
作品 0
西安
私信 提问
由fork()和vfork()回忆下OS的内存管理

使用fork产生的子进程复制了父进程的代码段和数据段, 我们现在假定在父进程中有一个变量var,初始值为88 子进程也有一个var,初始值也是88,修改子进程的var,父进程的var并不改变 而且父进程和...

晨曦之光
2012/04/13
160
0
随笔档案 - 2016年10月

前置:这里使用的linux版本是4.8,x86体系。 localirqdisable(); 这个函数是做了关闭中断操作。和后面的localirqenable相对应。说明启动的下面函数是不允许被中断抢占的。这个函数追下去会发...

王二狗子11
01/07
0
0
深入理解Linux内存管理-之-目录导航

转自:https://blog.csdn.net/gatieme/article/details/52384965 1 内存描述 2 页表管理 3 初始化内存管理

zwfgogo
04/20
0
0
windows和linux的内存管理

windows的内存管理很是严谨,使用内存必须首先分配,当然每个操作系统都是这样,然而windows的严谨在于分配的过程,分为保留和提交两个阶段,其中保留的含义就是在进程的虚拟地址空间保留一块...

晨曦之光
2012/04/10
238
0
缓存的位置

缓存的位置是很有说头的,在windows里,因为内核设计思想就是将一切都映射到虚拟内存空间(便于通过其强大又复杂的内存管理器来进行一致化管理),那么文件缓存当然也映射了一片虚拟内存(记...

晨曦之光
2012/04/10
148
0

没有更多内容

加载失败,请刷新页面

加载更多

linux之自定义命令

本人使用的是ubuntu系统,不喜欢建各种桌面快捷链接,但是每次启动个软件,去查找又麻烦,所以自定义了命令,来快捷的启动应用: 1、修改/etc/bash.bashrc,在文件末尾,加上如下List-1中的内...

克虏伯
7分钟前
0
0
linux基础

系统安全 sudo su chmod setfacl 进程管理 w top ps kill pkill pstree killall 用户管理 id usermod useradd groupad userdel 文件系统 mount umount fsck df du 网络应用 curl telnet mail......

关元
9分钟前
0
0
Caffe-源码分析(一)

CHECK_X函数,用于比较两个blob之间的值 CHECK_EQ(x,y)<<"x!=y",EQ即equation,意为“等于”,当x!=y时,函数打印出x!=y。 CHECK_NE(x,y)<<"x=y",NE即not equation,意为“不等于”,,...

Pulsar-V
9分钟前
0
0
三星Galaxy S10可能会配备TOF 3D摄像头

12月3日消息,据Phone Arena报道,三星Galaxy S10可能会配备TOF 3D摄像头。 Phone Arena报道称三星Galaxy S10一共有五颗摄像头(前置双摄+后置三摄),而5G版本的Galaxy S10后置四颗摄像头,...

问题终结者
32分钟前
9
0
fabric增删改查Mac

备份1.3版本,重新下载1.1版本到fabric文件夹 /opt/gopath/src/github.com/hyperledger/fabric -> /opt/gopath/src/github.com/hyperledger/fabric1.3 新建/opt/gopath/src/github.com/hype......

八戒八戒八戒
今天
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部