目录
1. 进程状态
1.1 并行和并发
1.2 时间片
1.3 运行状态
1.4 阻塞(等待)状态
1.5 挂起状态
2. Linux的进程状态
2.1 运行状态
2.2 sleep状态
2.3 Stop状态
2.4 X和Z状态
2.5 孤儿进程
1. 进程状态
如果你看任何一本关于操作系统的教材,都会有类似下面进程状态转换图。但具体的操作系统有Linux,windows等,所以操作系统教材需要上升到一个更高的维度,抽象出所有操作系统的共性并阐述它。因此,我们难以理解下面的进程状态转换图,许多概念很陌生。
本文会从Linux操作系统入手,讲解其进程的状态。
1.1 并行和并发
并行:多个进程在多个CPU下分别,同时进行运行,这称之为并行。
并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内,让多个进程都得以推进,称之为并发。
单个CPU执行进程代码,不是把进程代码执行完毕,才开始执行下一个进程代码。而是给每一个进程分配一个时间片,基于时间片,进行调度轮转。
- 当您使用C/C++编写一个死循环代码并运行时,您可能会观察到程序并不会导致整个系统卡顿。这是因为现代操作系统的调度机制发挥了作用。具体来说,CPU在执行该死循环程序时,并不是无休止地持续运行,而是在用完了分配给该程序的时间片之后,操作系统会包括保存当前程序的状态(代码和数据),然后调度另一个进程来执行。
1.2 时间片
时间片(Time Slice)是一种在操作系统中使用的调度技术。在多任务操作系统中,为了能够公平且高效地共享处理器(CPU)时间,操作系统将一个进程的运行时间分割成许多小的时间段,每一个时间段就是一个时间片。
- 那如果我在听音乐和看视频,为什么我们不会感到卡顿?因为每个时间片通常极为短暂,仅存在于毫秒级别,而CPU的处理速度则是惊人的,每秒能够执行几亿甚至几十亿次基本操作。这样的高速处理能力,使得CPU在各个任务之间迅速切换,而用户几乎无法察觉到这种频繁的交接。
上面基于时间片轮转(time-slicing)技术的操作系统,叫做分时操作系统,主要应用于民用级别计算机。因为这类操作系统的进程调度优先级没有明显的区分,所以要求能调度多个进程,并追求调度任务公平。
与之相对的就是实时操作系统,它更注重任务的及时性和可靠性。主要应用于那些对时间敏感、对任务完成时间有严格要求的领域,如工业控制、航空航天、医疗设备、汽车电子等。
1.3 运行状态
进程 = 内核数据结构(task_struct) + 程序的代码和数据
task_struct结构体对象不仅记录了进程的各种属性,还有某种数据结构来连接多个进程,完成管理所有进程的工作。
当一个程序启动变成进程,先会创建task_struct结构体对象,再将该程序的代码和数据从磁盘加载到内存当中。该进程的pid为1.
此时,操作系统会有一个运行队列结构体,叫做runqueue。它里面有许多属性,其中有个变量类型是task_struct结构体类指针。该指针会连接内存中的task_struct对象。
如果还有更多程序启动,假设里面存在结构体指针,那么新进程结构体对象会被已存在进程对象中结构体指针连接。这样就被runqueue结构体对象管理起来。
当CPU要调度pid为1的进程时,我们假设使用最简单的FIFO(先进先出)调度算法,那么CPU会到runqueue队列中找到第一个task_struct对象,获取该进程main函数地址和数据,放到CPU的寄存器中,并执行该进程。
当CPU调度时间到达该pid为1进程的时间片,该进程会自动记录执行到那行代码和数据,操作系统会将该结构体对象从runqueue队列中剥离下来,连接到最后一个task_struct结构体对象的后面。之后,CPU获取队头的进程代码和数据,执行该程序。
如此循环往复,就完成了基于时间片的轮转调度。
我们也可以得出一个结论,在Linux操作系统中,只要进程在运行队列中,该进程的状态就叫做运行状态。此时表明进程已经准备好了,可以随时被CPU调度。所以不是进程正在被执行,进程状态才是运行状态。一般来说,就绪状态和执行状态可以合二为一,叫做运行状态。
1.4 阻塞(等待)状态
除了CPU内部设备,计算机还有许多外部设备,如磁盘、显示器、键盘和网卡等。
- 操作系统作为管理计算机软硬件资源的核心软件,在处理各类对象时,始终遵循“先描述,后组织”的基本原则。为此,操作系统定义了一个名为“device”的结构体,该结构体内包含了设备的类型、状态等关键属性。
- 同时,为了有效地组织和管理这些设备,结构体还会包含某种数据结构。我们这里结社结构体中还包括了一个指向同类结构体的指针,通过链表这种数据结构,将所有设备对应的结构体对象串联起来,形成一个有序的设备管理链。
如图所示,当CPU调度到标识为PID 1的进程时,若该进程执行到scanf函数并等待用户从键盘输入数据,此时进程将处于等待键盘输入的状态。由于用户输入数据所需的时间通常会远远超过该进程分配的时间片,CPU不可能因此暂停其他进程的调度。因此,该进程会变为阻塞状态,那怎么理解阻塞状态呢?
上面我们解释了操作系统管理硬件时,也会为硬件创建一个结构体。该结构体中会包含一个task_struct结构体指针,叫做waitqueue,即等待队列。操作系统会把等待键盘输入的进程结构体对象从runqueue剥离下来,键盘结构体对象的waitqueue指针会连入该task_struct对象。
如果有其他进程需要等待某个硬件设备的数据,该硬件设备结构体中会存在一个等待队列,连接进程的结构体对象。
因此,我们可以得出以下结论:当进程处于运行状态或阻塞状态时,其对应的PCB将被置于特定的队列中。本质上,进程始终在访问某种硬件资源,当它正在使用CPU资源时,我们称其为运行状态;而当它等待其他外部设备资源时,则处于阻塞状态。
1.5 挂起状态
当内存资源严重不足时,操作系统会将在处于阻塞状态的进程直接放到磁盘中的一个区域,这个区域叫做swap分期,此时进程的状态叫做阻塞挂起状态。
但是由于处于运行状态的进程太多而导致内存资源不足,而运行队列尾部的进程一段时间都不会被调度,操作系统就会将这些进程放到磁盘中的swap分区,此时进程状态叫做运行挂起状态。这是极端的操作,风险较大,一般不会出现。
2. 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 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
上面是Linux源代码,表示进程的状态。分别有“R”“S”“D”“T”等状态。将会一个个展示。
2.1 运行状态
#include <stdio.h>
#include <unistd.h>int main()
{while(1){}return 0;
}
我们写个死循环,正常来说应该是运行状态。使用ps指令查询myproc进程的状态,会发现进程状态就是R+,其中“+”符号表示在前台运行。
2.2 sleep状态
#include <stdio.h>int main()
{int cnt = 0;while(1){scanf("%d", &cnt);}return 0;
}
当我们写一个scanf函数,进程会等待键盘数据,此时的进程状态是阻塞等待状态。使用ps指令查询myproc进程,会发现myproc程序的状态是S。对应最开始给出的Linux进程状态的解释是sleeping,即休眠。所以,Linux中的阻塞状态叫做休眠。
当我们想终止休眠状态下的进程,我们可以使用kill指令,发送信号终止进程。这种休眠状态叫做可中断睡眠,也叫浅睡眠。
与之相对的就是不可中断睡眠,即深度睡眠,就是disk sleep状态。disk sleep意思是磁盘休眠状态。当系统处在磁盘级拷贝,会进行大量的读写操作,此时无法使用kill指令终端进程。
2.3 Stop状态
#include <stdio.h>
#include <unistd.h>int main()
{int cnt = 0;while(1){printf("hello world, cnt: %d\n", cnt++);sleep(1);}return 0;
}
我们写一个死循环打印变量的程序,运行起来,会发现处于S状态。这是因为printf本质是向内存缓冲区写入数据,再输出到显示器上,而输入输出的时间相比于毫秒级的时间片很慢,所以百分制九十九以上的时间都在显示器的等待队列中。
我们可以使用kill指令,其中19号选项就是发送暂停信号。查看进程状态就是T,运行的进程也会出现终端,有Stopped提示。
如果想继续运行进程,可以使用kill指令的18选项,使暂停的进程继续启动。
但是你会发现该进程的状态变量了S,没有“+”符号,说明进程在后台运行。此时我们无法使用Ctrl+C终止程序,但可以使用kill指令9号选项终止程序。而一般出现T状态,说明进程做了非法饭不知名的操作。
后台进程是指在操作系统中,在后台独立运行的程序,它们不直接与用户交互。在Linux系统中,后台进程通常是那些不需要即时用户输入且执行时间较长的任务。这些进程在执行时,用户可以继续使用终端进行其他操作。相对应地,在Windows操作系统中,当用户将一个耗时的任务,如下载大文件,最小化其窗口后,该任务便在后台运行。此时,用户可以切换到其他应用程序,进行不同的工作,而下载任务则在后台安静地继续进行
2.4 X和Z状态
X状态指的是dead,就是死亡状态,顾名思义,指的是进程被终止了。
创建一个进程,是为了完成一个任务。父进程是要知道他的子进程完成任务的情况,子进程在退出时会记录一个退出信息。在Linux中,查看一个进程退出信息,可以使用echo $?指令获取。
执行ls指令,相当于执行一个程序,查看退出信息为0,说明进程执行任务成功。当使用ls查看不存在的文件,会报查不到该文件的错误,此时再查看退出信息就是非0的。我们所写的main函数一般都会返回0,就是告诉系统该任务执行成功。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("父进程运行,pid: %d,ppid: %d\n", getpid(), getppid());pid_t id = fork();if (id == 0){//子进程int cnt = 10;while(cnt){printf("我是子进程,我的pid: %d,ppid: %d, cnt: %d\n",getpid(), getppid(), cnt);sleep(2);cnt--;}}else {//父进程while(1){printf("我是父进程,我的pid: %d,ppid: %d\n", getpid(), getppid());}sleep(1);}return 0;
}
我们写一个代码,使用fork函数创建子进程,子进程执行几十秒就结束,父进程不退出。
我们执行该程序,并使用一个循环指令查看相关进程信息。发现父子进程都处于S状态。
当子进程执行完毕退出时,他的状态变量Z状态。其中Z状态指的是zombie,即僵尸状态。当一个程序退出,代码和数据都在内存都销毁了,还会剩下task_struct对象,等着父进程回收查看。
退出的子进程还剩下结构体对象,如果没有父进程接受管理,它会一直是僵尸状态,不断消耗内存,会造成内存泄漏!
2.5 孤儿进程
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>int main()
{printf("父进程运行,pid: %d,ppid: %d\n", getpid(), getppid());pid_t id = fork();if (id == 0){//子进程while(1){printf("我是子进程,我的pid: %d,ppid: %d\n",getpid(), getppid());sleep(1);}}else {//父进程int cnt = 10;while(cnt > 0){printf("我是父进程,我的pid: %d,ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}sleep(1);}return 0;
}
我们写一份代码,父进程退出,子进程死循环不退出。
父进程没退出前,父子进程都处于S状态。当父进程退出,子进程还在运行,状态没变,但是PPID为1,说明他的父进程变为pid为1的进程。
通过top指令查询进程信息,可以看到pid为1的进程,指令叫做systemd,即系统。说明子进程被系统进程领养,我们称这种进程为孤儿进程。
创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!