【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账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
wdfk_prog1 天前
[Linux]学习笔记系列 -- [fs]read_write
linux·笔记·学习
看见繁华1 天前
Linux 相关
linux·运维·服务器
源图客1 天前
CentOS系统安装Python3.12.10
linux·运维·centos
立夏陆之昂1 天前
Ubuntu下安装easyconnect
linux·ubuntu
运维之美@1 天前
linux主机ping不通问题排查
linux·运维·服务器
MyCollege19991 天前
以UEFI模式从U盘安装centos遇到空间不足
linux·运维·centos
optimistic_chen1 天前
【Redis系列】Java操作Redis客户端
java·linux·redis·客户端·服务端
Tfly__1 天前
Ubuntu20.04安装Genesis(最新)
linux·人工智能·pytorch·ubuntu·github·无人机·强化学习
习惯就好zz1 天前
在 Ubuntu 18.04 (WSL) 上配置 LazyVim
linux·ubuntu·nvim·lazyvim·1804