操作系统实战45讲-LMOS学习笔记

2022-10-25  本文已影响0人  dajielailin

使用汇编和C实现一个最简单的内核:

PC机的引导流程:

  1. PC机上电后的第一条指令:
    1.1 从PC机的BIOS固件中加载可引导设备中的GRUB程序;
    1.2 GRUB是多启动规范的实现,它允许用户可以在计算机内同时拥有多个操作系统,并在计算机启动时选择希望运行的操作系统;
    1.3 GRUB可用于选择操作系统分区上的不同内核,也可以向这些内核传递参数;
  2. 该指令的内容:
    2.1 负责检测和初始化CPU、内存及主机平台;
    2.2 加载引导设备(大概率是磁盘)中的第一个扇区数据,到0x7c00地址开始的内存空间,然后跳转到该地址处开始执行;
  3. GRUB在启动时会加载一个grub.cfg的文本文件,其中一部分涉及到启动项;
    3.1 刚上电时,PC机的第一条指令执行BOIS中的启动代码,然后加载grub到0x7C00位置,并跳转到grub;此过程全部在实模式下操作,内存空间只有1MB;
    3.2 grub启动操作系统过程中,会切换为保护模式,可访问内存空间扩展到4GB;

内核结构与设计

内核内部组成

  1. 管理CPU;
  2. 管理内存;
  3. 管理硬盘;
  4. 管理显卡;
  5. 管理网卡:
  6. 管理各种IO设备;

经典内核结构:

宏内核结构:

  1. 定义:把管理进程、内存、各种IO等部分的代码全部编成一个大的可执行程序,在处理器的特权模式下运行,对外通过系统API向用户提供软件接口;
  2. 优点:内核中组件间可以相互调用,性能极高;
  3. 缺点:宏内核结构代码高度耦合,没有模块化、没有扩展性和移植性;

微内核结构:

  1. 定义:微内核提倡内核功能尽可能少:既内核近保留进程调度、处理中断、内存空间映射、进程间通信等功能;把实际的内存管理、内存管理、设备管理、文件管理等服务打包成用户进程,微内核通过消息调度相关服务。
  2. 优点:微内核系统结构清晰,具有良好的移植性、可扩展等;
  3. 缺点: 同样一个内存分配请求,微内核下消息通信带来了很大的开销,性能相对较差;

混合内核架构

  1. 定义:本质是宏内核,但是汲取了微内核动态加载文件系统和网络组件的优点,保持了高性能和部分可扩展性;另外通过分离内核硬件层解耦了内核功能依赖于不同的硬件开发平台,操作系统可移植性大大增强。
  2. LMOS操作系统内核结构:
    2.1 内核接口层:定义一套UNIX接口的子集,满足系统API的功能,检查用户参数的合法性;
    2.2 内核功能层:进程管理(创建、销毁、调度进程)、内存管理(页面内存池和任意大小内存池)、中断管理、设备管理(创建设备、销毁设备、访问设备等);
    2.3 内核硬件层:初始化代码、CPU控制、中断处理、物理内存管理(内存空间映射、操作MMU、cache)等硬件平台相关代码;

评论区精彩留言:

  1. 保持中立,务实求真,对比之下,方见真章。

CPU工作模式:执行程序的三种模式

硬件核心:CPU,执行程序的核心部件。

实模式

实地址模式:

  1. 对运行指令的动作不做区分,直接执行指令的真实功能;
  2. 对任何地址不加限制地发往内存。

实模式寄存器:

  1. 通用寄存器:AX、BX、CX、DX、DI、SI、BP;
  2. 段寄存器:CS、DS、ES、SS;——用来存放一个内存段地基地址;
  3. CPU标志寄存器:FLAGS;——用来存放CPU执行运算指令产生的状态位;
  4. 程序指针寄存器:IP;——始终指向下一条指令的地址;
  5. 栈指针寄存器:SP;——始终指向当前栈顶;

实模式下访问内存模型、

实模式下指令和数据地址是如何计算的?
取指令地址:CS左移4位 + IP寄存器
访问内存数据:DS、ES、SS左移4位 + 通用寄存器/SP寄存器;
即:所有内存地址都是由段寄存器左移4位 + 一个通用寄存器的值/常数形成,然后再由该地址访问内存;
——分段内存管理模型。

实模式中断:

实模式下中断过程:先保存CS和IP寄存器,然后装载新的CS和IP寄存器。
硬件中断:中断控制器给CPU发送电子信号,CPU会对整个信号做应答;随后中断控制器会将中断号发送给CPU。
软件中断:CPU执行了”INT 软中断号“指令。
中断向量表:该表的地址和长度由CPU的特定寄存器IDTR指向,表中的一个条目由代码段基地址和段内偏移组成;
中断号 + IDTR寄存器 =》中断向量表中的条目,进而装载进CS(代码段基地址)、IP(代码段内偏移),最终响应中断。

保护模式

相比与实模式,保护模式增加了一些控制寄存器和段寄存器:

  1. CPU控制寄存器:CR0、CR1、CR2、CR3;——控制CPU的功能特性,如开启保护模式;
  2. 系统通过CR0寄存器设置标志而转换成保护模式;
    2.1 开启保护模式:mov ebp cr0; or ebp, 1; mov cr0 ebp; //CR0.P = 1
    2.2 关闭保护模式:mov ebp cr0; and ebp 0xfffffffe; mov cr0 ebp; //CR0.P = 0

保护模式特权级

为了区分哪些指令(如in、out、cli)和哪些资源(如寄存器、IO端口、内存地址)可以被访问,CPU实现了特权级;
特权级分为4级,R0~R3,R0可以执行所有指令,R1、R2、R3依次递减,只能执行上一级指令数量的子集;

保护模式段描述符:

对分段模型内存的保护,实质转化为了对段的保护。
把描述一个段的信息封装成特定格式的段描述符,放在内存中;占8字节,其中[44-45]两位称为DPL,用来表示段描述符权限级别(0-3);
全局描述符表:多个段描述符在内存中组合而成,该表的基地址和长度由CPU和GDTR寄存器指示;

保护模式段选择子:

保护模式下,段寄存器CS、DS等不再存储段基地址,而是内存段描述符索引的信息。
实际上,这里的段描述符索引信息包括了:

  1. 影子寄存器:硬件为降低查表损耗而设计的一个段描述符高速缓存,64位;
  2. 段描述符索引;
  3. TI:描述符索引表,TI = 0时从GDT表中查找,TI= 1时从LDT中查找;
  4. RPL:请求访问这使用的权限级别, 0-3;

当前权限级别CPL:通常由CS和SS中的RPL组成。当CPL <= 目标段的DPL时,表示CPU能访问对应段内存,否则CPU禁止访问。
因此,访问一个内存时,段寄存器(存放段描述符索引)首先会结合GDTR寄存器找到内存中的段描述符,再根据CPL和目标段描述符中的DPL判断能否访问目标段内存。

保护模式中断

中断门描述符:保护模式下需要权限检查以及特权级切换,因此需要扩展中断向量表,即使用中断门描述符替换原来条目中的段基地址和段内偏移;
中断实现:

  1. 产生中断后,CPU首先会检查中断号是否 > 最后一个中断门描述符(0~255),然后检查描述符类型、是否为系统描述符、是否存在内存中;
  2. 检查中断门描述符中的段选择子(段寄存器存储信息)指向的段描述符;
  3. 权限检查:如果CPL <= 中断门的DPL,并且CPL >= 中断门中的段选择子所指向段描述符的DPL;
  4. CPU加载中断门描述符中目标代码段选择子(段描述符)到CS寄存器,把目标代码段偏移加载到EIP寄存器中;

总结起来就是:保护模式下为了实现对中断进行权限检查,实现了中断门描述符(段选择子 + 段内偏移 + DPL权限),如果权限检查通过,则用对应的段选择子和内段内偏移装载CS:EIP寄存器。

长模式

长模式相比保护模式,增加了一些通用寄存器,并且扩展了通用寄存器的位宽到64位。
BOIS引导后,系统直接进入最简单、特权最大的实模式;而后告知CPU切换到保护模式,并运行到ring0。后续的用户进程,一般就在ring3,想执行特权指令需要通过中断,让操作系统切换到内核态ring0进行。

cache与内存

程序局部性原理

CPU大多数时间在执行相同的指令或者与此相邻的指令。
内存才是决定系统整体性能的关键。

Linux的自旋锁和信号量如何实现?

Linux原始自旋锁:

  1. 底层数据结构:使用一个volatile unsigned long lock来表示,0——代表锁未被占用,1——表示被占用;
  2. 自旋锁原理:
    2.1 当某个CPU核心进程请求加锁时,如果锁是未加锁状态,则加锁,然后操作共享资源,最后释放锁;
    2.2 如果锁已经被加锁,则进程并不会转入睡眠状态,而是循环等待该锁,一旦锁被释放,则第一个感知此信息的进程将获得锁;

Linux排队自旋锁:

Linux原始自旋锁会使所有其他进程陷入等待,当自旋锁被释放时,第一个获取该锁的进程是随机的,有失公平性;
改进:排队自旋锁,改进了自旋锁数据结构:

Linux信号量:

  1. 按照value划分位单值信号量和多值信号量;
  2. 作用:
    2.1 信号量 > 0时,所申请的进程可以锁定使用它;
    2.2 信号量 = 0时,说明被其他进程占用,申请的进程要进入睡眠队列,等待被唤醒;
  3. 模拟一次进程获取信号量失败的场景:
    3.1 进程通过down()函数获取信号量:发现此时semaphore.count <= 0,则让当前进程进入睡眠;
    3.2 进入睡眠流程:将当前task的状态设为TASK_UNINTERUPTIBLE, 然后添加到semaphore.wait_list的尾部;
    3.3 通过schedule_timeout()执行进程的调度器,直接调度到其他进程运行,当前进程进入睡眠状态;
    3.4 等到某一个进程执行了up函数释放信号量时,up函数发现semaphore当前等待列表不为空,于是执行唤醒进程操作;
    3.5 up函数将semaphore.wait_list的第一个waiter状态设为唤醒,然后丢给wake_up_process()将进程重新加入调度队列;
    3.6 waiter被重新调度后,程序会回到3.3中schedule_timeout函数的下一行代码处,沿着调用路径返回,最终从down函数中出来,即进程睡醒了;
  4. 单一资源的信号量也可以叫做互斥锁,当资源被加上互斥锁后,其他线程进入睡眠状态。

Linux读写锁:

读写锁定义: (本质是自旋锁的变种:当进程获取锁失败时,陷入循环等待lock计数,而不是直接睡眠)

读写锁也称为共享-独占锁:当读写锁使用读取模式加锁时,它是以共享模式上锁的;当以写入修改模式加锁时,它是以独占模式上锁的;

读写锁性质:

  1. 当共享数据没有锁时,读取和写入的加锁操作都可以满足;
  2. 当共享数据有读锁时,读取加锁操作可以满足,写入的加锁不能满足;
  3. 当共享数据有写锁时,所有的读取的加锁不能满足,所有写入的加锁也不能满足;

读写锁原理:

  1. 读写锁的原理本质是基于计数器,初始值为0x01000000;
  2. 获取读锁时对底层数据lock -1 ,若结果 >= 0,则表示获取成功;因为初始值为2^25比较大,意味着读取加锁一直可以成功;
  3. 如果读取加锁时,先执行lock -1, 如果结果 < 0,则调用read_lock_failed函数:循环测试lock + 1, 直到结果 > 1;(lock初始为0还是读取加锁失败)
  4. 获取写锁时,先对底层数据lock - 0x01000000,如果结果 == 0,则获取写锁成功;否则调用__write_lock_failed函数:循环测试lock + 0x01000000, 直到结果 == 0x01000000;
  5. 获取写锁时,使用lock - RW_LOCK_BIAS_STR:0x01000000, 是因为只有当锁值为初始值时,即没有任何进程持有锁的情况下,才能保证获取写锁是互斥的;

读写锁缺点:

  1. 从读取加锁的原理可知,读并发进程的最大个数就是0x01000000;
  2. 由于读锁和写锁互斥,如果一直有进程读取,lock值一直< 0x01000000, 那么加写锁时会一直失败,会陷入长时间的写饥饿状态,并且一直自旋,浪费CPU资源;

设置工作模式和与环境:建立计算机

搭建操作系统测试环境

内核映像文件

为了让GRUB只加载一个内核文件,可以将二级引导器模块、内核模块、图片和字库文件组装成内核映像文件;

生产虚拟硬盘

  1. 大多数虚拟机都是用文件来模拟硬盘的,即主机系统下特定格式的文件,虚拟机中操作系统的数据都是写入这个文件;
  2. 虚拟硬盘需要格式化才能使用,格式化即在硬盘上建立文件系统,这样成熟操作系统才能在上面存放数据;

转换虚拟格式硬盘

  1. 纯二进制的文件系统只能被Linux系统识别,但是不能被虚拟机识别,需要将原始文件系统转换为VDI格式的虚拟硬盘;
  2. 主机为Windows,启动虚拟机运行ubuntu(包括ssh远程访问),然后在ubuntu里运行Virtualbox启动HelloOs,俗称虚拟机里运行虚拟机,即套娃;
  3. 解决办法,在windows环境中启动virtualbox,然后将从Ubuntu服务器中制作的VDI文件保存到windows下某个路径下,在windows-virtualbox中指定加载VDI虚拟硬盘,最后启动HelloOs;

建造二级引导器

二级引导器

操作系统的先驱,收集机器信息,确认计算机能否运行对应的操作系统,对CPU、内存、显卡进行一些初级配置,放置好内核相关文件;

C语言调用BISO中断

  1. 初始gcc版本支持16位实模式,即C语言可以工作在16位实模式;但是后续的GCC版本全部改成只支持32位了;
  2. 因此,目前的C语言代码是工作在32位保护模式下,而BIOS中断这些汇编代码工作在16位实模式;
  3. C语言环境调用BIOS中断的步骤:
    3.1、保存C语言环境的CPU上下文,即保护模式下的所有通用寄存器、段寄存器、EIP、栈寄存器;
    3.2、切回实模式,调用BIOS中断,保存BIOS中断返回的结果到内存中;
    3.3、切换回保护模式,恢复C语言的CPU上下文;

Cosmos第一个C函数

hal层:硬件抽象层,用来分离内核上层和硬件层细节;

hal_start函数

执行hal层初始化:平台初始化, hal层的内存初始化, 中断初始化;

平台初始化

将二级引导器建立的机器信息结构machbstart_t *smbsp(位于物理地址1MB处)复制到hal层的全局变量kmachbsp中;
初始化图形显示驱动。

初始化内存

从kmachbsp中获取下一个空闲物理内存地址:&kmachbsp->mb_nextwtpadr,用它作为phymmarge_t* pmarge_adr的起始地址;
从&kmachbsp->mb_e820padr获取已分配的物理地址信息e820map_t *e8p,然后将e8p所指向的内存类型和大小拷贝给pmarge_adr;
综合效果就是将kmachbsp中的e820map_t信息结构体扩展为phymmarge_t信息结构体(内容比e820map_t更丰富)。

中断初始化

根据原因的不同,中断分为:异常中断(同步),异步中断;

中断入口处理程序:

保护CPU寄存器,中断发生时进程的上下文;
调用中断处理程序;
回复CPU寄存器,即恢复中断时程序运行的上下文;

中断处理框架:

CPU-》中断们描述符-》异常处理入口程序-》异常分发器/中断分发器-》中断异常描述符;

初始化中断控制器

多个设备的中断信号线都会链接到中断控制器上,他可以决定启用或者屏蔽哪些设备的中断以及设备中断的优先级;

进入内核层:

作为内核层入口init_krl(),它在hal_start()函数中调用;

Linux初始化(上):GRUB与vmlinuz

Linux初始化(下):从_start到第一个进程

解压后内核初始化

  1. 对32系统:setup.bin文件入口_start -> 16位下main函数 -> 切换CPU到保护模式 -> 跳转到vmlinux.bin文件中的startup_32(), 重新加载段描述符;
  2. 对64位系统:先进入startup_64() -> 切换CPU到长模式 ——> 解压Linux内核,并进入内核的startup_64函数;

为何要从_start开始?

  1. vmlinux.bin.gz文件是由Linux内核编译成elf文件,去掉文件的符号信息和重定位信息,压缩得到;
  2. GRUB将vmlinuxz的setup.bin部分读到内存地址——0x90000处,然后跳转到0x90200开始执行:
    2.1 即刚好跳过了前面512字节的bootsector,从_start开始执行;

16位main函数

setup.bin大部分代码都是16位实模式下的。汇编代码_start中调用了main():

  1. 初始化boot_params.hdr变量,初始化堆;
  2. 设置bois_mode,即告诉BOIS我们打算在什么CPU模式下运行它;
  3. 检查物理内存布局、初始化键盘、设置显卡的图形模式;
  4. 进入CPU保护模式:
    4.1 安装切换实模式的函数;
    4.2 开启a20地址线;——为了能访问1MB以上的内存空间;
    4.3 安装中断描述符——setup_idt()和全局描述符表——setup_gdt();
    4.4 保护模式下长跳转到boot_params.hdr.code32_start地址处(改地址定义为0x100000, 对应1MB的地址)
    事实上,GRUB会把vmlinuz中的vmlinux.bin文件放在1MB开始的内存中间中,因此上述main()函数最后跳转进入了vmlinux.bin中;

内存篇

如何划分与组织内存?

分段还是分页?

  1. 从表示方式和状态确定角度,由于页的大小固定,可以使用位图表示页的分配和释放;
  2. 从内存碎片的利用看,段由于长度不固定,更容易产生内存碎片,而碎片页可以通过MMU映射到连续的虚拟页面;
  3. 从内存和硬盘的数据交换效率考虑,内存页和硬盘交换数据,时间比较稳定;
  4. 段最大的问题是使得虚拟内存地址空间,难以实施。

如何表示一个页?

页信息:页状态,页地址,页的分配计数, 页的类型,页的链表;
struct msadsc_t;

内存区

  1. 硬件区:0~0x1FFFFFF(总共32MB),很多外部硬件可以直接和内存交换数据,如DMA,并且它只能访问低于24MB的物理内存;因此需要规定硬件区专门为这些外部硬件分配内存页;
  2. 内核区:0x2000000~0x3FFFFFFFF(), 很多时候内核需要大且连续的物理内存,因此需要有一段连续的物理内存空间映射到内核的虚拟地址空间;
  3. 应用区:0x4FFFFFFFF~0xFFFFFFFFFFFFFFFF, 对应用按需分配,通过缺页出发内存映射;

如何表示一个内存区?

  1. 需要的信息:内存区的开始地址、结束地址,物理页面个数,已分配物理页面,剩余物理页面等;
  2. 数据结构:struct memarea_t;

内存区关联内存页:

  1. memarea_t结构中包含一个内存分割合并memdivmer_t结构;所谓分割即内存分配,合并即内存释放;
  2. memdivmer_t结构中dm_mdmlielst是一个特殊的bafhlst_t类型数组,其中:

使用2的N次方寻址原因?

  1. 内存对齐,提升CPU寻址速度;
  2. 内存分配时,根据需求大小(2^N)可以快速定位从哪个bafhlst_t开始(memdivmer_t数组的索引即为对应的分组);
  3. 将算数运算转换为位操作,内存分配回收时,计算简单;
  4. 内存分配时,并发加锁,分组可以提升效率;

内存管理器-内存区-内存页关联结构

内存区-内存页相关结构体.jpg

内存页面初始化

Cosmos物理内存管理器初始化:

对应cosmos hal层的init_memmgr函数;

内存页结构的初始化

实质就是初始化msadsc_t结构体数组,对应一段不连续的物理内存页;

  1. 从mbsp->mb_e820expadr指向的地址处,扫描phymmarge_t结构体数组,找出所有可用类型物理内存;(可以是不连续的)
  2. 按照4KB大小划分1中找到的所有可用内存,构建连续的msadsc_t数组指向每个物理页,数组起始地址为mbsp->mb_memmappadr;
  3. 如何通过虚拟地址找到对应物理页地址?
    ——msadsc_t.md_phyadrs.paf_padrs = phyadrflgs_t.paf_padrs;

内存区结构初始化

  1. 从mbsp->mb_nextwtpadr地址开始处,划分4 * sizeof(memarea_t)空间用来初始化memarea_t数组;
  1. 其中4个内存区依次为:硬件层,内核层,用户层,共享层;
  2. 通过memarea_t_init()->memdivmer_t_init()->bafhlst_t_init(),保证了每个内存区内包含结构的初始化;
  1. 此时memarea_t.ma_mdmdata.dm_mdmlielst[MDIVMER_ARR_LMAX]中的分配列表af_alclst和释放列表af_frelst还没与上一步初始化的msadsc_t数组联系起来;

处理初始内存占用问题

  1. 要解决的问题:内核本身的执行文件(字体文件、MMU页表、ko等),包括内存页和内存区数据结构都要占用实际的物理内存,需要把这些已经占用的内存页面msadsc_t标记成"已分配",避免二次分配;
  2. 初始化阶段各种数据占用的开始、结束地址和大小都保存在machbstart_t* kmachbsp变量中;
  3. 搜索内核文件/数据所占用的内存页:

内存页与内存区建立联系

确定内存页属于哪个内存区:
  1. 获取msadsc_t结构的首地址和分配内存页个数:
    msadsc_t *mstatp = mbsp->mb_memmappadr + 0xffff800000000000;
    uint_t msanr = (uint_t)mbsp->mb_memmapnr;
  2. 获取memarea_t结构的首地址和分配的内存区个数:
    memarea_t *marea = mbsp->mb_memznpadr + 0xffff800000000000;
    (uint_t)mbsp->mb_memznnr;
  3. 遍历所有内存页[mstatp, mstatp + msanr]:
遍历memarea_t合并其中的msadsc_t
  1. 操作目的:
  1. 遍历每个内存区marea[index], 依次在该内存区中找出每个空闲地址块msadsc_t*: [retmsstartp, retmsendp], 长度为retfindmnr;
  2. 针对找到的每个空闲地址块msadsc_t*: [retmsstartp, retmsendp],在该内存区marea[index]中找到对应连续的bafhlst_t:

内存页管理器bafhp->af_frelst结构如下

页管理器对应可分配对象链表.jpg

内存页的分配和释放

分配内存页期间关闭中断 & 开启自旋锁保护

  1. 关闭中断:内联汇编"CLI"——禁止中断,保护当前代码运行;
    CLI:Clear Interupt
  2. 内核模式下关闭中断不能太久,需要尽快恢复中断——"STI";
    STI:Set Interupt

请求连续页算法

分配连续内存页算法.jpg

释放连续页算法

释放连续内存页算法.jpg
上一篇 下一篇

猜你喜欢

热点阅读