Learning and Implementation of a Small Operating System
从按下电源开始,,CS:IP置位,指向0xffff0的位置,这个地址是在ROM固化的BIOS程序,紧接着就是BIOS的POST自检过程,BIOS对计算机各个部件开始初始化,如果有错误会给出警告。当BIOS完成这些工作后,它的工作就是在外部存储设备中寻找操作系统。BIOS中有一张启动设备表,BIOS会按这个表列出的顺序查找可启动设备。如果这个存储设备的第一个扇区512个字节的最后两个字节是0x55和0xAA,那么该设备是可启动设备,这是一个约定。所以BIOS会对这个列表中的设备逐一检测,只要有一个设备满足要求,后续设备将不再测试。所以将mbr.bin加载到硬盘的第一个扇区,bochs会将mbr.bin加载到0x7c00这个位置,而一个扇区只有512字节,放不下很多内容,所以我们有了loader.asm。mbr负责将loader加载到指定物理地址(0x900)。
1、利用BIOS中断获取物理内存总量
2、进入保护模式,包括构建全局描述符表,打开A20,加载GDT,cr0寄存器第0位置1,然后刷新流水线
3、采用分页机制,包括准备好页目录表及页表,将页表地址写入控制寄存器,寄存器cr0的PG位置1
- 实模式下,操作系统和用户程序属于同一特权级,平起平坐没有区别对待
- 用户程序所引用的地址都是指向真实的物理地址,也就是说逻辑地址等于物理地址
- 用户程序可以自由的修改段基址,可访问所有内存没人能拦得住
- 访问超过64KB的内存区域要切换段基址,转来转去容易晕乎
- 一次只能运行一个程序,无法充分利用计算机资源
- 共20根地址线,最大可用内存为1MB,不够用
-
保护模式下内存段(如数据段, 代码段等)不再是简单的用段寄存器加载一下段基址就能用了,段的信息加了很多(段基址、段界限、段的读写权限、段的特权级别等),而这些信息定义在段描述符,段描述符存在描述符表中,一个段描述符只用来定义一个内存段
-
全局描述符表 GDT 是保护模式下内存段的登记表。GDT相当于描述符的数组,数组中的每个元素都是8字节的描述符,用选择子中提供的下标在GDT中索引描述符。全局描述符表位于内存中,需要有专门的寄存器指向它,这个专门的寄存器是GDTR
-
描述符表包括全局描述符表和局部描述符表,全局描述符表每个CPU只有一个,因为全局描述附表存内存中,每个CPU只有一个全局描述符表寄存器gdtr。Linux内存模式为平坦模式,yerOS kernel也模仿Linux采用平坦模式,即所有段描述符中的段基址为全0,段界限为全1,这样只需不同特权级的任务有不同的数据段描述符、代码段描述符(读写权限不同)
-
gdtr有48位,16位段界限,32位内存起始地址。16位的段界限表示全集描述符表最大的内存范围为2^16,能表示的内存段描述符数量为2^16/8 =8192个。局部描述符表的出现就是为了解决多任务情况下全局描述符表的段描述符数量不够问题,但由于我们采用平坦模式,所以所需的段描述符数量全局描述符表可以满足,所以在我们的kernel中并没有启用ldtr寄存器,也就是没有使用局部描述符机制。
-
gdtr提供基地址, 选择子提供段描述符表中的偏移量
- 实模式下内存采取"段基址: 段内偏移地址"形式,最大地址值为0xffff:0xffff,即0x10ffef。由于实模式下的地址线是20位,最大寻址空间是1MB,即0x00000~0xfffff。超过1MB内存的部分在逻辑上也是正常的,但物理内存中却没有与之对应的部分,因此CPU采取的做法是超过1MB的部分自动回绕到0地址,继续从0地址开始映射,应当于对1MB求模。
- CPU发展到了80286后,地址总线有原来的20位变成了24位。但任何时候Intel都会把兼容放在第一位,80286是第一款具有保护模式的CPU,他在实模式下表现也应该和8086/8088一模一样。按照兼容的要求,意味着80286以及后续CPU的实模式也应该与8086/8088完全一样,即仍然只使用20根地址线。但80286有24根地址线,即A0
A23, 也就是说A20地址线是开启的。如果访问0x1000000x10ffef间的内存, 系统将直接访问这块物理内存,并不会像8086/8088那样回绕到0 - 为了解决此问题,现在的计算机都默认关闭A20地址线,而当我们要进入保护模式时,需要打开A20地址线
- 采用bitmap,相当于一组资源的映射,bitmap中的每一位和被管理的单位资源都是一对一关系
- 内核和用户进程分别运行在自己的地址空间。实模式中程序地址就等于物理地址,但在保护模式中程序地址变成了虚拟地址,虚拟地址对应的物理地址是由分页机制做的映射,并通过页表将这两类地址关联
- 将物理内存分为两个内存池,一部分只用来运行内核,另一部分只用来运行用户进程,因为操作系统为了能正常运行,不能用户进程申请多少内存就分配多少内存,必须给自己预留出足够的内存才行,否则有可能会出现因为物理内存不足导致内核自己都无法正常运行
- 对于所有的任务(包括用户进程, 内核)都有各自4GB虚拟地址空间,因此需要为所有任务都维护自己的虚拟地址池,一个任务一个
- 进程 = 线程 + 资源
- 实现线程与两种方式:在用户空间中实现线程,在内核空间中实现线程:
- 进程或线程状态enum task_structs
- 中断栈 struct intr_stack,用于中断发生时保护程序(线程或进程)的上下文环境
- 线程栈 struct thread_stack:switch_to(线程切换函数)时保存寄存器环境
- 进程或线程的PCB struct task_struct
- 多线程调度:双向链表维护一个就绪队列和一个所有任务队列
-
锁采用信号量实现。在计算机中,信号量就是个0以上的整数值,当为0时表示已无可用信号,或者说条件不再允许,因此它表示某种信号的累积量,故称"信号量"
-
线程同步目的:不管线程如何混杂穿插执行,都不会影响结果的正确性
-
信号量是计数值,使用P(减少) V(增加)操作来表示信号量的减增,增加操作up包括两个微操作:
- 将信号量值加1
- 唤醒在此信号量上等待的线程
减少操作down包括三个子操作:
- 判断信号量是否大于0
- 若信号量大于0,则将信号量减1
- 若信号等于0,当前线程将自己阻塞,以在此信号量上等待
调度器负责挑选"有运行意愿, 准备好运行"的线程上处理器运行,即使再差的调度算法也会保证每个线程都有运行的机会,哪怕只运行几个时钟周期。所以,调度器并不决定线程是否可以运行,只是决定了运行的时机。
线程是否可以运行是由线程自己把控的,当线程被换上处理器运行后,在其时间片内线程将主宰自己的命运。阻塞是一种意愿,表达的是线程运行中发生了一些事情,这些事情通常是由于缺乏了某些运行条件造成的,以至于线程不得不暂时停下来。
因此阻塞发生时在线程自己的运行过程中,是线程自己阻塞自己,并不被谁阻塞。
已被阻塞的线程是无法运行的, 只能由锁的持有者释放
用锁实现终端输出:编写键盘驱动、环形输入缓冲区
- Linux 任务切换未采用Intel的做法, 而是用了一套自己的方法, 只是用了TSS的一小部分功能
- TSS任务状态段是处理器在硬件上原生支持多任务的一种实现方式 ,TSS是每个任务都有的结构,用于一个任务的标识
- TSS是硬件支持的系统数据结构,它和GDT等一样由软件填写其内容,由硬件使用
- 在CPU眼里,任务切换实质就是TR寄存器指向不同的TSS
- 在中断门实现系统调用,效仿Linux用0x80号中断作为系统调用入口
- 在IDT中安装0x80号中断对应的描述符,在该描述符中注册系统调用想对应的中断处理例程
- 建立系统调用子功能表syscall_table,利用eax寄存器中的子功能号在该表中索引相应的处理函数
- 用宏实现用户空间系统调用接口syscall,最大支持3个参数的系统调用,故只需完成syscall[0~3],寄存器传递参数,eax为子功能号,ebx保存第一个参数,ecx保存第二个参数,edx保存第三个参数
1、块是文件系统的读写单位。文件系统将文件以索引结构来组织,为每个文件的所有块建立了一个索引表,包含此索引表的结构称为inode。一个文件对应一个inode,也就是磁盘中有多少个文件就有多少个inode。
2、创建文件的本质是创建inode和目录项,所有文件的inode集中用数组inode_table管理,每个inode编号就是在该数组中的下标。
3、访问文件的本质是通过文件名找到所在的目录项,然后从该目录项中获得 inode 编号,然后用编号到 inode 数组中去找相关的inode,最终找到文件的数据块。
4、super block负责保存文件系统元信息的元信息:inode数组的地址及大小、inode位图地址及大小、根目录的地址和大小、空闲块位图的地址和大小。super block被固定存储在各分区的第二个扇区。
5、在操作系统引导块mbr后面的依次是:超级块、空闲块的位图、inode 位图、inode 数组、根目录、空闲块区域。