文章目录
- 1. 中断和异常处理机制
- 1.1 中断
- 1.2 异常
- 2. 系统调用
- 2.1 标志C库的例子
- 2.2 编程接口
- 3.系统调用的实现
- 4. 程序调用和系统调用的不同处
- 5. 中断、异常和系统调用的开销
1. 中断和异常处理机制
接下来看一看中断和异常的处理过程,看下图就比较清楚,中断和异常都有硬件的处理过程和软件的处理过程,合在一起才能够正确地完成中断和异常的操作系统服务。
关注上图,可以看出,
- 首先产生中断或者异常之后,它需要知道具体这个中断或者异常异应该是由哪个特定的服务例程来服务。这一点是需要判断的,为此需要建立好一个表,表的一侧是中断号或者是异常号,因为一旦每个中断和异常把它编号之后,就很容易区分出来,到底产生的是硬盘的中断,还是键盘的中断,还是鼠标的中断,这很容易区分出来,不同的外设产生的中断,具有一个特定的编号。
- 有编号后,特定的编号就有一个对应的地址,这个地址实际上就是针对这个特定中断的服务例程的地址。有这个地址后,就可以假定,操作系统收到这个中断之后,就可以直接查这个中断表,查到它对应的中断服务例程的那个起始地址,直接转跳到那去执行就 OK 了。
这实际是简单的描述,但为能够让整个系统能够正常地工作,还需要去完成一些更多的事情。
1.1 中断
当产生中断之后,是打断了当前的正常执行,来处理一个更加紧急的外设中断事件。那打断了程序正常执行的话,就需要在硬件和软件方面做出一定的保护,称之为保存恢复机制,有保存恢复才能够让操作系统在完成中断处理之后,能够正常地继续运行。这是中断处理过程中需要注意的具体实现细节。它分两部分来完成:
-
第一部分硬件,首先外设是一个硬件,当它需要让操作系统产生相应的支持之后,那需要产生一个标记,让 CPU 知道,外设产生一个中断标记,CPU 看到这个标记之后,它会得出到底是哪一号中断,为此会产生一个具体的中断号,然后把这个中断号发给操作系统,从而操作系统可以根据中断号找到对应的处理例程,这是硬件要完成的事情。
-
那软件呢?软件就是操作系统,它具体完成什么事情呢?
- 首先,操作系统需要保存被打断的执行现场。
什么叫被打断的执行现场?程序正在执行过程中,突然产生了中断,那首先需要把被打断的这个程序当前执行的一些状态给保证起来,比如它执行什么地方,它执行的寄存器的内容是什么,这些都要保证起来,便于后续恢复,能够让程序从被打断的点继续往下执行,这是操作系统来完成的,保存当前被打断程序的执行的现场。
- 再者,保存完之后,根据 CPU 给的这个中断号,查到对应的中断处理例程的地址,然后跳去执行,那这个过程中,会根据这个外设产生中断的具体情况来完成相应操作。
比如是网卡外设,来了个数据包,中断服务例程就需要把这个数据包取出来做进一步处理,这是中断服务例程要干的事情。
- 接下来,当中断服务例程处理完之后,应该让当前被打断的程序去继续执行,这就需要去恢复刚才保存好的数据,把它恢复回去,从而使应用程序在完全不知情的情况下继续它的执行过程。
这也是为什么中断整个执行过程其实是对应用程序透明的,应用程序完全可以不用感知到有中断产生,这是中断的处理过程。
1.2 异常
异常和中断不太一样
-
异常也是当应用程序执行特定指令之后,这条特定指令触发了一个异常事件,比如除零操作,触发这个异常事件,那么它也会有一个异常的编号。
-
CPU 会得到异常的 ID 号,根据这个 ID 号,操作系统需要保存产生异常的现场,产生异常这条指令的地址以及当前一些寄存器的内容,保存完之后,操作系统根据这个异常的编号去做相应的处理。
-
如果说处理过程是决定这个应用程序退出执行,操作系统就会把这个程序给杀死,让它不在继续执行,这是一种情况。另一种情况也有可能是说,操作系统认为这个程序产生异常是由于操作系统服务不到位,服务应该把产生异常这个现象给弥补好,让程序可以继续执行,这种情况下,操作系统要完成相应的弥补工作,弥补完之后,根据刚才异常产生的现场进行恢复保存的那些值,寄存器保存的地址恢复之后,让应用程序重新执行。
需要注意,这一点和前面的中断和系统调用不一样,是重新执行那条指令,那么这时候在重新执行过程中,由于操作系统已经把产生异常的原因给修补好了。所以应用程序再次执行这条指令就不会再产生异常了,那也意味着程序可以继续地往下执行。
其实在这过程中,它也是对应用程序透明的,应用程序其实根本不知道在执行到某个特定指令时会产生异常,这一点是和刚才中断处理过程是类似的。那这是中断和异常整个处理过程的简要描述。
2. 系统调用
系统调用和中断异常不太一样,系统调用是来源于应用程序需要操作系统提供服务,而服务不能由应用程序直接来执行,必须要操作系统来执行,那么这个过程就需要有一个接口,这个接口称为系统调用接口,那么有这个接口之后,就可以让操作系统来给应用程序提供各种各样的服务。
2.1 标志C库的例子
举个例子,下图可以看到:
应用程序很简单,它是简单的 C 程序,有一条指令叫 printf 打印一个字符串,打印在屏幕上,printf 是一条指令,最终会触发一条系统调用。
触发 write 系统调用,write 会带一些参数,参数包含要让哪个设备来显示这个字符串,以及字符串的内容,很明显需要这两个参数,然后操作系统在获取这个参数之后,去直接访问对应的设备,比如说屏幕,让屏幕把这个字符串给显示出来。
整个处理过程是操作系统来完成,不是应用程序来完成,应用程序只需发出这个请求就 OK 了,当操作系统完成这个请求之后,就会返回一个成功或者失败,当操作系统做完这个处理之后,应用程序能够继续后续的执行工作。
这是一个简单的一个例子,对于其他那些系统调用而言也是大致的同样的处理过程,只是具体完成内容不一样而已。那上图右边这个图可以看出来是一个通用的系统调用接口,有这层接口,应用程序就可以完成各种各样功能,来对整个计算机系统进行间接的控制和管理。
2.2 编程接口
-
为了方便应用程序能够使用操作系统的系统调用接口,有很多定义好的 API,比如用 C 语言或者 C++语言定了很多 API,比如 windows 系统专门有一个 Windows 32的 API,提供这个 API 之后, Windows 应用程序可以用这个 API 来访问 Windows 提供的各种各样的服务。
-
那对我们 UNIX 而言,比如说 Linux,还有其他一些 UNIX 的操作系统,那么它们会有一个标准的 POSIX 的 API,POSIX 意思就是通用可移植的系统调用接口标准,这个标准它会让写好应用程序在只要遵循这个标准的操作系统里可以执行,这样可以实现很好的跨平台的好处,那这是 UNIX 系统里面常见的一种系统调用的API 接口。
-
Java 也提供了很多 API,那些 API 是不是系统调用呢?其实那些 API 不是,那些 API 只是 Java 的虚拟机提供的一些支持,它由库来实现的,最终还是会通过类似于跑在 Windows 环境下 win 32的 API,如果跑在Linux 环境下,会用 POSIX API 来实现相应服务。
那这点是需要注意层次概念,最底层就是一个 WIN32 或者 POSIX API,它定义了操作系统到底能提供哪些系统调用,有不同系统调用之后,还需要了解什么呢?
3.系统调用的实现
既然学习操系统,就有必要去了解操系统怎么去完成这个系统调用
前面讲到它有 interface,应用程序会直接或者是间接地通过库来访问这个系统调用的接口。一旦访问系统调用接口之后,会触发从用户态到内核态的转换。
什么叫用户态?什么叫内核态?
~
用户态是指应用程序在执行的过程中 CPU 所处于执行特权级的状态,特权级特别低,不能够直接访问某些特殊的机器指令和 IO。
~
内核态是指操作系统运行过程中 CPU 所处于状态,在这个状态下,操作系统可以执行任何一条指令,包括特权指令,包括访问 IO 指令,这使得安全性可以得到保障,当然应用程序处于用户态的时候,它无法执行这些特权指令和 IO 指令,它无法完全地控制整个计算机系统。
当操作系统处于内核态的时候,它其实是可以完全控制整个计算机系统,这点是不一样的。
那有了系统调用接口之后,操作系统需要完成一个特殊的转化,就是说当应用程序调用系统调用的时候,会完成从用户态到内核态转换,从而使得控制权从应用程序交到了操作系统来,那么操作系统可以对应用程序发出这些系统调用的参数,系统调用的 ID 号做出标识,做标识之后,就可以被识别,然后完成具体的服务,这是它的处理过程。
4. 程序调用和系统调用的不同处
需要了解到这里面除了特权级转换之外,还需要有一些新的变化,主要是区别说系统调用和传统函数调用有区别的,区别在哪?
-
当应用程序发出函数调用的时候,它其实是在一个栈空间,在一个栈空间完成了参数的传递和参数的返回。
-
但是在系统调用的执行过程中,应用程序和操作系统实际上是拥有各自的堆栈,也就意味着当应用程序发出系统调用之后,当切换到内核里去执行的时候,需要去切换堆栈。同时还需要去完成特权级的转换,从用户态到内核态的转换。
那么这个转换和这个堆栈的切换都会需要一定的开销,也意味着当执行系统调用的时候,那么它带来的开销会比执行函数调用要大很多。 当然这个开销是有它相应的回报,这回报就是安全可靠,这是操作系统为此不得不付出时间上的代价。
系统调用、中断和异常在操系统、应用程序和外设的大致交互过程。这个过程可以看到,它其实跨越操作系统的边界,系统调用、异常和 中断这三者是跨越了操作系统和应用程序,操作系统和外设的边界,跨越边界其实是有一定的代价,这个代价是为了确保让操作系统能够安全可靠、正常的运行,那到底是哪些代价呢?
5. 中断、异常和系统调用的开销
-
操作系统要能够对产生不同的异常、中断和系统调用正常地进行处理,那首先需要有一个对应的映射关系,就是说对应具体的系统调用号,对应具体的中断号,那么它应该由哪个中断例程或者系统调用例程来处理,那么这个映射的表,操作系统在初始的环节就需要把表建好。
-
操作系统有自己的堆栈,不能和应用程序堆栈混为一谈,那这样的话就使得要维护堆栈的开销,需要知道操作系统退出时候需要把这堆栈保存,操作系统执行的时候要把堆栈恢复,同理也是一样,应用程序在退出执行到内核里面来执行之后,需要把应用程序的堆栈做保存,以及最后会做恢复的处理过程。
-
同时操作系统不信任应用程序,它可能会认为应用程序会有恶意的情况存在,所以说它在收到应用程序发出的请求之后,会对参数做检查。这个检查也是需要一定的时间开销,那这个开销主要是安全上的时间开销。
-
操作系统处理完某些数据之后,需要把这个数据从内核态传到用户态,就所谓的拷贝过程,那内存拷贝会有新的开销,它不像应用程序可以简单地使用指针传递的方式来实现。它必须要把内存空间的数据从内核空间拷贝到用户空间,这是拷贝开销。
-
以及随着应用程序的执行,有可能会引起内存状态的改变,这是后续会讲到,所谓页机制的转变。关于 CPU 的 cacheTLB 有可能会被刷新,刷新过程会导致额外的开销。
这些都是在执行中断、异常和系统调用上可能会带来的开销,但是大家要注意这些开销是值得的,也是必须的,因为这些开销的存在来使得操作系统能够保证是在安全可靠的环境中执行。