欢迎光临
我们一直在努力

【Linux】 进程(3)深度解析:从查看进程到进程状态


  Linux 进程管理深度解析:从查看进程到进程状态  

一、查看进程

        在 Linux 中,进程是资源分配的基本单位。

        系统中每时每刻都有大量进程在运行,如何查看它们是理解进程管理的第一步。

1.1 ps 命令

ps(process status)是最常用的进程查看工具,它报告当前系统中进程的快照信息。

常用组合:ps aux

ps aux

输出示例:

USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 169248 13136 ? Ss 08:00 0:02 /sbin/init
root 2 0.0 0.0 0 0 ? S 08:00 0:00 [kthreadd]
wxx 1234 0.5 1.2 523416 98304 ? Sl 08:05 0:15 ./myapp

字段解读:

字段含义
USER 进程所属用户
PID 进程 ID,全局唯一
%CPU 占用 CPU 百分比
%MEM 占用物理内存百分比
VSZ 虚拟内存大小(KB)
RSS 常驻内存大小(KB),即实际占用物理内存
TTY 进程所属终端,? 表示与终端无关(守护进程)
STAT 进程状态(后文详解)
START 进程启动时间
TIME 累计占用 CPU 时间
COMMAND 启动进程的命令

另一个常用组合:ps -ef

ps -ef

-e 显示所有进程,-f 显示完整格式。

与 aux 的区别在于:-ef 会显示 PPID(父进程 ID),这在追踪进程父子关系时非常有用。

UID PID PPID C STIME TTY TIME CMD
root 1 0 0 08:00 ? 00:00:02 /sbin/init
root 2 0 0 08:00 ? 00:00:00 [kthreadd]
wxx 1234 1100 0 08:05 ? 00:00:15 ./myapp


1.2 top 命令

ps 给的是快照,而 top 提供的是实时动态刷新的进程视图,默认每 3 秒刷新一次。

top

关键信息区域:

  • 第一行:系统时间、运行时长、登录用户数、系统负载(1/5/15 分钟平均)
  • 第二行:进程总数、运行中、睡眠、停止、僵尸进程数量
  • 第三行:CPU 使用情况(us 用户态、sy 内核态、id 空闲)
  • 第四五行:内存和交换分区使用情况
  • 下方列表:按 CPU 使用率降序排列的进程

常用交互按键:

按键功能
P 按 CPU 使用率排序
M 按内存使用率排序
T 按累计 CPU 时间排序
k 杀死指定 PID 的进程
q 退出 top

1.3 /proc 文件系统

        Linux 中一切皆文件,进程信息也不例外。/proc 是一个虚拟文件系统,它以文件的形式暴露内核数据结构,并不占用实际磁盘空间。

        每个进程在 /proc 下都有一个以其 PID 命名的目录:

ls /proc/1234/

关键文件:

文件内容
/proc/[pid]/status 进程状态信息(名称、PID、PPID、状态、内存等)
/proc/[pid]/cmdline 启动命令(以 \\0 分隔参数)
/proc/[pid]/cwd 当前工作目录的符号链接
/proc/[pid]/exe 可执行文件的符号链接
/proc/[pid]/maps 进程地址空间映射
/proc/[pid]/fd/ 进程打开的文件描述符目录

查看进程状态:

cat /proc/1234/status

Name: myapp
State: S (sleeping)
Tgid: 1234
Pid: 1234
PPid: 1100

VmRSS: 98304 kB

本质:ps、top 等工具底层都是读取 /proc 文件系统来获取进程信息的。


二、通过系统调用获取进程标识符

2.1 getpid() 与 getppid()

在用户态程序中,我们可以通过两个系统调用来获取进程 ID:

#include <sys/types.h>
#include <unistd.h>

pid_t getpid(void); // 获取当前进程的 PID
pid_t getppid(void); // 获取当前进程的父进程 PID(PPID)

  • getpid() 返回当前进程的 PID
  • getppid() 返回父进程的 PID,如果父进程已退出,则返回 1(init 进程)

2.2 代码示例

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
pid_t pid = getpid();
pid_t ppid = getppid();

printf("当前进程 PID: %d\\n", pid);
printf("父进程 PPID: %d\\n", ppid);

// 让进程暂停,方便用 ps 查看
while (1) {
sleep(1);
}

return 0;
}

编译运行:

gcc -o getpid_demo getpid_demo.c
./getpid_demo

输出:

当前进程 PID: 5678
父进程 PPID: 5600

另开一个终端验证:

ps aux | grep getpid_demo


2.3 内核视角:PID 是如何管理的

        在内核中,每个进程由 task_struct 结构体

        描述(定义在include/linux/sched.h)。其中与 PID 相关的字段:

struct task_struct {
pid_t pid; // 进程 ID
pid_t tgid; // 线程组 ID(对用户态,进程的 tgid == pid)
struct task_struct *real_parent; // 真正的父进程
struct task_struct *parent; // 父进程(通常相同)
// …
};

getpid() 系统调用在内核中的实现非常直接——返回当前进程 task_struct 中的 tgid 字段:

// kernel/sys.c
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current); // 返回当前进程的 tgid
}

为什么是 tgid 而不是 pid?

        因为在 Linux 中,线程和进程在内核层面都用 task_struct 表示。对多线程程序,每个线程有自己的 pid(内核线程 ID),但它们共享同一个 tgid(线程组 ID)。用户态看到的"进程 ID"实际上是 tgid。


三、通过系统调用创建进程——fork 初识

3.1 fork 函数原型

#include <unistd.h>

pid_t fork(void);

fork 是 Unix/Linux 中最核心的进程创建机制。调用 fork 后,内核会:

  • 为子进程分配新的 task_struct
  • 复制父进程的资源(页表、文件描述符表、信号处理等)
  • 父子进程从 fork 返回处继续执行

  • 3.2 fork 的返回值 ——  一次调用,两次返回

    这是 fork 最精妙也最容易困惑的地方:

    返回值含义
    > 0 在父进程中,返回子进程的 PID
    = 0 在子进程中,返回 0
    -1 创建失败(通常是进程数达到上限或内存不足)

    经典示例:

    #include <stdio.h>
    #include <sys/types.h>
    #include <unistd.h>

    int main() {
    printf("fork 之前,PID = %d\\n", getpid());

    pid_t ret = fork();

    if (ret < 0) {
    perror("fork 失败");
    return 1;
    } else if (ret == 0) {
    // 子进程执行路径
    printf("【子进程】PID = %d, PPID = %d, fork 返回值 = %d\\n",
    getpid(), getppid(), ret);
    } else {
    // 父进程执行路径
    printf("【父进程】PID = %d, 创建的子进程 PID = %d, fork 返回值 = %d\\n",
    getpid(), ret, ret);
    }

    return 0;
    }

    编译运行:

    gcc -o fork_demo fork_demo.c
    ./fork_demo

    可能的输出:

    fork 之前,PID = 6000
    【父进程】PID = 6000, 创建的子进程 PID = 6001, fork 返回值 = 6001
    【子进程】PID = 6001, PPID = 6000, fork 返回值 = 0

            注意:父子进程的执行顺序是不确定的,取决于内核调度器。可能父进程先打印,也可能子进程先打印。


    3.3 写时拷贝(Copy-On-Write, COW)

            早期的 fork 实现会把父进程的全部地址空间完整复制一份给子进程,这非常低效——因为很多情况下子进程紧接着就调用 exec 替换掉整个地址空间,之前的复制全白费了。

    现代 Linux 采用写时拷贝优化:

  • fork 时,子进程的页表指向父进程的物理页面(只读共享)
  • 父子进程都将这些页面标记为只读
  • 当任一方尝试写入某个页面时,触发缺页异常(Page Fault)
  • 内核为写入方复制一份独立的物理页面,并修改页表映射
  • 之后双方各自拥有该页面的独立副本
  • fork 前:
    父进程页表 ──→ 物理页面 A(可读写)

    fork 后(COW):
    父进程页表 ──→ 物理页面 A(只读) ←── 子进程页表

    父进程写入页面 A 时:
    父进程页表 ──→ 物理页面 A'(可读写,A 的副本)
    子进程页表 ──→ 物理页面 A(只读)

    COW 的核心优势:

            只有真正发生写入时才复制,fork 之后立即 exec 的场景几乎零拷贝开销。


    3.4 fork 之后父子进程共享什么?

    资源是否共享说明
    代码段(text) 共享(只读) 不可修改,天然共享
    数据段/堆/栈 写时拷贝 COW 机制
    文件描述符表 共享(引用计数) 父子共享同一文件偏移
    信号处理方式 复制 子进程继承父进程的信号处理
    进程 ID 不同 子进程获得新 PID
    挂起的信号 不继承 子进程的挂起信号集被清空

    文件描述符共享的坑:

    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>

    int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);

    pid_t pid = fork();

    if (pid == 0) {
    // 子进程写入
    write(fd, "child\\n", 6);
    } else {
    // 父进程写入
    write(fd, "parent\\n", 7);
    }

    close(fd);
    return 0;
    }

            由于父子进程共享文件偏移量,两次写入不会互相覆盖,而是依次追加。这也是为什么 fork 后需要注意文件描述符的管理。


    四、进程状态

    4.1 Linux 内核源码怎么说

            进程状态定义在内核头文件 include/linux/sched.h 中,通过 task_struct 的 __state 字段表示:

    struct task_struct {
    volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
    // …
    };

            具体的状态标志定义在 include/linux/sched.h:

    /* Used in tsk->state: */
    #define TASK_RUNNING 0x0000
    #define TASK_INTERRUPTIBLE 0x0001
    #define TASK_UNINTERRUPTIBLE 0x0002
    #define TASK_STOPPED 0x0004
    #define TASK_TRACED 0x0008
    /* Used in tsk->exit_state: */
    #define EXIT_ZOMBIE 0x0010
    #define EXIT_DEAD 0x0020
    /* Used in tsk->state again: */
    #define TASK_PARKED 0x0040
    #define TASK_DEAD 0x0080
    #define TASK_WAKEKILL 0x0100
    #define TASK_WAKING 0x0200
    #define TASK_NOLOAD 0x0400
    #define TASK_NEW 0x0800
    #define TASK_STATE_MAX 0x1000

            注意:state 字段用于运行中的状态,exit_state 字段专门用于退出过程中的状态(僵尸、死亡)。


    4.2 七大进程状态详解

    状态字符内核宏核心含义
    运行态 R TASK_RUNNING 正在运行或在就绪队列等待 CPU
    可中断睡眠 S TASK_INTERRUPTIBLE 等待事件,可被信号唤醒
    不可中断睡眠 D TASK_UNINTERRUPTIBLE 等待 I/O,不可被信号唤醒
    停止态 T TASK_STOPPED 被信号暂停,等待 SIGCONT 恢复
    追踪态 t TASK_TRACED 被调试器暂停(gdb 断点命中)
    僵尸态 Z EXIT_ZOMBIE 已退出,父进程未回收资源
    死亡态 X EXIT_DEAD 最终状态,task_struct 即将释放
    ① R — TASK_RUNNING(运行态/就绪态)

    #define TASK_RUNNING 0x0000

    • 进程正在 CPU 上执行,或者在运行队列中等待调度
    • 这是唯一可以在 CPU 上运行的状态
    • ps 中显示为 R

            注意:Linux 中没有单独的"就绪态",就绪和运行统一称为 TASK_RUNNING,区别仅在于是否正在被 CPU 执行。


    ② S — TASK_INTERRUPTIBLE(可中断睡眠态)

    #define TASK_INTERRUPTIBLE 0x0001

    • 进程在等待某事件发生(如等待终端输入、等待网络数据、等待锁)
    • 可以被信号唤醒(如 Ctrl+C 发送的 SIGINT)
    • ps 中显示为 S,这是最常见的状态

    典型场景:sleep(10)、read() 阻塞等待、wait() 等待子进程。


    ③ D — TASK_UNINTERRUPTIBLE(不可中断睡眠态)

    #define TASK_UNINTERRUPTIBLE 0x0002

    • 进程在等待不可中断的事件(通常是 I/O 操作,如磁盘读写)
    • 不能被信号唤醒,只能由等待的事件完成来唤醒
    • ps 中显示为 D

    为什么需要不可中断?

           假设进程正在执行磁盘 read 系统调用,内核已向磁盘发出 I/O 请求。如果此时允许信号中断,进程可能在 I/O 完成前就退出,导致内核态的 I/O 操作无法正常完成,可能造成数据不一致。因此 I/O 等待期间必须设为不可中断。

            排查系统负载高时,如果看到大量 D 状态进程,通常意味着磁盘 I/O 存在瓶颈。


    ④ T — TASK_STOPPED(停止态)

    #define TASK_STOPPED 0x0004

    • 进程被暂停执行,收到 SIGSTOP 或 SIGTSTP(Ctrl+Z)信号
    • 可以通过 SIGCONT 信号恢复运行
    • ps 中显示为 T

    典型场景:在终端中按 Ctrl+Z 将前台进程挂到后台。

    sleep 100
    # 按 Ctrl+Z
    # [1]+ Stopped sleep 100

    jobs # 查看停止的作业
    fg %1 # 恢复到前台运行
    bg %1 # 在后台继续运行


    ⑤ t — TASK_TRACED(追踪态)

    #define TASK_TRACED 0x0008

    • 进程被调试器追踪(如 gdb 附加到进程)
    • 当进程被断点命中时进入此状态
    • ps 中显示为 t(小写)

    与 T(STOPPED)的区别:TRACED 是被调试器暂停,只能由调试器通过 ptrace 恢复。


    ⑥ Z — EXIT_ZOMBIE(僵尸态)

    #define EXIT_ZOMBIE 0x0010

    • 进程已经执行结束,但父进程尚未调用 wait() / waitpid() 回收其资源
    • 进程的 task_struct 仍然保留(占用一个 PID 槽位),但用户态资源已释放
    • ps 中显示为 Z,通常标注为 <defunct>

    僵尸进程是怎么产生的:

    #include <stdio.h>
    #include <unistd.h>

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

    if (pid == 0) {
    // 子进程:立刻退出
    printf("子进程退出,PID=%d\\n", getpid());
    _exit(0);
    } else {
    // 父进程:不调用 wait,一直运行
    printf("父进程运行中,不回收子进程\\n");
    while (1) sleep(1);
    }
    return 0;
    }

    运行后查看:

    ps aux | grep Z
    # wxx 6500 0.0 0.0 0 0 ? Z 10:00 0:00 [zombie_demo] <defunct>

    如何避免僵尸进程:

  • 父进程调用 wait() 或 waitpid() 回收
  • 父进程注册 SIGCHLD 信号处理函数,在其中调用 waitpid()
  • 父进程先退出,子进程被 init(PID 1)收养,init 会自动回收
  • // 方法2:信号处理回收
    void sigchld_handler(int sig) {
    while (waitpid(-1, NULL, WNOHANG) > 0);
    }

    int main() {
    signal(SIGCHLD, sigchld_handler);
    // …
    }


    ⑦ X — EXIT_DEAD(死亡态)

    #define EXIT_DEAD 0x0020

    • 进程的最终状态,task_struct 即将被释放
    • 这个状态非常短暂,ps 几乎捕捉不到
    • ps 中显示为 X

    4.3 进程状态转换图

    fork()
    新建 ──────────────→ R (运行/就绪)

    ┌───────────┼───────────┐
    │ │ │
    等待事件 时间片到 收到信号
    │ │ │
    ▼ ▼ ▼
    S (可中断睡眠) R T (停止)
    D (不可中断睡眠) │
    │ │
    事件完成/信号 SIGCONT
    │ │
    └───────────┬───────────┘


    R

    exit() / 收到终止信号


    Z (僵尸) ──→ wait() 回收 ──→ X (死亡)


    五、进程状态查看

    5.1 ps 中的 STAT 列

    在 ps aux 的输出中,STAT 列显示进程状态。除了基本状态字符外,还可能有附加字符:

    字符含义
    R 运行/就绪
    S 可中断睡眠
    D 不可中断睡眠
    T 停止
    t 追踪停止
    Z 僵尸
    X 死亡
    s 会话首进程(session leader)
    l 多线程进程
    + 位于前台进程组
    < 高优先级(nice 值为负)
    N 低优先级(nice 值为正)
    L 有页面锁定在内存中

    例如 Ssl+ 表示:可中断睡眠(S)+ 会话首进程(s)+ 多线程(l)+ 前台进程组(+)。


    5.2 实用查看技巧

    查看指定进程的状态:

    ps -o pid,stat,cmd -p 1234

    只看僵尸进程:

    ps aux | awk '$8 ~ /Z/'

    查看进程状态分布统计:

    ps aux | awk '{print $8}' | sort | uniq -c | sort -rn

    输出示例:

    42 S
    3 R
    1 Ss
    1 S+
    1 Z

    用 top 查看状态:

    在 top 中按 f 进入字段选择,添加 S(Process Status)列即可实时查看每个进程的状态。

    查看进程的完整状态信息:

    cat /proc/1234/status | grep State
    # State:S (sleeping)


    总结

    主题核心要点
    查看进程 ps aux / ps -ef 快照,top 实时,底层读 /proc
    获取 PID getpid() 返回当前进程 PID,getppid() 返回父进程 PID
    fork 创建进程 一次调用两次返回;父返回子 PID,子返回 0;COW 写时拷贝
    进程状态 R 运行、S 可中断睡眠、D 不可中断睡眠、T 停止、t 追踪、Z 僵尸、X 死亡
    状态查看 ps 的 STAT 列,/proc/[pid]/status,注意附加字符含义

            理解进程状态是深入 Linux 系统编程的基石。从 fork 创建进程,到进程在各个状态之间流转,再到最终被回收,整个生命周期都围绕着 task_struct 和内核调度器展开。掌握这些原理,才能在实际开发中精准定位进程卡死、僵尸进程、负载过高等问题。


    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】 进程(3)深度解析:从查看进程到进程状态
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址