更新于 

操作系统的运行环境

操作系统的运行机制

预备知识

在学习之前,我们先来回忆一下程序是如何运行的。首先指令是指处理器cpu可以识别、执行的最基本命令。在生活中,很多人习惯将Linux,Windows,MacOS的小黑窗中的命令也称为“指令”,实际上这些是“交互式命令接口”,与本节的“指令”不同,本节中的“指令”是指硬件层机器所能识别的二进制指令(即01串)。

一条高级指令如C,JAVA等都会首先通过编译器翻译为机器能够读懂的二进制指令然后才能被硬件机器识别和执行。高级语言逻辑复杂更符合人类思维,而二进制指令则更对机器的执行友善,简单地01交并补就可以实现高级语言。但是相对应的指令长度和数量也就更多,所以一条高级语言的代码可能会翻译出许多条对应的机器指令(举个例子,实际上通过二进制指令和操作系统的代码实现输出函数printf就已经对于机器来说是一个非常复杂高级的指令了,如果你做过nemu的话会深有体悟)。而cpu就是一条一条的执行二进制指令,当然执行的速度非常快。

内核程序和应用程序

这两个程序有本质上的区别,对于应用程序我们再熟悉不过,普通程序猿写的程序大多都是应用程序,其大部分都是应用于软件层,最终运行在操作系统上。而例如微软、苹果、华为等一些顶级大牛会负责实现操作系统,如果你还记得上节的内容,应该知道操作系统本质上也是一个软件,只是他是连接软件层和硬件层的中间层。这些很多内核程序组成的“操作系统内核”,又叫做内核(kernel),内核是操作系统最重要的核心部分,也是最接近硬件的部分,可以说,一个操作系统只要有了内核基本上就够了例如Docke仅需要Linux内核,操作系统的内核是实现核心功能的部分,未必拥有操作系统的全部功能,录入图形化接口GUI就不在内核中实现。

特权指令和非特权指令

应用程序使用的都是“非特权指令”,例如加法指令,减法指令等,而操作系统内核作为管理者,就有权有时让cpu执行特权指令,如:内存清零指令,这些指令影响重大,一般会直接影响到操作系统,硬件上的工作,只能由“管理者”–操作系统内核使用。归根对比,应用程序使用的非特权指令权利很小,无权或者不能对操作系统和硬件层产生直接影响,并且一定是需要经过操作系统才能间接使用接口来和硬件层产生关联,而特权指令就是直接更改操作系统代码或者硬件层调度配合工作的代码,不可能暴露给外界以防产生恶意程序入侵破坏设备。在设计cpu时会划分特权指令和非特权指令,因此cpu可以执行一条指令前判断出指令的类型。

内核态和用户态

cpu有两种状态:内核态和用户态。处于内核态时,说明此时正在运行的是内核程序,此时cpu可以执行特权指令(注意是可以,也就是说此时还可以继续执行非特权指令)。而当处于用户态时,说明此时运行的是应用程序,此时只能执行非特权指令。这里用到了一个特殊地寄存器来存储程序状态–程序状态字寄存器(PSW),其中有个二进制位,1表示“内核态”,0表示“用户态”,这样cpu就可以随时判断出此时处于什么状态下。

当然这里有许多别名:

  • 内核态=核心态=管态(即管理状态)
  • 用户态=目态(即只能观看状态)

那么你一定会好奇仅仅用一个PSW就来判断cpu处于什么状态是否过于草率,那么只要更改这个位,岂不是可以按照人为意愿随意更改状态,更可怕的是如果有黑客此时病毒植入,更改了cpu状态然后执行了格式化等指令将系统破坏掉会造成很大的安全隐患,所以这里会有异常中断来避免这种情况,所以PSW不能随意更改,只能由特权指令更改。

这里有一个故事来描述这种情况的应急措施:首先,设备刚刚开机后首先会使cpu处于管态,此时操作系统内核程序先上cpu运行(原因是应用程序需要在操作系统上运行,所以操作系统需要先做准备工作提供接口环境),开机完成后,用户启动某个应用程序,待操作系统内核在合适的时候(准备工作完成)主动(此时处于管态,运行特权指令更改PSW是可以的)让出cpu,让该程序上cpu放入内存后上cpu执行,此时应用程序运行在目态,只能执行非特权指令,当此时有黑客在应用程序中植入一条特权指令(更改PSW)时,企图破坏系统,cpu此时在目态发现要执行的是特权指令(更改PSW)时发现自己是用户态时就会触发异常中断,此时操作系统发现中断信号会立刻夺回cpu的控制权以防有非法指令破坏系统,然后对引发中断的事件进行处理,处理完后在cpu使用权交给应用程序并且此时再次切回目态,这样就保护了系统不会受到入侵破坏了。

思考:内核态和用户态怎样切换

内核态->用户态:刚刚上面已经讲过了,当cpu处于内核态时可以执行一条特权指令修改PSW的标志位来实现主动切换到目态,这个动作意味着操作系统主动让出cpu的使用权。

用户态->内核态:任何情况下都不可能通过指令切换回管态,因为PSW只能通过特权指令更改判断位,而此时目态下cpu无权执行特权指令,但是可以通过中断信号引发,硬件自动完成变态过程,触发中断信号意味着操作系统将强行夺取cpu的使用权,因此除了非法使用特权指令以外,还会有许多事件触发中断信号,从而由目态切换到管态,但有一个共性是,但凡需要操作系统介入的地方,都会触发中断信号。

总结

操作系统内核

说了那么多,那么操作系统内核到底是什么,其实内核就是计算机上配置的底层软件,是操作系统最基本,最核心的部分,实现操作系统内核功能的那些程序就是内核程序。如下图:

那么哪些是不属于内核的操作系统的功能呢?例如记事本、任务管理器等设备自带的传说中免费的赠品软件APP,即使没有这些软件,我们仍然可以使用计算机。当然,这些非内核的功能用来推销也是不错哦😹:放松时刻

当然不同厂商对于内核的定义也不同,这里又对内核进行了细分:大内核和微内核。

操作系统的结构和企业的管理问题很相似,内核就是企业的管理层,负责一些重要的核心工作,只有管理层才能执行特权指令,普通员工就只能执行非特权指令。管态和目态之间的切换就相当于普通员工和管理层之间的工作交换。

大内核:企业初创时体量不大,人人都有官,人人皆高层,所以管理层的人会负责大部分的事情,有点事效率高,缺点就是组织结构混乱,难以维护。

微内核:随着企业的体量增大,管理层只负责最核心的一些工作,有点事结构清晰,方便维护,缺点是效率低。

中断和异常

中断的作用

cpu上会运行两种程序,一种是操作系统内核程序(是整个操作系统的管理者),另一种就是应用程序。前面已经基本上知道了中断实际上就是会使cpu由用户态变为内核态,使操作系统重新夺回对cpu的控制权。

在合适的情况操作系统内核会把cpu的使用权主动让给应用程序,而中断就是让操作系统内核夺回cpu使用权的唯一途径。如果没有中断机制,那么一旦cpu开始运行某个应用程序,cpu就会一直运行这个应用程序,那么又何来的并发性呢,所以当切换cpu上的应用程序时就是需要中断信息,使操作系统重新掌权,将cpu使用权让给其他的应用程序,所以中断保证了并发性。

中断的类型

所以中断切换状态是很常见的一种方法,那么根据不同触发的触发中断的情况我们可以分为两类–内中断和外中断。

内中断

内中断与当前执行的指令有关,一般来自cpu的内部,比如发现cpu执行了特殊的特权指令造成的异常或者除0出现计算异常等都是执行的指令自身引发的,这种就成为内中断。当然也不一定指令是出现错误才触发中断,比如应用程序想请求操作系统内核的服务时,此时会执行一个特殊的指令–陷入指令(在Nemu实验中也有,为trap()),此时该指令就会引发一个内部中断信号,也是内中断的一种,这种陷入指令虽然会触发中断,但是此动作意味着应用程序主动的将cpu控制权还给操作系统内核,系统调用就是通过陷入指令完成的。

外中断

外中断与当前的执行无关,不是当前指令引起的中断信号,所以自然不是来自于cpu内部,而是通过内核中某些算法(这些算法来实现任务间合理调度)引起的中断信号。比如内核中的时钟中断,它是由时钟部件发来的中断信号或者是IO设备发起的任务完成的中断信号。

例如时钟算法是用来分配调度任务之间的占用cpu的时间的,我们从上图可以看出时钟计时每50ms会发一个中断信号给cpu,而cpu每次执行完一条指令后都会例行检查是否有外中断信号。当检测到外中断信号时,就会由目态切换到管态。所以回忆之前的知识,可以猜出单批道操作系统的并发性实现即每隔一个时间片切换任务就是通过时钟算法外中断信号引起的。

中断分类的总结

经过上面两个的对比,我们可以看出外中断更符合我们广义上所说的中断,而内中断更多的像是故障,异常终止或者主动陷入,所以大多数的教材和讲义上中断都是特指的外中断,内中断一般称为异常。

中断机制的基本原理

那么对于不同的中断信号,如何知道该进行什么相应操作呢?这时cpu检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置。

总结

系统调用

什么是系统调用

我们在前面已经学到了操作系统作为用户和计算机硬件之间的接口,需要向上提供一些简单易用的服务,主要包括命令接口和程序接口。其中程序接口就是由系统调用组成的。例如C库函数中的system()就是一种库函数方法,需要通过程序接口实现。

一般系统调用是操作系统提供给程序猿等编程开发人员使用的接口,可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来获得操作系统内核的服务。

思考:系统调用和库函数的区别?

通过上图我们可以看出应用程序一般是直接通过系统调用来请求内核服务的,当然也有一部分是调用库函数时,库函数中需要系统调用来请求内核服务。但是并不是所有的库函数都需要系统调用的,比如“取绝对值”的函数sqrt()只是一个数学操作函数,虽然需要引入cmath库,但是是不需要系统调用的。而“创建一个新文件”局就涉及到了系统调用的库函数。所以一般来说,普通用户是不会手动触发系统调用的,只有编程人员调用库函数和应用程序可能会触发系统调用。

系统调用的必要性

那么为什么要有系统调用呢,即为什么程序需要每次都向内核发送服务请求呢而不是直接自己执行呢?这就涉及到了操作系统的自身的功能–协调分配任务,管理资源。比如两个人的电脑连接着一个打印机,第一个人按下了打印按钮,此时打印机开始打印第一个文件,但是在打印至一半时,第二个人也按下了打印按钮,开始带引他的文件。如果没有系统调用申请内核服务的话,那么两个进程就会互相随意地并发的共享计算机资源,最终造成两个文件混杂在一起的情况。而使用系统调用,触发陷阱发送中断信号请求内核对共享资源的统一的管理,操作系统就会向上提供“系统调用”服务,内核会对这几个进程进行协调处理,使其互相不干扰的并发进行,即在某个进程该工作占用cpu和打印机共享资源时工作,非这个进程阶段就进制此进程占用共享资源,这样就会使得最终的结果互补混杂了。所以系统调用对于共享资源的管理和任务之间的协调调度起着至关重要的作用。

系统调用的分类

应用程序通过系统调用来请求系统的服务,而系统中的各种共享资源都又操作系统内核统一掌管,因此凡是与共享资源有关的操作(如存储分配,I/O操作,文件管理)等,都必须通过系统调用的方式向操作系统内核发出服务请求等待响应,然后又操作系统内核代为完成(所以是在管态进行的分配服务),这样就保证了系统的稳定性与安全性,防止了用户的非法操作。

系统调用的过程

因为系统调用是应用程序主动然爱过出cpu的使用权,使用陷入指令触发的中断信号,所以系统调用一定是内中断。

我们可以看到系统调用时并不是立刻就进行陷入指令,而是首先在目态进行一系列准备工作,比如记录中断地址(毕竟最终操作系统服务完以后还要回到这个地址继续执行),还有传参指令即将系统调用需要的参数存放到制定的寄存器以便操作系统使用,最终才调用陷入指令(此时已经做好了移交cpu的准备工作),然后操作系统掌握cpu使用权(管态)进行服务,完成后最终再返回到中断位置继续执行后面的指令。

总结

操作系统的体系结构

我们通过上图可以看出一个操作系统内核部分和非内核部分可以组装,比如Ubuntu等就是建立在linux基础上再加以非内核功能组装住的操作系统。并且我们也已经知道内核是操作系统最基本,最核心的部分,实现操作系统内核功能的那些程序就是内核程序。并且内核分为了四个部分:

因为其对软硬件的操作程度不同,有区分成了大内核和微内核,我们前面是以企业模型分析了两种内核类别的效率和优缺点。这里我们再以变态次数分析一下,首先我们需要知道应用程序想要请求操作系统的服务时,这个服务会涉及到进程管理,存储管理,设备管理即对硬件操作不是很大的那层(橘色层)。然后在涉及到最接近硬件层的时钟管理,中断处理和原语部分。按照大内核和微内核的定义:

我们可以看出他们两种类型的内核布置造成了不同的变态次数。

对于大内核其认为两层均是内核功能部分,所以这两层都处于管态执行,这样四个功能之间的切换就不会在涉及到变态过程了,唯一造成变态的位置就是应用程序和大内核之间的切换,所以只有2次变态。而对于微内核,其任务进程管理,存储管理和设备管理(橘色层)不属于内核部分,所以此部分还是需要在用户态执行,这样虽然应用程序和橘色层之间不再需要变态了,但是由于这三个操作都是会涉及到时钟管理,中断处理和原语部分,所以每一个都需要经历两次变态,最后总体来看会造成6次变态。而变态的过程是有成本的,要消耗不少的时间,频繁的变态会降低系统的性能,所以这也是大内核效率更高的原因之一。

生活中的系统分类

典型的大内核/宏内核/单内核操作系统:Linux,UNIX

典型的微内核操作系统:Windows NT

总结