操作系统的结构
- 一共分为四层
- 内核由系统调用接口、系统调用接口下的各个功能模块和硬件抽象层组成
操作系统内核
内核特征
- 并发
- 共享
- 宏观上, 同时访问
- 微观上, 互斥访问
- 虚拟, 多道程序设计
- 异步
- 程序的执行不是一贯到底的, 而是走走停停, 向前推进的速度不可知
- 只要运行环境相同, OS需要保证程序运行的结果相同
内核结构分类
- 单内核
单内核(Monolithic kernel),是个很大的进程。它的内部又能够被分为若干模块(或是层次或其他)。但是在运行的时候,它是个单独的二进制大映象。其模块间的通讯是通过直接调用其他模块中的函数实现的,而不是消息传递。
单内核结构在硬件之上定义了一个高阶的抽象界面,应用一组原语(或者叫系统调用)来实现操作系统的功能,例如进程管理,文件系统,和存储管理等等,这些功能由多个运行在核心态的模块来完成。
尽管每一个模块都是单独地服务这些操作,内核代码是高度集成的,而且难以编写正确。因为所有的模块都在同一个内核空间上运行,一个很小的bug都会使整个系统崩溃。然而,如果开发顺利,单内核结构就可以从运行效率上得到好处。
很多现代的单内核结构内核,如Linux和FreeBSD内核,能够在运行时将模块调入执行,这就可以使扩充内核的功能变得更简单,也可以使内核的核心部分变得更简洁。
单内核结构是非常有吸引力的一种设计,由于在同一个地址空间上实现所有低级操作的系统控制代码的复杂性的效率会比在不同地址空间上实现更高些。 单核结构正趋向于容易被正确设计,所以它的发展会比微内核结构更迅速些。
单内核结构的例子:传统的UNIX内核—-例如伯克利大学发行的版本,Linux内核。
- 微内核
微内核(Microkernelkernel)结构由一个非常简单的硬件抽象层和一组比较关键的原语或系统调用组成,这些原语仅仅包括了建立一个系统必需的几个部分,如线程管理,地址空间和进程间通信等。
用户模块间通信使用消息传递。
微核的目标是将系统服务的实现和系统的基本操作规则分离开来。例如,进程的输入/输出锁定服务可以由运行在微核之外的一个服务组件来提供。这些非常模块化的用户态服务器用于完成操作系统中比较高级的操作,这样的设计使内核中最核心的部分的设计更简单。一个服务组件的失效并不会导致整个系统的崩溃,内核需要做的,仅仅是重新启动这个组件,而不必影响其它的部分。
微内核将许多OS服务放入分离的进程,如文件系统,设备驱动程序,而进程通过消息传递调用OS服务。微内核结构必然是多线程的,第一代微内核,在核心提供了较多的服务,因此被称为’胖微内核’,它的典型代表是MACH。它既是GNU HURD也是APPLE SERVER OS的核心,第二代为微内核只提供最基本的OS服务,典型的OS是QNX,QNX在理论界很有名,被认为是一种先进的OS。
微内核只提供了很小一部分的硬件抽象,大部分功能由一种特殊的用户态程序:服务器来完成。微核经常被用于机器人和医疗器械的嵌入式设计中,因为它的系统的关键部分都处在相互分开的,被保护的存储空间中。这对于单核设计来说是不可能的,就算它采用了运行时加载模块的方式。
微内核的例子:AIX,BeOS,L4微内核系列,Mach中用于GNU Hurd和Mac OS X,Minix,MorphOS,QNX,RadiOS,VSTa。
- 混合内核
混合内核它很像微内核结构,只不过它的的组件更多的在核心态中运行以获得更快的执行速度。
混合内核实质上是微内核,只不过它让一些微核结构运行在用户空间的代码运行在内核空间,这样让内核的运行效率更高些。这是一种妥协做法,设计者参考了微内核结构的系统运行速度不佳的理论。然而后来的实验证明,纯微内核的系统实际上也可以是高效率的。大多数现代操作系统遵循这种设计范畴,微软公司开发的Windows操作系统就是一个很好的例子。另外还有XNU,运行在苹果Mac OS X上的内核,也是一个混合内核。
混合内核的例子: BeOS 内核 ,DragonFly BSD,ReactOS 内核
Windows NT、Windows 2000、Windows XP、Windows Server 2003以及Windows Vista等基于NT技术的操作系统。
- 外内核
外内核系统,也被称为纵向结构操作系统,是一种比较极端的设计方法。
外内核这种内核不提供任何硬件抽象操作,但是允许为内核增加额外的运行库,通过这些运行库应用程序可以直接地或者接近直接地对硬件进行操作。
它的设计理念是让用户程序的设计者来决定硬件接口的设计。外内核本身非常的小,它通常只负责系统保护和系统资源复用相关的服务。
传统的内核设计(包括单核和微核)都对硬件作了抽象,把硬件资源或设备驱动程序都隐藏在硬件抽象层下。比方说,在这些系统中,如果分配一段物理存储,应用程序并不知道它的实际位置。
而外核的目标就是让应用程序直接请求一块特定的物理空间,一块特定的磁盘块等等。系统本身只保证被请求的资源当前是空闲的,应用程序就允许直接存取它。既然外核系统只提供了比较低级的硬件操作,而没有像其他系统一样提供高级的硬件抽象,那么就需要增加额外的运行库支持。这些运行库运行在外核之上,给用户程序提供了完整的功能。
理论上,这种设计可以让各种操作系统运行在一个外核之上,如Windows和Unix。并且设计人员可以根据运行效率调整系统的各部分功能。
虚拟机管理器(VMM)
建立和维护一个管理虚拟机的框架,同时为其他vxd程序(virtual X driver,虚拟设备驱动程序)提供许多重要的服务
VMW负责资源的隔离, 操作系统负责资源的管理
建议工具
- shell命令: ls, cd, rm, pwd, …
- 系统维护工具: apt, git
- 源码阅读与编辑工具: eclipse-CDT, understand, gedit,
vim
- 源码比较工具:
diff
, meld- 开发编译调试工具: gcc, gdb, make
- 硬件模拟器: qemu
用户态与内核态结构
实验内容
注意: 扩展实验的内容尽可能去做, 扩展ucore, 不局限于上面的内容
80836相关知识
四种运行模式
实模式, 保护模式, SMM模式和虚拟8086模式
实模式
- 加电启动后处于实模式
- 软件可访问的物理内存空间不超过1MB, 无法发挥
Intel 80386
以上级别的32位CPU的4GB内存管理能力
保护模式
- 支持内存分页
- 提供了对虚拟内存的良好支持
- 支持多任务
- 支持优先级, 不同的程序可以运行在不同的优先级上; 优先级一共分
0~34
个级别,操作系统运行在最高的优先级0上, 应用程序则运行在比较低的级别上- 配合良好的检查机制后, 既可以在任务间实现数据的安全共享也可以很好地隔离各个任务
逻辑地址, 线性地址, 物理地址 之间的区别
物理内存地址空间
- 处理器提交到总线上用于访问计算机系统中的内存和设备的最终地址, 此地址唯一
线性地址空间
- 在操作系统的虚存管理之下每个运行的应用程序能访问的地址空间
- 每个运行的应用程序认为自己独享整个计算机系统的地址空间, 这样可让多个运行的应用程序之间相互隔离
逻辑地址空间
- 应用程序直接使用的地址空间
地址转换
- 段机制启动, 页机制未启动: 逻辑地址 -> 段机制处理 -> 线性地址 = 物理地址
- 段机制和页机制都启动: 逻辑地址 -> 段机制处理 -> 线性地址 -> 页机制处理 -> 物理地址
寄存器
寄存器种类
8组寄存器
- 通用寄存器
- 段寄存器
- 指令指针寄存器
- 标志寄存器
- 控制寄存器
- 系统地址寄存器, 调试寄存器, 测试寄存器
通用寄存器
- EAX: 累加器
- EBX: 基址寄存器
- ECX: 计数器
- EDX: 数据寄存器
- ESI: 源地址指针寄存器
- EDI: 目的地址指针寄存器
- EBP: 基址指针寄存器
段寄存器
- CS: 代码段(Code Segment)
- DS: 数据段(Data Segment)
- ES: 附加数据段(Extra Segment)
- SS: 堆栈段(Stack Segment)
- FS: 附加段
- GS: 附加段
指令寄存器和标志寄存器
EIP(指令寄存器)
- EIP的低16位就是8086的IP, 存储下一条要执行指令的内存地址
- 在分段地址转换中, 表示指令的段内偏移地址
EFLAGS(标志寄存器)
- IF(Interrupt Flag): 中断允许标志位, 由CLI和STI两条指令来控制; 设置IF使CPU可识别外部(可屏蔽)中断请求; 复位IF禁止中断; IF对不可屏蔽外部中断和故障中断的识别没有任何作用
- CF, PF, ZF, …
ucore中用到的一些(通用)数据结构
传统的双向循环链表及其缺点
1 | typedef struct foo{ |
缺点: 需要为每种特定数据结构类型定义针对这个数据结构的特定链表插入, 删除等各种操作, 会导致代码冗余
ucore中使用的双向循环链表
1 | // 链表操作函数 |
计算机启动流程
注意:
内存可写与不可写
内存逻辑上统一, 物理上分割
BIOS(Base Input Output System)
以中断方式提供了基本的I/O功能
- INT 10h: 字符显示
- INT 13h: 磁盘扇区读写
- INT 15h: 检测内存大小
- INT 16h: 键盘输入
只能在x86的实模式下访问
CPU初始化
- CPU加电稳定后从0XFFFF0读第一条指令
- CS:IP = 0xf000:fff0
- 第一条指令是跳转指令
- CPU初始状态为16位实模式
- CS:IP是16位寄存器
- 指令指针PC=16*CS+IP
- 最大地址空间是1MB(2^20B)
BIOS初始化
- 硬件自检POST
- 检测系统中内存和显卡等关键部件的存在和工作状态
- 查找并执行显卡等接口卡BIOS, 进行设备初始化
- 执行系统BIOS, 进行系统检测
- 检测和配置系统中安装的即插即用设备
- 更新CMOS中的扩展系统配置数据ESCD
- 按指定启动顺序从软盘, 硬盘或光驱启动
BIOS启动固件
BIOS启动固件的内容
- 基本输入输出的程序(磁盘, 键盘, …)
- 系统设置信息(启动位置, …)
- 开机后自检程序
- 系统自启动程序等
Q: 为什么不直接从BIOS里头把操作系统的内核映像读进来?
A: 磁盘上有文件系统(多种多样), 不能在机器刚出厂的时候就限制死磁盘文件系统的格式, 同时也不能在BIOS中加上认识所有文件系统代码. 有一个基本约定, 不需要认识格式, 也能从磁盘中读第一块(加载程序).
系统启动规范
BIOS
固化到计算机主板上的程序
包括系统设置, 自检程序和系统自启动程序
BIOS-MBR(多分区需求, 主引导记录总共512字节, 只能描述最多4个分区, 每个占16字节), BIOS-GPT(全局唯一标识分区表, 可以描述更多的分区结构), PXE(网络启动, 加网络协议栈)
UEFI
统一可扩展固件借口
- 接口标准
- 在所有平台上一致的操作系统启动服务
增加可信启动流程
BIOS起来后, 读磁盘时, 对引导记录的可信性进行一个检查, 引导记录里有签名.
主引导记录MBR格式
分区引导扇区格式
加载程序
中断, 异常和系统调用
为什么需要系统调用
- 在计算机运行中, 内核是被信任的第三方
- 只有内核可以执行特权指令
- 方便应用程序
为什么需要中断和异常
- 当外设连接计算机时, 会出现什么现象?
- 当应用程序处理意想不到的行为时, 会出现什么现象?
系统调用希望解决的问题
- 用户应用程序是如何得到系统服务的?
- 系统调用和功能调用的不同之处是什么?
内核的进入与退出
三者的区别
- 系统调用(system call)
- 应用程序主动向操作系统发出的服务请求
- 异常(exception)
- 非法指令或者其他原因导致当前指令执行失败(如内存出错)后的处理请求
- 中断(hardware interrupt)
- 来自硬件设备的处理请求
中断和异常处理机制
硬件处理
在CPU初始化时设置中断使能标志
- 依据内部或外部事件设置中断标志
- 依据中断向量调用相应中断服务例程
软件处理
- 现场保存(编译器)
- 中断服务处理(服务例程)
- 清除中断标记(服务例程)
- 现场恢复(编译器)
中断嵌套
- 硬件中断服务例程可被打断
- 不同硬件中断源可能在硬件中断处理时出现
- 硬件中断服务例程中需要临时禁止中断请求
- 中断请求会保持到CPU做出响应
- 异常服务例程可被打断
- 异常服务例程执行时可能出现硬件中断
- 异常服务例程可嵌套
- 异常服务例程可能出现缺页
系统调用
概念
- 操作系统服务的编程接口
- 通常由高级语言编写(C或者C++)
- 程序访问通常是通过高层次的API接口而不是直接进行系统调用
- 三种最常用的应用程序编程接口(API)
- Win32 API用于Windows
- POSIX API用于POSIX-based system(包括UNIX, LINUX, Mac OS X的所有版本)
- Java API用于JAVA虚拟机(JVM)
实现
- 每个系统调用对应一个系统调用号
系统调用接口根据系统调用号来维护表的索引
- 系统调用接口调用内核态中的系统调用功能实现, 并返回系统调用的状态和结果
- 用户不需要知道系统调用的实现
需要设置调用参数和获取返回结果
操作系统接口的细节大部分都隐藏在应用编程接口后(通过运行程序支持的库来管理)
示例
文件复制过程中的系统调用序列
源文件———————————————>目标文件
- 获取输入文件名
- 在屏幕显示提示
- 等待并接收键盘输入
- 获取输出文件名
- 在屏幕显示提示
- 等待并接收键盘输入
- 打开输入文件
- 如果文件不存在, 出错退出
- 创建输出文件
- 如果文件存在, 出错退出
- 循环
- 读取输入文件
- 写入输出文件
- 直到读取结束
- 关闭输出文件
- 在屏幕显示完成信息
- 正常退出
1 |
read()
在ucore中库函数read()的功能是读文件
user/libs/file.h:
int read(int fd, void *buf, int length)
库函数read()的参数和返回值
int fd –> 文件句柄
void *buf –> 数据缓冲区指针
int length –> 数据缓冲区长度
int return_value: 返回读出数据长度库函数read()使用示例
int sfs_filetest1.c:
ret = read(fd, data, len);
系统调用库接口示例
ucore系统调用read(fd, buffer, length)的实现
- kern/trap/trapentry.S: alltraps()
用户态一个int进到内核, 这是一个软中断, 所有这些到这段汇编程序, 获取到中断所需的相关信息组成的数据结构(tf)
- kern/trap/trap.c: trap()
tf->trapno == T_SYSCALL
T_SYSCALL: 系统调用对应的中断向量
- kern/syscall/syscall.c: syscall()
tf->tf_regs.reg_eax == SYS_read
reg_eax: 系统调用编号
- kern/syscall/syscall.c: sys_read()
从tf->sp 获取fd, buf, length
参数(从用户态转到内核态)
- kern/fs/sysfile.c: sysfile_read()
读取文件
实现直接操作底下的驱动程序
- kern/trap/trapentry.S: trapret()
ireturn: 将读到的内容的长度返回给用户态
函数调用与系统调用的不同
系统调用
INT和IRET指令用于系统调用
有堆栈切换和特权级的转换
函数调用
CALL和RET用于常规调用
没有堆栈切换
注意
由于系统调用的量很大, 系统调用中断向量表之后总共占用一个中断编号
不同的系统调用功能, 用系统调用表来实现, 从而根据系统调用功能的不同, 选择不同的系统调用实现
所有的系统调用都是通过一个宏展开形成相应的函数
异常
OS发生异常时
- 有可能帮你解决问题, 如内存访问错误, 通过虚拟内存的方式, 从而执行下一跳指令
- 或是终止应用程序, 操作系统收回资源, 如除零发生时
中断, 异常和系统调用的开销
超过函数调用
开销
- 引导机制
- 切换用户态和内核态
- 建立内核堆栈
- 验证参数
- 内核态映射到用户态的地址空间, 更新页面映射权限
- 切换到内核态时, 访问代码有切换, 内核需要访问用户态的一些信息, 这些映射会导致缓存有变化
- 内核态独立地址空间, TLB
- Translation Lookaside Buffer
- 转换检测缓冲区是一个内存管理单元, 用于改进虚拟地址到物理地址转换速度的缓存
- 会有失效
x86启动顺序
寄存器初始值
第一条指令
CS = F00H, EIP = 0000FFF0H
实际地址是:
Base + EIP = FFFF0000H + 0000FFF0H = FFFFFFF0H
这是BIOS的EPROM(Erasable Programmable Read Only Memory)所在地
当CS被新值加载, 则地址转换规则将开始起作用
通常第一条指令是一条长跳转指令(这样CS和EIP都会更新)到BIOS代码中执行
处于实模式的段
从BIOS到Bootloader
BIOS加载存储设备(比如软盘, 硬盘, 光盘, USB盘)上的第一个扇区(主引导扇区, Master Boot Record, or MBR)的512字节到内存的0x7c00 …
然后跳转到
@ 0x7c00
的第一条指令开始执行
从bootloader到OS
bootloader做的事
使能保护模式(protection mode) & 段机制(segment-level protection)使能保护模式, bootloader/OS要设置CR0的bit 0 (PE)
段机制在保护模式下是自动使能的
从硬盘上读取kernel in ELF
格式的ucore kernel
(跟在MBR后面的扇区)并放到内存中固定位置
跳转到ucore OS的入口点(entry point)执行, 这时控制权到了ucore OS中
段机制
说明
GDT: 全局描述符表(段表), 由bootloader建立
gdtdesc: 段描述符
index: GDT中的索引
RPL: 段的优先级(0, 1, 2, 3)
GDTR: 段表寄存器
加载ELF格式的ucore OS kernel
1 | struct elfhdrd{ |
C函数调用的实现
- 理解C函数调用在汇编级是如何实现的
- 理解如何在汇编级代码中调用C函数
- 理解基于EBP寄存器的函数调用栈
其他需要注意事项
参数(parameters) & 函数返回值(return values)可通过寄存器或位于内存中的栈来传递
不需要保存/回复(save/restore)所有寄存器
GCC内联汇编
什么是内联汇编?
- 这时GCC对C语言的扩张
- 可直接在C语句中插入汇编指令
有何用处?- 调用C语言不支持的指令
- 用汇编在C语言中手动优化
如何工作?- 用给定的模板和约束来生成汇编指令
- 在C函数内形成汇编源码
x86中的中断处理
中断源
- 中断 Interrupts
外部中断 External(hardware generated) interrupts
串口, 硬盘, 网卡, 时钟, …
软件产生的中断 Software generated interrupts
The INT n 指令, 通常用于系统调用
- 异常 Exceptions
程序错误
软件产生的异常 Software generated exceptions
INTO, INT 3 and BOUND
机器检查出的异常S
确定中断服务例程(ISR)
切换到中断服务例程(ISR)
不同特权级的中断切换对堆栈的影响
从中断服务例程(ISR)返回
系统调用
- 用户程序通过系统调用访问OS内核服务
- 如何实现
需要指定中断号
使用Trap, 也称Software generated interrupt
或使用特殊指令(SYSENTER/SYSEXIT)
关键习题
- 批处理的主要缺点是()
- 效率低
- 失去了交互性
- 失去了并行性
- 以上都不是
- 多道批处理系统主要考虑的是()
- 交互性
- 及时性
- 系统效率
- 吞吐量
说明: 在单道批处理系统中, 内存中仅有一道作业. 无法充分利用系统中的所有资源, 致使系统性能较差. 为了进一步提高资源的利用率和系统吞吐量, 由此形成了多道批处理系统.
- 下列选项中, 不可能在用户态发生的事件是()
- 系统调用
- 外部中断
- 进程切换
- 缺页
说明:
系统调用是提供给应用程序使用的, 由用户态发出, 进入内核态执行; 外部中断随时可能发生; 应用程序执行时可能发生缺页; 进程切换完全由内核来控制.
- 中断处理和子程序调用都需要压榨你以保护现场. 中断处理一定会保存而子程序调用不需要保存其内容的是()
- 程序计数器
- 程序状态字寄存器
- 通用数据寄存器
- 通用地址寄存器
- CPU执行操作系统代码的时候称为处理机处于()
- 自由态
- 目态
- 管态
- 就绪态
说明: 内核态又称为管态