操作系统真相还原 第五章 保护模式进阶,向内核迈进
第五章 保护模式进阶,向内核迈进
启动内存分页机制
为什么分页
分段的缺点
- 内存换出时,只能换出整个段。IO较高,新进程缺少的内存可能很小。
- 段内存是基于段基址+偏移来寻址,是线性的,进程需要连续的内存。如果内存整体剩余满足,但是不连续,就无法加载该进程。
一级页表
兼容分段机制,分页机制建立在分段机制上。
分页机制的思想:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址其对应的物理地址可以不连续。
分页机制的作用有两方面
• 将线性地址转换成物理地址。
• 用大小相等的页代替大小不等的段。
每个进程都有自己的页表,所以每个进程都有自己的4G虚拟空间。
cpu中采用的页大小为4kb,即2的12次方,32位4G内存下,页表项的个数为2的20次方,即1M个。
二级页表
现代操作系统一般使用二级页表。
一级页表的缺点:
- 一级页表大小:页表项为2的20次方,页表项大小为4字节,所以一级页表占用内存总大小为4M。每个进程都有自己的页表,会占用比较多内存。
一级页表是将这 1M 个标准页放置到一张页表中。
二级页表是将这 1M 个标准页平均放置1K个页表中。每个页表中包含有 1K 个页表项。页表项是4字节大小,页表包含 1K 个页表项,故页表大小为4KB,这恰恰是 个标准页的大小。
二级页表的优点:
二级页表并没有减少页表项的数量和存储空间,数量还是2的20次方,即1M个,存储还是4M。但是二级页表可以不用全部加载到内存中,只需要加载页目录表,大小只有1页,使用地址从页目录表中取出页表项,加载使用到的页表项。
二级页表地址转换原理:
将 32 位虚拟地址拆分成高 10 位、中间 10 位、低 12 位三部分,它们的作用是:高 10 位作为页表的索引,用于在页目录表中定位一个页目录项 PDE,页目录项中有页表物理地址,也就是定位到了某个页表。中间 10 位作为物理页的索引,用于在页表内定位到某个页表项PTE ,页表项中有分配的物理页地址,也就是定位到了某个物理页。低 12 位作为页内偏移量用于在已经定位到的物理页内寻址。
由于页目录项和页表项都是4字节大小,给出了索引后,还需要在背后悄悄乘以4 ,再加上页表物理地址,这才是最终要访问的绝对物理地址。
页目录项( Page Directory Entry, PDE )
页表项 (Page Table Entry, PTE)
页目录项和页表项中的都是物理页地址,标准页大小是 4KB ,故地址都是 4K 的倍数,所以只需要记录物理地址高 20 位就可以啦,这样省出来的 12 位可以用来添加其他属性。
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.创建页表
启用分页
加载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 $
分页机制的内存映射过程
- 先要从CR3寄存器中获取页目录表物理地址。
- 用虚拟地址的高10位乘以4的积做为在页目录表中的偏移量去寻址目录项pde。
- 从pde中读出页表物理地址。
- 用虚拟地址的中间10位乘以4的积做为在该页表中的偏移量去寻址页表项pte。
- 从该pte中读出页框物理地址。
- 用虚拟地址的低12位做为该物理页框的偏移量。
- 终于完成虚拟地址到物理地址的映射。
快表TLB简介
每一个虚拟地址到物理地址的转换都需要很多步骤,处理器的速度和内存的速度完全是两个数量级,页表毕竟在内存中,转换过程中频繁的内存访问,使得地址转换速度慢上加慢。
根据程序的局部性原理,可以将近来常用的地址和指令加载到速度更快的设备中,因此我们都想到了缓存。处理器准备了一个高速缓存,可以匹配高速的处理器速率和低速的内存访问速度,它专门用来存放虚拟地址页框与物理地址页框的映射关系,这个调整缓存就是TLB,即Translation Lookaside Buffer,俗称快表。
高速缓存由于成本等原因,容量一般都很小,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:二进制文件格式标准。
段和节:
段和节是真正的程序体。
段:segment,如代码段和数据段等,可以包括多个节。
节:section,多个节链接之后被合并为一个段。
elf格式的作用体现在两个方面:链接阶段,运行阶段。
特权级深入浅出
特权级分级:
0、1、2、3级,数字越小权力越大。
操作系统位于0特权级,直接控制硬件和核心数据。
系统程序分别位于1,2级。一般为虚拟机,驱动程序等系统服务。
用户程序位于3级,“有需求时找操作系统”。
TSS简介
TSS,即Task State Segment,任务状态段。TSS是每个任务都有的数据结构。有三个栈指针,用于特权级切换时,压入切换前的栈指针。
任务:没有操作系统的情况下,可以认为进程就是任务。有了操作系统,任务一般需要用户进程和操作系统配合,所以任务一般包括特权级3的用户部分和特权级0的内核部分,完整的任务需要经历特权级的变化。