一、计算机简介

在讲解操作系统之前,我们先从整体上讲一下计算机,再从硬件讲到软件,最后再讲操作系统。

1.1 什么是计算机

我们生活中几乎到处都能接触到计算机,从我们日常使用的手机、平板,到办公使用的笔记本、台式机,到银行的ATM机,到各处可见的监控设备,还有我们平时看不见但是我们浏览的网页其所在的服务器,还有微信、抖音等我们日常所用的APP它们所在的服务器,等等,这些都是计算机。如果没有了计算机,我们的生活将难以想象。那么究竟什么是计算机呢,这个还真不好下定义的,那我们就来看一下百度百科对计算机的定义:计算机俗称电脑,是现代一种用于高速计算的电子计算机器,可以进行数值计算,又可以进行逻辑计算,还具有存储记忆功能。是能够按照程序运行,自动、高速处理海量数据的现代化智能电子设备。计算机的应用非常广泛,从我们日常最常见的台式机、笔记本到手机平板都是计算机,而且大到服务器、超级计算机,小到各种嵌入式设备也都是计算机。现在我们对计算机既有了感性的认识,又知道了的它的权威定义,那么计算机是怎么产生的呢,下面我们来看一看计算机的发展史。


(资料图片)

1.2 计算机发展史

计算机的发展史从概念上最早可以追溯到结绳计数、手指头计数,有了计数然后我们就有了加减乘除各种计算需求。但是人类天然的计算能力不太行,于是便开始创造工具、使用工具进行计算。最古老的计算工具当数中国的算盘了,算盘运用熟练的话可以使加减运算的速度大大加快。后来欧洲人发明了计算尺,通过对数方式把乘除运算转换为加减运算,使得乘除的计算速度有了很大的飞跃。欧洲在文艺复兴和科学革命之后,很多科学家都想发明一种通过齿轮和力学原理来驱动的机械计算机,最出名的当数帕斯卡发明的加法器了,可以进行6位数的加减运算。与牛顿同时期的著名数学家、哲学家莱布尼茨在帕斯卡之后发明了乘法器,可以进行加减乘除开方运算。莱布尼茨的理想远不止于此,他一直想把逻辑思维转化为数学运算,并且用机器实现逻辑运算。虽然在当时没有实现,但是为后来的计算机发展提供了很大的启发。据说莱布尼茨看了中国的易经八卦之后从中发现了二进制,并认为以后的计算机可能会采取二进制来实现。

19世纪早期的英国科学家巴贝奇发明了差分机,差分机可以进行一些非常复杂的数学运算,如多项式计算、求解三角函数等。后来他又设计出了分析机,但是由于分析机过于复杂,在当时并没有制造出来。虽然分析机没有造出来,但是它的意义却是非常巨大的。它的内部有存储部分,有碾磨(相当于现在的CPU),还有输入输出部分,已经可以看到现代计算机的影子了。而且巴贝奇是第一个意识到计算机是需要编程的人,在此之前人们一直觉得计算机如果造出来了,它就可以做任何它能做的事情,并不需要人类为它编程,当然之前的人也意识不到编程是什么意思。英国著名诗人拜伦的女儿Ada,自幼对数学兴趣浓厚,与科学家交往甚密。后来在其它人的介绍下认识了巴贝奇,两人很快就成为了亦师亦友的关系。虽然分析机并没有造出来,但是Ada还是为分析机写了很多程序,所以被后世称为历史上第一个程序员。估计绝大多数人都想不到世界上第一个程序员竟然是位女程序员。后来美国国防部设计了一种计算机语言,用的就是Ada的名字来命名的。

然后计算机从机械时代到了机电时代,很快又到了电子时代。1946年2月14日,被认为是世界上第一台电子计算机的ENIAC诞生了,之后计算机的发展就走上了快车道,从电子管到晶体管到集成电路到大规模集成电路,一直发展至今。这一段历史比较清晰,网上的资料也很多,而且这段历史中计算机的逻辑结构并没有发生多大的变化,所以我们就不展开细说了。

1.3 计算机的二元结构

计算机是由硬件和软件组成的,就像人是由肉体和灵魂组成的一样。计算机的各种硬件就像是人的大脑、心肝脾肺肾、骨骼、皮肤一样,支撑着人活着。但是人要是只有肉体没有灵魂,那就是植物人,电脑要是只有硬件没有软件,那就是一堆金属和塑料,没有用处。下面两章我们分别讲一下计算机的硬件体系结构和软件体系结构。

二、计算机硬件体系结构

我们按照从简单到复杂、从抽象到具体的过程来讲一讲计算机的硬件体系结构。

2.1 图灵机模型

什么是图灵机,我们为什么要讲图灵机?图灵机是计算机的数学模型,我们在这里讲图灵机并不是为了学习图灵机本身,图灵机的理论还是很复杂的。我们这里讲图灵机就是为了理解一下计算机的运行模型,明白CPU的运行是线性的。我们先来看一下图灵机的定义,图灵机是英国数学家阿兰·图灵于1936年提出的一种抽象的计算机模型,即将人们使用纸笔进行数学运算的过程进行抽象,由一个虚拟的机器替代人类进行数学运算。它有一条无限长的纸带,纸带分成了一个一个的小方格,每个方格有不同的内容。有一个机器头在纸带上移来移去。机器头有一组内部状态,还有一些固定的程序。在每个时刻,机器头都要从当前纸带上读入一个方格信息,然后结合自己的内部状态查找程序表,根据程序把信息输出到纸带方格上,并转换自己的内部状态,然后进行移动。我们可以看出图灵机的定义还是很简单的,图灵机的运行模式和我们现在的CPU运行模式还是很像的,我们下面画一个图灵机的运行模型。

从图中我们可以看到图灵机模型确实非常简单,有一个读写磁头,磁头有自己的内部状态,磁头根据自己的内部状态会读或者写当前方格里面的内容,然后相应地改变自己的状态,然后向左或者向右移动一格或者若干格继续执行。图灵机本身的理论非常复杂,想学习的同学可以去搜索一下或者查阅相关的书籍。我们在这里想要说的就一点,图灵机的运行是线性的,线性的意思不是说它是单向直线性的,而是说它只能在一条线上运行,而且所有运行的点按时间记录排序是线性的。

2.2 冯诺依曼结构

冯诺依曼结构是计算机的物理模型,是计算机能够制造出来的关键。冯诺依曼结构由三部分组成:一是计算机硬件实现上采用二进制;二是存储程序设计,就是把计算机程序放到存储器中,关于这一部分的论述就参看《深入理解编译体系》的第一章;三是计算机由控制器、运算器、存储器、输入设备、输出设备五个部分组成。控制器是整个计算机的神经中枢,控制着整个计算机的运转。控制器从存储器中取指令,解析指令,然后根据指令的内容让运算器进行相应的运算或者向其它部件发出一些命令。运算器是负责进行数学运算和逻辑运算的部件,数学运算包括加减乘除移位比较等运算,逻辑运算包括与、或、非、异或等运算。存储器是存储程序和数据的地方。输入设备和输出设备是负责和人交互的设备,人们通过输入设备向计算机输入命令和数据,通过输出设备得到计算机运算出来的结果。下面我们画一张冯诺依曼结构的图。

可以看到冯诺依曼结构比图灵机模型复杂了不少,但是总体上还是很简单的。有了冯诺依曼结构,计算机就有了被造出来的物理模型,我们也可以从冯诺依曼结构中更好地去理解计算机的逻辑结构。

2.3 现代计算机结构

其实并没有现代计算机结构这个术语,现代计算机结构是我把冯诺依曼结构再具体化一下,把具体计算机结构再抽象化一下总结出来的,可以让大家更方便更深入地理解计算机硬件体系结构。现代计算机和冯诺依曼结构最主要的区别是把控制器和运算器合二为一称作CPU,把存储器一分为二,分为内存和外存。我们来画一下现代计算机结构的图。

现代计算机的具体体系结构非常复杂,我只是捡其中比较关键的画了出来,很多细节没有画,如总线、南桥、北桥、各种控制器等,上图中也有很多连线没有画。计算机运行的时候先从外存中加载程序到内存,然后CPU不断地执行内存中的指令、读写内存数据,CPU也会使内存继续从外存中读取数据或者把内存的数据更新到外存。CPU通过IO端口或者内存映射IO来控制外设获取外设的状态或者数据,外设通过中断控制器(APIC)向CPU通知报告一些事情。计算机就是这样不断运行的。

三、计算机软件体系结构

从上面我们知道了硬件的整体结构和运行情况,下面我们来说一说计算机的软件体系结构。计算机软件整体上分为系统软件和应用软件,系统软件是面向硬件和面向程序员的软件,应用软件是面向普通用户的软件。

3.1 系统软件

最重要的系统软件是操作系统,操作系统是管理所有硬件并为进程提供运行环境的软件,所有的应用程序都要运行在操作系统之上,操作系统为应用程序提供服务,并管理所有的应用程序。关于操作系统的具体情况,请看第四章和第五章。

在操作系统启动之前还有两个软件,它们是Boot Firmware 和 BootLoader,Boot Firmware的存在有一个原因、两个作用:一个原因是为了克服计算机启动时鸡生蛋、蛋生鸡的矛盾,按照冯诺依曼模型,计算机启动时要从内存读取程序运行程序,而计算机启动时内存是空的啥都没有,要想让内存有程序就必须要从外存中加载程序,而从外存中加载程序的指令也是程序,这个程序也要先加载到内存,怎么办呢,我们提前写好一段程序,把它固化到ROM中去,ROM是掉电不会丢失内容的,再把这个ROM配置到特定的物理内存地址空间中,具体地址是什么要看CPU厂商了。CPU启动时会从特定的内存地址运行,也就是这个ROM所在地址,这样CPU就有程序可以执行了。Boot Firmware的两个作用分别是初始化硬件检测硬件和加载操作系统为操作系统提供一些基本的硬件信息。后来为了灵活性,Boot Firmware并不是直接加载的操作系统,而是先加载BootLoader再由BooLoader加载的操作系统。BootLoader的存在一是可以很方便配置一些信息如OS启动参数,二是支持多操作系统启动。桌面计算机常见的Boot Firmware是BIOS和UEFI,BIOS是PC最初的Boot Firmware,由于推出的时间比较早,所以存在很多问题,后来Intel又重新设计开发了UEFI,UEFI相比BIOS由很多的优点。现在基本上大部分桌面计算机都已经转向了UEFI。比较常见的Boot Loader如LILO、GRUB,现在桌面版Linux用的基本都是GRUB。

嵌入式系统的概念和这个不太一样,嵌入式系统最先启动的两个软件叫做Boot ROM和BootLoader,Boot ROM存在的原因和Boot Firmware存在的原因是一样的,但是Boot ROM并不具有Boot Firmware的功能,Boot ROM是比较轻的,相当于是UEFI/PI 中的SEC阶段,Boot Firmware中的其它功能被转移到BootLoader中来做,Boot ROM一般是由CPU厂商来开发的,不开源。BootLoader负责硬件初始化并直接加载启动操作系统。造成桌面计算机和嵌入式系统的这种差异的原因是两者的运行和使用环境大不相同造成的。

作为最基础的三个系统软件,Boot Firmware、BootLoader、操作系统,前两者很少有人熟悉,甚至很多人根本就没听说过,是因为它们启动过去之后基本就不再起作用了。除此之外还有一套很重要的系统软件,它是编译工具链,包括预处理器、编译器、汇编器、链接器,所有的软件要想从源码变成二进制程序都需要它们来处理,想要具体了解它们的情况请参看《深入理解编译系统》。除此之外还有一些系统查看、配置、维护工具也算是系统软件,如ps、top、sysctl等。

3.2 应用软件

应用软件就比较好理解了,就是那些面向普通用户实现某些特定需求的软件,如 上网-浏览器,听歌-千千静听,看视频-暴风影音,追剧-爱奇艺,社交-微信,购物-淘宝,刷视频-抖音。所有应用软件都运行在操作系统之上,享受操作系统的服务,并接受操作系统的管理。

四、操作系统组成结构

我们经常说操作系统,那么究竟什么是操作系统呢,我们先来从操作系统的构成成分的角度来讲一讲。操作系统是由三部分构成的,其中最重要的是内核部分,但是内核并不是操作系统的全部,除了内核外,OS库和OS进程也是操作系统的一部分。OS库是运行在用户空间进程中的共享库,虽然它是运行在用户空间,虽然它只是共享库,但是它是操作系统的一部分,因为它参与实现了操作系统的一部分功能。OS进程虽然是运行在用户空间的进程,但是它并不属于应用程序,因为它是实现操作系统功能必不可少的一部分。

4.1 内核

内核是操作系统最重要的部分,是操作系统的核心,它运行在内核空间,运行在CPU特权模式,直接与硬件打交道,并直接管理着进程的运行。内核可以分为宏内核与微内核,宏内核就是传统的内核方式,所有的内核功能都放在内核空间里。微内核是把一些最基本的无法移出内核的功能才放在内核里,其它能放到用户空间的功能全都放到用户空间进程里,叫做微服务,应用进程通过IPC与微服务沟通,微服务与微内核一起构成一个完成的内核。

微内核辩论:

我对微内核的态度是和Linus是一样的,下面的话是节选自Linus写的书《Just For Fun》。

微内核的理论依据是,操作系统是非常复杂的,所以要通过模式化来减少复杂性。微内核方法的原则,即核心的核心,是尽量减少功能。它的主要功能是传播。电脑所提供的一系列不同的服务都是通过微内核的传播渠道实现的。因此,应尽量分割问题的空间,使其不再复杂。我认为这种做法很愚蠢。是的,每一个单独的部分是简单的,但是相互作用的多种功能如果放在一起就要复杂得多 ,而 Linux 就是后者的情况。想一想自己的大脑。每一个单独的部分都很简单,但是各部分的相互作用构成了一个复杂的系统。这是一个整体比个别更大的问题。拿一个问题来说,如果你简单地将问题一分为二,说半个问题要容易一半,那么你就忽略了一个事实,即:你必须要考虑到两个半个之间的联系所带来的复杂性。微内核的理论是,如果把核分为五十份,那么每一份都只有五十分之一的复杂性。但是每个人都忽视了一个事实,即各部分之间的联系事实上比源系统更加复杂,而且那些个别部分也不是那么简单。这是我对微内核最重要的反驳:你想实现的简单化是错误的简单化。

上面是Linus的分析,我是非常赞同的,内核的复杂是内核的固有属性,是它内在逻辑的复杂,你把它人为的划分到不同的用户空间进程,并没有减少它内在的逻辑复杂性。把一个事物模块化能减少它的复杂性,但是模块化不是说非得要在形式上把它们分割到不同的进程,在逻辑上把它们分割开来就可以了。比如说有一个进程非常复杂,你把它弄成一坨不分模块是不对的,但是模块化的形式可以是把它分割成一个exe和n个so就可以了,不一定非要把它做成n+1个进程。n+1个模块无论是在进程内交互,还是在进程间交互,其本身的复杂性并没有减少,进程间通信还降低了效率。

微内核还有一个经常宣传的优点是如果某个微服务出了问题,只重启这个微服务就可以了,不用重启整个内核。但是这个优点是禁不起推敲的,以我在安卓系统上的工作经验为例,如果某个重要的JAVA进程崩溃重启了比如SystemServer,就会导致很多Java进程包括所有的APK进程都会重启,虽然内核没有重启,但是在用户看来还是整个系统都重启了。所以如果某个微服务重启了,一定会导致所有的应用进程重启的,在用户看来还是整个系统重启了,也许你会说微内核本身并没有重启,所以整个重启时间减少了,但是这点时间减少并没有多少意义。而微内核带来的整个系统性能的大幅度下降这个问题,则是无法容忍和无法克服的。

我发现很多嵌入式系统使用的都是微内核。因为嵌入式系统是一个相对封闭的系统,不会有经常下载新程序、突然运行很多进程的情况,所以嵌入式系统对性能的需求是稳定的。所以微内核只要在测试中能满足嵌入式系统的性能需求,问题就不大。通用操作系统包括手机系统、桌面系统、服务器系统,都有面临负载突然暴增的情况,所以对性能的要求非常高。

目前所有流行的通用操作系统内核都是宏内核,有些宣称自己是微内核或者混合内核的,本质上还是宏内核。《Mac OS X Internals A Systems Approach》6.2章节明确说XNU不是微内核,是宏内核,《Mac OS X and iOS Internals》第8章Hybrid Kernels这一节说,Windows、MacOS虽然是混合内核,但是实际上还是宏内核。

4.2 OS库

有些运行在用户空间的动态库,是属于操作系统的一部分,这些库虽然是库,但是并不是由应用程序开发者开发,而是由操作系统厂商开发,因为它们是构成操作系统功能的一部分。在Linux上常见的OS库有libc.so、ld-linux-x86-64.so、libm.so、libpthread.so、libdl.so、librt.so、libcrypt.so、libpam.so、libX11.so等。Linux上的OS库是非常多的,上面只是随便举几个例子,下面我们对其中最重要的几个进行一下分析。

libc.so,libc(C Library)的二进制动态库形式,它的源码实现有很多,Linux发行版一般用的是glibc,安卓上的libc的实现叫做bionic。libc不仅生成libc.so,还有libpthread.so、ld-linux-x86-64.so等很多库都是libc生成的。libc.so里面不仅有C标准库的实现,还有对系统调用的封装,所以libc.so不是一个普通的库,它是内核的对外接口库,是非常重要的,是进程运行必不可少的。你cat 任何一个进程的 /proc/pid/maps 几乎一定会看到libc.so,如果看不到,那是因为有些进程是把libc静态链接到自身了,进程里面还是包含libc的。

ld-linux-x86-64.so,是Linux系统的加载器,负载在进程启动时把进程所依赖的所有动态库都加载到内存并进行一些重定位工作。没有了ld-linux-x86-64.so,几乎所有的应用进程都无法启动运行。

libpthread.so,Linux的多线程实现库,想要使用系统的多线程功能的进程必须要调用这个so才行。

4.3 OS进程

并不是所有的操作系统功能都放进了内核里,还有很多功能是放在了用户进程里来实现的,我们把这些进程叫做OS进程。我总结了一些常见的放在用户空间的操作系统功能,它们是 init system、package system、window system。这些都被叫做某某系统,可见它们并不是简单的一个进程而已,它们一般是由一个进程加上一些配置文件,有的还提供一些so,它们有着自己的逻辑体系,下面我们来一一讲解一下。

Init system,是操作系统启动的第一个用户空间进程,它负责把整个用户空间给启动起来,启动系统所有的守护进程,设置一些系统配置,在系统运行的过程中监管着整个系统,系统的关机也由它负责。最早的init system 叫 sysv init,它的实现比较简单直接方便,大量使用脚本。后来sysv init 变成了系统启动速度慢的最大瓶颈,一是因为脚本的运行比较慢,二是因为它是单线程的,没能发挥出多核的优势。后面就出现了两个init system,分别是upstart 和 systemd 来解决sysv init 的问题,现在大部分linux发行版都把init system 换成了systemd。

Package system,一个系统上有那么多的软件,该如何安装、如何卸载、如何管理,安装包该采取什么格式,这便是package system要做的工作。目前Linux上有两个比较流行的package system,一个是redhat 推出的rpm,一个是debian推出的dpkg。

Window system 也叫做Graphics system,也就是一个操作系统的图形界面该如何管理,这是一个非常复杂的系统,牵涉面非常广,为什么是Android呢,因为我在手机厂商工作的时候做过这方面的工作,相对比较熟悉,对Linux的图形系统不太熟悉。

除了上面三个很大的系统之外,还有一些GUI程序,看起来像是应用程序,但是实际上应该划到操作系统的内容里面,比如桌面、文件管理器、设置等程序,因为它们是和操作系统紧密相关的,而且一般也是由操作系统厂商来开发和维护的。

五、操作系统本质解析

前面我们对操作系统的组成结构进行了分析,下面我们从操作系统的功能作用的角度再对操作系统进行具体的分析。我们先来思考一个问题,什么是操作系统,为什么要有操作系统?我们通过对操作系统发展历史的研究以及对Linux内核实现的深入研究发现,操作系统的存在就是为了一个目的,就是为了运行程序,如果再加个形容词的话,那就是多快好省地运行程序。为了实现这个目的,操作系统提供了两个作用:1.操作系统是一个更加高级的抽象计算机,2.操作系统是计算机资源管理器。下面我们对一个目的、两个作用展开讨论。

5.1 操作系统的目的

操作系统存在的目的就是为了运行程序。我们还可以用反证法来论证这个结论,假设操作系统不能用来运行程序,那么操作系统要它还有什么用呢。所以操作系统存在的目的就是为了运行程序,为了多快好省地运行程序,多就是多任务,快就是效率高,好就是安全稳定,省就是节省资源,为了实现这个目的,操作系统做了非常大的努力。

既然是为了运行程序,那么我们要问的第一个问题就是程序是怎么来的呢。首先程序是用计算机编程语言编写的。编写程序的工具是什么呢,是文本编辑器。编写的原则是什么呢,随便怎么写都可以吗,不是的,首先要符合所使用语言的语法才行。其次我们对程序还有几个要求,其中首先最重要的要求就是正确性,程序运行结果不正确可不行,除了编程时我们小心翼翼尽量保证程序逻辑正确外,我们在运行的时候发现程序不正确时还要用调试工具GDB来调试程序,找到程序的错误改正它。对于程序的内存错误,我们还可以使用asan、malloc-debug、valgrind等工具来排查错误。再者我们写的程序还要简洁清晰、可读性强、可维护性强,为了达到这个目的,我们要学习很多编程的艺术,在此给大家推荐几本书:《Agile Software Development》《Clean Code》《The Clean Coder》《Clean Architecture》《Code Complete》《Design Patterns》《Refactoring Improving the Design of Existing Code》。其中大家在很多博客上经常看到的一些编程原则如开闭原则、单一责任原则、依赖倒置原则等都是出自第一本书,汉译书名叫《敏捷软件开发》。再者我们写的程序还要具有高效性,程序效率低运行慢可不行,那么怎么让我们写的程序比较高效呢,为此我们要学习数据结构与算法,只有把数据结构与算法学透了,我们才能把我们学到的方法和我们的需求结合起来,写出来运行又快占用空间又小的程序。为此我向大家推荐几本书:《Introduction to Algorithms》《Algorithm Design and Applications》《Algorithms in a Nutshell》《Data Structures and Algorithms in Java》《Algorithms》《An Introduction to the Analysis of Algorithms》。如果是已经写好的程序嫌它运行慢应该怎么办呢,我们有性能剖析工具gprof,它能帮你找到热点代码,然后你就可以进行定向优化,必要时还可以使用汇编语言实现一些函数来提高效率。

当我们把源代码都写好了之后,程序就可以运行了吗,不行,CPU不认识源代码,只认识二进制程序,所以我们还需要把源代码编译、链接成二进制程序才行。二进制程序有没有格式呢,随便怎么生成都行吗,不行,不同的操作系统都有自己特定的二进制程序格式。Linux使用的二进制程序格式叫做ELF格式,ELF是一个总体格式,在不同的操作系统上、不同的CPU架构上又有一些具体细化的要求,如都有哪些段,函数怎么传递参数等,这些统称为ABI,应用程序二进制接口。源码生成的二进制程序可以分为exe主程序和so动态库程序,主程序可以直接执行,库程序不能直接执行,只能在别人的进程里被调用执行。程序运行起来就是进程,一个进程由一个exe主程序和n个so库程序构成。

还有一个概念叫做IDE,集成开发环境,它是一个图形界面程序,里面集成了文件编辑器、调试器、编译器、链接器、Build工具等,可以在一个程序里完成一系列开发操作。

5.2 操作系统的作用之一

操作系统为了运行程序提供了哪些作用呢,第一个就是操作系统是一个更加高级的抽象计算机。怎么理解这句话呢,我们从计算机发展的历史来讲解。最开始的时候是没有操作系统的,只有应用程序,应用程序的开发要直接面向硬件开发,因为没有人给你提供API。比如说你要读写一个文件,你该怎么办,要是搁现在那是很简单的,你只需要open(file path),read(fd),write(fd)就可以了。但是在当时读写却是非常复杂的,你需要了解磁盘的型号,知道磁盘的端口,知道磁盘的各个技术参数如磁盘有多少个盘面柱面扇区,你要自己编程管理每一个扇区,记录哪一个扇区用过了哪一个扇区没用过,想想都头疼,都想放弃。现在好了,现在有操作系统帮你做这些事情,你只需要按照操作系统给你的API去做就行了,那是十分的方便啊。操作系统向应用程序提供的功能,从底层实现的角度来看叫做系统调用,从上层应用的角度来看叫做API,系统调用和API看起来很像又不太一样,但是它俩又有很强的关联。

5.3 操作系统的作用之二

操作系统的第一个作用是从单个进程的角度来看的,操作系统是一个更加高级的抽象计算机。现在我们从多个进程的角度来看,又发现操作系统其实是个计算机资源管理器。这和操作系统的标准定义又很契合,我们来看一下操作系统的标准定义:操作系统是管理计算机硬件与软件资源的计算机程序。操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入设备与输出设备、操作网络与管理文件系统等基本事务。操作系统也提供一个让用户与系统交互的操作界面。

既然操作系统是计算机资源管理器,那么我们先来看一看计算机都有哪些资源呢?从第二章我们可以看出,计算机最主要的资源有CPU、内存、外存、外设,因此我们可以很轻易地看出操作系统的四个必要组成部分分别是CPU管理、内存管理、外存管理、设备管理。CPU管理就是CPU时间管理就是进程调度。内存管理是对内存资源进行管理,包括物理内存的管理和虚拟内存的管理。外存管理包括文件系统和磁盘驱动两部分。设备管理是最复杂的,一般都是由内核提供基础代码和驱动模型,由各个硬件厂商具体去写自家设备的驱动程序,系统还会制定用户空间接口,提供一些基础操作库。由于Linux支持的设备众多,因此设备驱动代码是Linux里面代码最多的部分,占Linux总代码的比例高达60%多。

操作系统管理计算机资源总体上有四种模式,分别是:1时间分割管理,2空间分割管理,3独占性管理,4直接管理。我们把资源分为共享性资源和非共享性资源。共享不共享是相对于进程来说的,有些资源直接对整个系统起作用,对进程不可用,我们把系统对这种资源的管理方法叫做直接管理,例如中断控制器。对于共享性资源,我们要根据资源自身的特点采取相应的管理方式。资源的特点包括时间分割相似性和空间分割相似性。时间分割相似性是指在一个较少的时间片上资源仍然是可用的。空间分割相似性是指把资源分割成若干份,每一个仍然能达到资源本身的作用。对于具有时间分割相似性的资源我们采取时间分割管理,对于具有空间分割相似性的资源我们采取空间分割管理,如果两者都不具有我们采取独占性管理。

CPU管理:

对CPU应该怎么管理呢,我们来思考一下。我们能不能采用空间分割管理呢?试一下,如果用空间分割管理,那么就是把一个CPU分割为n个部分,每个部分都是一个独立的执行单元,n个进程各占一个各运行各的,好像也行,但是根据我们在第二章所学的知识,CPU做不到啊。如果用时间分割管理呢,把时间分成一片一片的,轮流分给每个进程来使用,由于CPU是一条指令一条指令地执行,不同进程之间的指令是没有依赖关系的,所以是可以的。所以我们对CPU采取的管理方式是时间分割管理。对CPU进行时间分割管理就是进程调度。

进程调度需要统计进程的运行时间,被动调度需要有定时器的支持,文件系统需要给文件打上时间戳,用户空间也有查看时间、修改时间、设置定时器的需求。

内存管理:

我们再来看一下对内存应该使用什么管理方式。内存能不能进行时间分割管理呢?能,把时间分成一片一片的,每个时间片就只有一个进程就能独享整个内存。显然这么做很不利于充分使用内存,太浪费了,违背了多快好省地运行进程的原则。那么能不能对内存进行空间分割管理呢?能,每个进程占用一小块内存进行运行,这样能充分利用整个内存。这么做有没有什么缺点呢?有,内核和各个进程都在物理内存里,大家都可以相互访问甚至破坏别人的内存,显然是不安全的。对此有什么办法呢,最先想到的也是最直观的方法就是分段机制,修改CPU的运行原理,实现分段机制,内核、每个进程各自运行在各自的段里面。由于是在CPU硬件上实现的限制,谁也无法访问自己段外面的内存,这样就实现了进程隔离,非常安全了。但是这么做有没有缺点呢?有,物理内存本来就少,分完段之后物理空间就更小了,而且还有一个缺点就是程序启动的时候就要把程序全部加载到内存才行,不然运行到没有加载的程序就崩溃了。对此有什么解决办法吗,最刚开始想的办法是软件的,每个程序要自己要写个overlay manager,程序员要对自己程序的执行流程有清晰的认识,自己控制何时把哪部分即将会用到的程序加载到内存,何时将哪部分暂时不会使用的程序再放回外存,这样程序就可以使用较少的内存也能顺利运行了。但是这个做法非常麻烦,对程序员的考验非常大,要解决这个问题还是得使用硬件机制才行。于是后来就产生了虚拟内存/分页机制,每个进程都运行在虚拟内存上,并且独占整个虚拟内存空间,这样就实现了进程隔离,非常安全,同时进程并不立即分配物理内存,而是程序在运行的过程中产生page fault 按需调页,用到的时候再去分配物理内存,这样就可以节省物理内存。所以Linux对内存的管理方式是在形式上每个进程都独占整个虚拟内存空间,在实际上进程之间对物理内存进行空间分割管理,而且是延迟分配,用到的才分配,用不到就不分配,又节省了物理内存,分页机制真是太完美了。

外存管理:

显然,外存也是采用空间分割管理的。

外设管理:

有些设备比如相机,既不具有时间分割相似性,也不具有空间分割相似性,只能对其进行独占性管理。我们可以来假设一下,如果对相机进行时间分割管理的话,比如说A进程用了0.5秒拍照拍了一半,B进程又用了0.5秒拍照拍了一半,然后A进程又用了0.5秒把整个照片拍完了,接着B进程又用0.5秒把整个照片拍完了,显然这是不可能的,因为相机做不到。我们再假设对相机进行空间分割管理,比如说这个相机拍照的分辨率是1000×800的,A、B两个进程同时用相机,分别拍出了一个1000×400的照片,这也是不可能的,相机做不到。所以对相机只能采取独占性管理,每个进程在使用相机前都要先申请,使用完后再释放,使用期间进程对相机是独占的。

计算机中除了CPU、内存、主板、总线等之外的设备都叫做外设,内核对外设的管理叫做设备管理,也是操作系统中非常重要的一块。外设的运行需要驱动程序,内核一般会提供总线的驱动和一些设备类型的框架代码,然后由各个硬件厂商去编写具体的驱动程序。想学习Linux驱动编程的话,推荐阅读《Linux Device Drivers》《Essential Linux Device Drivers》《Linux设备驱动开发详解》。

有些设备,并不是单单有个驱动就能独立运行了,而是与用户空间库和进程一起构成了一个新的系统,比如网卡和显示器。网卡需要网卡驱动,但是只有网卡驱动,网卡还是没有用,网卡与网卡驱动还有网络协议还有一些相关的硬件软件共同构成了网络系统。再说显示器,光有个显示驱动也是没有意义的,显示器只是计算机图形系统中的一个组成部分。计算机图形系统还包括GPU、渲染库、窗口系统等很多组件。

中断机制:

计算机中外设与CPU的交互方式,除了CPU可以控制外设之外,外设也可以主动向CPU报告事情,避免了CPU轮询外设的低效行为,这种方式就叫做中断。中断除了这个作用之外还有其它一些作用,比如可以用来处理CPU异常、用来实现系统调用,定时器中断还可以用来实现抢占式多任务。所以中断机制对操作系统来说是非常重要的,重要性就相当于人的神经系统加呼吸系统一样重要。

编程设施:

计算机除了管理硬件之外,还为软件提供了两个编程设施,分别是线程同步和进程间通信。线程同步是因为多个执行流(线程)同时访问公共数据引起的,如果不采取一些措施的话就会产生程序错误,因此操作系统提供了线程同步这个编程设施来解决这个问题。线程同步中有一个非常常用的方法叫做自旋锁。

进程间通信是因为有了虚拟内存/分页机制之后产生了进程隔离,进程之间无法直接访问对方,所以需要通过内核提供的进程间通信机制来进行沟通,具体内容请参看《深入理解Linux进程间通信》。Linux还有一个重要的机制叫做信号机制(signal),它和IPC很像,但又不是典型的IPC,也不仅仅是IPC,它还是系统处理CPU异常的方法。

其它模块:

操作系统还有两个重要的模块:一个是和社会因素有关的安全防护,一个是和物理因素有关的电源管理。电源管理包括基本电源管理和高级电源管理,基本电源管理包括:睡眠、休眠、关机、重启,高级电源管理也就是各种省电机制,如CPUIdle、CPUFrequ、DEVFreq,另外温控也可以算到电源管理里面。

六、计算机运行模型

学了上面这些知识之后,我们心里要有几幅计算机的运行模型图,达到计算机就在我心中运行的境界。一是CPU运行模型图,二是进程调度图,三是总体软件体系结构图。

6.1 CPU运行模型

最简单的CPU模型是图灵机模型,非常简单,就是一个一直在线性运行的机器。其次是带中断的图灵机模型,这个模型可以在正常的执行流之外插入一段额外的执行流。然后是具体的CPU运行模型,进程一般运行在用户空间,借助中断机制可以陷入内核执行一些系统调用,外设发生中断会执行中断处理函数。

实际的CPU运行模型在这个基础之上又变得非常复杂了,如下图所示。在学习了中断机制、系统调用和进程调度之后,对下面这几张图应该能理解地比较透彻。

我们在这里说一下CPU执行场景,场景就是英文中的context,一般都翻译成上下文,我看到有一本书上翻译成场景,觉得这个翻译非常信达雅,所以后面就用场景这个翻译了。CPU的执行场景一共有两种:一个是进程执行场景,一个是中断执行场景。进程执行场景分为两种情况:用户空间进程执行场景和内核空间进程执行场景。进程一般在用户空间运行,发生系统调用时会进入到内核空间运行。中断执行场景只有一种情况,那就是内核空间中断执行场景,因为中断不能运行在用户空间。中断执行场景和进程执行场景还有一个很大的区别就是进程执行场景可阻塞可调度,中断执行场景必须一次性执行完,不能阻塞,因为它不可调度。

6.2 进程调度模型

在CPU运行模型的基础上,CPU在用户空间执行进程,可以通过系统调用进入内核空间,也可能由于中断或者异常发生而进入内核空间,在内核空间里进程可以由于阻塞而主动发生进程调度,或者在定时器中断里面由于时间片耗尽而发生调度。

图片展示的是UP(单处理器)调度的情况,每个时刻最多只有一个进程会被选中在CPU上运行。如果是SMP(多处理器)的话,就是有NR_CPU个这样的图同时运转,互不干扰,这是从运行的角度看是这样的,但是如果从空间的角度看的话,因为内核空间只有一个,所以图片应该是中心捏合在一起,但是翅膀各是各的那种样式的图。

对于调度器是如何选择进程进行调度的,我们心中要有下面这张图。

6.3 软件体系结构

下面我们再来看一下整个软件体系放在一起是什么样的:

我们可以看到内核运行在硬件之上,进程运行在内核之上,有init等进程是作为操作系统的一部分共同维护整个系统的运行。内核里面包括进程调度、内存管理、文件系统、BIO层、网络模块、其它驱动等模块。进程是由一个exe主程序和n个so库程序组成。每个进程中的libc.so,ld.so等库程序都是属于操作系统的一部分。所有进程通过进程调度共享CPU、轮流执行。

参考文献:

《Operating System Concepts》《Operating Systems Design and Implementation》《Operating Systems Three Easy Pieces》《Operating Systems In Depth》《Operating Systems Internals and Design Principles》《Operating Systems A Concept-Based Approach》《现代操作系统》(陈海波/夏虞斌著)《Windows Internals 7th Part 1》《Windows Internals 7th Part 2》《Mac OS X and iOS Internals》《Mac OS X Internals A Systems Approach》《Linux Kernel Development》《Understanding the Linux Kernel》《Professional Linux Kernel Architecture》《Mastering Linux Kernel Development》《Understanding the Linux Virtual Memory Manager》《Linux内核深度解析》《Linux操作系统原理与应用》《深度探索Linux操作系统》《ARM Linux内核源码剖析》《奔跑吧Linux内核》《Linux内核源代码情景分析》《Linux内核设计的艺术》《Linux内核完全注释》

作者简介:

程磊,某手机大厂系统开发工程师,阅码场荣誉总编辑,最大的爱好是钻研Linux内核基本原理。

推荐内容