操作系统真相还原 第五章 保护模式进阶,向内核迈进

第五章 保护模式进阶,向内核迈进

启动内存分页机制

为什么分页

分段的缺点

  • 内存换出时,只能换出整个段。IO较高,新进程缺少的内存可能很小。
  • 段内存是基于段基址+偏移来寻址,是线性的,进程需要连续的内存。如果内存整体剩余满足,但是不连续,就无法加载该进程。

一级页表

兼容分段机制,分页机制建立在分段机制上。

分页机制的思想:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。

image-20210315123716806

分页机制的作用有两方面

• 将线性地址转换成物理地址。

• 用大小相等的页代替大小不等的段。

image-20210315124116401

每个进程都有自己的页表,所以每个进程都有自己的4G虚拟空间。

cpu中采用的页大小为4kb,即2的12次方,32位4G内存下,页表项的个数为2的20次方,即1M个。

image-20210315165930493

二级页表

现代操作系统一般使用二级页表。

一级页表的缺点:

  • 一级页表大小:页表项为2的20次方,页表项大小为4字节,所以一级页表占用内存总大小为4M。每个进程都有自己的页表,会占用比较多内存。

一级页表是将这 1M 个标准页放置到一张页表中。

二级页表是将这 1M 个标准页平均放置1K个页表中。每个页表中包含有 1K 个页表项。页表项是4字节大小,页表包含 1K 个页表项,故页表大小为4KB,这恰恰是 个标准页的大小。

二级页表的优点:

二级页表并没有减少页表项的数量和存储空间,数量还是2的20次方,即1M个,存储还是4M。但是二级页表可以不用全部加载到内存中,只需要加载页目录表,大小只有1页,使用地址从页目录表中取出页表项,加载使用到的页表项。

image-20210407215734854

二级页表地址转换原理:

将 32 位虚拟地址拆分成高 10 位、中间 10 位、低 12 位三部分,它们的作用是:高 10 位作为页表的索引,用于在页目录表中定位一个页目录项 PDE,页目录项中有页表物理地址,也就是定位到了某个页表。中间 10 位作为物理页的索引,用于在页表内定位到某个页表项PTE ,页表项中有分配的物理页地址,也就是定位到了某个物理页。低 12 位作为页内偏移量用于在已经定位到的物理页内寻址。

由于页目录项和页表项都是4字节大小,给出了索引后,还需要在背后悄悄乘以4 ,再加上页表物理地址,这才是最终要访问的绝对物理地址。

页目录项( Page Directory Entry, PDE )

页表项 (Page Table Entry, PTE)

image-20210315182229687

image-20210315182248905

页目录项和页表项中的都是物理页地址,标准页大小是 4KB ,故地址都是 4K 的倍数,所以只需要记录物理地址高 20 位就可以啦,这样省出来的 12 位可以用来添加其他属性。

image-20210315183848980

P, Present ,意为存在位 若为 表示该页存在于物理内存中,若为 表示该表不在物理内存中。操作系统的页式虚拟内存管理便是通过P位和相应的 pagefault 异常来实现的。缺页异常,如果缺页,就加载页。

RW, Read/Write ,意为读写位。

US, User/Supervisor ,意为 通用户/超级用户位。

PWT, Page-level Write-Through ,意为页级通写位,也称页级写透位,若为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。

PCD, Page-level Cache Disable ,意为页级高速缓存禁止位,若为1表示该页启用高速缓存,为0表示禁止将该页缓存 。

A, Accessed ,意为访问位,若为1表示该页被 CPU 访问过啦,所以该位是由 CPU 设置的。

D,Dirty ,意为脏页位。当 CPU 个页面执行写操作时,就会设置对应页表项的D位为 1,此项仅针对页表项有效,并不会修改页目录项中的位。

PAT:Page Attribute Table,页属性位,在页一级的粒度上设置内存属性。
G:Global,全局位。与TLB有关,为1表示该页是全局页,该页在高速缓存TLB中一直保存。
AVL:Available,可用位。为1表示用户进程可用该页,为0则不可用。对操作系统无效。

P/A标志实现内存换出

分段中有P/A机制,分页中的换出和分段机制类似。A位来记录内存页的使用频率(被访问时置为1,操作系统定期将该位清0。定期统计1的次数),当内存不足时,换出使用频率低的页,并将P为设置为0。当下次访问该页时,P为0,缺页异常,加载此页到内存。

操作系统与用户进程的关系

用户进程需要依赖操作系统提供的功能,所以所有用户进程需要共享操作系统。

怎么共享?让操作系统位于所有用户进程的虚拟地址空间。每个用户进程都有4G虚拟地址空间,我们可以把 4GB 虚拟地址空间分成两部分,一部分专门划给操作系统,部分就归用户进程使用。比如 Linux ,它就运行在虚拟地址的 3GB 以上,其他用户进程都运行在3GB 以下。

为了实现共享操作系统,让所有用户进程 3GB-4GB 的虚拟地址空间都指向同一个操作系统,也就是所有进程的虚拟地址 3GB-4GB 本质上都是指向的同一片物理页地址,这片物理页上是操作系统的实体代码。

启用分页机制

启用分页机制步骤:

(1 )准备好页目录表及页表。

(2 )将页表地址写入控制寄存器 cr3。

(3 )寄存器 cr3的 PG 位置为1。

控制寄存器(cr0-cr7) cr3 用于存储页表物理地址,所以 cr3寄存器又称为页目录基址寄存器( Page DirectorγBase Register, PDBR )。

由于页目录表所在的地址要求在一个自然页内,即页目录的起始地址是 4KB的整数倍,低 12 位地址全是0。所以只要在 cr3 寄存器的第 31-12 位中写入物理地址的高 20 位就行了。

创建页目录表及页表

project/c5/b/boot/loader.S

;-------------   创建页目录及页表   ---------------
setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	     ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性(7)
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				     ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				     ; 创建Page Table Entry
   mov [ebx+esi*4],edx			     ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性US,RW和P位都为1
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

1.创建页目录表

2.创建页表

img

启用分页

加载gdt

修改栈地址,显存地址到内核中。

页目录表地址赋值给cr3寄存器,弃用cr0寄存器的pg位。

; 创建页目录及页表并初始化页内存位图
   call setup_page

   ;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
   sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

   ;将gdt描述符中视频段描述符中的段基址+0xc0000000
   mov ebx, [gdt_ptr + 2]  
   or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
					      ;段描述符的高4字节的最高位是段基址的31~24位

   ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
   add dword [gdt_ptr + 2], 0xc0000000

   add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

   ; 把页目录地址赋给cr3
   mov eax, PAGE_DIR_TABLE_POS
   mov cr3, eax

   ; 打开cr0的pg位(第31位)
   mov eax, cr0
   or eax, 0x80000000
   mov cr0, eax

   ;在开启分页后,用gdt新的地址重新加载
   lgdt [gdt_ptr]             ; 重新加载

   mov byte [gs:160], 'V'     ;视频段段基址已经被更新,用字符v表示virtual addr

   jmp $

分页机制的内存映射过程

  1. 先要从CR3寄存器中获取页目录表物理地址。
  2. 用虚拟地址的高10位乘以4的积做为在页目录表中的偏移量去寻址目录项pde。
  3. 从pde中读出页表物理地址。
  4. 用虚拟地址的中间10位乘以4的积做为在该页表中的偏移量去寻址页表项pte。
  5. 从该pte中读出页框物理地址。
  6. 用虚拟地址的低12位做为该物理页框的偏移量。
  7. 终于完成虚拟地址到物理地址的映射。

快表TLB简介

每一个虚拟地址到物理地址的转换都需要很多步骤,处理器的速度和内存的速度完全是两个数量级,页表毕竟在内存中,转换过程中频繁的内存访问,使得地址转换速度慢上加慢。

根据程序的局部性原理,可以将近来常用的地址和指令加载到速度更快的设备中,因此我们都想到了缓存。处理器准备了一个高速缓存,可以匹配高速的处理器速率和低速的内存访问速度,它专门用来存放虚拟地址页框与物理地址页框的映射关系,这个调整缓存就是TLB,即Translation Lookaside Buffer,俗称快表。

一步步编写操作系统 41  快表tlb 简介

高速缓存由于成本等原因,容量一般都很小,TLB也是,因此TLB中的数据只是当前任务的部分页表,而且只有P位为1的页表项才有资格在TLB中,如果TLB被装满了,需要将很少使用的条目换出。

缓存刷新的问题

一般的缓存可以定期刷新,甚至推迟几分钟都可以,但TLB和一般的缓存可不一样,TLB是页表的缓存,处理器寻址时最先访问的是TLB,TLB里面存储的是程序运行所依赖的指令和数据的内存地址,任意时刻都必须保证地址的有效性,否则程序必然出错,所以TLB必须实时更新。

可是如果实时读取内存中的页表去更新TLB的话,这又回到了从内存查询映射关系的老路,TLB反而成了鸡肋。为此,TLB并不自动更新,处理器也不负责TLB的有效性,它把TLB的维护工作交给操作系统开发人员,由开发人员手动控制。这的确是非常合理的,毕竟维护页表的代码是开发人员自己写的,他们肯定知道何时修改了页表,或是修改了哪些条目。

经验:难以处理的逻辑,可以换种思路,或许可以暴露接口给调用方触发

加载内核

用c语言写内核

使用c的原因:

纯汇编很辛苦。可以使用c语言编写。汇编的大部分指令是一条指令对应一条机器指令,可以看做最底层的语言。

c语言是一条语句对应几条汇编指令,更容易一点。比汇编慢的原因是,一条对应多条指令,可能会用冗余。

只使用c语法,不使用c的标准库。

gcc

-c:编译,不进行链接,只生成目标文件。

-o:命名输出文件

目标文件:即可重定位文件,文件中的符号(函数名,变量名等)还没有设置地址。需要把所有的目标文件链接到一起,然后重定位设置地址,生成可执行文件。

ld:链接命令

链接器默认把名为_start的函数作为程序入口,main函数其实并不是第一个函数,是编译器的处理,__start函数后续调用了main函数。

ld -m elf_i386 kernel.o -Ttext 0xc0001500 -e main -o kernel.elf

ld生成的文件格式是elf格式

二进制程序的运行方法

文件头:

程序头,用来描述程序的布局信息(大小,入口地址等),也即是元数据。

经验:头+体,是一种常见的形式,在协议中经常出现,用于两层或两个模块之间定义协议,有解耦的作用。元信息也经常使用,用于定义描述信息。

elf格式的二进制文件

elf:二进制文件格式标准。

image-20210424111712863

段和节:

段和节是真正的程序体。

段:segment,如代码段和数据段等,可以包括多个节。

节:section,多个节链接之后被合并为一个段。

elf格式的作用体现在两个方面:链接阶段,运行阶段。

image-20210424111754528

特权级深入浅出

特权级分级:

0、1、2、3级,数字越小权力越大。

操作系统位于0特权级,直接控制硬件和核心数据。

系统程序分别位于1,2级。一般为虚拟机,驱动程序等系统服务。

用户程序位于3级,“有需求时找操作系统”。

image-20210424113208533

TSS简介

TSS,即Task State Segment,任务状态段。TSS是每个任务都有的数据结构。有三个栈指针,用于特权级切换时,压入切换前的栈指针。

任务:没有操作系统的情况下,可以认为进程就是任务。有了操作系统,任务一般需要用户进程和操作系统配合,所以任务一般包括特权级3的用户部分和特权级0的内核部分,完整的任务需要经历特权级的变化。

posted @ 2021-10-22 16:08  dev_liufq  阅读(301)  评论(0编辑  收藏  举报