uCore lab0
最近想开始做清华学堂在线的OS Lab,得先深入理解ucore,自然也就需要了解支撑ucore运行的硬件环境,即了解处理器体系结构(了解硬件对ucore带来影响)和机器指令集(读懂ucore的汇编)。ucore目前支持的硬件环境是基于Intel 80386以上的计算机系统。(之前做的南京大学的NEMU也是i386)
Intel 80386运行模式
一般CPU只有一种运行模式,能够支持多个程序在各自独立的内存空间中并发执行,且有用户特权级和内核特权级的区分,让一般应用不能破坏操作系统内核和执行特权指令。80386处理器有四种运行模式:实模式、保护模式、SMM模式和虚拟8086模式。这里对涉及ucore的实模式、保护模式做一个简要介绍。
实模式
80386加电启动后处于实模式运行状态,在这种状态下软件可访问的物理内存空间不能超过1MB,且无法发挥Intel 80386以上级别的32位CPU的4GB内存管理能力。
实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,操作系统和用户程序并没有区别对待,而且每一个指针都是指向实际的物理地址。这样用户程序的一个指针如果指向了操作系统区域或其他用户程序区域,并修改了内容,那么其后果就很可能是灾难性的。(对于ucore其实没有必要涉及,这主要是Intel x86的向下兼容需求导致其一直存在。)
讲到了实模式,我们就不得不提一下BIOS。
BIOS
1.BIOS只能在实模式下做事!!!
2.BIOS是固化程序,写在ROM上的!!!
实模式下:
P
C
=
16
∗
C
S
+
I
P
PC = 16 * CS + IP
PC=16∗CS+IP
CS:IP = 0xf000:fff0(CS确定了代码段,IP确定了段内偏移;CS和IP都是16位的寄存器)
20位地址空间决定了只有1MB的物理内存(CS向左移4位,扩充至20位)
具体内存和磁盘分布见下图:
在计算机加电后:
-
首先是到【BIOS固件】的0XFFFF0读第一条指令,主要工作就是【初始化硬件】,具体包括
- 基本输入输出的程序(中断方式系统调用:键盘输入,显示器输出、磁盘扇区读写、检测内存大小)
- 系统设置信息(比如启动方式:硬盘、网络、光盘…)
- 开机自检(比如硬件:内存、显卡、即插即用设备)
- 系统自启动程序
- …
-
【BIOS】读取主引导扇区代码(主引导记录MBR格式)
-
启动代码(446字节)
- 检查分区表正确性
- 加载并跳转到磁盘上的【引导程序】
-
硬盘分区表(64字节)
- 描述分区状态和位置
- 有4个分区,每个分区占16字节
-
结束标志(2字节)
- 有效标志(0X55、0XAA)
-
-
主引导扇区代码读取磁盘上【活动分区】的引导扇区代码,即【引导程序】
- 分区引导的扇区格式
- 跳转到启动代码(平台相关)
- 文件卷头:描述文件系统信息
- 启动代码:跳转到【加载程序】
- 结束:55AA
- 分区引导的扇区格式
-
【引导扇区】代码读取文件系统的【加载程序】到0x7c00
- 跳转到CS:IP = 0000:7c00
- 【加载程序】也就是我们常说的【Bootloader】
-
【加载程序】将操作系统的代码和数据加载到内存中
- 【加载程序】从文件系统中读取启动配置信息
- 查看【启动菜单】,查看OS内核列表和参数
- 按配置加载指定内核,跳转到操作系统起始位置
流程图(BIOS→Bootloader)
文字或许不太清晰,那就画个图吧~
疑问
Q:为什么不直接让BIOS把内核读到内存里,要多个Bootloader?
A:为了灵活性,各个操作系统的厂商自己写好Bootloader,让Bootloader来读取内核,识别文件系统,而不是让BIOS来做这个事情,BIOS只负责读Bootloader就行,其实也是一定程度的解耦
Q:为什么要有【主引导分区】这个东西?直接让BIOS读不就完了?
A:以前电脑low,只有一个分区,确实直接读就行了;现在不一样啊,比如我自己的电脑就是分了两个分区,装双系统(不同分区装不同系统)。而主引导记录,就是负责告诉BIOS到哪个文件系统(活动分区)读加载程序,说白了,就是个“热心志愿者”,教你怎么走。
关于主引导扇区,再多说一点
硬盘的主引导扇区是用FDISK
进行硬盘分区时产生的, 它属于整个硬盘,而不属于某个独立的DOS 分区。
主引导扇区也是在硬盘中,而且并不是说系统装在哪个区,哪个区就是主引导;有可能主引导扇区在某个没装系统的分区待着呢。
主引导扇区位于整个硬盘的(0磁道,0柱面,1扇区),包括硬盘主引导记录MBR(Main Boot Record)和分区表DPT(Disk Partition Table)。
保护模式
保护模式的一个主要目标是确保应用程序无法对操作系统进行破坏。系统控制权交给操作系统接管。
实际上,80386就是通过在实模式下初始化控制寄存器以及页表,然后再通过设置CR0寄存器使其中的保护模式使能位置位,从而进入到80386的保护模式。
当80386工作在保护模式下的时候,其所有的32根地址线都可供寻址,物理寻址空间高达4GB。
在保护模式下,支持内存【分页机制】,提供了对虚拟内存的良好支持。
保护模式下80386支持多任务,还支持【优先级机制】,不同的程序可以运行在不同的特权级上。特权级一共分0~3四个级别,操作系统运行在最高的特权级0上,应用程序则运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好地隔离各个任务。
Intel 80386内存架构
地址是访问内存空间的索引。
一般而言,内存地址有两个:一个是CPU通过总线访问物理内存用到的物理地址,一个是我们编写的应用程序所用到的逻辑地址(也有人称为虚拟地址)。比如如下C代码片段:
int boo=1;
// foo是一个指向boo的整型指针变量,foo中储存的内容就是boo的逻辑地址
int *foo=&a;
80386是32位的处理器,即可以寻址的物理内存地址空间为2^32=4G字节。为更好理解面向80386处理器的ucore操作系统,需要用到三个地址空间的概念:物理地址、线性地址和逻辑地址。
物理内存地址空间是处理器提交到总线上用于访问计算机系统中的内存和外设的最终地址。一个计算机系统中只有一个物理地址空间。
线性地址空间是80386处理器通过段机制控制下的形成的地址空间。在操作系统的管理下,每个运行的应用程序有相对独立的一个或多个内存空间段,每个段有各自的起始地址和长度属性,大小不固定,这样可让多个运行的应用程序之间相互隔离,实现对地址空间的保护。
在操作系统完成对80386处理器段机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立全局描述符表,完成虚拟地址与线性地址的映射关系)后,80386处理器的段管理功能单元负责把虚拟地址转换成线性地址,在没有下面介绍的页机制启动的情况下,这个线性地址就是物理地址。
相对而言,段机制对大量应用程序分散地使用大内存的支持能力较弱。所以Intel公司又加入了页机制,每个页的大小是固定的(一般为4KB),也可完成对内存单元的安全保护,隔离,且可有效支持大量应用程序分散地使用大内存的情况。
在操作系统完成对80386处理器页机制的初始化和配置(主要是需要操作系统通过特定的指令和操作建立页表,完成虚拟地址与线性地址的映射关系)后,应用程序看到的逻辑地址先被处理器中的段管理功能单元转换为线性地址,然后再通过80386处理器中的页管理功能单元把线性地址转换成物理地址。
页机制和段机制有一定程度的功能重复,但Intel公司为了向下兼容等目标,使得这两者一直共存。
上述三种地址的关系如下:
-
分段机制启动、分页机制未启动:逻辑地址—>段机制处理—>线性地址=物理地址
-
分段机制和分页机制都启动:逻辑地址—>段机制处理—>线性地址—>页机制处理—>物理地址
Intel 80386寄存器
(略,汇编学过了;即使不会看手册就行了)
面向对象编程方法
OOP?C语言还能OOP?
虽然C 语言对面向对象编程并没有原生支持,但没有原生支持并不等于我们不能用 C 语言写面向对象程序。需要注意,我们并不需要用 C 语言模拟出一个常见 C++ 编译器已经实现的对象模型。如果是这样,还不如直接采用C++编 程。
uCore的面向对象编程方法,目前主要是采用了类似C++的接口(interface)概念,即是让实现细节不同的某类内核子系统(比如物理内存分配器、调度器,文件系统等)有共同的操作方式,这样虽然内存子系统的实现千差万别,但它的访问接口是不变的。这样不同的内核子系统之间就可以灵活组合在一起,实现风格各异,功能不同的操作系统。(我在之前的二学“面向接口编程”提过了函数指针实现接口的概念了,只不过当时比较针对Java,并没说太多)
接口在 C 语言中,表现为一组函数指针的集合。放在 C++ 中,即为虚表。接口设计的难点是如果找出各种内核子系统的共性访问/操作模式,从而可以根据访问模式提取出函数指针列表。
比如对于uCore内核中的物理内存管理子系统,首先通过分析内核中其他子系统可能对物理内存管理子系统,明确物理内存管理子系统的访问/操作模式,然后我们定义了pmm_manager(物理内存管理)
数据结构(位于lab2/kern/mm/pmm.h)如下:
// pmm_manager is a physical memory management class. A special pmm manager - XXX_pmm_ manager
// only needs to implement the methods in pmm_manager class, then XXX_pmm_manager can be used
// by ucore to manage the total physical memory space.
struct pmm_manager {
// XXX_pmm_manager's name
const char *name;
// initialize internal description&management data structure
// (free block list, number of free block) of XXX_pmm_manager
void (*init)(void);
// setup description&management data structcure according to
// the initial free physical memory space
void (*init_memmap)(struct Page *base, size_t n);
// allocate >=n pages, depend on the allocation algorithm
struct Page *(*alloc_pages)(size_t n);
// free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
void (*free_pages)(struct Page *base, size_t n);
// return the number of free pages
size_t (*nr_free_pages)(void);
// check the correctness of XXX_pmm_manager
void (*check)(void);
};
这样基于此数据结构,我们可以实现不同连续内存分配算法的物理内存管理子系统,而这些物理内存管理子系统需要编写算法,把算法实现在此结构中定义的init(初始化)、init_memmap(分析空闲物理内存并初始化管理)、alloc_pages(分配物理页)、free_pages(释放物理页)函数指针所对应的函数中。
而其他内存子系统需要与物理内存管理子系统交互时,只需调用特定物理内存管理子系统所采用的pmm_manager数据结构变量中的函数指针即可 。
算法,把算法实现在此结构中定义的init(初始化)、init_memmap(分析空闲物理内存并初始化管理)、alloc_pages(分配物理页)、free_pages(释放物理页)函数指针所对应的函数中。
而其他内存子系统需要与物理内存管理子系统交互时,只需调用特定物理内存管理子系统所采用的pmm_manager数据结构变量中的函数指针即可 。