(王道408考研操作系统)第二章进程管理-第一节2:进程状态及其切换

Posted 快乐江湖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(王道408考研操作系统)第二章进程管理-第一节2:进程状态及其切换相关的知识,希望对你有一定的参考价值。

一:进程状态

进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变换,使进程的状态也在不断发生变化。通常进程会有以下五种状态,其中前三种是进程的基本状态

  • 运行态(Running): 进程正在处理机上运行,占有CPU。单核处理机下,每一个时刻最多只能有一个进程处于运行态
  • 就绪态(Ready): 已经具备运行条件,但是由于没有空闲的CPU,从而暂时不能运行。系统中处于就绪状态的进程可能有多个,通常将他们排成一个队列,称为就绪队列
  • 阻塞态/等待态(Waiting/Blocked): 进程正在等待某一事件而暂停运行。比如等待某资源为“可用”或等待输入/输出完成。处于该状态下,即使处理机空闲,该进程也不能投入运行
  • 创建态: 进程正在被创建,尚未被转到就绪态。创建进程时首先申请一个空白的PCB,并向PCB中填写一些控制和管理进程的信息,然后由系统为该进程分配运行时所必需的资源,最后把该进程转入到就绪态
  • 结束态: 进程正在从系统中消失,可能是进程正常结束或其他原因中断退出运行。进程需要结束运行时,系统置该进程为结束态,然后再进一步处理资源释放和回收等操作

二:进程状态转换

进程状态转换路线图如下

  • 创建态: 由系统完成创建进程的一系列工作,不出意外接下来会转到就绪态
  • 就绪态->运行态: 处于就绪态的进程被调度后,获得处理机资源,于是进程由就绪态转为运行态
  • 运行态->就绪态: 处于运行态的进程在时间片到之后,不得不让出处理机,从而进程由运行态转为就绪态。 此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,优先级低的会被剥夺处理机的使用权(即使时间片未到),让更高优先级的进程运行
  • 运行态->阻塞态: 进程请求某一资源(比如外设)的使用和分配或等待某一事件的发生(比如I/O操作的完成),这是一种特殊的、由运行用户态程序调用操作系统内核过程的形式
  • 阻塞态->就绪态: 进程等待的事件到来时(比如I/O操作结束或中断结束),中断处理程序必须把相应进程的状态由阻塞态转为就绪态

需要注意以下几点

  1. 由运行态到阻塞态是一种进程自身做出的主动行为;而由阻塞态到就绪态不是进程自己可以控制的,是一种被动行为
  2. 不能由阻塞态直接转为运行态,也不能由就绪态直接转为阻塞态

补充:Linux中的进程状态及其转换

Linux源代码中定义进程状态如下

/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */   -运行或将要运行
"S (sleeping)", /* 1 */  -进程在等待事件完成
"D (disk sleep)", /* 2 */ -此状态进程通常等待IO结束
"T (stopped)", /* 4 */   -停止状态
"t (tracing stop)", /* 8 */ -追踪中
"X (dead)", /* 16 */  -死亡状态
"Z (zombie)", /* 32 */ -僵尸进程
};

(1)几个重要的状态

A:R(running)-运行状态

编写如下C语言文件,使其死循环打印

#include <stdio.h>
int main()
{
	prinf("this ");
}

同时查看该进程,进程后面的参数标识了这个进程的进程状态

但是有一个问题,左侧循环在不断打印,为什么进程状态是S呢。其实刚才的这样的操作属于I/O操作,字符被不断打印在屏幕上,外设的速度是远低于CPU的,所以CPU早都处理完了,但是屏幕上的还没有打印完,所以这里显示的是S。

所以如果修改C语言文件,如下,去掉其中的打印操作

#include <stdio.h>
int main()
{
	//prinf("this ");//去掉打印操作
}


可以发现此时就变成了R状态,因为此时没有输出了。

所以:运行状态并不意味着进程一定在运行当中,它表明进程要么是在运行,要么在运行队列里,等待CPU调度

  • 关于S+R+,其中的‘+’表示该进程是一个前台进程,可以使用ctrl+C终止。在运行程序时,加上取地址符&,比如./a.out &,能使进程到后台运行,后天运行的进程无法使用ctrl+C终止,必须使用命令kill -9 【进程pid】来终止

B:S(sleeping)-睡眠状态

睡眠状态意味着进程在等待事件完成,处于等待队列或阻塞队列中。上面的死循环的例子就是典型的睡眠状态。睡眠状态可以被立即唤醒

C:D(Disk sleep)-磁盘休眠状态

磁盘休眠状态和睡眠状态有点像,区别就是睡眠状态可被中断,也就是立即唤醒,但磁盘休眠状态不可被中断,在这个状态的进程通常会等待I/O结束。

磁盘休眠状态又可以叫做深度睡眠状态

睡眠状态存在着一定的弊端,比如一个程序中某个函数需要磁盘中的某个数据,那么就需要磁盘找到这个数据并拷贝给内存,但是磁盘的速度远远低于内存和CPU,所以当这个进程发出需要这个数据的指令后,磁盘就立马去寻找,由于时间较慢,所以进程就会进入等待状态,如果进程是睡眠状态,而此时假如内存又不足了,CPU就需要终止某些进程以保证系统的稳定性,此时此进程就有可能会被误删,这样等磁盘拿到数据时,进程早已挂掉,因此会引发一定的问题。所以如果这个进程是磁盘休眠状态,那么由于其不可中断的性质,所以不会被CPU杀掉,所以只有这个进程拿到数据后,才能进行下一步。

D:T(stopped)-停止

  • 之前咋们用到过kill -9来杀掉后台进程,其实kill还有其他相关选项

    还是以之前的死循环打印为例

(2)进程状态路线图

(3)特殊的进程状态-僵尸进程

A:僵尸进程是什么

简单点来说:僵尸进程就是子进程已经退出了,父进程还在运行当中,父进程没有读取到子进程的状态,子进程就会进入僵尸状态

B:一个例子

使用下面的C语言程序模拟一个僵尸程序,子进程在10秒后利用exit退出,但是父进程一直在运行

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
   // printf("还没执行fork函数时的本进程为:%d\\n",getpid());
    pid_t ret=fork();//其返回值类型是pid_t型的
    sleep(1);
    if(ret>0)//父进程返回的是子进程ID
    {
      while(1)
      {
        printf("----------------------------------------------------\\n");
        printf("父进程一直在运行\\n");
        sleep(1);
      }
    }
    else if(ret==0)//子进程fork返回是0
    {
      int count=0;
      while(count<=10)
      {
        printf("子进程已经运行了%d秒\\n",count+=1);
        sleep(1);
      }
      exit(0);//让子进程运行10s
    }
    else
      printf("进程创建失败\\n");

    sleep(1);
    return 0;
}

效果如下,可以发现,在10s后,子进程已经退出,父进程还在运行

根据上面的定义,当子进程先退出,父进程还在运行,由于读取不到子进程的退出状态,所以子进程会变为僵尸状态。为了方便演示,使用下面的脚本,来每1s监控进程

while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep;sleep 1;echo "###########";done

效果如下,可以发现当子进程结束后,父进程还是在运行,此时进程太变为Z,也就是僵尸状态

C:为什么要有僵尸进程

其实道理也很简单,子进程是由父进程创建的,父进程之所以要创建子进程,其目的就是要给子进程分配任务,那么在这个过程中,子进程平白无故的没了,而父进程却不知道子进程到底把自己交给它的任务完成的怎么样,成功了还好,失败的话就能再交代一个进程去操作。
所以进程结束时一定要给父进程返回一个状态,父进程一直不读取这个状态的话,那么子进程就会一直卡在僵尸状态,其中像代码这些资源已经被释放,但是这个进程却没有真正退出,因为PCB还在维护它,直到父进程读取到它的状态,才能进入死亡状态

  • 进程控制块中,一个进程退出后,还有一个退出码返回给父进程,如下是Linux内核中关于这部分的定义
  • 在Linux中一行命令就是一个进程,那么这个命令的父进程是bash,那么命令在结束的一瞬间也会给bash返回一个状态码,bash作为父进程,就是依靠这个返回码来判断命令是否正常结束,如果状态码为某一个值即可判定为没有这样的命令。在Linux中可以用echo $?来查看上一个输入命令的状态返回码,命令正确返回0,否则返回非0

(4)特殊的进程状态-孤儿进程

孤儿进程就是父进程没了,子进程还在。那么根据上面的僵尸进程,子进程在退出后由于没有父进程来读取它的状态,所以会一直卡在僵尸状态,那么这样就会存在一个问题,它的内存资源谁来回收,通俗点将就会造成 内存泄漏

修改上面的代码,让父进程先挂,如下

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
   // printf("还没执行fork函数时的本进程为:%d\\n",getpid());
    pid_t ret=fork();//其返回值类型是pid_t型的
    sleep(1);
    if(ret>0)//父进程返回的是子进程ID
    {
      int cout=0;
      while(cout<10)
      {
        printf("----------------------------------------------------\\n");
        printf("父进程运行了%d秒\\n",cout+=1);
        sleep(1);
      }
      exit(0);//让父进程挂了
    }
    else if(ret==0)//子进程fork返回是0
    {
      int count=0;
      while(1)
      {
        printf("子进程已经运行了%d秒\\n",count+=1);
        sleep(1);
      }
    }
    else
      printf("进程创建失败\\n");

    sleep(1);
    return 0;
}

效果如下,这里还有一个现象就是,当父进程挂了之后,子进程一直在运行,并且ctrl+C,无法结束进程。这是因为ctrl+C此时结束的是父进程,但是父进程早已结束,子进程像孤儿一样四处游荡

除非使用killl才能将其杀掉

那么问题来了,这个进程难道一直要占用资源吗,其实操作系统在设计的时候就考虑到了这一步。所以一旦父进程先挂了,那么这个子进程就会被1号进程领养

依然使用下面脚本进行观察

while :; do ps axj | head -1 && ps axj | grep a.out | grep -v grep;sleep 1;echo "###########";done

效果如下,可以发现,当父进程挂了,这个进程的ppid,也就是父进程更换为了1号进程

1号进程是什么呢,其实就是systemd

以上是关于(王道408考研操作系统)第二章进程管理-第一节2:进程状态及其切换的主要内容,如果未能解决你的问题,请参考以下文章

(王道408考研操作系统)第二章进程管理-第一节5:线程概念和多线程模型

(王道408考研操作系统)第二章进程管理-第一节3:进程控制(配合Linux讲解)

(王道408考研操作系统)第二章进程管理-第一节1:进程PCB及其特征

专栏必读王道考研408操作系统万字笔记(有了它不需要你再做笔记了):各章节内容概述导航和思维导图

(王道408考研操作系统)第三章内存管理-第一节2:内存管理的基本概念

(王道408考研操作系统)第三章内存管理-第一节7:非连续分配管理方式之基本分段管理方式