学堂在线_操作系统_notes_第3-4讲_bootloader启动、中断、异常、系统调用
20220628.No.1823
主机加电后,CPU执行的第一条指令在什么地方?CPU从磁盘上的什么地方读取OS的内容?
CPU加电,电流稳定后,CPU初始化寄存器(代码段寄存器CS当前值 左移4位,再加 指令指针寄存器IP当前值,得到 PC当前值,即第一条指令的内存地址)。CPU加电时,x86-32硬件系统 处于 实模式,地址总线 只有20位 地址空间 可用,即220 Bytes = 210 KB = 1 MB。所以,BIOS启动固件 只能存储 1MB地址空间。
为了从磁盘上读取数据,BIOS启动固件必须提供一些功能:基本I/O程序;系统设置信息(CPU加电时,BIOS里的设置 决定 系统是从磁盘、光盘、或网络启动);开机后硬件自检程序POST;系统自启动程序等。
依靠BIOS启动固件的功能,BIOS能从磁盘(的引导扇区)中把 加载程序bootloader 读取加载到 内存(内存由RAM和ROM组成,无论是否加电,ROM存储的内容都会存在)的ROM中。
然后,BIOS把系统控制权交给 bootloader。bootloader 把OS的代码从磁盘加载到内存,再跳转到OS的起始地址,把控制权交给 OS的Kernel代码。
BIOS系统启动规范固件 的发展
- BIOS-MBR
- BIOS-GPT
- PXE(通过网络从服务器上下载 Kernel镜像 来启动OS)。
UEFI系统启动规范固件(统一可扩展固件接口标准)
为了支持磁盘多分区,可能既要有磁盘上的主引导记录,也要有活动分区里的引导记录。设计这两层引导记录,显得复杂。所以,后来又有了 UEFI系统启动规范固件(统一可扩展固件接口标准)。
OS Kernel 与外界(外设、应用程序)打交道,基本只有3种接口(中断(interrupt)、异常(exception)、系统调用(system call))。
- interrupt:解决 硬件外设连接主机 时可能出现的问题(例如 键盘打字输入的信息需要CPU及时读取)。
- exception:解决 应用软件处理意想不到的行为 时可能出现的问题(例如 做除法时除以0,或内存出错)。
- system call:提供接口,既让应用软件方便得到 OS Kernel服务,又不影响 OS Kernel的安全。系统调用(用OS Kernel的服务)与功能调用(用函数库的服务)有区别。
system call 的三种最常用的应用程序编程接口(API)
- Win32 API 用于 Windows;
- POSIX API 用于 POSIX-based systems (包括UNIX,LINUX,Mac OS X的所有版本);
- Java API 用于JAVA虚拟机(JVM)。
system call 与函数调用的主要区别:
- INT和IRET指令用于system call。为保护OS Kernel,系统调用时,存在 堆栈切换和特权级的转换;
- CALL和RET用于常规调用。常规调用时,没有堆栈切换。
x86-32硬件系统的CPU指令手册:
Intel 64 and IA-32 Architectures Software Developer Manuals
http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html
system call 的时间开销
system call 的时间开销 > 函数调用的时间开销。因为system call 存在 用户态与内核态之间的切换,为了保障OS Kernel的安全性。
system call 示例:
文件复制过程中的系统调用序列
从 源文件 到 目标文件,依次执行——
获取输入文件名
在屏幕显示提示
等待并接收键盘输入
获取输出文件名
在屏幕显示提示
等待并接收键盘输入
打开输入文件
如果文件不存在,出错退出
创建输出文件
如果文件存在,出错退出
循环
读取输入文件
写入输出文件
直到读取结束
关闭输出文件
在屏幕显示完成信息
正常退出
上述操作涉及的 system call 大致有——
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_write 5 // 写
#define SYS_read 6 // 读
#define SYS_close 7 // 关
#define SYS_kill 8
#define SYS_exec 9
#define SYS_open 10 // 开
#define SYS_mknod 11
#define SYS_unlink 12
#define SYS_fstat 13
#define SYS_link 14
#define SYS_mkdir 15
#define SYS_chdir 16
#define SYS_dup 17
#define SYS_getpid 18
#define SYS_sbrk 19
#define SYS_sleep 20
#define SYS_procmem 21
在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()使用示例——
in sfs_filetest1.c: ret = read(fd, data, len);
system call 库接口示例
sfs_filetest1.c: ret=read(fd,data,len);
……
// 向堆栈中压栈:
8029a1: 8b 45 10 mov 0x10(%ebp),%eax
8029a4: 89 44 24 08 mov %eax,0x8(%esp)
8029a8: 8b 45 0c mov 0xc(%ebp),%eax
8029ab: 89 44 24 04 mov %eax,0x4(%esp)
8029af: 8b 45 08 mov 0x8(%ebp),%eax
8029b2: 89 04 24 mov %eax,(%esp)
// 压栈结束后,再函数调用:
8029b5: e8 33 d8 ff ff call 8001ed <read>
// 全部的system call 都是通过 宏 展开形成相应的函数:
syscall(int num, ...) {
...
asm volatile (
"int %1;" // system call 的指令。用户态的函数调用程序执行到此,执行软中断,转成 system call,进入Kermel,转到 汇编程序alltraps()。
: "=a" (ret)
: "i" (T_SYSCALL), // system call 的interrupt向量编号
"a" (num), // read的system call 编号
"d" (a[0]), // 参数
"c" (a[1]), // 参数
"b" (a[2]), // 参数
"D" (a[3]), // 参数
"S" (a[4]) // 参数
: "cc", "memory");
return ret;
ucore系统调用read(fd, buffer, length)
的实现
kern/trap/trapentry.S: alltraps()
// 获得 软中断 所需要的相关信息 组成的数据结构tf。kern/trap/trap.c: trap()
// trap()函数 依据T_SYSCALL,转到 系统调用函数syscall()。
tf->trapno == T_SYSCALL
// T_SYSCALL 是 system call 对应的interrupt向量。kern/syscall/syscall.c: syscall()
tf->tf_regs.reg_eax ==SYS_read
// eax 是 system call 编号。若eax ==SYS_read,说明进入Kernel的是system call,而且这个system call 要调用的功能是read。kern/syscall/syscall.c: sys_read()
从 tf->sp 获取 fd, buf, length // 从 堆栈sp 中获取 read()函数的参数。fd是文件句柄,buf是数据缓冲区头指针,length是数据缓冲区长度。kern/fs/sysfile.c: sysfile_read()
// sysfile_read()函数 读取文件,直接操作底下的驱动程序。
读取文件kern/trap/trapentry.S: trapret()
// trapret()函数 将返回值(读取到的数据长度) 返回给用户态。