目录
在教材中谈进程状态时,它的讲述是非常笼统的,它与具体的操作系统中的进程状态或多或是有有一些区别,但是却没有错。
所以接下来我要分为两步来为大家讲述进程状态,首先讲述笼统的进程状态,再讲述在Linux中具体的进程状态。
进程状态,其实就是PCB中的一个变量(字段),int status
,那么操作系统就要在创造一批状态,用数字来代替不同的状态,例如新建状态(1)、运行状态(2)、阻塞状态(3)等,进程状态被设置本质上就是PCB中的变量status被常量赋值,也就是PCB->status = NEW
,然后就可以根据不同状态干不同的事,所谓进程状态变化的本质就是将PCB中的status设置为不同的整形变量,在下图中我们能看到task_struct结构体(PCB)中第一个变量就是代表进程状态的变量。
// 假设这里是操作系统通过定义的数字来代表不同状态
#define NEW 1 // 新建状态
#define RUNNING 2 // 运行状态
#define BLOCK 3 // 阻塞状态
// ... 其他的状态
if(PCB->status == NEW) // 如果进程是新建状态干什么事情
else if(PCB->status == RUNNING) // 如果进程是运行状态干什么
// ... 如果进程是其他状态干什么
一、操作系统层面上的进程状态
1.1 新建状态(New State):
对应于进程被创建时的状态,尚未进入就绪队列。此时,操作系统为新进程分配必要的资源和建立必要的管理信息。新建状态在我们现在的操作系统中是看不到的,那么了解即可。
1.2 终止状态(Terminated State):
当进程完成任务或发生错误而终止时,进程进入终止状态。处于终止状态的进程不再被调度执行,其占用的资源将被系统回收。
1.3 运行状态(Running State)
不同的计算机有不同的配置,有点是单CPU有的是多CPU,这里以单CPU的角度进行讲解。
操作系统为了更好的进行进程调度,在操作系统中每个CPU都会在系统层面上配备一个属于自己该CPU的 运行队列(调度队列)。
当我们将程序加载到内存后就变为了进程,操作系统为了管理进程就会为其创建对应的PCB,如果当前进程需要运行,那么就可以将当前进程的PCB链入到运行队列中,并将进程的状态改为运行状态,当系统中存在大量想要运行的进程,那么操作系统就会将想要运行的进程的PCB用链表的形式链接起来构建成一个队列结构,那么操作系统想要调度运行一个进程时,就可以在运行队列中找即可。
- 老版本操作系统关于运行状态的定义:
只有正在被CUP调度运行的程序才是运行状态。 - 新版本操作系统关于运行状态的定义:
只要是在运行队列中的进程,它的进程状态都是运行状态,在运行队列中的进程是可以随时被调度的,所以在新版本中的操作系统中,新建状态/运行状态/就绪状态已经没有什么明显的区分了。
1.4 阻塞状态(Blocked State)
当CPU从运行队列中调度某个进程时,该进程的代码中一定或多或少会访问系统中的某些资源,例如:键盘、磁盘、网卡等,这里以键盘为例,当进程的代码中有scanf()/cin>>函数
需要从键盘中读取数据,但是用户就是一直不输入数据,键盘上的数据就一直没有就绪,也就是进程要访问的资源没有就绪,这个设备就不具有访问的条件,进程就无法向后继续执行.
操作系统需要底层硬件的信息,因为操作系统不仅仅会管理进程,还需要对驱动、底层硬件等进行管理,关于某个资源是否具有访问条件是操作系统最先知道的,并将这个信息告诉了进程,操作系统需要通过与管理进程相似的方式对底层硬件进行管理,也就是对底层硬件先描述再组织,对每个硬件都创建它对应的结构体对象,再通过指针将这些对象链接起来,那么操作系统对硬件的管理,就变成了结构体对象组成的链表进行管理。
若进程需要访问键盘上的资源,操作系统就会通过链表来查找键盘上的相关属性,如果显示键盘上的资源并没有准备就绪,操作系统会告诉进程键盘上的资源没有准备就绪,不具有访问条件,那么操作系统就需要将该进程的PCB从运行队列中取出来再链接到键盘上的等待队列中去,并将进程的进程状态改为阻塞状态,并且操作系统一定是最先知道进程状态的变化的。
操作系统中有非多的队列,运行队列、各个设备中的等待队列等等。
阻塞状态的定义:
进程由于等待某种时间(如设备资源未就绪、I/O操作完成等)导致进程无法向下执行的一种状态。
我们将一个进程从运行队列移动到等待队列的这个过程叫做进程阻塞了,当资源准备就绪了,操作系统识别到了它的状态变化,操作系统就等待队列中的进程重新链入到运行队列的过程叫做进程被唤醒了。
那么当一个进程阻塞了,用户会看到进程卡住了,因为进程的PCB不在运行队列上并且进程的进程状态不是运行状态,CPU不会调用该进程了。
#define KEYBOARD 1 //键盘
#define DISK 1 //磁盘
#define NETCORD // 网卡
// ... 其他硬件
struct dev
{
int type; // 硬件类型
int status; // 硬件状态(是否能正常使用)
// ... 其他属性
struct dev* next; // 链接指针
int datastatus; // 硬件资源是否准备就绪
PCB* wait_queue; // 等待队列
}
1.5 挂起状态(Suspend State)
1.5.1 阻塞挂起状态
当一个进程当前被阻塞了,那么注定了这个进程在它所等待资源没有就绪时,是无法被调度运行的。如果这时候操作系统内存严重不足,会将进程状态为阻塞状态的进程所对应的代码和数据置换到外事(如磁盘)中,操作系统中就会释放掉一些空间,此时这些进程的进程状态就是阻塞挂起状态。
注意:
- 将内存数据进行置换到外设,针对的是所有的阻塞进程。
- 有人会担心将代码和数据置换数据会导致操作系统效率变慢的问题,不用担心,这是必然的,现在关心的是让操作系统继续执行下去。
- 磁盘中有一个swap分区,操作系统中的代码和数据会被置换到swap分区中去。
- 当进程所等待的资源就绪后,进程将会被重新调度,曾经被置换出去的代码会被重新加载进操作系统中。
1.5.2 就绪挂起状态
只要是没有被运行的进程所对应的代码和数据都可以被置换到外设中,当操作系统将代码和数据加载到内存中后立马能够被调度运行的程序,这样的进程所对应的进程状态就是就绪挂起状态。
就绪挂起状态下的进程可以随时被调度,如果曾经操作系统挂起了大量的进程,当这些进程都要被调度的时候,操作系统就要将这些代码和数据加载到内存中,导致操作系统与外设之间大量时间都在进行换入换出,会导致操作系统的效率降低。
1.6 进程切换的本质
- 更改进程PCB中的整型变量status
- 将PCB链入到不同的队列中
二、Linux操作系统中具体进程状态
2.1 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 *task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"T (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)" /* 32 */
};
2.2 R - 运行状态(running):
运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
下面我用两台设备进行测试,一台设备用来运行进程,一台设备使用脚本来检测进程,当使用脚本来监控运行起来的代码,可以可以发现当前进程大部分时候是S睡眠状态,少部分时候是R运行状态。有人会问,这里一直在输出数据,进程状态不应该一直是运行状态吗?因为当我们使用printf进行打印的时候,本质上是向显示器上打印,进程是在内存中的,那么就是将内存中的数据打印到外设中去,CPU的速度相比于外设的存储速度来说是非常快的,这里频繁的调用printf函数,显示器中的资源不一定是准备就绪的,也就是说其实是IO太慢了导致我们查询进程状态时,大部分时候是睡眠状态。
#include <stdio.h>
int main()
{
while(1)
{
printf("Hello Linux\n");
}
return 0;
}
如果想一直看到运行状态,那我们可以从代码中去除printf,就可以不让IO影响进程状态了,因为此时当前进程并没有访问外设,所以使用脚本来监控进程时,进程的进程状态都是运行状态。
2.3 S - 睡眠状态(sleeping)
睡眠状态意味着进程正在等待事件完成,S睡眠状态又被称为浅度睡眠状态,浅度睡眠状态的进程可以被Ctrl + c , kill -9 终止,会对外部的信号做出响应。Linux操作系统中睡眠状态是操作系统中的阻塞状态。
#include <stdio.h>
#include <unistd.h>
int main()
{
int i = 0;
printf("please Enter:");
scanf("%d",&i);
return 0;
}
2.4 D - 磁盘休眠状态(Disk sleep)
磁盘休眠状态又被称为深度睡眠状态,在这个状态的进程通常会等待IO的结束。进程在深度睡眠状态下无法被任何信号唤醒,操作系统也不能将其终止,除非发生物理事件,例如断电。深度睡眠状态保证了进程在等待关键资源(如磁盘写入)时不会被操作系统误杀,从而确保了数据的完整性和任务的顺利执行。Linux操作系统中磁盘睡眠状态也是操作系统中的阻塞状态。
2.5 T - 停止状态(stopped)
可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。Linux操作系统中停止状态也是操作系统中的阻塞状态。
为什么进程要暂停呢?在进程访问资源的时候,可能暂时不让进程访问,就进行设置为停止状态。
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("Hello Linux\n");
sleep(1);
}
return 0;
}
2.6 t - 追踪停止状态(tracing stop):
追踪停止状态通常与调试和追踪进程的行为有关,通过调试器的操作(如设置断点并继续执行),进程可以进入tracing stop状态(t)。Linux操作系统中追踪停止状态也就是操作系统中的阻塞状态。
2.7 X - 死亡状态(dead)
死亡状态通常指的是进程已经终止执行并退出,这个状态只是一个返回状态,你不会在任务列表里看到这个状态,Linux操作系统中死亡状态也就是操作系统中的死亡状态。
我们创建一个进程的目的就是为了完成某种任务,但是如何让父进程知道子进程任务完成的怎么样呢?
在进程退出的时候,要有一些退出信息,用来表面自己认为完成的怎么样,退出信息会由操作系统来写入该进程的PCB中,可以运行进程的代码和数据立即释放,但是不允许进程的PCB立即释放,要让操作系统或父进程读取操作系统中的退出信息,知道进程退出的原因后才能释放。
如果进程退出了,但是进程的PCB没有被操作系统或父进程读取,那么操作系统必须维护这个退出进程的PCB结构,因为这个进程的代码和数据已经被释放了,该进程无法被调度,此时进程已经算退出了,此时进程的状态是僵尸状态,当操作系统或父进程读取PCB中的退出信息后,PCB的状态会由僵尸状态变为死亡状态,然后被释放。
上面讲到的退出信息和如何读取PCB中的退出信息会在后面的进程控制中讲到。
2.8 Z - 僵尸状态(Zombie State)
2.8.1 僵尸状态
僵尸状态的转变过程在上面的死亡状态中讲到了,可以翻到上面看一下。
当一个进程终止(比如因为正常退出或因为接收到一个信号而终止)时,它的进程描述符(task_struct结构体)以及进程的其他相关信息仍然保留在内存中,直到其父进程通过调用wait()或waitpid()等系统调用来读取这个已终止进程的退出状态信息。在这个阶段,该进程就被认为是处于僵尸状态,Linux操作系统中僵尸状态也就是操作系统中的死亡状态。
这里我使用fork函数创建一个子进程,并且让父进程一直运行,子进程运行一段时间后结束,让父进程不回收子进程的PCB,故意制造一个僵尸状态。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
int main()
{
pid_t id = fork();
if(id == -1)
return -1;
else if(id == 0)
{
// 子进程
int count = 5;
while(count--)
{
printf("I am child , run time : %d\n",count);
sleep(1);
}
printf("I am child , I am dead!!\n");
exit(-2); // 退出进程
}
else
{
// 父进程
while(1)
{
printf("I am father , running any time!\n");
sleep(1);
}
}
return 0;
}
2.8.2 僵尸进程
一个进程的进程状态为僵尸状态,那么这个进程就是僵尸进程。
僵尸进程的危害:
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护,那一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费,最终导致内存泄漏。
2.9 前台进程与后台进程
前台进程:当我们使用命令行解释器启动一个进程后,我们无法再通过命令行解释器使用指令,并且进程可以使用Ctrl + c 直接终止,这样的进程我们称之为前台进程,在操作系统中一个命令行解释器只能启动一个前台进程,当我们使用ps查询前台进程时,会发现前台进程的进程状态后面会多出来一个+。
后台进程:当我们使用命令行解释器在命令的末尾添加&符号来启动的进程我们称之为后台进程,例如./mytest &
,启动后台进程后,我们还可以通过命令行解释器继续使用指令,并且后台进程无法通过 Ctrl + c来终止进程,只有通过kill来杀死进程,当我们使用ps查询后台进程时,会发现后台进程的进程状态后面没有+。
2.10 孤儿进程
上面讲到僵尸进程时,我们让父进程一直运行,子进程运行一段时间后结束,模拟出了僵尸状态,那么这里让父进程运行一段时间后结束,子进程一直运行,父进程会出现僵尸状态吗?
当我们进行测试的时候发现,父进程并没有出现僵尸状态,而是直接退出进程了,原因是父进程有自己的父进程,当父进程结束后,父进程的父进程会读取父进程的退出学习,释放父进程的代码、数据和PCB。
那么如果父进程提前退出,那么当子进程退出后变为了僵尸状态应该怎么办?
父进程先退出,子进程则被称为孤儿进程,通过下图可以看到孤儿进程被1号进程所领养了,1号进程就是操作系统,新版内核中称为systemd,老版内核被称为init,那么孤儿进程要退出,就要通过操作系统进行回收了。
本文详细介绍了Linux环境下两大核心工具:yum软件包管理器和vim文本编辑器。yum部分涵盖了软件包基础、rzsz工具简介、软件包的查看、安装、卸载、更新及yum源更新方法。vim部分则从基本概念出发,深入讲解了vim的基本操作、命令模式与底行模式的常用命令,包括光标移动、文本编辑、查找与保存等。此外,还介绍了vim的高级功能如视图模式、替换模式及多文件编辑,并提供了简单的vim配置指南,帮助用户根据个人需求优化vim使用体验,提升代码编辑与管理效率。
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!