状态决定了进程接下来要做的工作,就像我们人一样吃饱了才有力气干活!!!
那么该怎样表示一个进程的状态呢?
状态在我们看来实际上就是一个数字,一个状态对应一个数字。
#define BUNNING 1
#define BLOCK 2
#define ... 3
struct task struct
{
int status;
}
理论上进程状态设计:
但实际上Linux的设计与上图不是一模一样的,因为理论是要用来指导实践的,而实践当中必会碰到一些困难,这些都是要进行修正的。 接下来,我们来看看Linux的进程状态是如何设计的。
我们学习数据结构的时候,是通过定义两个前后next和prev指针进行双链表的链接,从而可以指向下一个进程的起始地址,随意访问结构体里面的数据。
struct task_struct
{
struct list_node *next;
struct list_node *prev;
//...
}
而Linux中的源码设计并不是这样子的,那它又是设计的呢?
struct task_struct
{
struct list_node node;
//...
}
struct list_node
{
struct list_node *next;
struct list_node *prev;
}
linux中将prev和next进行封装,也可以做到链表的前后链接,但是并不是指向下一个进程的起始地址,那又该如何随意访问结构体中的任意元素呢???
既然我想要访问结构体中的任意元素,那么我就要得到该结构体的起始位置,从而做到访问任意元素的目的。
计算偏移量
为什么要这样做呢?意义何在?
运行状态:从计算机的硬件出发,我们所写的代码在硬盘中,要让程序运行起来就要加载到内存当中, 每一个程序(进程)都会有一个属于自己的PCB,通过PCB来进行排队,等待CPU的调度,为了方便调度管理,操作系统会维护一个运行队列,所有就绪状态的进程的PCB会被加入到这个队列当中, CPU在调度执行时就会通过这个运行队列拿到进程的PCB,进而调度执行该进程,在排队的时候就是运行状态。
我们说过输入、输出属于外设,运算器、控制器属于CPU,而这些都属于资源。而进程的本质是要竞争这些资源,无疑就两类资源:
有些人认为只有将PCB放在调度队列运行起来才能称作运行状态。
这里我们规定:运行状态 -->该进程的PCB必须处在CPU的调度队列runqueue中,只要运行状态在调度队列中,该进程就叫做运行状态 -->随时等待CPU调度执行!
阻塞状态:在CPU执行一个进程的时候,可能会需要访问系统的某些资源,就比如在C语言中写的scanf(),在使用这个函数的时候,需要调用键盘,等待键盘输入数据,当进程需要键盘资源的时候,会将进程的PCB加入到硬件设备结构描述的等待队列当中,并且把PCB设置为阻塞状态,当PCB在这个等待队列中等待数据资源时,这个状态就叫做阻塞状态。
int main()
{
int a = 0;
scanf("%d",&a);
return 0;
}
操作系统是软硬件资源的管理者,那么它就需要管理硬件。那该如何管理硬件呢?
先描述,再组织!!!
当程序跑到scanf时,此时就会等待键盘的响应,这就是典型的阻塞状态。
那如果键盘迟迟没有就绪呢?
操作系统会直接把当前进程从调度队列runqueue里断链,将PCB移动到键盘的结构体的等待队列里,如上所示。而我们把这种等待设备资源 --> 这种状态称为阻塞状态!
一旦键盘设备就绪时,谁最清楚键盘上有数据呢?操作系统最清楚,因为它是软硬件资源的管理者。此时会再把设备的等待队列放到CPU的调度队列中!!!
本质上:
内存资源是有限的,而进程加载到内存是要消耗内存的。那么,有没有一种可能内存资源会存在不足呢?这显然有可能。
内存资源不足时,操作系统是如何做的呢?
操作系统在内存中发现有一些进程会很晚才会放到cpu中进行调度,此时会将该进程的task_struct放入到设备的等待队列当中(在这又不干啥,占着内存资源的位置,不如先给其他人运行,等你就绪时再重新把你唤回到内存当中)。
当内存资源严重不足时:
为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状
态(在Linux内核⾥,进程有时候也叫做任务)。
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 */
};
R运⾏状态(running): 并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏
队列⾥。
S睡眠状态 (sleeping): 意味着进程在等待事件完成(有时候也叫做可中断睡眠(interruptible sleep))(简称 浅度休眠,它能够响应外部的事件 )。
int main()
{
while(1)
{
printf("hello status\n");
sleep(1);
}
}
在上面这段程序中,按照我们的理解应该是R状态才对,但我们看看下图结果。
不对啊???不应该是R状态吗?这是出错的吗?实际并不是。
大部分时间都是显示器不就绪状态,计算机一秒可能上亿次,大量时间都在运行队列和显示器这个外设的这个等待队列里,来回放(外设太慢了,CPU很快)。大量打印在显示器本质在访问外设。
D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个
状态的进程通常会等待IO的结束。(深度睡眠,不能响应外部事件)
在这里分享一段职场的小故事:
进程要将用户的1GB数据(含有1亿)写入到磁盘中,此时把自己设为s状态,等待磁盘的反馈(成功/失败)。此时的情况是内存资源严重不足了!!!操作系统本来因为内存资源严重不足而在烦恼,看到该进程正翘着二郎腿、磕着瓜子,占着内存资源的位置。本来就烦恼的操作系统看到该进程的行为更加来气,说到:我内存资源都快不足了,你还有空再这吹着凉风,为了保全自己,二话不说直接把该进程杀掉了。此时,磁盘正在写入用户的1GB数据,但是以失败告终,那么它就要向该进程反馈写入失败,但是找不到该进程了啊,该进程被操作系统杀掉了啊!!!那么磁盘该不该丢掉这1GB数据呢?该磁盘还要写入其他用户的数据,不得不丢掉该用户得的1GB数据。 这段小故事中,是谁犯错了呢? 操作系统说:没看到内存资源严重不足了吗?如果我不杀掉该进程,那么我们可能损失损失的价钱就不只1亿了。要问就去问进程和磁盘。 内存说:我在将用户的1GB数据交给磁盘进行写入啊,此时我再等待磁盘给我反馈结果,你操作系统二话不说就把我杀掉了,好意思说? 磁盘说:我只是执行进程吩咐给我的命令罢了,此时我写入失败了向进程反馈结果,但是找不到了啊。而我还有其他用户的数据需要写入啊,我也不得不丢掉该数据,这能赖上我?
那么,我今后赋予你这个进程一种能力,叫做 D状态。你可以不被OS杀掉,除非你自己醒来!!!
T停⽌状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的
进程可以通过发送 SIGCONT 信号让进程继续运⾏。
该指令可以向进程发出信号:
停止状态:
进程被追踪,因为断点而停下来!
可以看到实际上gdb是创建了个子进程进行调试
X死亡状态(dead):这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
为什么要创建进程?进程被创建出来,就是为了完成任务的。 既然有创建,那么就有销毁。因此,进程结束是进程创建的反过程。 进程结束的时候,我们需要知道任务完成的怎么样?因此不能立即释放进程的所有资源。 进程结束时,需要现处于一种“僵尸状态”,目的就是为了获取信息。代码数据会被释放掉,但是会把task _struct保留。 为什么要保留task_struct呢? 该进程记录着进程退出的退出信息,方便父进程读取出退出码。
前面说的是子进程先退出了,但父进程一直在运行没有读取子进程状态。
那如果父进程先退出了呢,子进程又该怎么办呢?
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)
{
//child
printf("I am child, pid : %d\n", getpid());
sleep(10);
}else
{
//parent
printf("I am parent, pid: %d\n", getpid());
sleep(3);
exit(0);
}
return 0;
}