【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

本质:pstop 等工具底层都是读取 /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 后,内核会:

  1. 为子进程分配新的 task_struct
  2. 复制父进程的资源(页表、文件描述符表、信号处理等)
  3. 父子进程从 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 采用写时拷贝优化:

  1. fork 时,子进程的页表指向父进程的物理页面(只读共享)

  2. 父子进程都将这些页面标记为只读

  3. 当任一方尝试写入某个页面时,触发缺页异常(Page Fault)

  4. 内核为写入方复制一份独立的物理页面,并修改页表映射

  5. 之后双方各自拥有该页面的独立副本

    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
  • 进程被暂停执行,收到 SIGSTOPSIGTSTPCtrl+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>

如何避免僵尸进程:

  1. 父进程调用 wait()waitpid() 回收

  2. 父进程注册 SIGCHLD 信号处理函数,在其中调用 waitpid()

  3. 父进程先退出,子进程被 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 和内核调度器展开。掌握这些原理,才能在实际开发中精准定位进程卡死、僵尸进程、负载过高等问题。


相关推荐
我星期八休息1 小时前
Linux—五种IO模型与非阻塞IO
linux·运维·服务器·网络·数据库·网络协议
啊啊啊啊啊!!!!1 小时前
【c++】二叉搜索树
开发语言·c++
hweiyu001 小时前
Linux命令:column
linux·运维·服务器
oooo_z1 小时前
WebSocket实时行情推送API实战:用iTick搭一个盯盘小工具
网络·websocket·网络协议
Csxyzj1 小时前
kubernetes集群部署方法
java·linux·kubernetes
深念Y1 小时前
OnlyOffice 打开来自 Windows/WPS 的 Office 文档时,出现排版错乱、文字空白、字体显示异常。
linux·windows·debian·onlyoffice·字体·wps·office
ShineWinsu2 小时前
对于 C++:C++20中Concept(概念) 与 Coroutine(协程)的解析
linux·开发语言·网络·c++·c++20·epoll
绿蚁新亭2 小时前
Markdown Wiki 网页代码块复制至欧拉 Linux Shell 脚本隐形字符故障完整复盘
linux·运维·服务器
Loongproxy2 小时前
2026年动态IP代理真实可用率怎么横评?爬虫选代理的关键是什么
服务器·网络·网络协议·tcp/ip