一、计算机体系结构与程序运行原理

        要理解进程,首先需明确程序如何在硬件与操作系统协同下运行,核心涉及CPU、内存、总线的交互及局部性原理

1.1 核心硬件交互

程序运行时,指令与数据的流转依赖三大硬件组件:

  • CPU:负责执行指令,包含 ALU(逻辑运算单元)、寄存器(如 EIP/PC 存放下一条指令地址)、缓存(缓解 CPU 与内存速度差)。
  • 内存:存储当前运行的指令与数据,通过地址总线定位内存单元,数据总线传输数据,控制总线区分 “读 / 写” 操作。
  • 磁盘:长期存储程序文件(如test.exe),程序需通过加载器(loader) 加载到内存后才能执行。

1.2 程序运行的局部性原理

CPU 访问指令和数据时存在 “就近倾向”,这是操作系统内存管理与缓存设计的基础:

  1. 指令局部性:执行当前指令后,下一条指令大概率在相邻地址(如顺序执行代码)。
  2. 数据局部性:访问某个数据后,大概率继续访问相邻数据(如数组遍历)。

二、进程的核心概念

进程是操作系统资源分配与调度的基本单位,需与 “程序” 明确区分,并掌握其五大核心特征。

2.1 进程与程序的区别

对比 程序(Program) 进程(Process)
状态 静态(磁盘上的指令集合) 动态(内存中运行的实例)
生命周期 永久(文件不删除则存在) 临时(从创建到终止)
资源占用 不占用 CPU / 内存 占用独立地址空间、CPU、内存等
实例关系 一个程序可对应多个进程 一个进程对应一个程序(或通过 exec 替换)

生动比喻:程序如 “乐谱”,进程如 “钢琴家按乐谱演奏的过程”—— 同一乐谱可由多个钢琴家同时演奏(多进程),每个演奏过程独立占用资源。

2.2 进程的五大特征

  1. 动态性:进程是程序的一次执行过程,有完整生命周期(创建→运行→阻塞→终止),是与程序最本质的区别。
  2. 并发性:多个进程在一段时间内 “同时” 运行(单核 CPU 通过时间片轮转实现,多核 CPU 通过物理核心并行)。并发≠并行:并发是 “宏观同时”(单核切换),并行是 “微观同时”(多核同时执行)。
  3. 独立性:每个进程拥有独立的虚拟地址空间,进程间默认无法直接访问内存(通过 IPC 机制可通信),保障系统稳定(如浏览器崩溃不影响音乐播放器)。
  4. 异步性:进程推进速度不可预知(依赖 CPU 调度、I/O 完成时间),可能导致竞态条件,需通过同步机制(如互斥锁)解决。
  5. 结构性:进程通过进程控制块(PCB) 描述,PCB 是进程存在的唯一标志,包含进程状态、资源信息等。

三、进程控制块(PCB)

PCB 是操作系统内核管理进程的核心数据结构,Linux 中对应task_struct结构体,包含四大类关键信息。

3.1 PCB 的作用

  • 操作系统通过 PCB “感知” 进程存在,所有进程操作(调度、资源回收)均依赖 PCB。
  • 进程创建时,内核为其分配 PCB;进程终止时,PCB 随之销毁。

3.2 PCB 的核心字段(Linux task_struct

1. 进程标识信息(唯一识别进程)
pid_t pid;                  // 进程唯一ID(PID)
pid_t tgid;                 // 线程组ID(主线程PID)
struct task_struct *parent;  // 父进程指针
struct list_head children;   // 子进程链表
struct list_head sibling;    // 兄弟进程链表
2. 进程调度信息(决定 CPU 分配)
volatile long state;         // 进程状态(就绪/运行/阻塞等)
int static_prio;             // 静态优先级
int policy;                  // 调度策略(如SCHED_NORMAL、SCHED_RR)
struct sched_entity se;      // 调度实体(参与调度的核心结构)
3. 处理器状态信息(保存执行现场)

进程切换时需保存的 CPU 上下文,确保恢复后可继续执行:

struct thread_struct thread; // 包含通用寄存器(ax、bx等)、程序计数器(PC/EIP)、栈指针(sp)
4. 进程控制信息(资源管理)
  • 内存管理:struct mm_struct *mm,记录代码段 / 数据段 / 堆 / 栈的地址范围、页表指针。
  • 文件管理:struct files_struct *files,记录已打开的文件描述符。
  • IPC 信息:struct ipc_namespace *ipc_ns,记录共享内存、消息队列等通信资源。

3.3 PCB 的组织方式

  1. 链表式:所有进程的task_struct通过tasks字段组成双向循环链表,便于遍历(如ps命令实现)。
  2. 索引式(哈希表):通过 PID 快速查找对应的task_struct,提升查询效率。

3.4 查看进程 PCB 信息

Linux 中通过/proc文件系统暴露进程信息,或使用工具查询:

# 查看当前进程状态
cat /proc/$$/status
# 查看PID=1的进程打开的文件
ls /proc/1/fd/
# 查看所有进程(工具)
ps aux | grep nginx
top -p PID1,PID2  # 监控指定PID进程

四、进程状态与转换

进程在生命周期中会在不同状态间切换,操作系统通过状态管理实现高效调度。

4.1 进程的五态模型(经典模型)

状态 含义
创建态 进程正在被创建(分配 PCB、加载程序到内存),尚未进入就绪队列。
就绪态 具备运行条件(资源齐全),等待 CPU 调度(位于就绪队列)。
运行态 正在 CPU 上执行指令(单核 CPU 同一时刻仅 1 个进程,多核 CPU 对应核心数)。
阻塞态 等待某个事件(如 I/O 完成、锁释放),即使 CPU 空闲也无法运行(位于阻塞队列)。
终止态 进程执行完毕或被杀死,PCB 暂存(等待父进程回收),也称 “僵尸态”。

4.2 状态转换触发条件

进程状态切换由操作系统事件触发,核心转换路径如下:

  1. 创建态 → 就绪态:进程创建完成(如fork()调用成功),加入就绪队列。
  2. 就绪态 → 运行态:CPU 空闲,调度器选择就绪队列中的进程分配时间片。
  3. 运行态 → 就绪态:时间片用完,或被更高优先级进程抢占。
  4. 运行态 → 阻塞态:进程请求 I/O(如read())或等待资源(如锁)。
  5. 阻塞态 → 就绪态:等待的事件发生(如 I/O 完成),从阻塞队列移回就绪队列。
  6. 运行态 → 终止态:进程正常退出(exit())或被信号杀死(kill())。

关键面试题

  • Q:为何区分 “就绪态” 和 “阻塞态”?A:就绪态是 “缺 CPU”,阻塞态是 “缺资源 / 事件”,区分后调度器仅需从就绪队列选择进程,提升调度效率。
  • Q:单核 CPU 同一时刻能运行多少进程?A:1 个。多核 CPU 同一时刻运行进程数等于物理核心数。

五、进程管理系统调用(Linux)

进程的创建、等待、终止、替换均通过系统调用实现,核心涉及fork()wait()exit()exec系列函数。

5.1 进程创建:fork()

fork()通过复制父进程创建子进程,是 Linux 中最核心的进程创建接口。

函数原型
#include <unistd.h>
pid_t fork(void);
返回值规则
  • 父进程:返回子进程的 PID(正整数)。
  • 子进程:返回 0。
  • 失败:返回 - 1(如资源不足)。
核心特性
  1. 写时复制(Copy-On-Write, COW):子进程不立即复制父进程地址空间,而是共享只读内存页;仅当父子进程修改数据时,内核才复制对应页,提升效率。
  2. 调用一次,返回两次fork()后,父子进程从fork()的下一条指令开始执行,通过返回值区分角色。
代码示例
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("Child: PID=%d, PPID=%d\n", getpid(), getppid());
    } else {
        // 父进程
        printf("Parent: PID=%d, Child PID=%d\n", getpid(), pid);
    }
    return 0;
}
fork()vfork()的区别(vfork()已过时)
特性 fork()(推荐) vfork()(废弃)
地址空间 写时复制(独立) 共享父进程地址空间
执行顺序 父子进程顺序不确定(调度器决定) 父进程挂起,子进程先执行
安全性 安全(独立地址空间) 危险(子进程修改会破坏父进程)

5.2 进程等待:wait()waitpid()

父进程需通过等待函数回收子进程资源,避免僵尸进程,同时获取子进程退出状态。

1. wait()(简单等待)
#include <sys/wait.h>
pid_t wait(int *wstatus);
  • 作用:阻塞父进程,直到任意一个子进程终止,回收资源并获取退出状态。
  • 参数wstatus存储退出状态(需通过宏解析,如WIFEXITEDWEXITSTATUS)。
  • 返回值:成功返回终止子进程的 PID,失败返回 - 1。
2. waitpid()(灵活等待)

wait()的增强版,支持指定子进程、非阻塞等待:

pid_t waitpid(pid_t pid, int *wstatus, int options);
  • 参数
    • pid-1(等待任意子进程)、>0(等待指定 PID 子进程)、0(等待同组子进程)。
    • optionsWNOHANG(非阻塞)、WUNTRACED(返回暂停子进程)。
代码示例(非阻塞等待)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程工作5秒
        sleep(5);
        exit(100); // 退出状态码100
    }

    // 父进程非阻塞等待
    int status;
    while (1) {
        pid_t res = waitpid(pid, &status, WNOHANG);
        if (res == -1) {
            perror("waitpid failed");
            break;
        } else if (res == 0) {
            // 子进程未结束,父进程做其他工作
            printf("Parent: Child is still running...\n");
            sleep(1);
        } else {
            // 子进程结束,解析状态
            if (WIFEXITED(status)) {
                printf("Child exited with status: %d\n", WEXITSTATUS(status));
            }
            break;
        }
    }
    return 0;
}

5.3 进程终止:exit()_exit()abort()

进程终止分正常退出与异常终止,不同函数清理行为不同。

函数 类型 清理行为 适用场景
exit(int status) C 库函数 1. 调用atexit注册的清理函数;2. 刷新 I/O 缓冲区;3. 调用_exit() 正常退出(如 main 函数返回)
_exit(int status) 系统调用 直接终止进程,不清理用户空间(如缓冲区)。 子进程退出、信号处理中退出
abort(void) C 库函数 发送SIGABRT信号,终止进程并生成 Core Dump(用于调试),不调用清理函数。 程序异常(如断言失败)
代码对比(缓冲区刷新)
// exit()会刷新缓冲区(输出"hello")
#include <stdio.h>
#include <stdlib.h>
int main() {
    printf("hello"); // 无换行符
    exit(0);
}

// _exit()不刷新缓冲区(无输出)
#include <stdio.h>
#include <unistd.h>
int main() {
    printf("hello"); // 无换行符
    _exit(0);
}

5.4 进程替换:exec函数族

exec函数族将当前进程的代码段、数据段替换为新程序(PID 不变),常用于fork()后子进程执行新任务。

函数族原型(6 个函数)
#include <unistd.h>
// l=列表传参,v=数组传参,p=PATH查找,e=自定义环境
int execl(const char *path, const char *arg, ... /* NULL结尾 */);
int execlp(const char *file, const char *arg, ... /* NULL结尾 */);
int execle(const char *path, const char *arg, ... /* NULL, envp[] */);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]); // 系统调用
核心特性
  • 成功时:不返回(代码已替换)。
  • 失败时:返回 - 1(需检查errno)。
  • 命令行参数:argv[0]通常为程序名,数组以NULL结尾。
代码示例(模拟ls -l
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程替换为ls -l
        char *args[] = {"ls", "-l", NULL};
        execvp("ls", args); // 利用PATH查找ls
        perror("execvp failed"); // 仅失败时执行
        exit(1);
    }
    // 父进程等待
    wait(NULL);
    return 0;
}

六、特殊进程:僵尸进程与孤儿进程

6.1 僵尸进程(Zombie Process)

定义

子进程终止后,父进程未调用wait()/waitpid()回收,导致 PCB 残留(状态为Z)。

危害
  • 占用 PID 资源:系统 PID 数量有限(如/proc/sys/kernel/pid_max默认 32768),耗尽后无法创建新进程。
  • 消耗内核资源:残留的 PCB 占用进程表空间。
产生原因

父进程未处理子进程退出,如:

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

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程立即退出
        printf("Child PID=%d, exiting\n", getpid());
        _exit(0);
    }
    // 父进程休眠10秒,不回收子进程
    sleep(10);
    return 0;
}
解决方法
  1. 父进程主动调用wait()/waitpid()回收。
  2. 杀死父进程:僵尸进程被init(PID=1)收养,init会自动回收。

6.2 孤儿进程(Orphan Process)

定义

父进程先于子进程终止,子进程被init收养(PPID 变为 1)。

特点
  • 正常运行(状态为R/S),不占用额外资源。
  • init会定期调用wait()回收,无资源泄漏。
代码示例
#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程休眠5秒,期间父进程退出
        printf("Child PID=%d, PPID=%d\n", getpid(), getppid());
        sleep(5);
        printf("Child: New PPID=%d (init)\n", getppid());
    } else {
        // 父进程立即退出
        printf("Parent PID=%d, exiting\n", getpid());
        _exit(0);
    }
    return 0;
}
僵尸进程 vs 孤儿进程
对比维度 僵尸进程 孤儿进程
状态 Z(已终止) R/S(运行 / 休眠)
资源占用 仅 PID+PCB 正常 CPU / 内存资源
产生原因 父进程未回收 父进程先退出
危害 耗尽 PID,无法创建新进程 无(init自动回收)
解决方法 杀死父进程或父进程调用 wait 无需处理
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐