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和内存使用情况等。
相关推荐
ID_180079054735 小时前
Python结合淘宝关键词API进行商品价格监控与预警
服务器·数据库·python
ZFB00015 小时前
【麒麟桌面系统】V10-SP1 2503 系统知识——救援模式显示异常
linux·kylin
落笔画忧愁e5 小时前
腾讯云轻量服务器 + OpenClaw 部署全攻略:从购买到飞书接入
服务器·飞书·腾讯云
第七序章5 小时前
【Linux学习笔记】初识Linux —— 理解gcc编译器
linux·运维·服务器·开发语言·人工智能·笔记·学习
AI科技星5 小时前
从ZUFT光速螺旋运动求导推出自然常数e
服务器·人工智能·线性代数·算法·矩阵
迎仔5 小时前
A-总览:GPU驱动运维系列总览
linux·运维
tiantangzhixia5 小时前
Master PDF Linux 平台的 5.9.35 版本安装与自用
linux·pdf·master pdf
AI_56785 小时前
阿里云OSS成本优化:生命周期规则+分层存储省70%
运维·数据库·人工智能·ai
choke2335 小时前
软件测试任务测试
服务器·数据库·sqlserver