从内核视角彻底搞懂Linux进程状态:运行、阻塞、挂起与内核链表的底层实现

在这里插入图片描述

🌈 say-fall:个人主页
🚀 专栏:《手把手教你学会C++》 | 《系统深入Linux操作系统》 | 《数据结构与算法》 | 《小游戏与项目》
💪 格言:做好你自己,才能吸引更多人,与他们共赢,这才是最好的成长方式。

📝 前言

提到进程状态,很多人的第一反应是课本上那个经典的"三态模型"——运行、就绪、阻塞。但当你真正打开Linux终端,用ps aux命令查看进程状态时,会看到R、S、D、T、Z等多种状态,这时候你可能会疑惑:这些状态和课本上的三态有什么关系?为什么会有这么多细分状态?进程状态转换的底层到底发生了什么?

课本上的三态模型是高度抽象的,而在Linux内核中,每一种状态都对应着一个具体的队列。进程的状态转换,本质上就是内核把进程控制块(PCB,在Linux中是task_struct结构体)从一个队列移动到另一个队列的过程。

通过本文,你将掌握:

技能 应用场景
理解内核三态模型的本质 从数据结构角度理解进程管理
掌握Linux 7种进程状态 实际排查进程问题(如D状态进程杀不死)
理解侵入式链表设计 阅读内核源码、理解内核数据结构
掌握container_of宏原理 内核驱动开发、嵌入式系统编程
复现各种进程状态 Linux系统运维、性能调优
理解僵尸进程与孤儿进程 避免内存泄漏、进程资源管理

📌 前置知识: 基本的C语言语法、Linux常用命令

文章目录


一、🔥 内核视角下的三态模型:运行、阻塞与挂起

课本上的三态模型是高度抽象的,而在Linux内核中,每一种状态都对应着一个具体的队列。进程的状态转换,就是内核把task_struct从一个队列移动到另一个队列的过程。

1.1 运行态(Running):在CPU调度队列中等待执行

运行态的本质:进程的task_struct结构体位于**运行队列(runqueue)**中,并且已经获得了CPU资源,正在执行代码。

注意:在Linux内核中,"运行态"其实包含了课本上的"运行"和"就绪"两种状态。也就是说,所有在运行队列中的进程,状态都是R (running)。调度器会从运行队列中选择优先级最高的进程,分配CPU时间片让它执行。

  • 一个CPU对应一个运行队列(单核心系统只有一个运行队列)
  • 运行队列中的进程按照调度算法(如FIFO、CFS)排序
  • 当进程的时间片用完,或者有更高优先级的进程进入运行队列时,当前进程会被抢占,重新回到运行队列中等待

💡 在Linux内核源代码中,所有运行在系统里的进程都以task_struct双链表的形式存在内核里。

1.2 阻塞态(Blocked):在硬件等待队列中等待资源

阻塞态的本质:进程需要等待某种硬件资源或事件(如键盘输入、磁盘IO、网络数据)就绪,因此被内核从运行队列中移除,加入到对应硬件设备的**等待队列(wait queue)**中。

这是很多人容易误解的地方:阻塞的进程不是"什么都不做",而是被内核挂到了具体硬件设备的等待队列上。当硬件设备完成操作后,会触发中断,内核在中断处理函数中,会把等待该设备的进程从等待队列中唤醒,重新加入运行队列。

比如:

  • 当你在终端输入字符时,等待键盘输入的进程会被挂到键盘设备的等待队列上
  • 当你读取一个文件时,等待磁盘IO完成的进程会被挂到磁盘设备的等待队列上
  • 当你等待网络数据时,进程会被挂到对应网卡的等待队列上

1.3 挂起态(Suspended):被换出到磁盘交换分区

挂起态的本质:当系统内存不足时,内核会把一些暂时不运行的进程(包括运行队列中等待时间较长的进程,和等待队列中等待资源的进程)的内存数据**换出(swap out)**到磁盘的交换分区(swap partition)中,释放出物理内存给更需要的进程使用。

被挂起的进程,它的task_struct结构体仍然保留在内存中,但它的代码和数据已经不在物理内存里了。当内核需要运行这个进程时,会把它的代码和数据从交换分区**换入(swap in)**到物理内存中,然后重新加入运行队列。

挂起态是对阻塞态的进一步优化:当内存充足时,阻塞的进程仍然保留在内存中;当内存不足时,阻塞的进程会被换出到磁盘,进入挂起态。


二、🔗 进程状态流动的三大核心场所

现在我们来详细拆解进程状态流动的三个核心场所:运行队列、硬件等待队列和磁盘交换分区。理解了这三个队列,你就理解了进程状态转换的全部逻辑。

2.1 运行队列(Runqueue):CPU的"待办事项列表"

运行队列是内核中最重要的队列之一,它保存了所有等待CPU执行的进程。调度器的核心工作,就是从运行队列中选择下一个要运行的进程。

在单核心系统中,只有一个运行队列;在多核心系统中,每个CPU核心都有自己独立的运行队列。这样设计的好处是避免了多个CPU竞争同一个运行队列的锁,提高了调度效率。

运行队列中的进程状态:全部都是R (running)状态。

进程进入运行队列的时机

  1. 进程被创建时(fork系统调用)
  2. 阻塞的进程被唤醒时(资源就绪)
  3. 挂起的进程被换入内存时
  4. 进程的时间片用完,被抢占后重新回到运行队列

进程离开运行队列的时机

  1. 进程需要等待资源,进入阻塞态
  2. 进程退出(exit系统调用)
  3. 内存不足时,被换出到交换分区,进入挂起态

2.2 硬件等待队列(Wait Queue):设备的"等待者列表"

每一个硬件设备(键盘、磁盘、网卡、显示器等)在内核中都有自己的等待队列。当进程需要访问某个硬件设备,而设备当前忙时,内核会把进程加入到该设备的等待队列中,然后调度其他进程运行。

当硬件设备完成操作后,会触发一个硬件中断。内核在中断处理函数中,会遍历该设备的等待队列,把所有等待该事件的进程唤醒,将它们的task_struct从等待队列移动到运行队列。

等待队列中的进程状态:主要是S (可中断睡眠)D (不可中断睡眠)状态,这两种状态都属于阻塞态。

进程进入等待队列的时机

  • 调用read()读取键盘、磁盘、网络等设备
  • 调用write()写入设备
  • 等待信号量、互斥锁等同步原语
  • 调用sleep()等延时函数

进程离开等待队列的时机

  • 等待的资源就绪,被中断处理函数唤醒
  • 可中断睡眠状态的进程收到信号
  • 内存不足时,被换出到交换分区,进入挂起态

2.3 磁盘交换分区(Swap Partition):内存的"后备仓库"

交换分区是磁盘上的一块特殊区域,用作物理内存的扩展。当系统物理内存不足时,内核会使用页面置换算法(如LRU),把一些不常用的内存页面换出到交换分区中。

对于进程来说,如果它的所有内存页面都被换出到交换分区,那么这个进程就进入了挂起态。挂起态的进程无法被调度执行,因为它的代码和数据都不在物理内存中。

交换分区中的进程状态:没有单独的状态标志,内核通过页表项来判断进程的页面是否在物理内存中。

进程被换出到交换分区的时机

  • 系统物理内存不足
  • 进程长时间处于阻塞态,没有被调度执行

进程被换入到物理内存的时机

  • 进程被唤醒,需要被调度执行
  • 系统有足够的空闲物理内存

三、🧩 为什么一个PCB会存在于多个数据结构中?

现在我们来回答一个关键问题:为什么一个进程的task_struct会同时存在于多个数据结构中?这是理解内核进程管理的核心。

3.1 内核需要从多个维度管理进程

内核需要从不同的角度来管理进程,因此需要把同一个task_struct加入到多个不同的链表中:

  1. 调度维度:需要把task_struct加入到运行队列中,以便调度器选择下一个要运行的进程
  2. 设备管理维度:当进程等待某个设备时,需要把task_struct加入到该设备的等待队列中
  3. 内存管理维度:需要把task_struct加入到LRU队列中,以便内存不足时选择要换出的进程
  4. 进程关系维度:需要把task_struct加入到进程树中,以便管理父子进程关系
  5. 信号管理维度:需要把task_struct加入到信号队列中,以便处理发送给进程的信号

如果一个task_struct只能存在于一个链表中,那么内核就无法同时从多个维度管理进程。因此,task_struct结构体中包含了多个链表节点(list_head结构体),每个链表节点对应一个链表。

3.2 侵入式链表:内核数据结构的精髓

为了实现一个数据结构同时存在于多个链表中,Linux内核使用了**侵入式链表(Intrusive Linked List)**的设计。

普通链表的设计是:链表节点包含数据指针,指向具体的数据结构。而侵入式链表的设计正好相反:数据结构包含链表节点,链表节点嵌入在数据结构内部。

我们来对比一下代码实现:

普通链表

struct Node {
    void *data; // 指向数据的指针
    struct Node *next;
    struct Node *prev;
};

内核侵入式链表

// 链表节点结构体,只包含前后指针
struct list_head {
    struct list_head *next;
    struct list_head *prev;
};

// 进程控制块结构体,包含多个链表节点
struct task_struct {
    // ... 其他成员 ...
    struct list_head run_list;  // 运行队列节点
    struct list_head wait_list; // 等待队列节点
    struct list_head lru_list;  // LRU队列节点
    struct list_head sibling;   // 兄弟进程节点
    // ... 其他成员 ...
};

侵入式链表的优势非常明显:

  1. 一个数据结构可以同时存在于多个链表中,每个链表对应一个list_head成员
  2. 不需要额外的内存分配来存储链表节点,节省了内存开销
  3. 链表操作非常高效,只需要修改指针即可

四、🎯 内核链表如何找到数据结构?container_of宏的魔法

现在你可能会问:当我们遍历链表时,得到的是list_head结构体的指针,怎么通过这个指针找到包含它的整个task_struct结构体呢?

这就要用到Linux内核中最经典的宏之一:container_of。这个宏的作用是:通过一个结构体成员的指针,计算出整个结构体的指针。

4.1 container_of宏的原理

container_of宏的定义如下(简化版):

#define container_of(ptr, type, member) ({ \
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \
    (type *)( (char *)__mptr - offsetof(type, member) ); \
})

其中:

  • ptr:指向结构体成员的指针
  • type:整个结构体的类型
  • member:结构体成员的名字

这个宏的原理非常巧妙:

  1. 首先把地址0强制转换为type*类型,然后访问它的member成员,这样就得到了member成员在结构体中的偏移量
  2. 然后把ptr指针转换为char*类型,减去这个偏移量,就得到了整个结构体的起始地址

我们用一个例子来理解:

struct task_struct {
    int pid;
    int state;
    struct list_head run_list;
    // ... 其他成员 ...
};

// 假设我们有一个指向run_list成员的指针
struct list_head *ptr = &task->run_list;

// 使用container_of宏得到整个task_struct的指针
struct task_struct *task = container_of(ptr, struct task_struct, run_list);

在这个例子中,offsetof(struct task_struct, run_list)会计算出run_list成员在task_struct结构体中的偏移量(假设是8字节)。然后把ptr指针减去8字节,就得到了task_struct结构体的起始地址。

4.2 内核链表的遍历

有了container_of宏,内核链表的遍历就变得非常简单了。内核提供了list_for_each_entry宏来遍历链表:

#define list_for_each_entry(pos, head, member) \
    for (pos = list_entry((head)->next, typeof(*pos), member); \
         &pos->member != (head); \
         pos = list_entry(pos->member.next, typeof(*pos), member))

其中list_entry就是container_of的别名。

比如,遍历运行队列中的所有进程:

struct task_struct *task;
list_for_each_entry(task, &runqueue, run_list) {
    // 处理每个进程
    printk("PID: %d\n", task->pid);
}

五、📊 Linux内核中真实的进程状态

Linux内核在include/linux/sched.h文件中定义了7种进程状态。下面我们来看看这些状态之间的完整转换关系。

Linux内核中定义的进程状态数组如下:

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 */
};

现在我们把这些状态和我们之前讲的三态模型对应起来:

Linux状态 对应三态模型 说明
R (running) 运行态 进程在运行队列中,正在运行或等待调度
S (sleeping) 阻塞态 可中断睡眠,可以被信号唤醒
D (disk sleep) 阻塞态 不可中断睡眠,只能被资源就绪唤醒
T (stopped) 阻塞态 进程被停止,收到SIGCONT信号后继续运行
t (tracing stop) 阻塞态 进程被调试器暂停
X (dead) 终止态 进程已经完全退出,资源已经被释放
Z (zombie) 终止态 进程已经退出,但父进程还没有调用wait()回收资源

5.1 R状态(Running):运行态

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

也就是说,所有在运行队列中的进程,无论是否正在CPU上执行,状态都是R。

5.2 S状态(Sleeping):可中断睡眠

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

S状态是最常见的阻塞状态,进程在等待资源时可以被信号唤醒。比如sleep命令、等待键盘输入的进程都处于S状态。

⚠️ S状态的进程收到信号后会被唤醒,所以如果你用kill命令给S状态的进程发信号,它会立即响应。

5.3 D状态(Disk Sleep):不可中断睡眠

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

D状态是Linux特有的状态,也是很多人最困惑的状态。很多人会遇到这样的情况:一个进程处于D状态,用kill -9都杀不死,这是为什么呢?

D状态的本质:进程正在执行关键的磁盘IO操作,内核为了保证数据一致性,不允许这个进程被任何信号中断。

当进程执行磁盘IO操作时(如读取文件、写入文件),它会向磁盘控制器发送IO请求,然后进入D状态等待IO完成。在这个过程中,如果进程被信号中断,可能会导致IO操作被打断,从而造成文件系统损坏或者数据丢失。因此,内核把这些进程设置为不可中断睡眠状态,只能等待IO操作完成后才能被唤醒。

D状态进程的特点

  • 不响应任何信号(包括SIGKILL信号)
  • 只能等待IO操作完成后自动唤醒
  • 如果IO操作永远无法完成(比如磁盘损坏),那么这个进程会永远处于D状态,只能通过重启系统解决

什么时候会进入D状态

  • 执行磁盘读写操作(read、write系统调用)
  • 执行文件系统操作(如mount、umount)
  • 等待块设备就绪

5.4 T状态(Stopped):停止状态

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

T状态的进程不执行任何代码,只是被暂停了。它仍然保留在内存中,可以被恢复执行。

5.5 t状态(Tracing Stop):跟踪停止

t跟踪停止状态(tracing stop):进程被调试器(如gdb)设置断点后进入的状态。

当你用gdb调试一个程序,在断点处停下来时,进程就会进入t状态。

5.6 Z状态(Zombie):僵尸进程

Z僵死状态(Zombies):是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用)没有读取到子进程退出的返回代码时,就会产生僵死(尸)进程。

僵死进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就进入Z状态。

僵尸进程的危害

  • 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了
  • 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中
  • 一个父进程创建了很多子进程,就是不回收,就会造成内存资源的浪费
  • 这就是内存泄漏

⚠️ 僵尸进程已经释放了大部分资源,只保留了task_struct结构体和退出状态,等待父进程回收。

5.7 X状态(Dead):死亡状态

X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

进程已经完全退出,所有资源都已经被释放,这个状态非常短暂,几乎看不到。

5.8 孤儿进程(Orphan Process)

除了上面7种状态,我们还需要了解孤儿进程的概念。

孤儿进程:父进程如果提前退出,那么子进程后退出,进入Z状态之后,该如何处理呢?

父进程先退出,子进程就称之为"孤儿进程"。孤儿进程会被1号init/systemd进程领养,由init/systemd进程负责回收。

💡 孤儿进程不是僵尸进程!孤儿进程的父进程变成了init进程,init进程会定期调用wait()回收子进程资源,所以孤儿进程不会导致内存泄漏。


六、🔧 实际实验:在Linux中复现各种进程状态

理论讲了这么多,现在我们来做实际实验,在自己的Linux机器上复现每一种状态。

6.1 实验准备

打开Linux终端,我们会使用以下命令:

  • ps aux:查看所有进程的状态
  • top:实时查看进程状态
  • kill:向进程发送信号
  • dd:用于产生磁盘IO,复现D状态

💡 ps aux命令中:a表示显示一个终端所有的进程,包括其他用户的进程;x表示显示没有控制终端的进程;u表示以用户为中心的格式显示进程信息。

6.2 复现R状态(运行态)

R状态的进程正在占用CPU或者等待CPU调度。我们可以用一个死循环来复现:

# 后台运行一个死循环
while :; do :; done &

然后用ps aux查看这个进程的状态:

ps aux | grep "while :"

你会看到输出类似这样:

user     12345  99.9  0.0   4356   720 pts/0    R    10:00   0:10 bash

其中R就表示这个进程处于运行态。

6.3 复现S状态(可中断睡眠)

S状态是最常见的状态,大部分后台进程都处于S状态。我们用sleep命令来复现:

# 后台运行一个sleep进程,睡眠1000秒
sleep 1000 &

然后用ps aux查看:

ps aux | grep sleep

输出:

user     12346   0.0  0.0   4356   720 pts/0    S    10:01   0:00 sleep 1000

其中S表示可中断睡眠状态。我们可以用kill命令给它发信号唤醒它:

kill -9 12346

6.4 复现D状态(不可中断睡眠)

D状态的进程正在执行磁盘IO操作。我们用dd命令读写一个大文件来复现:

# 后台运行dd命令,创建一个10GB的文件
dd if=/dev/zero of=test bs=1M count=10000 &

然后立即用ps aux查看dd进程的状态:

ps aux | grep dd

你会看到输出类似这样:

user     12347  10.0  0.0   4356   720 pts/0    D    10:02   0:05 dd if=/dev/zero of=test bs=1M count=10000

其中D表示不可中断睡眠状态。现在你可以尝试用kill -9杀死它:

kill -9 12347

你会发现,这个进程并没有立即被杀死,而是会继续运行一段时间,直到IO操作完成后才会退出。这就是D状态的特点:不响应信号,只能等待IO完成。

6.5 复现T状态(停止)

我们可以用kill -STOP命令让一个进程进入停止状态:

# 后台运行一个sleep进程
sleep 1000 &
# 查看进程ID
ps aux | grep sleep
# 发送SIGSTOP信号
kill -STOP 12348
# 再次查看状态
ps aux | grep sleep

输出:

user     12348   0.0  0.0   4356   720 pts/0    T    10:03   0:00 sleep 1000

其中T表示停止状态。我们可以用kill -CONT命令让它继续运行:

kill -CONT 12348
# 再次查看状态
ps aux | grep sleep

输出:

user     12348   0.0  0.0   4356   720 pts/0    S    10:03   0:00 sleep 1000

6.6 复现Z状态(僵尸进程)

僵尸进程是子进程退出后,父进程没有回收资源导致的。我们写一个简单的C程序来复现:

// zombie.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // 子进程,立即退出
        printf("子进程退出,PID: %d\n", getpid());
        exit(0);
    } else if (pid > 0) {
        // 父进程,不调用wait,睡眠100秒
        printf("父进程运行,PID: %d\n", getpid());
        sleep(100);
    } else {
        perror("fork失败");
        return 1;
    }

    return 0;
}

编译并运行:

gcc zombie.c -o zombie
./zombie &

然后用ps aux查看:

ps aux | grep defunct

输出:

user     12350   0.0  0.0      0     0 pts/0    Z    10:04   0:00 [zombie] <defunct>

其中Z表示僵尸状态,<defunct>表示这是一个僵尸进程。100秒后,父进程退出,僵尸进程会被init进程回收。

6.7 复现孤儿进程

孤儿进程是父进程先退出,子进程还在运行的情况。我们写一个简单的C程序来复现:

// orphan.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t id = fork();
    
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id == 0) {
        // 子进程,睡眠10秒
        printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
        sleep(10);
        printf("Child exit, ppid now: %d\n", getppid());
    } else {
        // 父进程,睡眠3秒后退出
        printf("I am parent, pid: %d\n", getpid());
        sleep(3);
        exit(0);
    }
    
    return 0;
}

编译并运行:

gcc orphan.c -o orphan
./orphan

运行后观察输出,你会发现父进程退出后,子进程的ppid变成了1(init/systemd进程)。


七、🧠 知识体系总结

整个进程状态管理体系是一个层层递进的结构,从抽象的理论模型到具体的内核实现,再到底层的数据结构支撑:

  1. 最上层:课本上抽象的三态模型,它是对进程状态的高度概括
  2. 中间层:内核的具体实现,通过三个核心队列来管理进程状态的流动
  3. 底层:数据结构的支撑,task_struct、侵入式链表和container_of宏构成了内核进程管理的基石
  4. 最下层:Linux内核中真实的7种进程状态,它们是对三态模型的细化和扩展
  5. 最后:通过实际实验验证我们的理论,加深对进程状态的理解

八、🤔 几个思考题

学完本文,来试试回答这些问题:

1️⃣ 为什么D状态的进程用kill -9都杀不死?

答: D状态是不可中断睡眠状态,进程正在执行关键的磁盘IO操作。内核为了保证数据一致性,不允许这个进程被任何信号中断。如果强行中断,可能会导致文件系统损坏或数据丢失。所以D状态的进程只能等待IO操作完成后自动唤醒。

💡 如果D状态进程一直不退出,可能是磁盘故障或驱动问题,需要检查硬件。

2️⃣ 僵尸进程和孤儿进程有什么区别?

答:

  • 僵尸进程:子进程已经退出,但父进程没有调用wait()回收子进程的资源。僵尸进程保留了task_struct和退出状态,会造成内存泄漏。
  • 孤儿进程:父进程先退出,子进程还在运行。孤儿进程会被init/systemd进程(PID=1)领养,由init进程负责回收,不会造成内存泄漏。

3️⃣ 为什么一个task_struct需要同时存在于多个链表中?

答: 内核需要从多个维度管理进程:调度维度(运行队列)、设备管理维度(等待队列)、内存管理维度(LRU队列)、进程关系维度(进程树)、信号管理维度(信号队列)。如果task_struct只能存在于一个链表中,就无法同时从多个维度管理进程。侵入式链表的设计让一个数据结构可以同时存在于多个链表中,每个链表对应一个list_head成员。

4️⃣ container_of宏的原理是什么?

答: container_of宏通过结构体成员的指针,计算出整个结构体的指针。它的原理是:

  1. 利用offsetof宏计算出成员在结构体中的偏移量
  2. 将成员指针转换为char*类型,减去偏移量,得到结构体的起始地址

公式:结构体地址 = 成员指针 - 成员偏移量


九、💡 结语

通过这篇文章,我们从内核数据结构的视角,彻底拆解了Linux进程状态的本质。我们明白了:

  • 进程状态不是抽象的概念,而是进程在不同队列之间的流动
  • 一个PCB会存在于多个数据结构中,因为内核需要从多个维度管理进程
  • 内核使用侵入式链表和container_of宏,高效地实现了一个数据结构同时存在于多个链表中
  • Linux特有的D状态是为了保证磁盘IO的数据一致性,不能被信号中断
  • 僵尸进程会造成内存泄漏,孤儿进程不会
  • 所有的进程状态转换,本质上都是链表节点的增删改查

理解了这些底层原理,你再遇到进程状态相关的问题时(比如D状态进程杀不死、僵尸进程过多等),就能够从根本上分析问题的原因,而不是只会用kill命令盲目尝试。

✅ 本节完…

📝 作者:say-fall | 编辑:say-fall | 🌟 原创不易,如果对你有帮助,记得 👍 点赞 + ⭐ 收藏 哦!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐