计算机体系结构概述
计算机的结构可以简化为上图。上图中内存分为ROM(只读存储器)和RAM(随机存储器)。系统初始化代码从ROM里面读取并开始执行。
电脑加电的时候会去执行BIOS部分。
约定加电的时候,CPU完成初始化之后从上图中地址开始执行代码。注意BIOS这部分大小是在1MB左右的,因为此时地址空间只有20位,所以(2^{20})bit即1MB可以用。
BIOS部分需要提供的功能如上图所示,基本输入输出用于从磁盘上读取/写数据、从键盘上读输入、在显示器显示输出等。系统设置的例子有从哪个盘启动,或者是不是从网络启动。
最后根据配置加载程序和操作系统内容。具体过程是:
之所以不能从BIOS里面直接读取系统的内核映像,是因为要先确定了磁盘上的文件系统(市面上的文件系统有很多种)才可以读取。我们是直接预先约定好不需要知道文件系统的类型,就可以直接读取第一块数据,然后根据这些数据来识别磁盘上的文件系统,最后读取磁盘上的操作系统的内核映像并加载到内存。
最后提一下BIOS提供的功能和限制:
注意,在进入保护模式之后,即离开了实模式之后就不能使用BIOS了,也就没办法使用BIOS提供的功能,这时候如果需要使用这些功能就要操作系统自己想办法实现。
系统启动流程
要找主引导引导记录来去确定从哪个文件系统里面去读取加载程序,因为可能不只是有一个分区,不同分区使用的可能不是同一种文件系统。在确定主引导扇区之后就可以确定去哪个活动分区读取程序了。
自检是为了确定关键的几个硬件正常工作。系统检测主要是确定有没有系统存在,例如从U盘中启动系统(WinToGo)前就会先检测一下有没有系统在你的U盘里。最后会从指定的软盘、硬盘或者光驱读取第一块扇区。
读进来之后就要读取主引导记录:
读取完之后就会跳到活动分区的引导扇区上:
JMP部分与平台相关,不同平台不相同。启动代码来确定程序存放在哪,可以改动,程序的位置也可以改动。
启动配置文件的格式由系统决定。
BIOS已经有相应的标准,在写代码的时候需要按照标准来写(这样就不需要再每一种平台上都要实现不同的BIOS)。MBR是最早的,主引导记录只能最多描述4个分区,每个占16字节,总长512字节,但是现在计算机往往要用到4个以上的分区,所以就提出了GPT(全局唯一标识分区表),这样就可以不受只能描述4个分区的限制。PXE是网络启动的标准。UEFI还提供了对磁盘签名的认证,如果签名不对,那么会拒绝继续读取磁盘上的内容。
中断、异常和系统调用比较
为了给程序提供服务,同时不让程序执行特定的操作(安全问题)。关于图中的问题:外设连接计算机时,为了让系统能够对外设的输入做出适当的反应,就需要用到中断(用轮询的话太耗资源)。程序出错的时候也需要有相应的措施来应对这种意外情况。通过系统调用来提供接口,这样就可以在提供服务的前提下不至于导致出现安全问题。不同在于系统调用会有移植性的问题,因为不同系统会有不同的调用函数,速度也有一定差别,通常系统调用比功能调用快,还有一些别的,可以看这里。
可以看到程序和内核交流基本上就围绕着中断、异常、系统调用。
三者的区别:
必须要有使能,否则无法使用中断(之所以设置使能中断的功能是因为有些时候系统要执行一些必须一次性完成的操作,这时候不能够去响应中断,所以就需要暂时关闭中断功能)。
系统调用
系统调用是操作系统对上提供服务的接口。
上图为C程序的例子。
上图来自Daniel King的博客。
程序调用系统调用的时候首先通过中断进入到系统内核,然后转到系统调用表,这时候通过中断进来的系统调用的编号会被用来在系统调用表里面查对应的系统调用实现,得到结果之后返回去给程序。
两种调用使用的堆栈不同。
系统调用的开销如上图所示。
系统调用示例
上图左侧是要实现的程序(例子),右侧红色的就是用到的系统调用。
注意,返回读出数据长度必然小于等于缓冲区长度。
上半部分主要是压相关参数入栈并调用函数,下半部分是函数调用对应的汇编代码,会经过宏展开形成相应的函数。T_SYSCAL
前的i
实际上是系统调用的中段向量编号,num
就是系统调用read
系统调用的编号,然后后面就是相应的参数。执行到int %1
的时候会转成系统调用进入到内核去。
alltraps()
会获取到中断相关信息组成的数据结构,T_SYSCAL
是系统调用对应的中断向量。trap()
函数会转到系统调用的函数syscall()
里面,在函数里面会读取对应的EAX,即系统调用编号(编号3那部分)。第四部分用来获取文件、缓冲区、头指针(即一开始填进来的参数,此时已经从用户态转变到了内核态)。最后第五部分,在这个函数里面完成相应的文件读写功能,这个函数直接操作底下的驱动。最后第六部分,会将读取到的内容长度返回给用户态。
基本就这些,但是如果可以的话建议亲自去看uCore的代码,来看具体是怎么实现的。