我是荔园微风,作为一名在IT界整整25年的老兵,今天我们来重新审视一下Windows这个我们熟悉的不能再熟悉的系统。
我们每天都在用Windows操作系统,但是其实我们每天直接在打交道的并不是Windows操作系统的内核,而是Windows操作系统的人机交互界面,这个界面其实只是Windows操作系统的一个组件,在Linux上,我们使用Linux系统所使用的界面则只是Linux系统上的一个程序。所以说,我们接触的并不是这些系统的内核。久而久之,我们基本会慢慢把界面里所展示的直观的东西误认为是Windows操作系统本身,其实这是不对的。
我们来重新审视Windows操作系统,看看Windows操作系统本身典型特征和主要功能。由Windows操作系统的功能,可以知道Windows操作系统提供的服务有下面这些:提供了一个用以执行程序的环境,提供的服务有程序执行、I/O操作、文件操作、资源分配与保护、错误检测与排除等。
Windows操作系统借用了多道程序设计的理念,所谓多道程序设计技术就是指在内存中同时存放两道或两道以上的作业,这些作业同时处于运行状态,且它们在管理程序控制下,相互穿插运行。这些作业共享处理器、外设以及其他资源。
Windows操作系统的中断处理程序只能是操作系统程序,不可能是应用程序。中断处理属于系统中会对系统产生重大影响的动作,因此只允许核心态程序执行。而应用程序通常指用户程序,运行在用户态下,不能进行这些操作。
Windows操作系统的系统调用按功能可分为6类,包括进程管理、文件操作、设备管理、主存管理、进程通信和信息维护。
如果Windows程序正在试图读取某个磁盘的第100个逻辑块,则使用Windows操作系统提供的系统调用。本文所需要的接口就属于文件操作相关的调用。
在多道程序设计下,宏观上进程是同时运行的,但是在微观上,单处理器某时刻只能处理一个进程,所以进程与进程之间不能并行执行。处理器、通道、设备都能并行执行,比如同时打印(设备)、计算(处理器)、传输数据(通道控制内存与外存间数据交换)。这里要注意区别并发与并行的含义,并行性是指两个或多个事件在同一时刻发生,而并发性是指两个或多个事件在同一时间间隔内发生。虽然同时刻只能处理一个进程,但多个进程可以并发执行。
实际上,Windows操作系统在系统调用、中断、库函数、原语这些概念中,只有系统调用是操作系统提供的接口。系统调用是能完成特定功能的子程序,当应用程序要求操作系统提供某种服务时,便调用具有相应功能的系统调用。
库函数则是高级语言中提供的与系统调用对应的函数(也有些库函数与系统调用无关),目的是隐藏指令的细节,使系统调用更为方便抽象。但要注意,库函数属于用户程序而非系调用,是系统调用的上层;中断及原语是计算机系统底层的基础功能,属于系统调用的下层。
系统提供封装好的系统调用供应用程序使用,应用程序无须考虑系统底层的内容,仅考虑上层的操作即可;中断是系统内部对于事件响应的机制,对于应用程序来说是透明的,不会提供给应用程序直接使用;同样库函数和原语都是面对操作系统底层的,不会直接提供给应用程序。
缺页处理与时钟中断都属中断,会对系统造成影响,因此只能在核心态执行。进程调度属于系统的一部分,也只能在核心态执行。命令解释程序属于命令接口,是操作系统提供给用户所使用的接口,因此可以在用户态执行。
CPU状态分为核心态和用户态,核心态又称特权状态、系统态或管态。通常,操作系统在管态下运行,CPU在管态下可以执行指令系统的全集。用户态又称常态或目态,机器处于用户态时,程序只能执行非特权指令,用户程序只能在用户态下运行。CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序不能使用。
Windows操作系统常见的特权指令有以下几种:
(1)有关对I/O设备使用的指令,如启动I/O设备指令、测试I/O设备工作状态和控制I/O设备动作的指令等。
(2)有关访问程序状态的指令,如对程序状态字(PSW)的指令等。
(3)存取特殊寄存器指令,如存取中断寄存器、时钟寄存器等指令。
(4)其他特权指令。
本文中缺页处理程序、时钟中断处理程序都是要修改中断寄存器,进程调度程序要修改程序状态字。
对于Windows操作系统的系统调用、外部中断、进程切换、缺页这几种中,一般来讲不可能发生在用户态的是进程切换。判断能否在用户态执行的关键在于事件是否会执行特权指令。
首先看系统调用,系统调用是系统提供给用户程序调用内核函数的,当用户程序执行系统调用时,会使CPU状态从用户态切换至核心态并执行内核函数,执行结束之后将控制权还给用户程序,并且CPU状态从核心态切换至用户态。从这个过程可以看出,虽然系统调用的执行过程中CPU需要切换至核心态,但系统调用(或者引用、调用)是在用户态发生的,是系统特意为用户态设计的,因此系统调用可以发生在用户态。
外部中断,大家一听到“中断”很容易认为涉及中断的都应该是核心态的事情,而不能在用户态执行,这样理解是不对的!中断在系统中经常发生,如键盘输入会引发外部中断(外部中断是指由外部事件引起的中断,比如单击鼠标和键盘输入等操作引起的中断),进程缺页会产生缺页中断等,这些都经常发生在用户进程中,自然这些也都是用户态的事件。以键盘输入举例,一个用户进程需要用户输入一串命令,当用户用键盘输入时会引发外部中断(此时CPU还是用户态),此时系统会切换至核心态执行中断处理程序(这时CPU转变为核心态),处理程序处理之后将输入结果返回给用户程序并将CPU状态切换为用户态,中断处理结束。由此过程可见,中断的发生和处理与系统调用类似,都是发生在用户态,通过切换至核心态完成对应功能,然后返回至用户态。系统调用和中断的发生是在用户态,处理是在核心态。
再来看缺页,与外部中断类似,用户态执行进程缺页时会产生缺页中断(中断发生在用户态),然后系统转入核心态进行缺页中断处理,再返回用户态,将控制权交还给用户进程。因此缺页也可以发生在用户态。
那为什么进程切换不能发生在用户态。进程切换实际上是对于程序状态的修改,因此要修改程序状态字,这是特权指令,必须在核心态执行。
大家是否理解我上面说的内容?
下面来看一下计算例子。一个多道批处理系统中仅有P1和P2两个作业,P2比P1晚5ms到达。他们的计算和I/O操作顺序如下:
P1:计算 60ms,I/O 80ms,计算 20ms。 P2:计算 120ms,I/O 40ms,计算 40ms。
若不考虑调度和切换时间,则完成两个作业需要的时间最少是多少呢?我们来画一下图:
经过图这么一画,就知道完成两个作业需要的时间最少是260ms。
Windows操作系统中断的发生通常是突然的,如地址越界等,往往是系统无法预知的(外部输入中断也是无法预知的,计算机不会知道用户什么时候用键盘输入)。当系统发生中断时要转入中断处理程序,处理完之后要返回到发生中断时的指令处继续执行,由于处理中断时CPU可能会切换状态(如果在核心态发生中断则始终为核心态,不需要切换),因此中断处理返回时就需要还原当时的程序状态,包括处理器信息等,这就用到了程序状态字寄存器所存储的内容。程序状态字寄存器用于记录当前处理器的状态和控制指令的执行顺序,并且保留和指示与运行程序有关的各种信息,其主要作用是实现程序状态的保护和恢复,所以中断处理时一定要将PSW压栈保存。子程序调用是系统能够预知的,而且子程序调用通常是在进程内部执行,不会更改程序状态,即便更改程序状态,只要更新寄存器就行,而不需要保存,因为一切都是系统预料到的,不需要保护和恢复,所以,子程序调用主要保存局部参数信息等,不需要将程序状态字压栈。
所以,Windows操作系统中断处理和子程序调用都需要压栈以保护现场。中断处理一定会保存而子程序调用不需要保存其内容的是程序状态字寄存器。中断处理和子程序调用对程序计数器、通用数据寄存器、通用地址寄存器的操作都是相同的。
用户平时开机时首先启动的是存于主板上ROM中的BIOS程序(它被固化在主板的ROM芯片上,保存着计算机最基本的输入/输出程序、开机后自检程序和系统自启动程序,其主要功能是为计算机提供最底层的、最直接的硬件设置和控制),其次再由它去调用硬盘中的Windows系统,将操作系统的程序自动加载到内存中的系统区,这段区域是RAM。
当Windows操作系统一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(内核态)。此时,处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态),即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序时突然被中断程序中断,此时用户程序也可以象征性地称为处于进程的内核态,因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似:
(1)用系统调用时进入核心态。Linux对硬件的操作只能在核心态,这可以通过写驱动程序来控制。在用户态操作硬件会造成core dump。
(2)要注意区分系统调用和一般的函数。系统调用由内核提供,如read()、write()、open()等。而一般的函数由软件包中的函数库提供,如sin()、cos()等。在语法上两者没有区别。
(3)一般情况下,系统调用运行在核心态,函数运行在用户态。但也有一些函数在内部使用了系统调用(如fopen()),这样的函数在调用系统调用时进入核心态,其他时候运行在用户态。
经过上面的说明内容,我们再来看一些具体情况:
比如整数除以零,会引发中断,会进入内核态。比如sin()函数调用,是由软件包中的函数库提供,在用户态下即可执行。比如系统调用,肯定需要进入内核态。
trap指令、跳转指令和压栈指令均可以在用户态执行,其中trap指令负责由用户态转换成为内核态。而关中断指令为特权指令,必须在核心态才能执行。外部中断处理过程,程序计数器的内容由中断隐指令自动保存,通用寄存器的内容由操作系统保存。
假定Windows操作系统下面指令已装入指令寄存器,则执行时不会导致CPU从用户态变为内核态的有哪些呢?
第一条命令 DIV R0,R1;(R0)/(R1)->R0
第二条命令 INT n; 产生软中断
第三条命令 NOT R0; 寄存器R0的内容取非
第四条 命令 MOV R0,addr; 把地址addr处的内存数据放入寄存器R0中
第一条命令中若R1中的内容为0,则会出现内中断,从用户态变为内核态;第二条 命令软中断在内核态执行;第三条命令寄存器取非不会产生中断,且不属于其他操作系统内核,故不会变为内核态;第四条addr是主存地址,访存需要进入内核态。
批处理系统中,将作业依次以脱机输入方式输入到磁带上,监督程序依次执行磁带上的作业,作业执行时用户无法干预其运行。批处理系统按照发展历程可分为单道批处理系统和多道批处理系统,主要区别为内存中同时存在单个或多个作业。多道批处理系统中的一道程序因I/O请求而暂停执行时,借助中断技术,CPU转而去运行另一道程序。
Windows操作系统执行系统调用的过程包括一些主要操作,他们正确的执行顺序是:
传递系统调用参数——〉执行陷入trap指令——〉执行相应的服务程序——〉返回用户态
Windows操作系统执行系统调用时,首先将系统调用所需的参数传递至系统内核,然后通过陷入指令进入内核态,将返回地址压入栈中以备使用,接下来CPU执行相应的内核态服务程序,最后返回用户态。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。