Linux下 进程(二)(进程状态、僵尸进程和孤儿进程)

欢迎来到我的频道 【点击跳转专栏】

码云链接 【点此转跳】

文章目录

  • [1. 广义操作系统下的进程状态](#1. 广义操作系统下的进程状态)
    • [1.1 运行](#1.1 运行)
    • [1.2 阻塞](#1.2 阻塞)
    • [1.3 挂起](#1.3 挂起)
      • [1.3.1 磁盘的swap分区&&挂起](#1.3.1 磁盘的swap分区&&挂起)
      • [1.3.2 就绪挂起状态&&阻塞挂起状态](#1.3.2 就绪挂起状态&&阻塞挂起状态)
    • [1.4 作者说](#1.4 作者说)
  • [2. Linux下进程的状态](#2. Linux下进程的状态)
    • [2.1 运行状态](#2.1 运行状态)
    • [2.2 可被中断的睡眠状态](#2.2 可被中断的睡眠状态)
      • [2.2.1 kill(发送信号)](#2.2.1 kill(发送信号))
      • [2.2.2 + 的含义(前台进程)](#2.2.2 + 的含义(前台进程))
    • [2.3 不可被中断的休眠状态](#2.3 不可被中断的休眠状态)
    • [2.4 停止状态](#2.4 停止状态)
      • [2.4.1 停止状态的场景](#2.4.1 停止状态的场景)
      • [2.4.2 追踪状态](#2.4.2 追踪状态)
    • [2.5 死亡状态](#2.5 死亡状态)
      • [2.5.1 僵尸进程&&僵尸状态](#2.5.1 僵尸进程&&僵尸状态)
      • [2.5.2 进程具体怎么回收](#2.5.2 进程具体怎么回收)
      • [2.5.3 僵⼫进程危害](#2.5.3 僵⼫进程危害)
    • [2.6 孤儿进程](#2.6 孤儿进程)
  • [3. 进程状态查看(ps)](#3. 进程状态查看(ps))

1. 广义操作系统下的进程状态

1.1 运行

一般情况下,一个CPU一个调度队列,其中一个常见算法就是FIFO(先进先出),在Linux中 内存里面会有一个对应于CPU的运行队列,会把所有进程的PCB都放在一个队列里面,然后选择头部的PCB的程序进CPU里去运行,去调度,然后新的进程来了,新的进程的PCB必须放在队列尾部,这个就称 操作系统的调度队列 ,然后凡事在这个队列里的进程和CPU运行的进程(并不是说在CPU里面跑的才叫运行状态)就叫 运行状态 ,它对应于task_struct里面的某个属性!

1.2 阻塞

比如说C语言里的 scanf 用程序读一个数据,但数据还在键盘上没读到内存------这时候,Linux 不会让 CPU 干等着,而是让这个进程进入阻塞状态

在 Linux 中,阻塞状态(Blocked State)进程 的一种睡眠等待状态:当进程需要等待某个外部事件完成(如 I/O 操作、信号、锁等),而该事件尚未就绪时,内核会主动将其置为阻塞状态,释放 CPU 资源,直到事件发生后再唤醒它,此时进程不占用 CPU,不能被调度执行,直到它等待的条件满足(比如从键盘上读取数据)。

比如说 在Linux中 每个设备(键盘、磁盘等)都在内存中都会有个对应的结构体,里面包含里硬件的所有属性,其中有一个struct task_struct * 的指针 里面是一个等待队列 ,当该进程进入阻塞状态的时候 该进程对应的PCB就会就会从运行队列 转移到等待队列 上,并且对应结构体的某些属性会发生变化。

⚠️:所以阻塞还是运行的本质,就是看你的task_struct在哪个队列中。

1.3 挂起

1.3.1 磁盘的swap分区&&挂起

在正常的操作系统中(无论是win、mac还是linux)都会在磁盘中预留一个一块空间(在win中独立于C、D盘,甚至你看不到),一般是内存的两倍,它的存在是当内存不足的时候,进程的PCB依然在内存,但进程对应的数据和代码,会被交换到swap分区(磁盘),用于减少内存压力

其中对应数据进入swap分区换出 过程就叫挂起

1.3.2 就绪挂起状态&&阻塞挂起状态

  1. 当一个进程处于阻塞状态 的时候,其对应的PCB在对应的等待队列 中,此时你的进程也不在调度使用,代码和数据放在内存有的时候太浪费,此时就会对该进程进行挂起 ,让其数据换出swap分区,此时该进程就会进如阻塞挂起状态
  2. 阻塞挂起状态的进程中的数据swap分区换入内存 后,此时的过程就叫激活,此时进程又变成了阻塞状态
  3. 就绪挂起状态也是同理(就绪状态后面讲,其实在Linux中运行状态就等于操作系统中的运行状态+就绪状态

注意⚠️:过度swap会导致系统变慢,而swap分区存在的本质就是:用时间换空间;而swap空间不设置太大也是为了防止内存过度依赖swap空间导致系统卡顿!

  • 当系统过于卡顿的时候在linux下,就会选择性的杀死特定的进程。

1.4 作者说

在《操作系统》一书中,为了确保在任何系统中都能匹配,所以概念会进程一些抽象化,而我这里是为了就业 不是为了考408,所以下面就对着Linux具体讲了。

2. Linux下进程的状态

为了弄明⽩正在运⾏的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有⼏个状态(在Linux内核⾥,进程有时候也叫做任务)。

下⾯的状态在kernel源代码⾥定义。

c 复制代码
//可以用不同的数字表示系统的状态
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 */
};

2.1 运行状态

R 运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中,要么在运行队列里。

Linux中,运行状态=广义操作系统学科里的 运行状态+就绪状态

2.2 可被中断的睡眠状态

S 睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。

Linux系统中,这个就相当于 广义操作系统中的 阻塞状态

注意下面代码:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{       
        while(1)
        {
            printf("hello world!\n");
        }

return 0;
}

当我们运行程序的时候,明明在死循环,但我们发现进程状态是S+不是R

这是因为,每次printf都会进行一次IO操作,由于CPU显卡、显示器速度差距过大(CPU速度远远更快),假设外设每次响应需要1s,才能打印一次,而CPU处理打印操作可能只要0.0001s,二者差距太大,CPU不可能一直在等你,所以大部分时间进程都会处于休眠状态,其实如果你运气好的话可能查看到R+状态,不过因为概率太低,所以你查看基本都是S+


当你把 printf("hello world!\n");注释掉此时进程就处于R+了:

2.2.1 kill(发送信号)

我们除了通过ctrl+c杀死进程,还可以通过kill -9 进程的pid发送信号杀死进程(-9就是对应的信号 以后会讲)

当一个休眠可以被信号中断,那么这个进程就叫可被中断睡眠,也就是浅睡眠。

2.2.2 + 的含义(前台进程)

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

int main()
{
        while(1)
        {
            sleep(1);
        }
return 0;
}

当我们输入指令的时候 我们当前的bash(一个终端一个bash,我可以一个账户开多个终端)会没有反应这种叫 前台进程 ,而前台进程 后面往往会带+

当我们不想让程序变成前台进程 ,我们运行的时候可以带个& 此时进程就没有+了,此时bash依然可以响应,此时的进程叫 后台进程

2.3 不可被中断的休眠状态

D 磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待 I/O 的结束。

Linux系统中,这个也相当于 广义操作系统中的 阻塞状态 ,不过是Linux特有的。

比如说 当你往磁盘写入500mb的数据 如果写入失败后,若进程直接被杀死,无法向上面传达,磁盘数据被直接丢弃了,如果这500mb是银行的转账数据怎么办??得捅多大娄子啊!此时就需要一个状态 尤其是在涉及到磁盘I\O的时候 ,那么就需要一个可以等待的进程,于是就设计了磁盘休眠状态,此时该进程是无法被杀死的。

2.4 停止状态

T 停止状态(stopped) :可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。

  • 处于该状态的进程既不占用 CPU,也不会被调度运行,直到被显式恢复,但是进程仍保留在内存中,所有资源(如内存、文件描述符等)未被释放。

休眠往往是因为某种条件短暂的等会自动醒来,而停止往往需要手动醒来。我们可以通过kill -l来查看所需命令:
我们可以通过 kill -19手动将进程进入停止状态 :

我们可以通过kill -18将进程恢复(不过从前台进程变成了后台进程 ):

2.4.1 停止状态的场景

首先,我们需要知道一件事情:前台进程可以从键盘获取数据,而后台进程是不许的。

当前台进程想要获取键盘值的时候,此时进程处于S状态,而当后台进程试图获取键盘数据的时候,此时进程处于T状态,所以说这也是你ctrl+c为什么无法杀死后台进程的原因

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

int main()
{
        int i;
        scanf("%d",&i);
        while(1)
        {
//            printf("hello world!\n");
              sleep(1);
        }

return 0;
}

2.4.2 追踪状态

Linux 中,"追踪状态 " 通常指的是进程被调试器(如 gdb)跟踪(traced) 时所处的特殊暂停状态 。这种状态在进程状态字段中显示为小写的 t


当我们开始debug时我们发现 此时执行的是 gdb myprocess_debug 这个进程 这个进程是gdb启动出来的:

当我们在第八行打个端点然后运行程序到第八行停止 此时我们发现r的本质是让gdb启动一个gdb myprocess_debug子进程 ,然后遇到断点,停止,可以理解成父进程子进程 发送了-19的信号 ,当你输入n让程序运行一步,你就可以理解成不断发送-18 -19信号:

2.5 死亡状态

X 死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态,是一个释放资源时候的瞬时状态。

2.5.1 僵尸进程&&僵尸状态

  • 僵尸状态(Zombies) 是一个比较特殊的状态。当进程退出并且父进程(使用 wait() 系统调用)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
  • 僵尸进程 会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
  • 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态。

有一个大爷在路上死了,警察过去第一件事情去要确认大爷是怎么死的 确认完死因后 大爷被抬走,而这个过程就叫僵尸状态 ,抬走后才是死亡状态 ,当一个进程死亡后,意味着这个进程的资源会直接或者未来被释放掉。


为什么会有z状态

因为进程的存在是有存在意义的,z状态的本质就是检查该进程退出原因,然后给出用户,当z状态读取完毕后,才能释放掉资源。


z状态具体是什么?

进程 的代码资源已经释放,此时进入z状态PCB仍然保留在内核的进程表中,用于传递对应进程结束信息。

我们可以用下面代码模拟出僵尸进程:

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

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

    pid_t id = fork();
    if(id == 0)
    {
        // child
        while(1)
        {
            printf("我是子进程: %d, 我的父进程是: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else
    {
        // father
        while(1)
        {
            printf("我是父进程: %d\n", getpid());
            sleep(1);
        }
    }
  }

当我们杀死子进程后,我们发现子进程变成了僵尸状态:

里面的defunct表示 失效

2.5.2 进程具体怎么回收

在回答这个问题前,有三个前置问题。


  1. 进程退出后,退出信息是什么?

一个进程退出后,包含一个进程的退出数字和退出信息(例如非正常退出时候的信号值)。

比如main函数返回值 我们一般是0 如果是0,这个信息是告诉操作系统表示你进程是正常退出的。


  1. 进程退出后,退出信息保存在哪里?

不废话,在对应进程的task_struct里面。


  1. 检测z状态进程,回收z状态进程,本质是在做什么?

当进程进入僵尸状态,此时其代码段和数据段已经被释放了,但它的进程控制块(PCB)------在 Linux 中就是 task_struct ------ 仍然保留在内核的进程表中,至于为什么需要保留这些信息?

父进程可能需要知道子进程是怎么退出的(正常退出、崩溃、返回值是多少)。如果内核立即彻底删除子进程的所有信息,父进程就无法获取这些状态了。

因此,僵尸进程 = 只剩"户口"没注销,身体早已清空


  1. 具体怎么回收?

需要父进程通过系统调用,调用底层回收(wait)(了解)

2.5.3 僵⼫进程危害

  1. 进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态,子进程一直不回收。
  2. 维护退出状态(僵尸状态)本身就是要用数据维护 ,也属于进程基本信息,所以保存在 task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护。
  3. 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!因为数据结构对象本身就占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!从而产生内存泄漏

当一个进程结束的时候,就不存在内存泄漏问题(无论是否free)但是我们大部分软件都是一直处于while循环,我们称呼常驻内存,所以我们需要手动释放资源;同理,如果父进程不结束,我们也要手动释放子进程的资源。

2.6 孤儿进程

⽗进程先退出,⼦进程就称之为 孤⼉进程

我们可以通过以下代码创建孤儿进程

cpp 复制代码
#include <stdio.h>
#include <unistd.h>

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        // child
        while(1)
        {
            printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else{
        // parent
        int cnt = 5;
        while(cnt)
        {
            cnt--;
            printf("我是父进程,pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
            sleep(1);
        }
    }
    return 0;
}

我们发现当父进程结束后,子进程仍在运行(不是僵尸);同时子进程的ppid会变成1,也就是说此时子进程会被systemd自动收养,同时把这个进程自动变成后台进程 (所以你此时无法ctrl+c退出):

我们可以通过以下命令查看

cpp 复制代码
 while :; do ps axj | head -1 ;ps axj | grep processTest |grep -v grep ;sleep 1 ;echo "#####################################################" ;done

具体情况:

⚠️:systemd 是现代 Linux 系统中默认的初始化系统(init system)和系统与服务管理器,负责在内核启动后引导整个用户空间,并管理所有系统进程、服务、设备、挂载点、定时任务等。(了解)

孤儿进程是无害的!

因为 Linux 内核有 "进程收养机制":

  • 一旦发现进程的父进程退出,立即把它的父 PID 改为 1
  • init/systemd 会定期调用 wait()回收,或者在系统关闭的时候自动清理;

所以孤儿进程退出后不会变成僵尸。

3. 进程状态查看(ps)

shell 复制代码
ps aux / ps axj 命令
  • a:显示一个终端所有的进程,包括其他用户的进程。
  • x:显示没有控制终端的进程,例如后台运行的守护进程。
  • j:显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
  • u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户、CPU和内存使用情况等。
相关推荐
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
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式