启动、中断、异常和系统调用

内容摘要

  • 启动
    • 计算机结构概述
    • 计算机内存和硬盘布局
    • 系统启动流程
  • 中断、异常和系统调用
    • 背景(3.3)
    • 中断、异常和系统调用相比较(3.3)
    • 中断和异常处理机制(3.3)
    • 系统调用的概念和实现(3.4)
    • 程序调用与系统调用的不同之处(3.4)
    • 开销(3.4)
    • 系统调用示例

3.1 BIOS

计算机体系结构概述

计算机体系结构概述

启动时计算机内存和磁盘布局

启动时计算机内存和磁盘布局
CPU加电后会对里面寄存器做一个初始化到一个指定的状态,然后去执行第一条指令,第一条指令存储在内存中。内存会分为RAM随机访问存储和ROM只读存储两部分,系统的初始化代码存储在ROM中。
系统CPU完成初始化后,它处于实模式下,在实模式下它的地址计算把段寄存器左移4位,然后加上它的当前指令指针,这两个加在一起作为当前访问第一条指令的位置,还有一条限制是说在加电的时候,它处于实模式,这个时候地址总线并不是像我们现在用到的通常系统是32位,它只有20位的地址可用即用的区域是2的20次方就是1M,所以放的区域就只能放在最底下1M里头的一小块。
这块代码为了从硬盘上读数据,它必须提供相应的服务,如果没有这些服务,是没有办法访问到磁盘设备的,为了做到这件事情,在BIOS里头它需要提供这样的一些功能:
(1)基本的输入输出:完成能够从磁盘上读数据,从键盘上读用户的输入并且可以在显示器上显示相应的输出。
(2)系统的配置信息:由BIOS的设置来决定加电时是从硬盘启动、从网络启动还是说从光盘启动。依据这些设置执行它的启动程序,并且能从硬盘把加载程序和操作系统内容加载到系统当中来。
加载程序的内存地址空间
具体过程:

BIOS

  1. 初始化完成后,将加载程序从磁盘的引导扇区(这个引导扇区长度只有512字节,更长的它没有这个能力在BIOS程序,它不允许能读更多内容)加载到指定的位置(Ox7c00);
  2. 跳转到其中的固定位置(CS:IP=0000:7c00);
  3. 把控制权转到从磁盘上读进来的程序,这里是加载程序;

加载程序

  1. 将操作系统的代码和数据从硬盘加载到内存中;
  2. 跳转到操作系统的起始位置,把控制权交给操作系统,来继续执行操作系统功能;

BIOS可以从磁盘读取加载程序,那么为什么不直接从BIOS里将操作系统的内核映像读取进来而通过加载程序再去读取呢?
说明:由于磁盘是由文件系统的并且文件系统多种多样,不能限制磁盘只使用一种文件系统,而且又不能在BIOS上加上认识所有文件系统的代码。为了增加这种灵活性,在这里就有一个基本约定就是我不需要认识格式也能从里头读到读到第一块,读到这块后会用这块里的加载程序来识别磁盘上的文件系统,认识磁盘上的文件系统后就可以读到操作系统内核的镜像并把它加载到内存当中来。这整个过程就是用加载程序读到操作系统来,这个过程后再把相应的控制权转到读进来的操作系统内核上,之后操作系统就可以运行了。

总结一下BIOS最初存放在内存中的ROM部分,加载程序和操作系统则存放在磁盘中,BIOS首先从磁盘中读取加载程序放入内存中,然后通过内存中的加载程序将磁盘中存储的操作系统也读取到内存中,最终跳转到操作系统在内存中的起始地址,把控制权交给操作系统。

(3)开机自检和系统启动程序

BIOS系统调用

  • BIOS以中断调用的方式提供了基本的I/O功能
    • INT 10h:字符显示
    • INT 13h:磁盘扇区读写
    • INT 15h:检测内存大小
    • INT 16h:键盘输入
  • 只能在x86实模式下访问,如果是在保护模式下上述功能无法使用

3.2 系统启动流程

计算机启动流程

之前所说的加电后去读BIOS,BIOS再去读你的加载程序,加载程序去读内核映像,这个实际上又可以细化下去,因为我们在加载程序的时候,在BIOS里头并不能直接去读加载程序(bootloader)

最早系统里只有一个分区的时候可以直接在分区里找文件系统,然而现在大多数的计算机里头并非只有一个分区,可能会有几个分区,并且每个分区可能会装不同的系统,这时就需要在前边加上一个主引导记录,这个主引导记录的目的就是说明要从哪个文件系统内去读加载程序。有了主引导记录后就进到当前某个分区里面,分区里面又有一个分区的引导扇区,这个活动分区的引导扇区再来加载文件系统的加载程序。这个过程当中实际上我们就需要知道中间这几个部分的格式是什么样子的,如果不知道,那么写出来的程序最终存到磁盘上机器是不能够从里头认识的。

总结一下首先系统加电,BIOS初始化硬件,之后BIOS读取磁盘上最前边的主引导扇区代码(主引导记录),由主引导记录得知我们知道接下来要进入哪个分区,进入分区后主引导记录读取活动分区引导扇区代码,引导扇区再来读取文件系统内的加载程序,层层递进,类似栈的结构。
计算机启动流程

CPU初始化

具体说来有这样的几个过程,首先CPU加电完成它的初始化到一个确定的状态去读第一条指令,我们需要知道CPU初始化之后它的代码段段寄存器和当前指令指针寄存器这两个寄存器的内容,算出它的第一条指令在内存当中的什么地方,这是它计算的依据。
因为它是实模式,所以CS和IP都是16位的,CS左移4位后与IP相加算出当前访问的第一条指令的位置。并且BIOS存放在内存中最底下的1M位置,原因是实模式下地址总线是只有20位。
CPU加电稳定后从0XFFFF0(CS:IP=0xf000:fff0)读第一条指令,第一条指令是跳转指令。
CPU初始化

BIOS初始化过程

BIOS除了从磁盘上读取加载程序,实际上还有很多事情要做。
(1) 硬件自检POST:顾名思义硬件自检是为了检测出硬件的好坏。
(2) 检查系统中内存和显卡等关键部件的是否存在和工作状态。
(3) 查找并执行显卡等接口卡BIOS,进行设备初始化因为这些关键性的接口卡里也有它自己的初始化程序。
(4) 执行系统BIOS,进行系统检测,检测和配置系统中安装的即插即用设备(系统初始化)。比如我想从一个USB接口的光驱里启动系统,如何启动?在这个BIOS里的自检是能够做到系统的自检,检测并配置这些即插即用的设备。
(5) 更新CMOS中的扩展系统配置数据ESCD。上述都做完了后就已经知道系统里连接了哪些硬件,在BIOS里有一个系统配置表(ESCD),就是扩展系统配置数据。通过这个数据就可以知道当前系统里有些什么设备,并且这个数据会随着设备的改变而改变。
(6) 按指定启动顺序从软盘、硬盘或光驱启动。第5步也做完后就将控制权转移到从外部读进来的数据里或读进来的代码里,而这就是按照我们在BIOS里指定的顺序,从软盘、硬盘或者光盘或者指定的其他设备上读进第一块扇区。

主引导记录MBR格式

读进扇区后面临多个分区,这时候就需要主引导记录。主引导记录总共512字节,分为三部分:启动代码、硬盘分区表和结束标志字。

启动代码

启动代码占主引导记录中的446字节,它的主要作用有两点:

  1. 检查分区表正确性。如果分区表是错误的,那么程序时无法正常加载的;
  2. 加载并跳转到磁盘上的引导程序;

硬盘分区表

硬盘分区表则占64字节,硬盘分区表负责描述分区状态和位置,每个分区描述信息占16字节。

结束标志字

结束标志字占剩余的2字节。所有的引导扇区都有一个结束标志,这个结束标是55AA。有了结束标后,它才认为这是一个合法的主引导记录。

分区引导扇区格式

由主引导记录进入分区后同样需要面对分区引导扇区,分区引导扇区由4个部分组成:

  1. 跳转指令:跳转到启动代码。这条跳转指令与平台相关,CPU不同跳转指令不同(我猜是因为汇编语言分为Intel格式和AT&T格式两种)
  2. 文件卷头:文件系统描述信息。
  3. 启动代码:跳转到加载程序。启动代码说明加载程序的位置,加载程序可以放在任意的地方只需启动代码标识出来即可。
  4. 结束标志:55AA,和主引导记录的结束标志字相同。

加载程序(bootloader)

加载程序同样可以细化,主要分成三步:

  1. 从文件系统中读取启动配置信息。加载程序并非直接去加载内核,而是从文件系统中读一个启动配置文件(这时候加载程序是能够认识文件系统的格式的),这个启动配置文件在不同的操作系统里是不一样的,比如Windows和Linux都有自己的格式,这样Windows和Linux都有自己的加载程序的格式。
  2. 可选的操作系统内核列表和加载参数。依据配置信息选择启动的参数,比如是正常启动,还是在安全模式下启动,更或者是在调试状态下启动系统,这些区别都可以读出来。
  3. 依据配置加载指定内核并跳转到内核执行。参数已经选择好后,配置信息导致加载程序在加载内核的时候内核会不一样,依据配置加载内核。

加载程序

虽然整个过程的描述已经细化,但是介绍的仍然是很粗的。如果要想写出实际的程序,那么还需要知道CPU的手册、CPU加电时的状态,BIOS里的规范,第一条指令在磁盘中的位置和它的格式,内核编译时的一些相应信息。有很多需要考虑的因素,这种考虑的因素又有很多细节和实际的硬件环境或者说周围的情况密切相关,这时就需要制定一组相应的标准作为系统启动的规范。

系统启动规范

系统启动规范主要分为BIOS和UEFI两种。

BIOS

BIOS是现在广泛使用的在PC机上的启动流程标准。BIOS是主板上的一段程序,包括系统设置,自检程序和系统自启动程序,它可以完成系统的启动。
BIOS从70年代后期最早出现,至今已有几十年的发展并发生很多变化,主要有BIOS-MBR、BIOS-GPT和PXE三种。之前提到的主引导记录BIOS-MBR实际上相当于最早的BIOS,它是从主板加电自检后进到磁盘上的唯一的一个分区上去加载它的引导记录,然而有了多分区磁盘后就需要选择从哪个分区启动,这时就在前面加上一个主引导记录来说明是选择了哪个分区进行启动。由前文介绍主引导扇区的格式可知,主引导记录里只能描述最多4个分区,每个占16个字节,因为启动代码和结束标志字已经将剩余的448字节全部占满,然而现在的计算机很多都会超过4个分区。为了解决这一问题出现了GPT(全局唯一标识分区表),GPT可以在分区表里描述更多的分区结构,这样就不会有4个分区的限制了。BIOS-MBR和BIOS-GPT是BIOS的两个发展,PXE实际上是网络启动的一个标准,举个例子就是机器启动后想听过局域网或者其他的网络连接服务器,从服务器上下载内核镜像来执行,PXE就是这种启动的标准。
总的来说BIOS可以有一些局部的修改来完善对后续的支持,但这种支持总是会受到前边的制约,比如说在主引导记录里为了支持多分区就在中间加成了磁盘的主引导巨鹿,然后再加上活动分区里的引导记录,多了两层但实际多的这两层意义并不是特别的必要。所以可以设计一种全新的规范来解决这一问题,这就是UEFI。

UEFI

UEFI(统一可扩展固件接口)想达到的目的是在所有平台上提供一致的操作系统启动服务,为了做到这一点它从90年代开始推出它的第一个版本,直到现在都在不断的演变的过程中。
系统启动规范

3.3 中断、异常和系统调用比较

这节介绍了中断、异常和系统调用的作用,是为了解决什么问题,主要的应用场景。以及他们之间的区别和共同点,还介绍了中断、异常和系统调用的实现机制。

背景

之前介绍过计算机启动后会加载操作系统的内核,然后将控制权交给操作系统内核,这一阶段是可以信任的。但在操作系统内核之上,实际还有很多的应用程序,没有办法做到对这些应用程序的完全的信任,然而这些应用程序要使用操作系统内核提供的服务,并且只有操作系统执行特短指令(具有特殊权限的指令,这类指令只用于操作系统或其他系统软件,一般不直接提供给用户使用),这时就需要解决一个操作系统内核与外界打交道的问题,也就是说可以信任的内核必须对外界提供某种访问的接口。

同样在使用计算机的过程中我们除了跟应用程序打交道外,程序或计算机系统在运行过程中会有各种各样的问题,为了能够让计算机系统对外界作出适当的反映比如及时反映键盘的输入,需要提出中断机制,也就是当外设与系统有交互的时候需要如何处理。
还有一种情况是使用应用程序的过程中出现了一些问题,这些问题是程序编写者事先没有预料到的,对于这种异常情况把它的控制权转交给操作系统,由操作系统来处理它,这就是应用程序执行中遇到意外交由异常来做处理。

系统调用则是为了解决用户程序如何来使用系统服务的问题。操作系统需要通过系统调用来提供一个接口,让应用程序既方便的使用内核提供的服务,又不至于用户的行为对内核的安全产生影响。提供服务的方式有多种可以通过内核提供服务,还可以使用函数库,这里需要作出判断。

  • 为什么需要中断、异常和系统调用
    • 在计算机运行中,内核是被信任的第三方
    • 只有内核可以执行特权指令
    • 方便应用程序
  • 中断和异常希望解决的问题
    • 当外设连接计算机时,会出现什么现象?
    • 当应用程序处理意想不到的行为时,会出现什么现象?
  • 系统调用希望解决的问题
    • 用户应用程序时如何得到系统服务?
    • 系统调用和功能调用的不同之处是什么?

内核的进入与退出

内核的进入与退出
从这个图中可以看到操作系统内核和外界打交道基本上就是中断、异常和系统调用这三个接口。

中断、异常和系统调用

系统调用(System call)是应用程序主动向操作系统发出的服务请求。
异常(Exception)则是非法指令或者其他原因导致的指令执行失败(如:内存出错)之后的处理请求。
中断(hardware interrupt)是硬件设备对操作系统提出的处理请求。

中断、异常和系统调用的比较

  • 源头
    • 中断:外设
    • 异常:应用程序意想不到的行为
    • 系统调用:应用程序请求操作提供服务
  • 响应方式
    • 中断:异步
    • 异常:同步。因为异常是与当前指令有关的,必须处理完当前纸条异常所产生指令所导致的问题才可以继续下去。
    • 系统调用:异步或同步
  • 处理机制
    • 中断:持续,对用户应用程序是透明的
    • 异常:杀死或者重新执行意想不到的应用程序指令。异常会处理当前所出现的问题。
    • 系统调用:等待和持续。等待用户提出之后处理,等待然后再继续。

中断处理机制

这节标题中的中断实际上可以理解为系统调用、中断和异常这三种情况的总称。

硬件处理

  • 在CPU初始化时设置中断使能标志。也就是说在许可外界打扰CPU的执行之前CPU是不会对外界的任何中断请求发出响应
    • 依据内部或外部事件设置中断标志
    • 依据中断向量调用相应中断服务例程。

中断产生了后通常是一个电平的上升沿或者说是一个高电平,那CPU会记录下这间事情,也就是说会有一个中断标志表示出现了一个中断,然后这时候需要知道中断是由什么设备产生的,需要知道中断源的编号,这一部分是由硬件来做的。

软件

  • 现场保护(编译器)
  • 中断服务程序(服务例程)
  • 清除中断标记(服务例程)
  • 现场恢复(编译器)

中断嵌套

  • 硬件中断服务例程可被打断
    • 不同硬件中断源可能硬件中断处理时出现
    • 硬件中断服务例程中需要临时禁止中断请求
    • 中断请求会保持到CPU做出响应
  • 异常服务例程可能被打断
    • 异常服务例程执行时可能出现硬件中断
  • 异常服务例程可嵌套
    • 异常服务例程可能出现缺页

3.4 系统调用

系统调用

  • 操作系统服务的编程接口
  • 通常由高级语言编写(C或者C++)
  • 程序访问通常是通过高层次的API接口而不是直接进行系统调用。写程序的时候通常并不直接去使用系统调用而把系统调用封装到一个库里面,应用程序是访问这些库里的库函数来实现的。
  • 不同的系统里用户使用的接口是不一样的,三种最常用的应用程序编程接口(API)
    • Win32 API 用于 Windows
    • POSIX API 用于POSIX-based systems(包括UNIX,LINUX,MAC OS X的所有版本)
    • Java API用于JAVA虚拟机(JVM)

系统调用的实现

  • 每个系统调用对应一个系统调用号
    • 系统调用接口根据系统调用号来维护表的索引
  • 系统调用接口调用内核态中的系统调用功能实现,并返回系统调用的状态和结果
  • 用户不需要知道系统调用的实现
    • 需要设置调用参数和获取返回结果
    • 操作系统接口的细节大部分都隐藏在应用编程接口后

      通过运行程序支持的库来管理

函数调用和系统调用的不同处

调用一个函数需要把参数压到堆栈里面去,然后转到相应函数去执行,执行时候从堆栈里获取参数信息执行,返回的结果放在那里再返回回来,这样在上面的函数调用就知道相关的返回结果,然后利用这个结果继续往下执行。而对于系统调用来说,它由于内核是受保护的,而应用程序是它自己的区域,为了保护内核的实现,这里内核和用户态的应用程序之间使用不同的堆栈,所以在这里会有一个堆栈的切换,切换之后由于处于内核态,就可以使用特权指令,这些特权指令所导致的结果就是这时可以直接对设备进行控制,而这种操作在用户态是不可能进行的。

系统调用使用的是INT和IRET指令用于系统调用,函数调用使用的是CALL和RET指令,这四条指令在指令集是完全不同的。

  • 系统调用
    • INT和IRET指令用于系统调用

      系统调用时,堆栈切换和特权级的转换

  • 函数调用
    • CALL和RET用于常规调用

      常规调用时没有堆栈切换

中断、异常和系统调用的开销

  • 超过函数调用。原因是有一个用户态到内核态的切换
  • 开销:
    • 引导机制
    • 建立内核堆栈
    • 验证参数
    • 内核态映射到用户态的地址空间

      更新页面映射权限

    • 内核态独立地址空间

      TLB