(Linux操作系统)进程状态
看这篇文章之前,介意先去看看这篇文章初识进程
说到进程状态我们先看看课本上是这样说的:进程状态是一个task_struct内部的一个整数
这个概念太笼统了,现在我们来具体讲一讲什么是进程状态,首先我们要知道,操作系统管理硬件会经历一个先描述后组织的一个过程,会把硬件在操作系统内部用一个struct描述好,再通过一些数据结构给他们组织起来,当操作系统想要管理硬件时,不会直接去找到这个硬件,而是通过操作系统里面每个硬件自己的struct,操作系统会去找到对应硬件的struct对里面的内容进行修改,然后由硬件驱动代执行,最终去控制硬件。
而在操作系统管理进程中也是如此的,操作系统会在内部建立一个进程的task_struct,然后把每个进程的task_struct给联系起来,每个task_struct会有自己的代码和数据。通过修改进程的task_struct,而(task_struct内部就包含进程状态)最终去达到控制进程的目的
有了上面的了解之后,我们现在来了解一下进程的三种形态
运行,阻塞,挂起
首先我们来看一张图片
现在我们来具体说一下
首先我们要知道一个概念: 一个cpu一个进程调度队列。
如果我们的一个进程,在运行队列里面停止运行,需要等待我们用户从键盘上面输入东西才能进行下一步时,这个进程就会处于阻塞状态,那么此时cpu就无法处理后面的进程,就会出现阻塞,那么此时操作系统肯定就不会不管不顾,这时这个进程会被操作系统从CPU上面拿下来,然后放到特定设备的等待队列里面,如果是键盘就放键盘的等待队列里面,是显卡就放显卡的等待队列里面 (这时候这个进程就不在运行队列里面了,这个进程就不会被调度就处于阻塞状态),所有从运行到阻塞的本质是把task_struct链入到不同的队列结构当中。
当操作系统发现对应的设备准备就绪时 (也就是操作系统检测到从键盘上面输入了东西时) ,操作系统就会去检查当前设备的task_struct节点,并把状态设置为活跃的,然后再会去检查当前设备里面对应的等待队列,发现等待队列不为空就将该等待队列中的进程设为运行状态,并把他重新链入到运行队列,当cpu运行到这个进程时,就会把键盘上面输入的东西喂给这个进程做处理。
所以进程从阻塞回到运行状态的本质是找到对应的task_struct并把这个重新链入到运行队列当中
下图展示的是一个详细的过程,进程从运行到阻塞,再到运行。
注意下面展示的队列切换,只是会去拿task_struct进行切换,并不会把该进程自己的代码和数据进行来回切换,这里是为了画图方便 操作系统不会去移动或复制进程的代码和数据,因为这些信息已经存储在进程的内存空间中。
有了上面的了解之后,我们可以得出一个结论:进程状态的变化表现之一就是在不同的队列里面进行切换,本质都是对数据结构的增删查改!
下面我们再说说挂起
挂起分运行挂起和阻塞挂起
阻塞挂起:
我们知道操作系统是一个非常纯正的管理软件,他是非常智能的,当我们第一次安装linux操作系统时,我们在分配磁盘空间时基本都会看到一个叫swap分区的一个东西,而这个swap分区的作用就是用来给进程做挂起用的
为什么会有挂起这个进程形态,如果我们计算机的内存在空间不足的情况下,操作系统就会使用到这个swap分区,当操作系统发现我们的进程处于阻塞状态时,并且又在内存资源紧张的情况下,就会把该进程对应的代码和数据给放到磁盘的swap分区中去,只保留task_struct在内存里面,当内存资源不紧张,或者该进程形态从阻塞变为运行时,操作系统会把磁盘中该进程的代码和数据重新加载到内存中去再和对应的task_struct形成指针映射关系。
运行挂起:
那么运行挂起是什么意思呢,本质上和阻塞挂起差不多,都是把对应的代码和数据给换入
到swap分区中去,但这个是比阻塞挂起还要极端的一种情况,运行内存紧张到需要把正在运行的进程给换入到swap分区里面情况,那么此时的操作系统可能会把运行队列里面末端的进程给交换到swap分区里面,在内存紧张时,操作系统这样的目的可以是理解一个操作系统自身的一个自保的行为。
有了上面的了解之后,我们再看看内核链表
linux内核链表
Linux内核链表是Linux内核中广泛使用的一种数据结构,用于管理一系列元素。在Linux内核中,链表是非常重要的,因为它们可以用来组织多种类型的对象,如进程、文件系统节点、等待队列等。
下面是一个节点的定义
struct list_head {struct list_head *next, *prev;
};
Linux内核链表是一个双向循环链表。这意味着每个链表元素都包含指向上一个元素和下一个元素的指针。
但是他这里的双向链表和我们平常写的链表不一样,我们的前驱和后继指针是指向整个节点,而这里的内核链表是不一样的他是插入在要管理的数据,比如说进程,当task_struct里面的一个成员。
而在task_struct里面是这样存在的
操作系统要管理进程就是通过内核链表进行管理的,那么这里有个问题,我们要访问每个链表的list_head成员很简单,那么我们想要访问整个task_struct该怎么办呢? 我们访问到进程时可以知道list_head这个成员的地址,我们可以通过一个offsetof这个宏函数得到这个相较于起始地址的偏移量然后再用这个list_head的地址去减去偏移量就得到了这个task_struct的起始地址,下面这个代码可以帮助理解
#include <stdio.h>
#include <stddef.h> // offsetof 宏// 定义双向链表节点
struct list_head
{struct list_head* next;struct list_head* prev;
};// 定义一个类似 `task_struct` 的结构体
struct my_struct {int x; // 第1个变量int y; // 第2个变量int z; // 第3个变量struct list_head links; // 链表节点
};#define container_of(ptr, type, member)\
((type *)((char *)(ptr) - offsetof(type, member)))int main() {struct my_struct obj; // 创建结构体实例struct list_head* list_ptr = &obj.links; // 获取 `links` 成员的地址// 打印每个变量的地址printf("Address of obj: %p\n", (void*)&obj);// printf("Address of obj.x: %p\n", (void*)&obj.x);// printf("Address of obj.y: %p\n", (void*)&obj.y);// printf("Address of obj.z: %p\n", (void*)&obj.z);printf("Address of obj.links: %p\n", (void*)&obj.links);// 计算 `links` 在 `my_struct` 结构体中的偏移量size_t offset = offsetof(struct my_struct, links);printf("偏向量: %zu bytes\n", offset);// 通过 `links` 成员地址计算 `obj` 结构体起始地址struct my_struct* calculated_obj = container_of(list_ptr, struct my_struct, links);printf("计算出的结构体地址: %p\n", (void*)calculated_obj);return 0;
}
运行结果
那么这时我们可以得到一个结论:如果一个task_struct里面有多个list_head,那么他就可以属于不同的数据结构,也就是为什么进程在内存中只会存在一份,但是他既可以属于全局链表,又可以属于运行队列,就是靠这个内核链表实现的
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 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};
R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列 里。
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
D磁盘休眠状态(Disksleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状。
不是说进程状态是一个task_struct内部的一个整数吗,为什么这里定义的都是字母? 我们可以看到每个字母后面都有一个对应的数字注释,这个数字就是对应的进程表示整数。
我们可以来做实验来看看上面的进程状态
首先我们写一段代码来测试查看进程,用一个死循环,一直在屏幕上面打印出东西
当我们用了查看进程的指令查看之后,我们发现他的状态居然是S(sleeping)状态,也就是阻塞状态,但是我们的进程不是一直在运行吗,为什么会出现这种情况,那是因为我们调用了 printf函数(printf函数是在屏幕上打印内容,printf函数会使用到显示器这个硬件,会出现等待设备就绪的情况,当显示器就绪了,printf函数才会执行) 我们的程序执行printf函数可能只用了一纳秒,但是我们的程序运行到printf时会出现等待设备就绪的情况,等待可能用了一秒钟我们查看进程状态可能用了两秒钟,我们的程序大部分时间都在等待,所以我们很难查到进程是R运行状态,如果当我们查看进程和我们的程序运行到printf函数正好在同一时间上,那么就可以看到进程的状态是R状态,所以可以得出一个结论:我们的程序在运行队列和阻塞队列之间来回的切换,这里为什么会状态后面跟一个加号,这是因为我们的进程是在前台运行的,会阻塞掉我们的命令行,让我们的命令行无法输入命令
如果我们就是想要看到程序是运行的情况,那可以就让我们的程序做纯计算,不在屏幕上打印出内容,就可以看到程序一直处于R状态了。
现在来说说s状态,s在linux中是阻塞状态
我们可以来写个代码看看
当我们运行这个代码的时候,我们再去查看他的进程状态,发现是s状态,这是因为我们的程序调用了scanf从键盘上输入东西,调用这个的时候,我们的程序就会等待键盘就绪,这个进程就会被操作系统放进阻塞队列里面,所以状态是s
t(tracing stop)状态
tracing的意思是追踪,我们要看到这个进程状态,我们可以把我们的代码用gdb调试打断点看看,断点就是让程序运行到指定的地方停下来,我们可以看看
首先我们添加断点调试信息到我们的代码中去,然后开始执行代码到我们打的断点处停下来,再去查看进程状态
这时发现状态就是小t了
T (stopped)状态
当我们的程序执行时,由我们手动输入crtl+z程序就会被暂停掉,这时我们再去查看进程状态就是T状态了
这个是由我们手动设置的,那么在操作系统中,操作系统也会自动暂停掉某些进程,这些进程是操作系统判断有什么不对的地方,操作系统就会把这个进程给暂停掉,那他为什么不直接把这个进程杀掉呢,那是因为这个进程可能有危害,但危害不那么大,当这个进程被暂停了,操作系统会通知我们这个进程有问题,让我们查看进程,并找到问题并解决他。
下面来说说D (disk sleep)状态
首先我们看看这个状态的名称,disk是磁盘的意思,说明这个状态和磁盘有关系
下面我们举个例子:
当我们的进程在进行对磁盘进行写入数据时,我们的进程首先要通知磁盘,告诉他我将要写入100mb的数据,然后进程就会把100mb的数据给交给磁盘,让磁盘进行写入,磁盘拿到数据的时候就会去找自己内部的剩余空间,并开始写入数据,这个时候我们的进程就会开始进行等待,等待磁盘回应,磁盘在那里写数据,操作系统就在那管理进程,假设这时内存资源已经开始紧张了,当管理到这个进程时发现他没有做事情,操作系统就会把这个进程杀掉,但此时磁盘在写数据时发现空间不够了,这时候他就会去通知进程,但发现进程已经不在了,那此时磁盘拿到这个需要写入的数据只有把他丢弃了,我们就丢失了100mb的数据! 假设这些数据是银行的存款数据,那就麻烦了。
根据这种情况,我们的进程需要设置一个状态D状态,就是用来告诉操作系统,我这个进程不能被杀掉,我正在做关键性io的操作!杀掉会造成关键性数据丢失!我不可被中断
如果我们的进程长时间处于D状态,说明我们的磁盘可能快坏掉了。
现在我们来看看一个特殊的进程状态
僵尸进程
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵死(尸)进程 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
我们可以写个代码来让僵尸进程故意出现
这个代码我们手动创建子父进程,并让父进程一直打印内容,不停止,这样他就不能停下来获取运行完的子进程信息
僵尸进程的危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的事情,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态,维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护,那一个父进程创建了很多子进程,就是不回收,就会造成内存泄漏,因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
那么什么样的进程产生僵尸进程的概率大呢,那就是常驻内存里面的进程,像我们的操作系统,操作系统也是一个纯管理的软件,是一个由多个组件和进程组成的复杂系统,操作系统难免会产生一些僵尸进程,在我们日常使用电脑的时候,如果长时间不关机(在排除电脑硬件因为长时间运行导致温度升高,硬件性能下降的情况下),是不是就会变卡,当我们重启电脑之后,我们的操作系统又流畅许多了,这就是因为僵尸进程的存在,导致内存泄漏,运行卡顿