点我查看操作系统秘籍连载


进程状态以及状态转换

进程并非总是处于运行中,例如CPU没运行在它身上时它就是非运行的。进程在创建之后会改变状态,不同的状态之间可以实现状态切换,可以通过ps或top等命令捕获进程的状态。包含以下几种状态:

  • 创建态(new):进程正在被创建中,过程非常短暂,用户无法捕捉
  • 运行态(running):进程正在执行中,即CPU正在该进程上
  • 就绪态(ready):进程已经准备好可以运行,存放在就绪队列中等待被调度,操作系统调度时将从就绪队列中选择下一个要运行的进程
  • 阻塞态(waiting/sleeping/blocking):也称为睡眠态,进程因为某些原因(如等待IO完成或等待其它事件发生)停止了、睡眠了、阻塞了,CPU转让出去,调度时也不会调度到它
  • 终止态(terminated):进程执行完成或发生某种特殊事件,进程将退出,但还未被内核清理(显然,如果已被清理,任何手段都无法捕获到该进程的信息),这就是终止状态,也是**僵尸态(Zombie或Defunct) **

进程在发生某些事件之后会改变自己的状态,各状态之间的转换方式如图:(如果不好理解,可参考稍后的示例分析)

其中:

上面的状态转换中,主要关注的是运行态、就绪态、睡眠态这3者之间的转换。

运行态转为就绪态表示当前正在执行的进程已经耗尽了分配给它的时间片,只能交出CPU的控制权,自己进入到就绪队列等待下次被调度选中后继续运行。

就绪态转为运行态表示调度器从就绪队列中调度进程时,正好选中了该进程,于是该进程将获取到CPU并开始执行。

运行态转为睡眠态一般是等待某事件的出现,在事件出现之前,进程无法继续执行,只能先暂停进入到睡眠态,例如等待信号通知、等待IO完成。信号通知很容易理解,而对于IO等待(比如想要从磁盘文件中读取一行数据,等待用户敲下一个字符,等待数据全部输出到终端屏幕等等),假如在发生IO等待的时候进程继续保持运行态,它必将继续持有CPU直到IO的完成,但这个时候的进程根本没有继续向下执行,而是完全处于无谓的等待中, CPU在这时候也没有做任何事情,处于空转状态,由于IO操作相比于CPU来说是非常慢的,而CPU是极其珍贵的资源,不能随意浪费,所以出现IO等待的进程都应该进入阻塞状态,交出CPU让它去处理其它进程。

睡眠态转为就绪态表示睡眠的进程所等待的事件已经发生了,这个睡眠的进程已经可以继续执行了,于是内核唤醒该进程,将其放入到就绪队列中等待下次被调度到继续执行。

注意没有”就绪 -> 睡眠”和”睡眠 -> 运行”的状态切换,这很容易理解。不存在”就绪 -> 睡眠”是因为就绪态的进程本就是停止的,当然不可能发生因等待某些事件而进入到睡眠态,只有正在运行中的进程才可能会需要等待某些事件才进入睡眠态。不存在”睡眠 -> 运行”是因为调度器只会从就绪队列中挑出下一次要运行的进程,所以睡眠态的进程等待的事件完成后,也必须先放入到就绪队列中,才能等待被调度执行。

关于进程的状态,还有几点需要说明。

  1. 睡眠态是一个非常宽泛的概念,分为可中断睡眠(interruptiable sleep)和不可中断睡眠(un-interruptiable sleep)
  • 可中断睡眠是允许接收外界信号和内核信号而被唤醒的睡眠,绝大多数睡眠都是可中断睡眠,能ps或top捕捉到的睡眠也几乎总是可中断睡眠;
  • 不可中断睡眠只能由内核发起信号来唤醒,外界无法通过信号来唤醒,只能在事件完成后由内核唤醒,主要表现在和硬件交互的时候。例如cat一个文件时需要从硬盘上加载数据到内存中,在和硬件交互的那段时间一定是不可中断的睡眠,否则在加载数据的时候突然被人为发送的信号手动唤醒,而被唤醒时和硬件交互的过程又还没完成,那么即使唤醒了也没法将cpu交给它运行。而且,不可中断睡眠若能被人为唤醒,更严重的后果是硬件崩溃。由此可知,不可中断睡眠是为了保护某些重要进程,也是为了让cpu不被浪费。
  1. 终止态表示的是进程已被终止(比如已经执行完了所有任务),但是内核还没有将这个进程从内核表项中清理掉。所以,终止态是进程消失前的一个状态,因为如果进程已被内核清理,任何手段都无法去捕获该进程的信息。这个状态其实就是常说的僵尸态,这个状态是非常重要的状态,后文还会详细介绍僵尸进程。
  2. 除了上面描述的几种状态,通常还有一种状态称为stopped状态,它也是一种睡眠态进程,只是比较特殊:它可以通过信号手动唤醒然后继续运行,所以它是一种可中断睡眠状态。之所以要从睡眠态中区分出stopped状态,主要是为shell的作业提供一种控制手段的。例如,可以按下ctrl+z让前台运行的命令进入到Stopped状态,因为进入到了stopped状态就是进入了睡眠态,所以放弃CPU,该进程自然就进入到后台,这其实是发送了SIGTSTP信号给该进程(所以也可以通过kill命令手动发送该信号给进程使其进入stopped状态);另外,对stopped状态的进程可以手动发送SIGCONT信号,使其从stopped状态恢复,也就是唤醒该进程,使其进入到就绪队列等待被调度继续执行,此外,shell作业控制的两个命令fg和bg命令其实在内部都会发送SIGCONT信号。关于信号和shell的作业控制,后面的文章会详细介绍。

示例分析进程转换

前面说了一大段关于进程的状态已经进程状态转换的理论,现在用一个简单的示例分析一下,这个示例如果了解fork和exec会更容易理解,不知道也没关系,稍后会介绍。

假如,在命令行下执行一个”cat a.log”命令。

首先,shell(比如bash)需要解析命令行,比如检查命令行语法,命令行解析完成后(通过fork())创建一个新的进程(bash进程的子进程),并为其分配好内存,进程在创建过程中处于创建态,创建完成后立即放入就绪队列中成为就绪态进程,此时进程还不叫cat进程,而是子bash进程。

当调度到该进程后,进程转变为运行态,它将(通过exec函数)调用cat程序将其加载到内存中覆盖替换子bash进程,这个时候的进程才叫做cat进程,于是cat进程开始执行。

cat进程执行时,发现要读取文件,但是cat进程是用户模式下的进程,它没有权限打开文件,于是通过open()系统调用请求操作系统帮忙打开,于是陷入到内核,内核进程帮忙打开文件后返回一个文件描述符给cat进程并进入用户模式下,cat进程通过该文件描述符读取a.log文件,但是当它开始读数据的时候,cat仍然无权限读取文件数据,于是通过read()系统调用请求操作系统帮忙读取,操作系统将读取到的数据放入内存,然后回到cat进程,cat进程直接从内存中读取数据,并将读取到的数据输出到终端屏幕,但是cat进程仍然没有权限执行写终端硬件(Linux下设备也是文件),于是又发送write()系统调用请求操作系统帮忙写数据到终端,于是数据显示在屏幕上。

上面的过程中,读取磁盘文件数据、输出数据到屏幕都是速度非常慢的IO操作,cat进程都将在这些过程发生时(即IO等待时)进入到不可中断睡眠状态,当IO完成时cat进程将进入就绪态,当再次调度到cat进程时,cat进程将转为运行态。

当输出数据完成后,cat进程将终止退出,于是进入终止态或者僵尸态等待内核清理该进程,直到内核清理该进程后,cat进程将永久的消失。