前言
这个系列是《操作系统导论》的读书笔记。力求简洁、清晰、易懂。
1.OS
Operating System,位于硬件与上层应用之间一种特殊的软件,它负责提供硬件的抽象,与上层应用直接沟通,确保整个系统维持在一个高效稳定的状态。构建它要考虑多个方面的因素,分别是抽象,高性能,可靠性 ,安全,能耗。在移动设备盛行的今天,还需要考虑操作系统的可移植性。
抽象:作为操作系统中最基本的一个目标,它的功能是让操作系统便于理解和使用,因此操作系统引入诸如“进程”,“文件”,“虚拟存储器”的抽象概念,使得用户编写应用程序时只考虑相应的抽象接口即可,无须了解硬件设备的底层原理。如图1-11所示。
高性能:在其他要求满足的情况下,操作系统在运行时应尽可能地减少开销,包括时间上(更少的指令)和空间上(更少的内存和磁盘)。
可靠性:所有的应用程序都运行于操作系统之上,这要求OS必须足够可靠以保证应用程序的顺利运行。
安全:应用程序之间并发运行,要求OS能控制它们的边界和行为。当有应用程序突破了OS的"封锁",对其他应用程序或者对OS造成了恶意影响时,OS能将影响降低到最低。
图1-11
OS的功能可分为三个部分,虚拟化(virtualization),并发(concurrency),持久性(persistence)。
2.虚拟化
CPU虚拟化:为满足不同程序对CPU资源的要求,操作系统将CPU虚拟化成了多个虚拟CPU,以进程的视角来看,就好像是自己一直在占用一个物理CPU一样。
上述OS使用的技术被称为“时分共享”,它给每个进程分配一个独占CPU的时间片,计时结束后CPU切换到下一个进程运行,这能让OS运行多个并发进程。但它也不可避免的带来了一些额外开销,因为进程切换需要额外的时间和空间以保存进程的相关信息,以便此进程后续运行时能够恢复。这个过程称为上下文切换(context switch)。
实现虚拟化需要低级的机制(mechanism)和高级的策略(policy)相互协作。机制是低层的协议或者方法,它属于战术层次,表示虚拟化的步骤具体应该怎么实现,上文中的时分共享就是一个具体的实现方法。策略是OS作出决策的算法,属于战略层次,它聚焦于某一时间段应该对哪个进程提供虚拟CPU才能使得系统运行状态较好。操作系统使用进程的调度策略充当这一角色。分离机制与策略的模块化做法可以提高系统的鲁棒性,需要修改策略时可以不用改变机制的实现。
具体阐述虚拟化的过程离不开操作进程。首先对进程的相关知识进行盘点。
3.进程
顾名思义,进程即“进行中的程序”,它是CPU、内存、I/O设备的抽象表示,是操作系统对正在运行的程序的一种抽象。“进程”概念的出现使得操作系统能够将程序的状态信息和行为信息具体化,并追踪系统内部动态变化的规律,为多道程序处理打下了基础。
进程:一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统进行资源分配和动态执行的基本单元。
操作系统引入进程的概念的原因:
从理论角度看,是对正在运行的程序过程的抽象;
从实现角度看,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机内存运行的程序。
进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为进程。
3.1 特征
3.2 构成
进程由程序、数据、进程控制块(PCB,processing control block)三部分组成。其中进程控制块(PCB)是系统为了管理进程设置的一个专门的数据结构。系统用它来记录进程的外部特征,描述进程的运动变化过程。同时,系统可以利用PCB来控制和管理进程,创建进程即创建进程的PCB,撤销进程即撤销进程的PCB,所以说,PCB(进程控制块)是系统感知进程存在的唯一标志。
3.3 API
上文说过进程是对运行程序的抽象,那么它必然对外提供了接口,让用户在无需了解其内部具体实现的情况下实现对进程的相关操作。
- 创建 (create)
通过fork()和exec()这样一对系统调用来创建新进程,其中fork()用于在一个进程中创建子进程(child)。原来的进程称为父进程。int process_child=fork();
子进程创建完毕,但并不是创建完毕后就立即运行子进程,这依赖于OS的调度策略。
由于在子进程复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。如果运行父进程,此时fork()返回的值是新创建的子进程的进程描述符PID(process identifier)。如果运行的是子进程,它地址空间中的代码不会包含fork()之前的代码,因此子进程从fork()之后的代码开始运行,此时fork()返回的值是0,就好像是子进程调用了自己一样。其实就相当于链表,进程形成了链表,父进程的fork函数返回的值指向子进程的进程id, 因为子进程没有子进程,所以其fork函数返回的值为0。fork()根据父或子进程返回不同值,这样很容易编写出考虑两种不同条件的代码——使用if else
。
大多数时候我们运行的子进程都需要完成与父进程不同的任务,这就需要在fork()的基础上利用系统调用exec()。虽然理论上我们也能在if(process_child)==0
的代码下自己手动实现各种功能,但exec()的出现极大降低了重复造轮子的成本,大部分的程序我们拿来就能用。这是通过给exec()传递可执行程序的名称及相应的参数(如文件名,数据,环境变量等)实现的。exec()是execute的缩写,linux的shell中可使用它来执行其他命令。
注意exec()是一个函数簇,它有多种函数。调用exec()并没有创建一个新进程,而是将执行程序的代码和静态数据以及堆、栈和内存空间覆盖到原进程中,但进程的标识符pid不变。一旦调用exec()之后,原代码中exec()之后的代码不会被执行,因为此时进程已经彻底改头换面,原进程中的代码段都已被覆盖了。
进程创建的具体过程
通过系统调用我们能直接创建进程,这是在利用进程的API。进程创建的具体过程如下:OS首先找到磁盘上程序的位置,将其代码段和静态数据加载到内存中,这个加载过程被称为惰性加载,即程序执行时需要加载的代码才会加载。同时为程序运行栈和堆分配内存,后者需要通过显示请求如malloc()来完成,并执行与I/O设置相关的操作。这一切完成之后,OS将从main()函数开始执行,进程获得CPU的独占权。
-
等待
因为无法判断父进程和子进程谁先返回,但有时候父进程需要等待子进程先完成,这时父进程就用wait()调用来控制,OS将CPU的使用权移交给子进程。 -
销毁
-
其他控制
-
状态
3.4进程状态
既然进程是动态运行的,它肯定也逃不过“生老病死”。具体来说,进程主要有以下几种状态。
状态 | 说明 |
---|---|
运行 running | 进程占用CPU,执行指令 |
就绪 ready | 进程已准备好占用CPU,由于某些原因OS暂时没有给予它使用权 |
阻塞 blocked | 进程在进行另外的操作,无需使用CPU的一种状态,此时CPU的使用权不能分配给它,以免造成资源浪费。常见于进程进行I/O操作时,在完成此操作后,进程才会转为就绪状态,拥有使用CPU的资格 |
3.5 数据结构
操作系统抽象出多种概念以保障其平稳高效地运行。这些“抽象”的具体实现通常是c语言中定义的一个结构体,即一个数据结构,其中包含了各种变量和指针。
进程在具体实现时作为一种数据结构,主要包含了下列信息:
- 寄存器状态(上下文信息)
- 进程自身状态(枚举变量)
- 进程内存分配起始地址和大小
- 内核栈的起始地址
- 进程ID
- 指向父进程的指针
- 指向中断结构的指针
- 指向当前打开文件和目录的指针
4.总结
OS离不开虚拟化,而虚拟化离不开对进程的处理,上文对进程的各项信息做了一个简单的介绍。在此基础上,下篇笔记将要对虚拟化的具体实现——低级机制(上下文切换)和高级策略(进程调度策略)进行一个全面的盘点。
网友评论