我学习操作系统,向来不喜欢死记硬背,无论是当初上大学的时候,还是现在为了夯实基础重新开始学习。我一直觉得操作系统是计算机中最有魅力的一个方向,并且坚信,现代操作系统的原型有如今这样一番样貌都是通过一点一点的改进而形成的。所以,在学习操作系统的时候,我更喜欢经常反问自己,比如为什么需要进程这个概念?为什么需要lru算法,其他的算法有什么优劣。我相信,只有你真正把一个现象想通了,并且知道了他的来龙去脉,才能最终在你的脑海里帮你形成一张操作系统的网络。

为什么会有进程这个概念

当一台服务器同时接到很多网络请求的时候,如果现在让你设计这台服务器的操作系统处理请求的逻辑,你想怎么办呢?是想让这些请求都排着队一个接一个的处理,还是说以阳光普照的形式,给每一个请求都一个执行的的机会,即使某个请求在某次机会上并没有被完全处理。稍加思考就可以知道,以请求的发起者来考虑,肯定不想自己的请求排在后面,并且一旦位置靠前请求包含了些昂贵的操作,如i/o等,那么排在队尾耗时较少的请求很可能就会等待非常多的时间。这显然是不合理的。

为了照顾每一个到来的请求,我们必须要想出一个办法来管理这些请求的执行,切换,以及关闭。进程的概念由此提出。 在操作系统中,进程定义是一个正在运行的程序。如果接着上面的例子来讲的话,一个请求可能就会启动一个进程来进行处理,那么多个进程执行的时候就需要对进程进行切换,挂起,保存上下文等操作。就单核的计算机来讲,某一瞬间只有一个进程在执行,但是一秒之内却有很多进程在执行,这就给使用者造成了一种错觉,计算机在并行的执行我们的请求。其实上面说的通过进程间切换的方式达到的仅仅是一种伪并行的效果,严格来讲应该叫做并发。只有在硬件层面多出几个cpu的时候,才是真正意义上的并行。

进程的概念以及它的数据结构,都为操作系统对进行的操作和管理提供了极大的便利。

进程的模型

一个进程通常包括以下几部分:数据,程序,进程相关的信息。在一个多道程序的操作系统中,会在不同的进程间进行切换,每一个进程所获得执行时间可能是不同的,并且在某一个时间点也并不能确定是哪个进程在运行。进程和程序将比较起来,按我的理解,两者主要的区别就是:进程是动态的但是程序是静态的,进程不单单需要程序,还需要处理一定的数据甚至有的进程还会最终输出一些数据。cpu通过一定的调度算法去管理系统中众多进程的运行和挂起。如果同一份程序运行了两个,那么内存中将会有两个进程的信息,但是他们共享的程序代码在内存中仅有一份。

创建进程

其实在我对操作系统的了解当中,无论是用户自己主动创建的,还是操作系统创建的,亦或是操作系统本身,我觉得他们都是属于进程的。为什么会这么想呢?很简单,因为进程是正在运行的一段程序,有了这个概念你就知道,无论是什么软件还是操作系统本身都是一段在内存当中运行的程序。要创建一个进程的方式有很多种:

  1. 用户自己主动创建,如用户点击一个软件的图标
  2. 系统初始化,操作系统在初始化的时候为了能让我们正常的使用它,肯定会启动一系列的进程
  3. 正在执行的进程创建它们的子进程
  4. 批处理作业的初始化

系统在初始化的时候会启动很多进程,这些进程有运行在“前台”的,也有运行在“后台”的。在前台的进程负责和用户进行交互,后台的进程和用户无关,但是却在后台提供着相应的服务。这类没有父进程的进程被称作是守护进程,他们一直驻留在后台,等待提供服务。

通过上面所讲的集中进程的创建方式来看,一个独立的进程通常都是被一个正在运行当中的进程通过调用一个系统调用所创建而来的(至于我刚才说的守护进程是没有父进程的,这是比较特殊的一种情况,它和unix操作系统当中的进程层级有关,涉及到一种特殊的行为叫“脱壳”),unix系统中与创建进程唯一相关的一个系统调用就是fork。

在一个进程调用了fork创建一个新的进程的时候,操作系统会创建一个有着与其父进程同样副本的新进程。在刚刚创建完成的时候,子进程和其父进程的内存空间里的内容是一样的,是直接copy了父进程内存空间中所有的数据。但是父进程和子进程的地址空间是不一样的,也就是说,两个进程在内存当中有着不同的位置,占用了两份空间。如果你确实有认真思考fork函数的行为就知道,到此为止fork只是创建了一个子进程而已,但是子进程的内存空间中存的还是父进程相关的数据以及代码,我们只有紧接着通过指定一些参数来调用execve函数,将一个新的程序load到子进程的内存空间中运行,此时才算做是真正的启动了一个新的进程。如shell一般就是先创建一个子进程,然后把我们指定的程序load到刚刚创建的进程的内存空间中进行执行。unix之所以将创建一个新进程的操作设计成两段,就是因为在执行完fork函数之后,子进程有时间调整自己对一些文件描述符的控制以及对标准输出,输入,错误等重定向的问题。这和window平台略有不同,window中都是通过调用一个CreateProcess函数一部到位完成所有的操作,因为该函数所接受的参数也是较多的。

终止进程

进程被终止的几种场景其实也很容易想到。

  1. 程序正常执行完毕
  2. 遇到错误了,但是命中了程序正常的错误处理逻辑
  3. 严重的错误,无法执行下去
  4. 被其他进程干掉

其中前两种场景没什么好说的,都是我们预料之中的。第三种是因为一些比较严重的错误,如除数为0等,无法继续执行下去。此时正常情况来讲是应该马上终止这个进程的执行,但是在一些操作系统中允许进程自己捕获一些因某些错误系统发送的中断信号,从而按照自己的意愿来处理这个错误。

进程的层次结构

操作系统在启动的时候,必定是有一个进程来负责一些初始化工作的,然后以此创建更多的与系统相关的进程,直到系统启动完成。这个负责初始化的进程叫做init。也就是说,一个unix系统当中所有的进程都是以Init这个初始化的进程为起点创建出来的。系统中所有的进程结构类似于unix目录,是一个树形结构,根节点就是init进程。一棵树有层级之分,那么unix系统中的进程也是有层级之分的,进程之间的层级主要体现在父进程和子进程上面,一个进程只有一个父进程,但是可以有很多子进程。能操作这个进程本身的只有他的父进程。window则不同,虽然它也有父子进程的概念,但是因为父进程掌握了子进程句柄,并且可以把它传递给其他进程来操作其创建的子进程,这样一来就没有严格的进程层级关系了。

这里还要提到的,就是我们之前说过的守护进程。守护进程是一系列没有正在运行父进程的进程的集合,它们的进程层级仅位于init进程之下。之所以说守护进程没有正在运行的父进程,是因为守护进程在被其父进程创建并且开始运行之后终止了其父进程,这种行为就叫做“脱壳”。但是这种kill掉自己父进程但是自己还完好无损的行为在一些系统上是不可行的,因为有的系统一旦父进程被干掉,随之他所有的子线程也都会被干掉。

进程的三种状态

本科上操作系统课程的时候,学过进程的三态图,它标识了进程在操作系统中的三种状态:

  1. 阻塞态
  2. 运行态
  3. 就绪态

其中就绪态和运行态之间的转换是由操作系统的进程调度程序来完成的。系统为了对所有的进程一视同仁,都给了他们一定的运行时间。所以,某一个进程不能一直占用着cpu时间,进程调度程序可以决定下一时刻哪个进程可以运行,运行多长时间,哪个进程这一轮的cpu时间已经消耗完毕要转入就绪态。相反,就绪态中的进程也会按照顺序一个一个的去享受属于他们的cpu时间执行相应的操作。

一个进程如果在运行的过程当中需要依赖一些其他程序的运行结果或者说一些外部事件发生才能够继续向下执行的时候,进程就进入了阻塞态。如果其等待条件一直没有满足,那么是有可能饥饿或者饿死的。一旦条件满足,进程就马上会进入就绪态,等待系统给他分配cpu运行时间执行相应的操作。

从进程模型的角度来考虑,在某一时刻内,系统中肯定有一个正在运行的进程,以及一些和系统相关的进程。如磁盘进程,终端进程等。和系统相关的进程此时应该都处于阻塞状态,当用户或者正在运行当中的进程出发了一些条件,就可能会唤醒阻塞的进程。操作系统中对于进程的运行,终止以及中断等操作的逻辑都在进程调度的程序当中,可以理解为进程调度这个程序是在底层的,其上层就是因为各种原因创建的普通进程,进程调度根据系统的信号管理着上面的普通进程。

为什么三种状态之间只有四种转换

按理来说,三个状态之间应该有6种转换才对。实际上,阻塞态和运行态是可以相互转化的,在cpu空闲的时候,如果一个刚刚被阻塞的进程所需的运行条件很快被满足,那么该进程是可以从阻塞态直接进入运行态的。但是就绪态是不可能像阻塞态进行转化的。因为处于就绪态的进程,首先是没有可用的cpu时间,其次阻塞态明显是一个运行的进程因为某些条件不满足而进入的下一个状态,处于就绪态的进程不可能做出任何的事情的,只有运行的进程才能被阻塞。