Linux内存地址映射-8086分段分页与缺页异常

8086体系实模式地址映射

   8086cpu有16根数据线和20根地址线。

寄存器:

  1. 段寄存器:描述内存分段时,保存段的信息。8086 内部有4 个段寄存器。其中,CS 是代码段寄存器,DS 是数据 段寄存器,ES 是附加段(Extra Segment)寄存器,IP 指令指针寄存器。
  2. 物理地址:对应内存条上的实际地址。
  3. 偏移地址: 段内存储单元相对于短地址的距离。
  4. 段地址: 段的起始地址(对应着一个物理地址)。
  5. 逻辑地址: 短地址+偏移地址。

    我们把一个程序分为数据段与代码段,在数据段寄存器(DS)中存储数据段的段地址,代码段中存储代码段的段地址。

       物理地址 = 段基址(DS/CS)+偏移地址/逻辑地址即段上的偏移量(IP寄存器)

      这种获取到的物理地址也称为实模式。实模式下的分段。

      CPU上电强制进入实模式,寻址空间是2^20 = 1M. 内核image 从0x100000(1M)开始加载,之前属于实模式的地址空间

        当处理器访问内存时,它把指令中指定的 内存地址看成是段内的偏移地址,而不是物理地址。这样,一旦处理器 遇到一条访问内存的指令,它将把DS 中的数据段起始地址和指令中提供的段内偏移相加,来得到访问内存所需要的物理地址。

       当一段代码开始执行时, CS 指向代码段的起始地址,IP 则指向段内偏移。这样,由CS 和IP 共同 形成逻辑地址,并由总线接口部件变换成物理地址来取得指令。然后, 处理器会自动根据当前指令的长度来改变IP 的值,使它指向下一条指令。

       8086的处理器地址引线:20根,那么逻辑地址就是20位。而我们的寄存器只有16位,为了解决这个问题:段地址实际上也是20位,将段寄存器中的值左移4位(每个分段必须加载到地址最低位为0的位置,相对于16进制表示而言)。偏移地址仍然是16位,也就意味着每个段的最大长度为65536个字节。

      8086 处理器的逻辑分段,起始地址都是16 的倍数,这称为是 按16 字节对齐的。

        同样在不允许段之间重叠的情况下,每个段的最大长度是64KB,因 为偏移地址也是16 位的,从0000H 到FFFFH。在这种情况下,1MB 的 内存,最多只能划分成16 个段,每段长64KB,段地址分别是0000H、1000H、2000H、3000H,…,一直到F000H。

同一段内存,多种分段方案如下:

         10000H到100FFH组成一个段,起始地址( 基础地址,向左移4位) 为10000H,段地址为1000H, 偏移的大小为100H。当这段内存分成两个段后,起始地址( 基础地址 )为10000H和10080H,段地址为1000H和1008H, 大小均为80H。

        我们知道当段地址向左移4位,实际上就是乘以16,换句话说一个段的起始地址也一定是16的倍数。

        偏移地址为16位,16 位地址的寻址能力为64K,所以一个段的长度最大为64K。

现在有一个物理地址为21F60H,段地址为2000H,把段地址向左移4位后就是20000H,再用物理地址减去段地址,那么就很容易推出偏移地址为1F60H

        在8086PC机中存储单元地址是怎样表示的呢?如果数据在21F60H内存单元中,段地址是2000H,地址表示如下:

        数据存在内存2000:1F60单元中
        数据存在内存的2000H段中的1F60H单元中
        另外,段地址和数据都是重要的,因此处理器至少需要提供两个段寄存器,分别是代码段寄存器(Code Segment,CS)和数据段寄存器(Data Segment,DS)。对CS内容的改变将导致处理器从新的代码段开始执行。同样的,在开始访问内存中的数据之前,也必须让DS寄存器指向数据段。

        通常情况下,段地址的选择取决于内存中哪些区域是空闲的,例如从物理地址00000H开始,到82215H之间的内存都被其他程序占用了,那么可以从82215H后面的空闲的内存区域开始加载程序。

保护模式下的内存分段的地址映射

    所谓工作模式,是指CPU的寻址方式、寄存器大小、指令用法和内存布局等。

         实模式
     段基址:段内偏移地址”产生的逻辑地址就是物理地址,即程序员可见的地址完全是真实的内存地址。
         保护模式
     在保护模式中,内存的管理模式分为两种——段模式和页模式。其中页模式也是基于段模式的。也就是说,保护模式的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式,否则这是纯段模式。
     保护模式下的段寄存器 由 16位的选择器 与 64位的段描述符寄存器 构成

  •  段描述符寄存器: 存储段描述符
  •  选择器:存储段描述符的索引

在这里插入图片描述

        原先实模式下的各个段寄存器在保护模式下的作用是段选择器,仅仅是每个位表示的作用不同于实模式(实模式下是段基址)

        请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。00 表示最高级别  11 表示最低级别

        高13位表示在段描述符表 中的索引号。 

        关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。


     全局描述符表GDT


        全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张,GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

        全局描述符表在系统中只能有一个,且可以被每一个任务所共享.任何描述符都可以放在GDT中,但中断门和陷阱门放在GDT中是不会起作用的.能被多个任务共享的内存区就是通过GDT完成的,

        GDTR寄存器中的基地址指定GDT表在内存中的起始地址,表长度指明GDT表的字节长度值。

        指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。

在这里插入图片描述

 

        在8086中,段寄存器中存储内存的段基址(上图中的内存的起始地址),在80386中保护模式下,内存的起始地址存放在GDT中,段寄存器 的作用是选择子。

        GDT共有2^13,8192个,因为占用的是段寄存器的高13位

在linux内核源码中:arch/i386/kernel/head.S

/*
 * The boot_gdt_table must mirror the equivalent in setup.S and is
 * used only by the trampoline for booting other CPUs
 */
	.align L1_CACHE_BYTES
ENTRY(boot_gdt_table)
	.fill GDT_ENTRY_BOOT_CS,8,0
	.quad 0x00cf9a000000ffff	/* kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* kernel 4GB data at 0x00000000 */
#endif
	.align L1_CACHE_BYTES
ENTRY(cpu_gdt_table)
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x0000000000000000	/* 0x0b reserved */
	.quad 0x0000000000000000	/* 0x13 reserved */
	.quad 0x0000000000000000	/* 0x1b reserved */
	.quad 0x0000000000000000	/* 0x20 unused */
	.quad 0x0000000000000000	/* 0x28 unused */
	.quad 0x0000000000000000	/* 0x33 TLS entry 1 */
	.quad 0x0000000000000000	/* 0x3b TLS entry 2 */
	.quad 0x0000000000000000	/* 0x43 TLS entry 3 */
	.quad 0x0000000000000000	/* 0x4b reserved */
	.quad 0x0000000000000000	/* 0x53 reserved */
	.quad 0x0000000000000000	/* 0x5b reserved */

	.quad 0x00cf9a000000ffff	/* 0x60 kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* 0x68 kernel 4GB data at 0x00000000 */
	.quad 0x00cffa000000ffff	/* 0x73 user 4GB code at 0x00000000 */
	.quad 0x00cff2000000ffff	/* 0x7b user 4GB data at 0x00000000 */

	.quad 0x0000000000000000	/* 0x80 TSS descriptor */
	.quad 0x0000000000000000	/* 0x88 LDT descriptor */
.......

在其中定义了系统预先占用的全局描述符表项,剩下的可以使用。每一项段描述符:

.quad 0x 00 cf 9a 00 00 00 ff ff   占8个字节

内存的起始地址:B0-B15  ,B16-B23,B24-B31 一共32位

内存的段大小:L0-L15,L16-L19, 一共20位   2^20=1M 长度

G:表示长度的单位  0:字节  1:页(4K),因此段大小有可能是1*1M = 1M byte 或4K * 1M = 4G byte,32位linux内核给每个进程都会分配一个虚拟地址空间 4G ,3G用户空间,1G内核空间

保护模式下的内存分段的地址映射

如DS 段寄存器:

首先左移三位得到index,在GDT中获取内存的起始地址,

然后 IP寄存器相加得到线性地址;

检查是否开启分页机制,如果没有则线性地址就是物理地址。

如果开启分页机制,线性地址经过多级的页表映射得到物理地址 

局部描述符表LDT


        局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图

在这里插入图片描述

        局部描述符表在系统中可以有多个,通常情况下是与任务的数量保持对等,但任务可以没有局部描述符表.任务间不相干的部分也是通过LDT实现的.这里涉及到地址映射的问题.和GDT一样,中断门和陷阱门放在LDT中是不会起作用的.

         LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。

        LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。

        由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如上图,如果装载的是Selector 2则LDTR指向的是表LDT2。举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。

  1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR
  2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h
  3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h
  4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)

        由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。

         当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。

        GDT表只有一个,是固定的;而LDT表每个任务就可以有一个,因此有多个,并且由于任务的个数在不断变化其数量也在不断变化。如果只有一个LDTR寄存器显然不能满足多个LDT的要求。因此INTEL的做法是把它放在放在GDT中。

段选择子


        在保护模式下,段寄存器的内容已不是段值,而称其为选择子.该选择子指示描述符在上面这三个表中的位置,所以说选择子即是索引值。

        当我们把段选择子装入寄存器时不仅使该寄存器值,同时CPU将该选择子所对应的GDT或LDT中的描述符装入了不可见部分。这样只要我们不进行代码切换(不重新装入新的选择子)CPU就会不会对不可见部分存储的描述符进行更新,可以直接进行访问,加快了访问速度。一旦寄存器被重新赋值,不可见部分也将被重新赋值。

在这里插入图片描述

        段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。

        index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。

        然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址,

        段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。

   

例如给出逻辑地址:21h:12345678h转换为线性地址

a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=0000000000100 也就是4即选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1

b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

任务寄存器TR


        TR用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。

        TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

实例


1:访问GDT

当TI=0时表示段描述符在GDT中,如上图所示:

①先从GDTR寄存器中获得GDT基址。

②然后再GDT中以段选择器高13位位置索引值得到段描述符。

③段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

在这里插入图片描述

2:访问LDT

在这里插入图片描述

当TI=1时表示段描述符在LDT中,如上图所示:

①还是先从GDTR寄存器中获得GDT基址。

②从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。

③以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。

④用段选择器高13位位置索引值从LDT段中得到段描述符。

⑤段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。
 

分页模式:线性地址到物理地址的映射

控制寄存器

  控制寄存器用于控制和确定CPU的操作模式。控制寄存器有Cr0Cr1Cr2Cr3Cr4Cr1被保留了,Cr3用于页目录表基址,其他的将继续详细讲解。

Cr0

  Cr0是一个十分重要的寄存器,可以说它是总开关的集合体。如下图所示:

  PE位是启用保护模式(Protection Enable)标志。若PE = 1是开启保护模式,反之为实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PEPG标志都要置位。
  PG位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE标志。PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。
  WP位对于Intel 80486或以上的CPU,是写保护(Write Proctect)标志。当设置该标志时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作;当CPL < 3的时候,如果WP = 0可以读写任意用户级物理页,只要线性地址有效。如果WP = 1可以读取任意用户级物理页,但对于只读的物理页,则不能写。

Cr2

  当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中,如下图所示:

Cr4

  Cr4的结构如下图所示:

  VME用于虚拟8086模式。PAE用于确认是哪个分页,PAE = 1,是2-9-9-12分页,PAE = 010-10-12分页。PSE是大页是否开启的总开关,如果置0,就算PDE中设置了大页你也得是普通的页

    当Cr0中PG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。

从线性地址到物理地址的映射过程为:
(1)从CR3寄存器中获取页面目录(Page Directory)的基地址;
(2)以线性地址的dir位段为下标,在目录中取得相应页面表(Page Table)的基地址;
(3)以线性地址中的page位段为下标,在所得到的页面表中获得相应的页面描述项;
(4)将页面描述项中给出的页面基地址与线性地址中的offset位段相加得到物理地址。

          CR3寄存器的值从哪里来的?每个进程都会有自己的地址空间,页面目录也在内存不同的位置上,这样不同进程就有不同的CR3寄存器的值。CR3寄存器的值一般都保存在进程控制块中,如linux中的task_struct数据结构中。

          页目录项一项占四字节,一共是4*2^10=4K,同理页表大小也是4K,都是4K对齐,从地址0开始,每一个页面(大小4k)的起始地址都是4K的整数倍

         分页模式,4K对齐,也就是低12位为0,页目录中一项的大小为32位,低12位用于权限等标志,仅仅用高20位存储页表基址,同理有项表项

在进程切换时,需要切换进程的目录的起始地址放入cr3寄存器中

         在linux内核启动时,会检测当前内存的大小,将内存分成一个个page(大小4k),用于内存分配

      如果页表项中:

  • 高20位是0,最低位也是0,表示物理页面还没有分配过,会触发缺页异常
  • 高20位不是0,最低位是0,表示物理页面在交换分区中,此时页表项中存储物理页在swap分区中的位置
  • 高20位不是0,最低位不是0,表示物理页面正常使用

      在页表项中,存储着物理内存页的起始地址,由于4k对齐,实际上只需要使用20位表示2^20=1M大小的范围,即 0 , 1, 2 ......2^20-1,因此也称为框号。

    线性地址转化为物理地址的计算过程,32位系统下两级页表的过程中在MMU中计算完成。

    虚拟内存的好处:   

 1 安全:每个进程的地址空间是相互隔离(0-4G),不受其他影响的

2  虚拟地址连续,物理地址可以不连续,物理内存可以得到充分使用,使用LRU算法置换内存页面,将不经常使用的页面置换到磁盘。

问题:

1 能不能在用户空间定义一个指针指向内核空间的内存:不能,权限问题

2 能不能在内核空间定义一个指针指向用户空间的内存:不能

   内核空间的地址映射和上面的地址映射(用户空间)不同;内核空间的地址映射

 _pa 把内核的虚拟地址转为物理地址,也就是x - 3G

_va  把内核的物理地址转为虚拟地址,也就是x+ 3G,

也就是内核的虚拟地址空间在3G-4G,内核启动放置镜像从0x100000  1M的位置(之前的位置放置实模式下的东西),映射到虚拟内存的0xc0100000

bochs使用 查看分段分页

资源:https://download.csdn.net/download/LIJIWEI0611/86539758

安装后:

 拷贝资源中的mylinux到安装目录下:

打开:

依次点击:

 

 

看到界面:

 调试命令类似gdb:

c:继续

 进入mycode 目录下

 执行a.out 程序进入死循环

 因为data不可能为0,所以程序不可能停止运行。如何停止呢

data的虚拟地址是0x3fffef4,如果能找到物理地址,修改物理地址中的内容为0,那么while就可以停止运行。

data是局部变量,在栈上,在ss 栈内存中,输入命令sreg

 ss在0x0017 h,转换成二进制 0000 0000 0001 0111 B;

根据上面段选择子内容的介绍:

 在这里插入图片描述

        段选择子包括三部分:描述符索引(index)、TI、请求特权级(RPL)。

        index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。

        然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址,

        段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。

在 0000 0000 0001 0111 B;中

第1,2位11 表示用户空间,

第3位1 表示LDT

剩下的表示index = 2,这块内存的信息存放在LDT[2]中。

        如何找到LDT?

ldtr:0x0068h 即   0000 0000 0110 1(index=13)0(GDT)00(内核权限)

LDT存储在GDT中index=13的位置中

一个GDT表项占8字节,

输入: xp/2w 0x0000000000005cb8+13*8   xp 查看字节偏移,w表示一个字节 2w表示两个字节,因为一个段描述符是两个字节8位, 0x0000000000005cb8是gdtr的基地址,偏移13(index)*8(段描述符8bit)

 小端模式:低地址存低字节

因此是:

0x000082fa

 0xd2d00068

对照:

0x000082fa : 0xe2d00068   对应的二进制为:

0000

82fa

d2d0

0068

那么:

内存的起始地址:B0-B15  ,B16-B23,B24-B31 一共32位 红色标出

内存的段大小:L0-L15,L16-L19, 一共20位   2^20=1M 长度  绿色标出

G:表示长度的单位  0:字节  1:页(4K),因此段大小有可能是1*1M = 1M byte 或4K * 1M = 4G byte,32位linux内核给每个进程都会分配一个虚拟地址空间 4G ,3G用户空间,1G内核空间

内存起始地址:    0x 00fa d2d0 

内存的段大小:  0x0068

G:0 表示字节

data存储在ldt[2]中,即 0x 00fa d2d0 +2*8 ,查看 xp/2w 0x00fad2d0+2*8

 查看这64位段描述符的意义:

  0x00003fff      0x10c0f300

10c0

f300

0000

3fff

起始地址是1000 0000H

偏移量即逻辑地址 0x3fffef4

线性地址= 起始地址+偏移量=  1000 0000+  3ff fef4 = 0x13ff fef4

分段已经完成,得到线性地址,现在查看是否分页:


  PG位是启用分页机制。在开启这个标志之前必须已经或者同时开启PE标志。PG = 0PE = 0,处理器工作在实地址模式下。PG = 0PE = 1,处理器工作在没有开启分页机制的保护模式下。PG = 1PE = 0,在PE没有开启的情况下无法开启PGPG = 1PE = 1,处理器工作在开启了分页机制的保护模式下。

     现在CR0=0x8000001b: PG=1,PE=1 开启了分页模式

0x13ff fef4 对应的二进制

二进制 0001 0011 1111 1111 111 1110 1111 0100 

十进制 79 1023  3828

页目录的基地址在cr3寄存器中:

 

页目录基地址为0

找到页目录的79项,一个项占四个字节 :

页目录项内容的高20位表示页表项地址 :

页表项地址 = 00fb1000 + 1023*4:

 最后的内容是a,因为data=10,也就是16进制的a; 物理地址是0x0000000000f95ef4

 setpmem 0x0000000000f95ef4       4 0   设置物理地址0x0000000000f95ef4 起始的四字节内容为0

 输出 c 继续执行程序,发现程序结束!

mm_struct

 

        如上图所示,当进程需要访问一个虚拟地址,如0x00FFF213时:

        首先通过mm_struct 中的 mmp,mmp是一个红黑树,这样查找速度可以达到logn,加快虚拟地址的查找速度。mmp将进程的虚拟地址空间用一个个vm_area_struct组织起来,vm_area_struct中表示了它维持的虚拟地址空间区域的起始位置结束位置,读写权限等,属于哪个段;

       当查找mmp时找不到该地址所在的地址区间,则报错 segment fault (段错误)导致程序crash.

       当从mmp中找到后0x00FFF213所在的虚拟地址空间存在且合法后:

       访问pgd,页目录表起始地址,通过分段和分页最终找到物理地址。当访问pgd时找不到相应物理页面,则会触发缺页异常。

     关于分段分页可以参考:Linux内核网桥注释v0.11 :

第六章 引导启动程序 6.4 head.s  在:

在进入保护模式首先创建一个内存页目录表(4k)。0.11 内核是所有进程公用一个页目录表(唯一)。然后创建四个页表供内核代码使用;创建全局描述符表gdt.

         

 5.3 Linux内核对内存的管理与使用

13章内存管理

缺页异常:do_page_fault

 

 

 

 

         区别:如中断指令(系统调用)执行完中断处理函数,执行下一条指令;

                    异常:如缺页异常,执行完do_page_fault后执行执行当前指令。

关于缺页异常:在linux0.11 内核完全注释中,13章内存管理 对写时拷贝和按需加载有详细的描述:

linux内核2.6 中:

do_page_fault 是页面异常的异常服务函数:

在这个函数中,通过handle_mm_fault处理:

dotraplinkage void __kprobes
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
	struct vm_area_struct *vma;
	struct task_struct *tsk;
	unsigned long address;
	struct mm_struct *mm;
	int fault;
	int write = error_code & PF_WRITE;
	unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE |
					(write ? FAULT_FLAG_WRITE : 0);

	tsk = current;//当前CPU正在执行的进程
	mm = tsk->mm;//当前进程的mm_struct

	/* Get the faulting address: */
	//获取出错地址,cr2 寄存器放置缺页异常时的地址
	address = read_cr2();

	......
   //查找mm_struct中address所在的vma 
	vma = find_vma(mm, address);
	if (unlikely(!vma)) {//没有找到  段错误  cause a SIGSEGV 
		bad_area(regs, error_code, address);
		return;
	}//找到该区间(除了栈区域,其他段都是从低地址到高地址增长)
	if (likely(vma->vm_start <= address))
		goto good_area;
    //判断是不是栈区域
	if (unlikely(!(vma->vm_flags & VM_GROWSDOWN))) {
		bad_area(regs, error_code, address);
		return;
	}
	if (error_code & PF_USER) {
		/*
		 * Accessing the stack below %sp is always a bug.
		 * The large cushion allows instructions like enter
		 * and pusha to work. ("enter $65535, $31" pushes
		 * 32 pointers and then decrements %sp by 65535.)
		 */
		if (unlikely(address + 65536 + 32 * sizeof(unsigned long) < regs->sp)) {
			bad_area(regs, error_code, address);
			return;
		}
	}
	//判断在不在栈的增加区域,不踩到其他区域
	if (unlikely(expand_stack(vma, address))) {
		bad_area(regs, error_code, address);
		return;
	}

	/*
	 * Ok, we have a good vm_area for this memory access, so
	 * we can handle it..
	 */
	// address是合法的vma区域,解决该异常:
good_area:
     //判断当前指令的操作和vma节点描述的内存区域的内存属性是否一致
	 //如 在只读的vma属性中执行写操作
	if (unlikely(access_error(error_code, vma))) {
		bad_area_access_error(regs, error_code, address);
		return;
	}

	/*
	 * If for any reason at all we couldn't handle the fault,
	 * make sure we exit gracefully rather than endlessly redo
	 * the fault:
	 */
	//核心处理函数 pgd-> 页目录 ->页表 -> 物理地址
	fault = handle_mm_fault(mm, vma, address, flags);

......
}

__handle_mm_fault

/*
 * By the time we get here, we already hold the mm semaphore
 */
int __handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma,
		unsigned long address, int write_access)
{
	pgd_t *pgd;
	pud_t *pud;
	pmd_t *pmd;
	pte_t *pte;

	__set_current_state(TASK_RUNNING);

	count_vm_event(PGFAULT);

	if (unlikely(is_vm_hugetlb_page(vma)))
		return hugetlb_fault(mm, vma, address, write_access);
    //查找页目录项pgd :page dir
	pgd = pgd_offset(mm, address);
	pud = pud_alloc(mm, pgd, address);
	if (!pud)
		return VM_FAULT_OOM;
	pmd = pmd_alloc(mm, pud, address);
	if (!pmd)
		return VM_FAULT_OOM;
	//创建 pgd 指向的页表如果不存在,创建 pgd 指向的页表 :page table entry
	pte = pte_alloc_map(mm, pmd, address);
	if (!pte)
		return VM_FAULT_OOM;

	return handle_pte_fault(mm, vma, address, pte, pmd, write_access);
}

handle_pte_fault

static inline int handle_pte_fault(struct mm_struct *mm,
		struct vm_area_struct *vma, unsigned long address,
		pte_t *pte, pmd_t *pmd, int write_access)
{
	pte_t entry;
	spinlock_t *ptl;
    //entry 记录了页表项的内容 8字节描述符
	entry = *pte;
	//当前页表项还没有记录物理页面的有效信息:没有分配物理页面(按需加载)
	if (!pte_present(entry)) {
		//1 页表项是空的,没有记录物理页面信息->分配物理页面
		if (pte_none(entry)) {
			if (vma->vm_ops) {
				if (vma->vm_ops->nopage)
				    //分配物理页面
					return do_no_page(mm, vma, address,
							  pte, pmd,
							  write_access);
				if (unlikely(vma->vm_ops->nopfn))
					return do_no_pfn(mm, vma, address, pte,
							 pmd, write_access);
			}
			return do_anonymous_page(mm, vma, address,
						 pte, pmd, write_access);
		}
		//2 页表项是空的,页表项指向一个文件的内容:
		// mmap file文件映射到内存:共享内存
		if (pte_file(entry))
			return do_file_page(mm, vma, address,
					pte, pmd, write_access, entry);

	    //3 做页面的交换:页表项并不空,但是已经不在物理内存中
		//在swap交换分区中,重新加载到物理内存中,更新pte的内容
		return do_swap_page(mm, vma, address,
					pte, pmd, write_access, entry);
	}

	ptl = pte_lockptr(mm, pmd);
	spin_lock(ptl);
	if (unlikely(!pte_same(*pte, entry)))
		goto unlock;
    // 页表项是正常的,当前的指令需要写操作
	if (write_access) {
		//没有写权限
		if (!pte_write(entry))
		    //wp(write on page) 写时拷贝:
			//父进程创建子进程时,复制页目录项,页表,此时共用
			//物理内存,共用的物理内存双方只读,不能写,如果需要写
			//则需要给子进程分配单独的物理页面,并且修改标志位可读

			return do_wp_page(mm, vma, address,
					pte, pmd, ptl, entry);
		entry = pte_mkdirty(entry);
	}

	//LRU 最近最久未使用算法,更新最新访问的页面
	entry = pte_mkyoung(entry);
	//更新物理page对应的pte
	if (ptep_set_access_flags(vma, address, pte, entry, write_access)) {
		update_mmu_cache(vma, address, entry);
		lazy_mmu_prot_update(entry);
	} else {
		/*
		 * This is needed only for protection faults but the arch code
		 * is not yet telling us if this is a protection fault or not.
		 * This still avoids useless tlb flushes for .text page faults
		 * with threads.
		 */
		if (write_access)
			flush_tlb_page(vma, address);
	}
unlock:
	pte_unmap_unlock(pte, ptl);
	return VM_FAULT_MINOR;
}

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值