#操作系统
程序是静态的文本代码,为了使计算机程序得以运行,计算机需要加载代码,同时也要加载数据,然后由处理器执行指令。整个过程可以总结为编译、链接、装载、执行。
整个过程为:代码 -> 编译器翻译 -> 链接器链接 -> 机器指令(二进制文件)。
在这个过程中,一条高级语言的代码翻译过来可能会对应多条机器指令,程序运行的过程其实就是CPU执行一条一条的机器指令的过程。
注意,“指令”就是处理器(CPU)能识别、执行的最基本命令。Linux、Windows、MacOS 的“小黑框”中使用的命令行(command)也称为“指令”,其实这是“交互式命令接口”,注意与本节的“指令(Instruction)”区别开。本节中的“指令”指二进制机器指令(0-1数字)。
我们普通程序员写的程序都是应用程序。但是,微软(Microsoft)、苹果(Apple)有一帮人负责实现操作系统,他们写的是内核程序。由很多内核程序,组成了操作系统内核,或简称内核(Kernel)。
- 内核是操作系统最重要最核心的部分,也是最接近硬件的部分。因此,它可以完全控制系统中的所有内容。硬件和软件的每个操作都由内核进行管理。
- 内核被保留并通常加载到单独的内存空间中,称为受保护的内核空间。它受到保护,不会被应用程序或操作系统中不太重要的部分访问。其他应用程序,如浏览器、文字处理器、音频和视频播放器,使用单独的内存空间,称为用户空间。
- 由于这两个独立的空间,用户数据和内核数据不会相互干扰,也不会造成任何不稳定和缓慢。
甚至可以说,一个操作系统只要有内核就够了,例如,Docker仅需Linux内核,Docker利用Linux核心中的资源分离机制,例如cgroups,以及Linux核心命名空间(namespaces),来建立独立的容器(containers)。
当然,操作系统的功能未必都在内核中,如图形化用户界面 GUI。
在操作系统的分级保护域(hierarchical protection domains)中,应用程序只能使用“非特权指令”,如:加法指令、减法指令等。 操作系统内核作为 “管理者”,有时会让CPU执行一些“特权指令”,如:内存清零指令。这些指令影响重大,只允许“管理者”——即操作系统内核来使用。
在CPU设计和生产的时候就划分了特权指令和非特权指令,二者相互隔离和独立,因此CPU执行一条指令前就能判断出其类型。
CPU有两种状态,内核态(Kernel Mode)和用户态(User Mode):
- 处于内核态时,说明此时正在运行的是内核程序,此时可以执行特权指令。
- 处于用户态时,说明此时正在运行的是应用程序,此时只能执行非特权指令。
拓展:
CPU 中有一个寄存器叫程序状态字寄存器(PSW,Program Status Word),其中有个二进制位,1表示“内核态”,0表示“用户态”。
别名:内核态 = 核心态 = 管态;用户态 = 目态
用户模式与内核模式 | |
用户模式是一种受限模式,应用程序正在执行并启动该模式。 | 内核模式是计算机在访问硬件资源时进入的特权模式。 |
模式 | |
用户模式被视为从属模式或受限模式。 | 内核模式是系统模式、主模式或特权模式。 |
地址空间 | |
在用户模式下,进程获取自己的地址空间。 | 在内核模式下,进程获取单个地址空间。 |
中断 | |
在用户模式下,如果发生中断,只有一个进程失败。 | 在内核模式下,如果发生中断,整个操作系统可能会失败。 |
限制 | |
在用户模式下,访问内核程序存在限制。无法直接访问它们。 | 在内核模式下,可以访问用户程序和内核程序。 |
问题:如何实现CPU状态的切换?
- 刚开机时,CPU 为“内核态”,操作系统内核程序先上CPU运行。
- 开机完成后,用户可以启动某个应用程序。
- 操作系统内核程序在合适的时候主动让出 CPU,让该应用程序上CPU运行。
- 应用程序运行在“用户态”。
- 此时,一位猥琐黑客在应用程序中植入了一条特权指令,企图破坏系统。操作系统内核在让出CPU之前,会用一条特权指令把 PSW 的标志位设置为“用户态”。
- CPU发现接下来要执行的这条指令是特权指令,但是自己又处于“用户态”。
- 这个非法事件会引发一个中断信号,CPU检测到中断信号后,会立即变为“内核态”,并停止运行当前的应用程序,转而运行处理中断信号的内核程序。
- “中断”使操作系统再次夺回CPU的控制权。
- 操作系统会对引发中断的事件进行处理,处理完了再把CPU使用权交给别的应用程序。
CPU 上会运行两种程序,一种是操作系统内核程序,一种是应用程序,在合适的情况下,操作系统内核会把CPU的使用权主动让给应用程序(进程管理)。“中断”是让操作系统内核夺回CPU使用权的唯一途径。
中断(Interrupt),指处理器接收到来自硬件或软件的信号,提示发生了某个事件,应予以注意,这种情况就称为中断。
中断是让操作系统内核夺回CPU使用权的唯一途径,如果没有中断机制,那么一旦应用程序上CPU运行,CPU就会一直运行这个应用程序。这就与操作系统的并发属性产生冲突。
中断的设计之初,是用以提高计算机工作效率、增强计算机功能。最初引入硬件中断,只是出于性能上的考量,如果计算机系统没有中断,则处理器与外部设备通信时,它必须在向该设备发出指令后进行忙等待(Busy waiting),反复轮询该设备是否完成了动作并返回结果。这就造成了大量处理器周期被浪费。
初看这句话,会觉得难以理解,为什么中断还反而提高了效率?我在做事的时候,最讨厌别人打断我了,这会严重降低我的效率。
对比现实世界,这个中断,其实很好理解。比如我是一个厨子,我现在要做一顿饭,需要烧水、刷锅、切菜、炒菜、煮饭这几个步骤,但是我相信没有人会傻到,把这几件事情一件一件地干完。这样非常浪费时间,我可以把水烧了,就开始去煮饭,等锅在煮的时候,自己去切菜,等水烧开了,然后先把手头的事情放下,那么把水装到水壶里,再去刷锅,然后等这些做完了,在回头去切菜。
与中断对应的是忙等待。这里的忙等待,是指一种进程执行状态。进程执行到一段循环程序的时候,由于循环判断条件不能满足而导致处理器反复循环,处于繁忙状态,该进程虽然繁忙但无法前进。
忙等待,在现实世界里,就是一直不停的等着别人做,然后等别人做完了,自己再做。就很像那种身边讨厌的人,自己的活没做完,但是总是催着别人做。这样的效率其实并不高,反倒是在得知别人还没做完的时候,先做自己的事情,然后等自己做的差不多了,然后跑过来对齐下进度。
回到中断一开始引入的目的,再引入硬件中断以后,当处理器发出设备请求后就可以立即返回以处理其他任务,而当设备完成动作后,发送中断信号给处理器,后者就可以再回过头获取处理结果。这样,在设备进行处理的周期内,处理器可以执行其他一些有意义的工作,而只付出一些很小的切换所引发的时间代价。
后来,用于CPU外部与内部紧急事件的处理、机器故障的处理、时间控制等多个方面,并产生通过软件方式进入中断处理(软中断)的概念。回顾整个中断,其实是和操作系统的并发性一脉相承,由于操作系统的并发性让多个程序并发执行,所以在需要一种切换机制,这就是中断。
中断的过程发生如下:
当发生异常或中断时,执行会从用户模式转换到处理异常或中断的内核模式。具体来说,必须采取以下步骤来处理异常或中断: 进入内核,必须首先将当前正在执行的进程的上下文(所有 CPU 寄存器的值)保存到内存。 内核现在已准备好处理异常/中断。
- 确定异常/中断的原因。
- 处理异常/中断。 处理完异常/中断后,内核将执行以下操作步骤:
- 选择要还原和恢复的进程。
- 恢复所选进程的上下文。
- 恢复所选进程的执行。 因此,中断的本质,就是发生中断就意味着需要操作系统介入,开展管理工作。按照分类,中断分为外中断和内中断。
内中断指,与当前执行的指令有关,中断信号来源于CPU内部。外中断指,与当前执行的指令无关,中断信号来源于CPU外部。一般而言,狭义的中断特指外中断,而内中断很多时候也叫做异常和例外。所以在上图中,由用户态切换成内核态的原因可能由外部事件触发(中断)和内部程序执行异常(内中断)导致。
其中,内中断(Exception)的三种常见类型:
- 陷入(trap):由陷入指令引发,是应用程序故意引发的,比如,read, fork, execve...
- 例如,某应用程序想请求操作系统内核的服务,此时会执行一条特殊的指令——陷入(trap)指令,该指令会引发一个内部中断信号。
- 系统调用(system call),就是通过陷入(trap)指令完成,由外部调用系统接口,完成用户态转化成内核态的状态。
- 故障(fault):由错误条件引起的,可以被错误处理程序纠正并返回正常程序。内核程序修复故障后会把 CPU使用权还给应用程序,让它继续执行下去。
- 例如,缺页故障(Page fault),当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。
- 终止(abort):由致命错误引起,内核程序无法修复该错误,因此一般不再将CPU使用权还给引发终止的应用程序,而是直接终止该应用程序。
- 如:整数除0、非法使用特权指令。CPU发现当前状态非内核态,该行为是非法状态,引发中断信号(Signal),强行转化成内核态,然后运行处理中断信号的内核程序。
联想到做饭的例子,其实也可以类比现实世界。比如我正在切菜,突然自己想起自己有些特别重要的事情急着做,比如有人马上来家做客,这个时候需要发下微信,问下别人到哪了,这个时候切菜的过程就中断了。这就类似于陷入,通过个人主观地中断去做另一件事。
或者,当我切菜的时候,突然发现自己少洗了一道菜,比如我要做西红柿炖牛腩,突然发现西红柿没有,这个时候我就要去超市临时买点西红柿,等买回来,在继续切菜。这是一种故障,是由于错误的条件引起(忘记备菜了),但是我可以很快去楼下买完回来继续做,不影响后面出菜。
最差的情况就是我切菜的时候,切到手了,鲜血直流,我得马上找布包扎上药膏,然后跟家人说,今天这个饭目前我是做不了, 把活丢给了别人。这种就是致命错误导致的终止,而且无法修复,只能终止这个过程。
外中断的两种常见类型:
- 时钟中断,由时钟部件发来的中断信号;
- 时钟部件每隔一个时间片(如 50ms)会给CPU发送一个时钟中断信号。
- CPU接受中断信号,转成内核态。
- CPU处理时钟中断的内核程序,操作系统内核决定接下来让另一个应用程序上CPU运行。
- I/O 中断请求:
- 当输入输出任务完成时,向CPU发送中断信号。
- 处理I/O中断的内核程序。
不同的中断信号,需要用不同的中断处理程序来处理。当CPU检测到中断信号后,会根据中断信号的类型去查询中断向量表(interrupt vector table,IVT),以此来找到相应的中断处理程序在内存中的存放位置。这里的中断向量表,中断向量表 是一种数据结构,它将中断处理程序列表与中断向量表中的中断请求类型列表相关联。中断向量表的每个条目(称为中断向量)都是中断处理程序(interrupt handler,也称为ISR)的地址。
大多数处理器都有一个中断向量表,例如,常见的中断向量表的前几位有:
中断编号 | IVT 地址 | 中断名称 |
---|---|---|
0 | 00-03 | CPU 除以零, CPU divide by zero |
1 | 04-07 | 单步调试,Debug single step |
2 | 08-0B | 不可屏蔽中断, Non Maskable Interrupt (NMI input on processor) |
3 | 0C-0F | 调试断点, Debug breakpoints |
... | ... | ... |
显然,中断处理程序一定是内核程序,需要运行在“内核态”。
操作系统作为用户和计算机硬件之间的接口,需要向上提供一些简单易用的服务。主要包括命令接口和程序接口。其中,程序接口由一组系统调用组成。
“系统调用”是操作系统提供给应用程序(程序员/编程人员)使用的接口,可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务。
程序 | 系统调用关系 |
---|---|
普通应用程序 | 可直接进行系统调用,也可使用库函数。 |
编程语言 | 向上提供库函数。有时会将系统调用封装成库函数,以隐藏系统调用的一些细节,使程序员编程更加方便。 |
操作系统 | 向上提供系统调用,使得上层程序能请求 |
并不是不是所有的库函数都会系统调用:
- 不涉及系统调用的库函数,例如“取绝对值”的函数,
abs()
函数。 - 涉及系统调用的库函数,例如“创建一个新文件”的函数,
fopen()
函数。
【问题】由于计算机内运行多个程序,如何保证各个程序有条不紊地并发运行?
由操作系统内核对共享资源进行统一的管理,并向上提供“系统调用” ,用户进程想要使用共享资源,只能通过系统调用向操作系统内核发出请求,换而言之,系统调用是用户唯一能使用的操作系统对外暴露的接口,内核按照系统调用,会对各个请求进行协调处理。
应用程序通过系统调用请求操作系统的服务,而系统中的各种共享资源都由操作系统内核统一掌管,因此凡是与共享资源有关的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方式向操作系统内核提出服务请求,由操作系统内核代为完成。这样可以保证系统的稳定性和安全性,防止用户进行非法操作。 常规的系统调用的类型有以下几类方式:
- 进程控制:如创建进程、终止进程、等待事件、唤醒事件分配和释放内存。fork,wait,exit,exec函数。
- 文件管理:如创建文件、删除文件、打开、关闭、读、写。create ,open,read,write,close,link,unlink,lseek,chmod,rename函数。
- 设备管理:如请求设备、释放设备、读、写。swapon、swapoff函数。
- 信息维护:如读取时间或日期、设置时间或日期。time函数。
- 进程通信:如创建、删除通信连接,发送、接收消息。socket函数。
系统调用过程主要分以下几个步骤:
- 应用程序,传递系统调用参数 ,传递陷入(trap)指令;
- CPU执行陷入指令,引发内中断(用户态);
- CPU转入相应中断处理程序,即系统调用入口程序;
- CPU根据寄存器中的参数判断用户究竟需要哪种系统调用服务;
- 执行相应的内请求核程序处理系统调用(内核态);
- 返回应用程序。
注意:
- 陷入指令是在用户态执行的,执行陷入指令之后立即引发一个内中断,使CPU进入核心态;
- 发出系统调用请求是在用户态,而对系统调用的相应处理在核心态下进行;
- 别名:陷入指令 = trap 指令 = 访管指令。
内核是操作系统最基本、最核心的部分。实现操作系统内核功能的那些程序就是内核程序。计算机操作系统的内核部分主要包含以下:
- 时钟管理(Digital Clock Manage):实现计时功能,
- 中断处理(Interrupt Processing):负责实现中断机制,
- 原语(primitive):是一种特殊的程序,具有原子性。也就是说,这段程序的运行必须一气呵成,不可被“中断”。运行时间较短,调用频繁。例如设备驱动、CPU切换等。
- 对系统资源管理:例如进程、存储器、设备、文件。
前三项主要是与硬件关联较紧密的模块。最后一项更多的是对数据结构的操作,不会直接涉及硬件。
非内核部分,例如图形界面接口。Ubuntu、CentOS 的开发团队,其主要工作是实现非内核功能,而内核都是用了Linux内核。
操作系统内核需要运行在内核态,操作系统的非内核功能运行在用户态。现在,应用程序想要请求操作系统的服务,这个服务的处理同时涉及到进程管理、存储管理、设备管理。在这一过程中,状态切换需要时间成本,因此频繁地变态会降低系统性能。根据内核的设计理念区别,操作系统的内核可以分为微内核和宏内核。
宏内核,简单理解其实就是把上面所有的功能都整合在一起。我们可以把进程管理、管理内存、管理硬盘、管理各种I/O设备……这些功能看作一个个模块。在宏内核中,这些模块都是集成在一起的,运行在内核进程中,只有处于内核态下才能运行。而微内核则和宏内核结构相反,它提倡内核中的功能模块尽可能的少。内核只提供最核心的功能,比如任务调度,中断处理等等。其他实际的模块功能如进程管理、存储器管理、文件管理……这些则被移出内核,变成一个个服务进程,和用户进程同等级,只是它们是一种特殊的用户进程。
典型的大内核/宏内核/单内核操作系统:Linux、UNIX,从Linux的内核架构可以看出,Linux的内核是将诸多功能融合进内核中,例如用户、系统、文件、网络等等。
典型的微内核操作系统: Windows NT、Mac OS X。Window 的内核设计是混合型内核,可以看到内核中有一个 MicroKernel 模块,这个就是最小版本的内核,当今 Windows 7、Windows 10 使用的内核叫 Windows NT,NT 全称叫 New Technology。而整个内核实现是一个完整的程序,含有非常多模块。
可以用企业的管理,形象比喻操作系统的体系结构。内核就是企业的管理层,负责一些重要的工作,只有管理层才能执行特权指令,普通员工只能执行非特权指令。用户态、核心态之间的切换相当于普通员工和管理层之间的工作交接。
- 宏内核,类比于企业初创时体量不大,管理层的人会负责大部分的事情。优点是效率高,核心管理与基层员工直接进行信息交流;缺点是组织结构混乱,难以维护。
- 微内核,类比于随着企业体量越来越大,管理层只负责最核心的一些工作。优点是组织结构清晰,方便维护;缺点是效率低,因为企业的命令传达需要经过多次传递。从核心到高管到中层管理再到基层。
参考链接: