第一次作业:基于Linux的源码分析

1、操作系统是怎么组织进程

1.1什么是进程

一个进程就是一个正在运行的程序。一个进程应该包含以下内容:
(1) 程序的代码,既然进程是一个正在运行的程序,自然需要程序的代码
(2) 程序的数据
(3) CPU寄存器的值,包括通用寄存器,程序计数器
(4) 堆(heap)是用来保存进程运行时动态分配的内存空间
(5) 栈(stack)有两个用途,1保存运行的上下文信息。2在函数调用时保存被调用函数的形参或者局部变量
(6) 进程所占用的一组系统资源,如打开的文件

1.2 进程的特性

动态性、独立性、并发性是进程的三大特性。

A 动态性

在程序运行的过程中,它的状态是在不断变化的。例如一个程序在运行过程中,它是一条指令接着一条指令执行,而每执行一条指令,CPU中那些通用寄存器的值也会发生变化,程序计数器(Program Counter)的值也在变化,每次都指向下一条即将执行的指令。另外堆和栈的内容也在不断变化,数据在不断进栈出栈,堆空间在不断分配和释放。总之变化无时无刻不在进行。

B 独立性

一个进程是一个独立的实体,是计算机系统资源的使用单位。每个进程都有"自己"的寄存器和内部状态,在它运行的时候独立于其他的进程。当然这个"自己"是带引号的,也就是说:在物理上,CPU中只存在一套寄存器,如PC寄存器只有一个,但是没有进程都有属于自己的逻辑上的PC。物理上的寄存器是真正的硬件寄存器。

C 并发性

对于单CPU的情况,从宏观上来看,每个进程是同时在系统中运行的,而实际上从微观上来看,在某一特定时刻,只有一个程序运行,换言之各个进程之间实际上是一个接一个顺序运行的。因为CPU是有一个,那么某一个时刻只能有一个进程去使用它。

1.3 进程的创建和终止

以下几类操作可以创建和终止线程。

A 创建进程

(1) 系统初始化会创建新的进程

(2) 当一个正在运行的进程中,若执行了创建进程的系统调用,那么也会创建新的进程

(3) 用户发出请求,创建一个进程

(4) 初始化一个批处理作业时,也会创建新的线程

从本质上来说在技术上只有一种创建新进程的方法,即在一个已经存在的进程中,通过系统调用来创建一个新的进程。如Linux中可以使用fork函数来创建新进程。windows中可以用createProcess函数来创建新进程。

B 进程终止

(1) 正常退出

(2) 错误退出

(3) 致命错误

(4) 被其他进程kill

 

1.4 进程控制块(PCB)

上节中说是基本概念,那么在实际操作系统中,如何设计和实现进程的机制呢?首先就是要设计一个合理的数据结构用来描述进程的概念。这种结构就是进程控制块(Process Control Block,PCB)。系统为每个进程维护了一个PCB,用来保存与该进程有关的各种状态信息。PCB只是基本原理中的说法,对于一个真实的操作系统可能不叫PCB,比如Linux中叫做任务结构体(task struct)。示例如下
struct task_struct 
{
volatile long state;
pid_t pid;
unsigned long timestamp;
unsigned long rt_priority;
struct mm_struct *mm, *active_mm
}

前面讲过逻辑寄存器和物理寄存器的概念,物理寄存器只能有一份,但逻辑寄存器则是每个进程都有一份,逻辑寄存器是通过内存变量来实现的,那么这些存储的变量就对应着PCB的相应字段。

 PCB是进程的唯一标识。每生成一个新的进程,就要为它创造一个PCB然后对其进行初始化。若要撤销一个进程时只要回收PCB即可。进程切换就可以通过操作PCB进行。

 

2.进程状态如何转换

2.1 进程的状态

一个进程并不是总是占用着CPU的,那么如何描述一个进程的当前状态呢?一般来说一个进程被创建开始,一直到它的生命结束为止,它只可能存在三个阶段:运行、就绪、阻塞。当然在具体实现中有的操作系统的状态个数可能不是三个。如可能把就绪和阻塞统称为暂停状态,或者是新增两个状态:创建和结束。但最核心的还是运行、就绪、阻塞。

A 运行状态

进程占用着CPU,并且在CPU上运行。显然处于这种状态的进程数量<=CPU的数目。若只有一个CPU那么任何时刻最多只能有一个进程处于运行状态。

B 就绪状态

进程已经具备了运行的条件,但由于CPU正忙着运行其他的进程,所以暂时不能运行。不过只要把CPU分给它,它就立刻可以运行。正所谓万事俱备只欠东风。

C 阻塞状态

进程因为某种事件的发生暂时不能运行的状态。例如它正等待某个输入输出的操作完成,或者它与其他线程之间存在同步关系,需要等待其他进程给它输入数据。这种情况下即使CPU已经空闲下来,这个进程还是不能运行。

D 状态切换

运行可转化为阻塞、就绪。

阻塞可转化为就绪。

就绪可转化为运行。

你的自行车坏了,需要推到修车师傅那里去修理。运行状态:若师傅很空闲立即给你修车。就绪状态:若师傅忙着修别的自行车,让你的自行车在旁边等待,师傅一有空就帮你修忙。阻塞状态:若你的自行车需要一个新坐垫,但是师傅没有这个坐垫,即使师傅空闲下来,也不能帮你修车。

2.2 状态队列

对于一个分时操作系统而言,假设时间片是20ms这意味着1s内需要进行50次进程切换。另外有些进程处于就绪状态,有些进程处于阻塞状态,它们的阻塞原因又不尽相同。那么对于一个操作系统而言,采用什么样的方式把所有的进程的PCB组织起来,将会直接影响到对进程的管理效率。

操作系统采用的方法是维护一组状态队列。处于运行状态的进程构成运行队列,处于就绪状态的进程构成就绪队列,处于阻塞状态的队列处于阻塞队列。若一个进程从运行状态变为就绪状态,或者从阻塞状态变为就绪状态,就把它的PCB从一个状态队列脱离出来,加入到另外一个队列中。

 

3.进程是如何调度的

进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用SCHED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_RR调度策略。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照FIFO(一次机会做完)或者RR(多次轮转)规则调度的。

3.1实时进程的调度

  实时进程,只有静态优先级,因为内核不会再根据休眠等因素对其静态优先级做调整,其范围在0~MAX_RT_PRIO-1间。默认MAX_RT_PRIO配置为100,也即,默认的实时优先级范围是0~99。而nice值,影响的是优先级在MAX_RT_PRIO~MAX_RT_PRIO+40范围内的进程。

  不同与普通进程,系统调度时,实时优先级高的进程总是先于优先级低的进程执行。知道实时优先级高的实时进程无法执行。实时进程总是被认为处于活动状态。如果有数个 优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序选择进程。假设当前CPU运行的实时进程A的优先级为a,而此时有个优先级为b的实时进程B进入可运行状态,那么只要b<a,系统将中断A的执行,而优先执行B,直到B无法执行(无论A,B为何种实时进程)。

   不同调度策略的实时进程只有在相同优先级时才有可比性:

   1. 对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。由此可见相当霸道。

   2. 对于RR的进程。一旦时间片消耗完毕,则会将该进程置于队列的末尾,然后运行其他相同优先级的进程,如果没有其他相同优先级的进程,则该进程会继续执行。

   总而言之,对于实时进程,高优先级的进程就是大爷。它执行到没法执行了,才轮到低优先级的进程执行。等级制度相当森严啊。

3.2各linux系统的进程调度

“优先级”明确了哪个进程应该被调度执行,而调度程序还必须要关心效率问题。调度程序跟内核中的很多过程一样会频繁被执行,如果效率不济就会浪费很多CPU时间,导致系统性能下降。

在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序需要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);

在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每一个链表代表一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出位于链表头的进程即可。这样就大大提高了调度程序的效率,复杂度为O(1);

在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(可以想象成平衡二叉树)中。每次调度,调度程序需要从树中找出优先级最高的进程。复杂度为O(logN)。

3.3红黑树算法(R-B Tree)

R-B Tree,全称是Red-Black Tree,又称为“红黑树”,它一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。

红黑树的特性:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

注意
(01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
(02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

红黑树示意图如下:

 

红黑树的基本操作:

将一个节点插入到红黑树中,需要执行哪些步骤呢?首先,将红黑树当作一颗二叉查找树,将节点插入;然后,将节点着色为红色;最后,通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。详细描述如下:

第一步: 将红黑树当作一颗二叉查找树,将节点插入

 第二步:将插入的节点着色为"红色"

第三步: 通过一系列的旋转或着色等操作,使之重新成为一颗红黑树

根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点。
    处理方法:直接把此节点涂为黑色。
② 情况说明:被插入的节点的父节点是黑色。
    处理方法:什么也不需要做。节点被插入后,仍然是红黑树。
③ 情况说明:被插入的节点的父节点是红色。
    处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。

 

4.个人对Linux操作系统进程模型的看法

从linux 2.6早期到近期linux 2.6版本,调度程序选择进程时的复杂度反而增加了,我认为这是一种进步。

因为,与此同时,调度程序对公平性的实现从上面提到的第一种思路改变为第二种思路(通过动态调整优先级实现)。而O(1)的算法是基于一组数目不大的链表来实现的,按我的理解,这使得优先级的取值范围很小(区分度很低),不能满足公平性的需求。而使用红黑树则对优先级的取值没有限制(可以用32位、64位、或更多位来表示优先级的值),并且O(logN)的复杂度也还是很高效的。

初次了解Linux系统,对其中的一些细节还并不是很了解,本文经过查阅了众多资料后,结合了自己的一些想法,兴许有些错误的理解,希望能与大家多多交流学习,共同探讨更深层次的内容

 

 
posted @ 2018-05-02 14:15  LeonardCohen  阅读(195)  评论(0编辑  收藏  举报