thuOS笔记(一) 第三讲 启动、中断、异常和系统调用
段机制 页机制
通用寄存器 段寄存器 指令指针寄存器 标志寄存器 控制寄存器 系统地址寄存器 调试寄存器
MD→MKDIR→Make Directory
CD→CHDIR→Change Directory
RD→RMDIR→Remove Directory
ls →list
gcc GNU C Compiler
gdb GNU project debugger
make clean 找makefile这个文件来执行clean的命令
退出vim的方法,键入 : 来显示命令输入行,然后键入q!来退出
3.1 BIOS
计算机系统从加电时,从什么地方去读第一条指令?
CPU在完成初始化后,是处于实模式下。实模式下,地址总线并非32位,而是只有20位,因此只能放在最底下1M大小的空间里
约定:在计算机系统CPU初始化完成之后,根据CS和IP 这两个寄存器中的值确定从何处开始第一条指令,通常是PC=0xf fff0,这条指令是跳转指令。
BIOS
- 将加载程序从磁盘的引导扇区(512字节)加载到0x7c00
- 跳转到CS:IP = 0000:7c00
加载程序
- 将操作系统的代码和数据从硬盘加载到内存中
- 跳转到操作系统的起始地址
BIOS系统调用
以中断调用的方式,提供基本的I/O功能:
- INT 10h : 字符显示
- INT 13h : 磁盘扇区读写
- INT 15h : 检测内存大小
- INT 16h : 键盘输入
BIOS只能在x86的实模式下访问,如果操作系统工作在保护模式下,那这些功能就不能使用了
3.2 系统启动流程
对3.1进一步细化
对于现在分区,BIOS需要从主引导扇区读取主引导记录,从哪个文件系统里加载程序。主引导扇区代码读取活动分区的引导扇区代码,引导扇区代码读取文件系统的加载程序。
BIOS——(读取)——>
主引导扇区代码——(读取)——>
活动分区的引导扇区代码——(读取)——>
文件系统的加载程序
CPU初始化:加点稳定后,从0xFFFF0读第一条跳转指令
BIOS初始化:从磁盘读取加载程序
- 硬件自检POST(Power On Self Test)
- 检测系统中内存和显卡等关键部件的存在和工作状态
- 查找并执行显卡等接口卡BIOS,进行设备初始化
- 执行系统BIOS,进行系统检测,检测和配制系统中安装的即插即用设备
- 更新CMOS中扩展系统配制数据ESCD(扩展系统配制数据)如果有硬件变化,这个ESCD就会变化
- 按指定启动顺序从软盘、硬盘或光驱启动
主引导记录MBR格式
446+164+2 = 512
启动代码+硬盘分区表4+结束标志字
512个字节是固定死的,所以最多BIOS-MBR这种方式最多只能有4个分区
有了结束标志字,才认为这是一个合法的主引导记录
活动分区引导扇区格式
如ppt里所述,跳转指令和平台有关。
只要在启动代码中标识出来去哪加载程序即可去任意的地方加载。
加载程序(BootLoader)
系统启动规范
不需要在不同平台单独制定标准,在工业界有系统启动规范。即BIOS是固化到计算机主板上的程序,包括系统设置、自检程序和系统自启动程序
BIOS-MBR、BIOS-GPT(全局唯一标识分区表)、PXE
BIOS-GPT可以描述更多的分区结构,就不会受4的限制
PXE是网络启动的一个标准,机器想要通过局域网或者其他的网络连到服务器上去,从服务器上下载内核镜像来执行。这是需要再BIOS里加网络协议栈。
因为BIOS-MBR 需要从主引导记录到引导记录,所以提出了一个优化的规范——UEFI(同一可扩展固件接口)
它的目的是在所有平台上提供一致的操作系统启动服务。
3.3 中断、异常和系统调用比较
在操作系统内核之上的应用程序,是无法完全信任的。
外设与计算机交互 中断
应用程序处理意想不到的行为 异常
系统调用:应用程序主动向操作系统发出的服务请求
异常:非法指令或者其他原因导致当前指令执行失效
中断和异常处理机制:
硬件执行部分:
- 依据内部或外部事件设置中断标志
- 依据中断向量调用相应的中断服务例程
软件执行部分:
- 现场保存(编译器)
- 中断服务处理(服务例程)
- 清楚中断标记(服务例程)
- 现场恢复(编译器)
区别:
中断嵌套
硬件中断服务例程可以被打断
- 不同硬件中断源可能在硬件中断处理时出现,这时根据优先级高低来判断执行次序
- 硬件中断服务例程中,需要临时禁止中断请求,比如说处理电源的问题时,其他中断都不重要了,需要赶紧先把电源问题解决掉
- 中断请求会保持到CPU做出响应
异常服务例程可以被打断
- 异常服务例程执行时,可能出现硬件的中断。比如说,虚拟存储中,访问到的存储单元的数据不存在,
第三讲习题
3.4 系统调用
标准C库的例子
下图所示,左图表示:应用程序调用 printf()
时,会触发系统调用write()
右图表示:更关心系统调用这个借口的话,把应用程序和库都算到用户态了,进到内核中去,根据write()
编号不同,选择不同的系统调用编号
系统调用
操作系统服务的编程接口
通常由高级语言编写(C或者C++)
程序访问通常是通过高层次的API接口而不是直接进行系统调用
三种最常用的应用程序编程接口(API)
系统调用的实现
每个系统调用对应一个系统调用号,系统调用接口进入到内核体现为一个软中断,然后根据系统调用表来实现系统调用
系统调用和函数调用的不同
INT
和IRET
指令用于系统调用
- 系统调用时,由于内核是受保护的,内核和用户态使用不同的堆栈,也就是堆栈切换和特权级的转换
- 切换之后处于内核态,就可以使用特权指令,然后可以对设备进行控制,这在用户态是不可能进行的
CALL
和RET
用于常规调用
- 把参数压到堆栈里去,然后转到相应的函数去执行,执行时从堆栈获取参数信息执行,然后获得返回结果
- 常规调用时没有堆栈切换
中断、异常和系统调用的开销
系统调用比函数调用更安全,但是如下图所示,所有的这些都会导致,用户态和内核态切换的时候,系统调用开销大于函数调用
第三讲习题
3.5 系统调用示例
文件复制过程中的系统调用序列
涉及的系统调用有:键盘的输入,屏幕的溢出,文件的输入和输出
下图为System call numbers
用户态
在ucore中库函数read()的功能是读文件,调用形式为
user/libs/file.h : int read(int fd, void *buf, int length)
int fd
——文件句柄(读写的文件,读出来的数据放到什么地方)
void *buf
——数据缓冲区指针(缓冲区头指针)
int length
——数据缓冲区长度(这个缓冲区域并不是无限大的)
int return_value
: 返回读出数据长度(实际读出来的,可能要比这个短,因为实际的文件中可能没有这么多数据,读出来最多是缓冲区的长度)
系统调用阶段
ucore系统调用read(fd, buffer,length)
最开始的一段汇编程序叫做alltraps()
1.kern/trap/trapentry.S : alltraps()
这里会获取到中断所需要的相关信息组成的数据结构
2.kern/trap/trap.c : trap()
tf->trapno == T_SYSCALL 系统调用对应的中断向量
3.kern/syscall/syscall.c : syscall()
tf->tf_regs.reg_eax = =SYS_read
就会转到系统调用这个函数里面
4.kern/syscall/syscall.c : sys_read()
从tf->sp获取fd,buf,length
从堆栈里获取之前填进堆栈里的三个参数,这样就会转到相应的系统调用实现里面
5.kern/fs/sysfile.c : sysfile_read()
读取文件,直接操作底下的驱动程序
6.kern/trap/trapentry.S : trapret()
将长度返回给用户态
3.6 ucore+系统调用代码
saferead
-->read
--> sys_read
--> syscall
-->
{_bulltin_va_start
; _bulltin_va_arg
}
syscall
中的asm volatile
是我们需要关注的
num
是系统调用编号
T_SYSCALL
是系统调用中断号
从用户态出发,再往下就跟踪不到了