C学习笔记

C 语言 进程概述 — 学习总结 (Day6)

目录

  1. 基础概念:什么是进程
  2. 查看进程 — ps 命令
  3. 进程的状态
  4. 多进程的运行原理
  5. 进程的创建
  6. 进程管理
  7. 思考与总结

1. 基础概念:什么是进程

1.1 进程的定义

进程(Process)= 正在运行的程序

  • 程序:存在磁盘上的静态文件(如编译好的可执行文件 a.out
  • 进程:程序被加载到内存中,由 CPU 执行的那个动态实例
程序(磁盘上的文件)  ──加载到内存──▶  进程(正在运行的实体)
    a.out                               PID=3210

1.2 进程 = 资源分配的最小单位

操作系统以进程为基准来分配系统资源,每个进程拥有:

资源 说明
独立的地址空间 每个进程有自己的虚拟内存(代码段、数据段、堆、栈)
进程 ID(PID) 系统内唯一标识一个进程的正整数
打开的文件描述符表 每个进程独立维护自己打开的文件
寄存器上下文 CPU 寄存器状态,用于进程切换时保存/恢复

同一用户同时运行同一个程序多次,会产生多个不同的进程,每个有不同的 PID。

1.3 进程的地址空间布局

┌──────────────────┐  高地址
│    内核空间       │
├──────────────────┤
│     栈 (stack)   │  ← 局部变量、函数调用返回地址
│       ↓↓↓        │
│                  │
│       ↑↑↑        │
│     堆 (heap)    │  ← malloc 分配的内存
├──────────────────┤
│   未初始化数据段  │  ← BSS 段 (全局未初始化变量)
├──────────────────┤
│   已初始化数据段  │  ← 全局已初始化变量、静态变量
├──────────────────┤
│    代码段         │  ← 程序指令(只读)
└──────────────────┘  低地址

2. 查看进程 — ps 命令

2.1 基本用法

ps [options]
选项 作用
(无选项) 显示当前终端下的进程
-e 显示系统中所有进程
-u 以用户友好格式显示详细信息
-o 自定义输出格式,如 pid,ppid,comm
-t 查看指定终端的进程

2.2 常见组合

# 1. 查看全部进程的详细信息
ps -e u
# 输出列:USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND

# 2. 自定义格式:只看 PID、父进程 PID、命令名
ps -o pid,ppid,command

# 3. 查看 tty2 终端上的进程
ps -t tty2

# 4. 结合 grep 筛选
ps -e u |grep fish      # 筛选包含 "fish" 的进程

2.3 管道(pipe)与 grep

Shell 的 | 管道可以将前一命令的输出传给后一命令:

ps -e u | grep bash
#  ps -e u 的全部输出  ──通过管道──▶  grep 筛选含 "bash" 的行

3. 进程的状态

3.1 ps 输出的 STAT 字段含义

状态码 含义 说明
R Running / Runnable 正在运行或就绪(在运行队列中等待 CPU)
S Sleeping (可中断睡眠) 等待某个事件完成(如读终端输入、文件 I/O)
D Disk Sleep (不可中断睡眠) 等待 I/O 完成,不能被信号打断
T Stopped 被暂停(如 Ctrl+Z
Z Zombie(僵尸) 子进程已结束,但父进程还未回收其退出状态
W Paging 没有足够的内存页可分配(极少见)

附加标记:

标记 含义
s 该进程是会话领导者(session leader),拥有子进程
+ 位于前台进程组

3.2 为什么程序多数时候是 S(睡眠)状态?

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

int main() {
    sleep(2);           // 进入 S 状态,等 2 秒后唤醒
    getchar();          // 进入 S 状态,等键盘输入
    printf("done\n");
    return 0;
}
  • sleep():主动让进程进入睡眠,到期后系统唤醒
  • getchar() / scanf():阻塞等待 I/O,也进入 S 状态
  • 只有 CPU 计算密集型的短时刻进程才处于 R 状态

3.3 进程状态转换图

                    ┌──────────┐
           创建 ───▶│  READY  │ ◀─────────┐
                    │  就绪态  │            │
                    └────┬─────┘            │
                   调度  │  时间片用完       │
                         ▼                  │
                    ┌──────────┐            │
                    │ RUNNING  │ ───────────┘
                    │  运行态  │
                    └────┬─────┘
              等待事件  │  事件完成
                    ┌───▼───────┐
                    │  SLEEP    │
                    │  睡眠态   │
                    └───────────┘

     此外还有:T(暂停态)← Ctrl+Z
              Z(僵尸态)→ 父进程 wait() 回收后消失

3.4 前台与后台运行

# 前台运行(默认)
./myprogram

# 后台运行(命令行末尾加 &)
./myprogram &

# 查看后台任务
jobs

# Ctrl+C  →  终止前台进程
# Ctrl+Z  →  暂停前台进程(进入 T 状态)

3.5 进程调度命令

操作 命令 说明
终止前台进程 Ctrl + C 发送 SIGINT 信号
暂停前台进程 Ctrl + Z 进入 T(Stopped)状态
调回前台 fg %job号 将暂停/后台进程调回前台
放入后台 bg %job号 让暂停的进程在后台继续运行
杀死进程 kill PIDkill %job号 发送终止信号

4. 多进程的运行原理

4.1 核心矛盾

一台机器的 CPU 核心数通常远小于正在运行的进程数。

4.2 时间片轮转(Time Slice)

操作系统通过极快地轮流切换来制造"同时运行"的假象:

时间轴: ─────────────────────────────────────────────────▶

CPU:    [进程A][进程B][进程C][进程A][进程D][进程B][进程C]...
         ← 每个时间片只有几毫秒到几十毫秒 →
概念 说明
时间片 每个进程一次能连续运行的最大时间(通常 1~100ms)
上下文切换 保存当前进程状态,恢复下一个进程状态
调度器 内核中决定"下一个该谁运行"的模块
优先级 高优先级进程获得更频繁或更长的时间片
*不代表低优先级进程不会运行,只是高优先级进程运行更加频繁

4.3 调度策略

  • 采用抢占式多任务:高优先级进程可以打断低优先级进程
  • 不可抢占的部分(如内核代码、不可中断睡眠 D 状态)必须等执行完
  • 时间片用完 → 强制切走,重新排队

5. 进程的创建

5.1 两个关键 ID

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

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

示例:

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

int main() {
    printf("我的 PID  = %d\n", getpid());
    printf("父进程 PID = %d\n", getppid());
    return 0;
}
// 输出示例:
// 我的 PID  = 3210
// 父进程 PID = 2891  (通常是 bash)

5.2 system() — 执行 Shell 命令

#include <stdlib.h>

int system(const char *command);

在当前进程中调用 Shell 执行一条命令,命令结束后返回

原理:

你的程序  ──system("ps -e")──▶  Shell  ──fork──▶  新进程执行 ps -e
                                                (你的程序阻塞等待)
                                ps 结束后控制权返回你的程序

示例:

#include <stdlib.h>

int main() {
    // 执行 ps 命令查看进程
    system("ps -e u | grep bash");
    return 0;
}

⚠️ system()阻塞当前进程,直到命令执行完毕。
⚠️ 使用 system() 有安全风险:如果 command 由用户输入构成,可能被注入恶意命令。


5.3 fork() — 创建子进程

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

pid_t fork(void);

fork() 是 Linux 下创建新进程的唯一方法
调用一次,返回两次

返回值详解

返回值 所在进程 含义
> 0 父进程 返回值 = 子进程的 PID
== 0 子进程 表示当前是子进程
< 0 父进程 fork() 失败(如内存不足)

fork 执行流程

                     调用 fork()
         父进程  ──────────────────────────▶
         PID=3210                          继续执行 fork 之后的代码
                                           返回值 = 3211 (子进程 PID)
                         ┌─────────────────
                         │ 子进程
                         │ PID=3211
                         │ 返回值为 0
                         └──▶ 继续执行 fork 之后的代码

fork执行阶段

示例

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

int main() {
    pid_t pid;
    int x = 8;

    pid = fork();  // 从这里开始,两个进程并行执行

    if (pid < 0) {
        // fork 失败
        perror("fork failed");
        return -1;
    } else if (pid == 0) {
        // 子进程进入这里
        x = 100;
        printf("子进程: PID=%d, 父PID=%d, x=%d\n",
               getpid(), getppid(), x);
    } else {
        // 父进程进入这里
        x = 200;
        printf("父进程: PID=%d, 子PID=%d, x=%d\n",
               getpid(), pid, x);
    }

    // 两个进程都会执行到这里
    printf("进程 %d 结束\n", getpid());
    return 0;
}

可能的输出:

父进程: PID=3210, 子PID=3211, x=200
进程 3210 结束
子进程: PID=3211, 父PID=3210, x=100
进程 3211 结束

🔑 关键理解:子进程修改 x = 100 并不影响父进程的 x(仍是 200)——
因为 fork() 后它们拥有独立的内存空间!

写时复制(Copy-On-Write)

现代 Linux 中,fork() 并不立刻复制所有内存,而是:

  1. 父子进程共享同一物理内存页(只读)
  2. 当任意一方写入某页时,内核才复制该页(COW)
  3. 大大减少了 fork() 的开销

5.4 execlp() — 执行另一个程序

#include <unistd.h>

int execlp(const char *file, const char *arg0, ... /* (char *)NULL */);

用另一个程序替换当前进程的内存空间,执行新程序。
成功时不返回,失败返回 -1

参数 说明
file 要执行的程序名(PATH 环境变量中查找)
arg0 习惯上 = 程序名本身(argv[0]
... 后续参数,必须以 NULL 结尾

典型用法:fork + execlp

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

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

    if (pid == 0) {
        // 子进程:用 execlp 执行 ps 命令
        execlp("ps", "ps", "-e", "u", NULL);
        // 如果 execlp 执行成功,永远不会执行到下一条语句
        perror("execlp failed");
        return -1;
    } else if (pid > 0) {
        // 父进程:等待子进程结束
        wait(NULL);
        printf("子进程 ps 已执行完毕\n");
    }

    return 0;
}

execlp 执行过程

父进程 fork() 后:

子进程内存:                 execlp("ps",...) 后:
┌─────────────┐            ┌─────────────┐
│ 原程序代码   │            │  ps 的代码   │ 
│ 原程序数据   │   ───▶    │  ps 的数据   │
│ 原程序堆栈   │            │  ps 的堆栈   │
└─────────────┘            └─────────────┘
PID 不变!                   PID 还是原来的

执行了 execlp 后,子进程不再执行原程序中的任何代码,完全变为 ps 进程。
PID 不变,这就是为什么 ps 看到的进程号和 fork 出来的子进程号一致。


6. 进程管理

6.1 wait() — 等待子进程结束

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

pid_t wait(int *wstatus);

父进程调用 wait()阻塞,直到任意一个子进程结束。
返回结束的子进程 PID,失败返回 -1

参数 说明
wstatus 传出参数,存储子进程退出状态;不关心可传 NULL
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

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

    if (pid == 0) {
        printf("子进程 PID=%d 开始工作...\n", getpid());
        sleep(2);
        printf("子进程结束\n");
        exit(42);  // 退出码 42
    } else {
        int status;
        pid_t finished = wait(&status);
        printf("父进程: 子进程 %d 已结束\n", finished);

        if (WIFEXITED(status)) {
            printf("退出码: %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

退出状态解析宏:

作用
WIFEXITED(status) 子进程是否正常退出(exit() / return
WEXITSTATUS(status) 获取正常退出的退出码
WIFSIGNALED(status) 子进程是否被信号杀死
WTERMSIG(status) 获取杀死子进程的信号编号

6.2 waitpid() — 等待指定子进程

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

pid_t waitpid(pid_t pid, int *wstatus, int options);

wait() 更灵活,可以指定等待哪一个子进程。

参数 说明
pid 子进程 PID;-1 = 任意子进程(等同 wait
wstatus 传出退出状态
options 0(阻塞等待)或 WNOHANG(非阻塞轮询)

示例:

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

int main() {
    pid_t child1 = fork();
    if (child1 == 0) { sleep(3); return 111; }

    pid_t child2 = fork();
    if (child2 == 0) { sleep(1); return 222; }

    // 等待特定的子进程 child2
    int status;
    waitpid(child2, &status, 0);
    printf("子进程 %d 退出码: %d\n", child2, WEXITSTATUS(status));

    // 再等 child1
    waitpid(child1, &status, 0);
    printf("子进程 %d 退出码: %d\n", child1, WEXITSTATUS(status));

    return 0;
}

wait vs waitpid

wait() waitpid()
等待目标 任意子进程 指定 PID 的子进程
阻塞方式 必须阻塞 可选非阻塞(WNOHANG
灵活性

6.3 孤儿进程与僵尸进程

孤儿进程(Orphan Process)

父进程先于子进程结束,子进程变成孤儿,由 init 进程(PID=1) 收养。

父进程终止  ──▶  子进程的 PPID 变成 1(init 收养)

孤儿进程不会造成系统问题,init 进程会自动回收它。

僵尸进程(Zombie Process)⭐ 重点

子进程已结束,但父进程没有调用 wait() 回收它的退出状态。
子进程变成僵尸(STAT=Z),占用进程表条目但不占用其他资源。

子进程结束 ──▶ 父进程没 wait() ──▶ 僵尸(Z)
                  │
                  └── wait() 后,僵尸被清理

危害: 大量僵尸进程会耗尽 PID 资源,无法创建新进程。

解决方法: 父进程中必须调用 wait()waitpid() 回收子进程。

僵尸进程演示:

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

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

    if (pid == 0) {
        printf("子进程 %d 结束\n", getpid());
        return 0;  // 子进程结束
    } else {
        printf("父进程 %d 休眠,不 wait\n", getpid());
        sleep(30);  // 这时子进程变成僵尸!
        // 没有 wait(),子进程状态 = Z
    }
    return 0;
}
// 在另一个终端运行:ps -e u | grep Z
// 可看到子进程状态为 Z(僵尸)

7. 思考与总结

7.1 核心函数速查表

函数 作用 函数定义 关键特点
getpid() 获取当前进程 PID pid_t getpid(void); 总是成功,无参数
getppid() 获取父进程 PID pid_t getppid(void); 孤儿进程返回 1(init)
system() 执行 Shell 命令 int system(const char *command); 返回命令终止状态,有安全风险
fork() 创建子进程 pid_t fork(void); 调用一次返回两次,父返回子 PID,子返回 0,COW 优化
execlp() 替换进程为另一个程序 int execlp(const char *file, const char *arg, ...); 成功不返回,失败返回 -1,PID 不变
wait() 等待任意子进程 pid_t wait(int *status); 阻塞等待,防止僵尸进程,返回终止子进程 PID
waitpid() 等待指定子进程 pid_t waitpid(pid_t pid, int *status, int options); 支持非阻塞(WNOHANG),可指定进程

7.2 fork + execlp 经典范式

创建工作子进程执行其他程序的标准写法:

pid_t pid = fork();

if (pid == 0) {
    // 子进程中
    execlp("程序名", "程序名", "参数1", "参数2", ..., NULL);
    perror("execlp failed");  // 只有失败才会走到这里
    _exit(1);
} else if (pid > 0) {
    // 父进程中
    int status;
    waitpid(pid, &status, 0);  // 等子进程结束,防止僵尸
}

7.3 关键对比

对比项 fork() system()
创建方式 复制自身 调用 Shell 再 fork
新进程内容 与父进程相同 执行指定命令
是否阻塞 不阻塞,父子并行 阻塞等待命令完成
灵活性 高,可精确控制 低,只能执行一条命令
效率 高(COW) 较低(多一层 Shell)

7.4 常见陷阱

  1. 忘记 wait() → 产生僵尸进程,PID 资源泄漏
  2. fork 返回值没判断 → 父子和错误情况分不清
  3. execlp 之后还写代码 → 成功执行 execlp 后不会返回,后面的代码只在失败时才走到
  4. execlp 参数必须以 NULL 结尾 → 忘记 NULL 会导致未定义行为
  5. system() 的用户输入system(user_input) 可能被注入 rm -rf / 等恶意命令
posted @ 2026-06-25 11:02  小怪兽zzy  阅读(2)  评论(0)    收藏  举报