【Linux】进程状态

目录

个人主页:矢望

个人专栏:C++LinuxC语言数据结构

一、进程状态

1.1 什么是运行状态?

一个CPU对应一个调度队列

准备好运行并且需要CPU资源的任务,会在CPU的调度队列中排队。凡是在这个队列中的进程,它的状态都是运行状态

1.2 什么是阻塞状态?

  • 阻塞的现象:

我们都写过这样一份代码:

c 复制代码
#include <stdio.h>

int main()
{
    int a;
    scanf("%d", &a);
    printf("%d\n", a);

    return 0;
}

编译并运行

当你运行之后,没有输入数据,此时就卡在那里了,这就是进程堵塞

  • OS角度

当我们运行这个程序,程序就会陷入等待,等待的是键盘上输入的数据,也就是硬件就绪。硬件一旦就绪,程序就可以运行了。

硬件的管理者是OS,所以操作系统可以第一时间知道硬件是否就绪。操作系统通过先描述,再组织管理硬件资源。

假设现在运行这个程序,这个程序就是运行状态,如果键盘上迟迟没有数据输入,此时进程就会从调度队列上剥离下来,从而进入键盘资源的等待队列,此时由运行状态改为堵塞状态,当有数据输入时,OS会再次将堵塞状态改为运行状态,并且这个进程会再次加入到调度队列中运行。

阻塞与运行的本质:你的PCB(task_struct)在哪个资源提供的队列中

1.3 什么是挂起状态?

现在操作系统在正常执行进程,突然操作系统的内存空间不足了。此时操作系统发现有一部分进程一直在阻塞状态,在白白的浪费资源,此时OS就会把该进程对应的代码和数据交换到磁盘中的swap分区里,把该进程所占用的内存空间释放掉,这样内存就被节省出来了。等到该进程就绪之后,在将这个进程的PCB重新链入到调度队列里,当进程真正被调度时,再将swap分区中的代码和数据重新调回内存。

进程被挂起:进程的task_struct在内存里,而进程对应的代码和数据因为内存资源不足,被交换到磁盘的swap分区。

swap分区

swap分区存在的本质:用时间换空间。

swap分区不建议太大,这个分区一般和内存一样大,或者是内存空间的1.5 - 2倍,如果这个分区过大,会导致操作系统过度依赖swap分区,频繁的进行换入、换出操作。这种过度进行swap会导致系统变慢

  • 扩充

当操作系统将所有的阻塞进程的代码和数据全部换入到swap分区里,但此时内存空间还是不够时,操作系统会将没有被调度的进程对应的代码和数据换入到swap分区里,这叫做运行挂起如果这样内存空间还是不足,LinuxOS会选择性的杀掉特定的进程

二、Linux具体的进程状态

cpp 复制代码
/*
*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): 运行状态,并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
   while(1)
   {
       printf("我是一个进程,pid: %d\n", getpid());
   }

   return 0;
}

如上,当我执行这个程序时,我们观察它的进程状态。

如上图,显示的是S状态,这可是睡眠状态,可是进程一直在打印这句话呀,这是为什么? 我们接下来把这代码注释掉试试。

注释掉之后,再次执行程序进程状态就变成了运行状态,这是为什么?

原因 :当我们没有注释掉的时候,printf的本质是向显示器做写入,而我们执行的程序在内存里,所以printf也就是从内存向显示器做写入printf的每一次调用都要进行IO,而只要进行IO,程序的大部分时间都是在等的,因为死循环进行写入时,缓冲区一下就写满了,之后写入条件就不具备了,只有程序在向缓冲区做写入时,那一刻的状态才是运行状态R,绝大部分状态都是S状态。而当代码被注释之后,程序运行直接使用的是CPU资源所以查出的是R状态。
S(sleeping): 睡眠状态 ,意味着进程在等待事件完成,这里的睡眠有时候也叫做浅度睡眠 / 可中断睡眠interruptible sleep,也就是我们上面介绍的阻塞状态

复制代码
#include <stdio.h>

int main()
{
   int a;
   scanf("%d", &a);
   printf("%d\n", a);

   return 0;
}

运行该程序:

如上图,这就是睡眠状态。
扩充1使用信号杀掉进程 kill -9 [进程pid],之前我们都是使用Ctrl + C终止进程。

扩充2:状态之后的+意味着什么

我们以一个循环程序为例,当我们执行它时,我们会发现此时我们输入其它的命令,bash就没有反应了。

我们把这种进程叫做前台进程在查前台进程的进程状态时,往往后面会带有加号+ ,这种进程运行时,bash就在后台了。

如果你不想让进程在前台运行,可以在程序的执行过程中加上&,此时它就在后台运行了。

此时就不影响正常命令的执行。

如上图,这种进程就称之为后台进程 。另外Ctrl + c无法终止后台进程,可以通过kill -9 杀掉后台进程。

前后台进程的最大区别:前台进程可以获取键盘输入,而后台进程不行 。而Ctrl + c属于键盘输入,所以无法杀掉后台进程。
D(Disk sleep)Linux特有的状态,休眠状态,有时候也叫不可中断睡眠状态uninterruptible sleep在这个状态的进程通常会等待IO的结束,通常IO完成后会自动恢复
处于该状态的进程往往是在磁盘进行写入,往往非常重要,因此D状态的进程不能被信号或者操作系统杀掉
D状态进程是Linux系统的"保护机制",但也是"单点故障"。少量短时D状态正常,但大量或长期的D状态需要立即处理,否则可能导致级联故障。最好的策略是预防和监控。如果系统已严重挂起,真到了万不得已时的最后手段就只有拔电源了。
T(stopped): 停止状态,可以通过发送 SIGSTOP 信号给进程来停止进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
kill -l:列出所有可用的信号。

kill -19

如上图,该进程果然处于了暂停状态。接下来再将其恢复,kill -18

暂停状态出现的场景 :1.进程做出非法操作,操作系统暂停进程 。2.t (tracing stop):追踪暂停,追踪一个进程,在gdb调试的时候所处的状态就是t状态

首先来看第一种场景,当一个需要获取键盘输入的进程被放在后台,此时就会出现T状态,因为后台进程是无法获取键盘输入的,也就是进程做出了非法操作,OS将其暂停了。

复制代码
#include <stdio.h>

int main()
{
   int a;
   scanf("%d", &a);
   printf("%d\n", a);

   return 0;
}

如上图,有两次执行,第一次是前台进程,进程状态是睡眠状态。第二次是后台进程由于进行了非法操作,被OS暂停了。

接下来,我们在来看第二种场景,我们编译时加上 -g 选项,之后进行用 gdb 进行代码调试。

刚开始gdb code时,我们只看到 gdb 一个进程,在打完断点,执行 r 命令时,我们看到 gdb 是父进程,而 code 是子进程,所以 gdb 运行的本质是 gdb 作为父进程,启动了一个子进程,然后这个子进程遇到断点就停下来了,这个子进程此时处于 t 状态 ,可以理解为 gdb 遇到断点时,给子进程发送了 19 号信号,把子进程暂停了。当你向继续运行时,可以输入 n,此时代码就会继续运行,相当于父进程给子进程发送了 18 号信号,执行完后接收到 19 号信号又会停下来。所以 gdb 可以通过不断给子进程进行发送18、19号信号,从而使子进程暂停、运行,这样就可以达到逐语句,逐过程的目的
X(dead)死亡状态 ,这个状态只是⼀个返回状态,你不会在任务列表里看到这个状态。
Z (zombie)僵尸状态 ,进程已终止但父进程尚未读取其退出状态。

一个进程死亡时,它不能直接是 X 状态,它必须先处于 Z 状态,为什么? 一个进程运行是有它自己要执行的任务的, 所以在它结束前,我们需要获取它退出时的相关信息,所以当一个进程结束时,会将它的 PCB 设置为 Z 状态,那么此时操作系统不能释放处于 Z 状态的进程,OS会通过一定方式获取进程的退出信息,之后再将这个进程设置为 X 状态,此时才能杀掉该进程。

观察 Z 状态:

复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
   printf("我是一个进程,pid: %d\n", getpid());
   sleep(3);

   pid_t id = fork();

   if(id == 0)
   {
       while(1)
       {
           printf("我是一个子进程,pid:%d, ppid: %d\n", getpid(), getppid());
           sleep(1);
       }
   }
   else
   {
       while(1)
       {
           printf("我是一个父进程,pid: %d\n", getpid());
           sleep(1);
       }
   }
   return 0;
}

接下来,我们编译运行这个程序,并在执行过程中将子进程杀掉,观察子进程的状态。

如上图,因为父进程没有对子进程进行回收,所以会看到子进程处于Z状态

  • 几个子问题:
    1.进程退出了,退出信息是什么?
    main函数的返回值,或者是收到的信号值
    2.进程退出了,退出信息保存在哪里?
    一般保存在进程自己的task_struct结构体中
    3.检测、回收Z状态进程本质是在做什么?
    本质是在检查、获取task_struct内部的退出信息数据,获取完成之后就可以将进程状态改成X,接下来释放它
    所以Z僵尸状态指的是只保留进程的task_struct,未来让父进程或者OS获取到子进程的退出数据Z状态的核心:最小化保留的task_struct
    4.怎么回收,谁来回收?
    一般是父进程通过系统调用回收子进程
    如果就是不回收子进程会发生什么呢?此时子进程就会永远处于Z状态,也就是task_struct不会被释放了,它可是占据着内存空间的,这就是内存泄漏
    所以最佳实践:子进程必须回收

扩充 :如果对应的进程结束了,内存泄露问题还存在吗? 答:不存在了用户态进程退出时,操作系统会自动回收它的所有内存 。我们生活中的所有软件你会发现它都是死循环,你会发现它一般不退出(你看似把软件退出了,实则它在后台运行),它也叫常驻进程,所以才会在意内存泄漏。

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
A小辣椒17 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒20 小时前
TShark:基础知识
linux
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言