Linux 进程与操作系统

进程是Linux操作系统最核心的抽象概念,从程序加载到资源调度,一切运行逻辑都围绕进程展开。本文将系统梳理Linux进程管理的底层原理与实践细节,搭建完整的知识体系。


一、冯诺依曼体系结构

1. 核心组成

输入设备:键盘、鼠标、扫描仪等,负责向系统输入数据。

存储器(内存):数据与程序的临时存储中心,CPU 只能直接读写内存。

中央处理器(CPU):

运算器:执行算术与逻辑运算。

控制器:协调各硬件组件,控制指令流。

输出设备:显示器、打印机等,负责输出处理结果。

2. 关键规则

所有外设(输入/输出设备)只能与内存交换数据,无法直接访问 CPU。

CPU 也只能读写内存,不能直接操作外设。

存储层次金字塔(速度由快到慢,容量由小到大):1. 寄存器(L0) 2. L1/L2/L3 高速缓存(SRAM) 3. 主存(DRAM) 4. 本地磁盘(二级存储) 5. 远程存储(分布式文件系统、Web 服务器)

二、操作系统(OS)核心概念

1. 定义与组成

狭义 OS(内核 Kernel):负责进程/线程管理、内存管理、文件管理、驱动管理。

广义 OS:内核 + 外壳(Shell)、glibc 库、原生库、预装系统级软件等。

2. 设计目的

对下:与硬件交互,管理所有软硬件资源。

对上:为用户程序(应用程序)提供稳定、高效的执行环境。

3. "管理"的本质

描述对象:用 struct 结构体抽象硬件/进程的属性(如 PCB)。

组织对象:用链表、红黑树等数据结构管理多个对象实例。

4. 系统调用与库函数

系统调用:内核暴露给上层的底层接口,功能基础、对开发者要求高。

库函数:对系统调用的封装,方便上层开发者二次开发(如 C 标准库 printf 封装了 write 系统调用)。

三、进程核心概念

3.1 进程基础

3.1.1 定义

课本概念:程序的一次执行实例,正在运行的程序。

内核观点:担当分配系统资源(CPU 时间、内存)的实体。

Linux 实现:进程 = 内核数据结构 task_struct + 程序代码与数据。

3.1.2 进程控制块(PCB)

本质:进程属性的集合,Linux 下用 task_struct 结构体实现。

核心内容:

标示符:PID(进程 ID)、PPID(父进程 ID)

状态:运行、睡眠、停止、僵尸等

优先级:PRI、NI(nice 值)

程序计数器:下一条待执行指令的地址

内存指针:指向代码、数据及共享内存块

上下文数据:CPU 寄存器快照(进程切换时保存/恢复)

I/O 状态信息:打开的文件、I/O 请求

记账信息:CPU 时间、时钟数等

组织方式:所有进程以 task_struct 双向链表形式存在于内核中。

3.1.3 查看进程信息

命令行工具:

ps aux / ps axj:查看进程详细信息

top:实时监控进程状态

/proc 文件夹:内核暴露的进程信息虚拟文件系统(如 /proc/1 查看 PID=1 的进程)

系统调用获取 PID/PPID:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    printf("pid: %d\n", getpid());   // 获取当前进程ID
    printf("ppid: %d\n", getppid()); // 获取父进程ID
    return 0;
}

3.1.4 创建进程:fork() 系统调用

核心特性:

调用一次,返回两次:父进程返回子进程 PID,子进程返回 0,失败返回 -1。

父子进程代码共享,数据写时复制(Copy-On-Write),各自拥有独立地址空间。

基础示例:

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

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

3.2 进程状态

3.2.1 Linux 内核状态定义

|------------------|-------------------------------------|
| 状态 | 含义 |
| R (Running) | 运行态:进程正在 CPU 上执行,或在运行队列中等待调度 |
| S (Sleeping) | 可中断睡眠:等待事件完成(如 I/O),可被信号唤醒 |
| D (Disk Sleep) | 不可中断睡眠:等待磁盘 I/O 完成,不能被信号唤醒 |
| T (Stopped) | 停止态:收到 SIGSTOP 信号后暂停,可通过 SIGCONT 恢复 |
| t (Tracing Stop) | 调试停止态:被 ptrace 跟踪时暂停 |
| X (Dead) | 死亡态:进程即将销毁,仅内核可见 |
| Z (Zombie) | 僵尸态:进程已退出,但父进程未读取其退出状态,PCB 仍保留 |

3.2.2 僵尸进程(Zombie)

成因:子进程退出后,父进程未调用 wait()/waitpid() 读取其退出码,子进程进入 Z 态。

危害:PCB 占用内存,若大量僵尸进程存在会导致内存泄漏。

示例代码:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 1;
    } else if (id > 0) { // 父进程
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(30); // 长时间不回收子进程
    } else { // 子进程
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5);
        exit(EXIT_SUCCESS); // 子进程先退出
    }
    return 0;
}

查看:ps aux 中状态列为 Z+。

3.2.3 孤儿进程

成因:父进程先退出,子进程被 init 进程(PID=1)收养,子进程退出后由 init 回收,不会变成僵尸进程。

3.3 进程优先级与调度

3.3.1 基本概念

优先级(PRI):数值越小,优先级越高,越早被 CPU 执行。

nice 值(NI):优先级修正值,范围 -20 ~ 19,共 40 个级别。

公式:PRI(new) = PRI(old) + nice

若 nice < 0:PRI 变小,优先级升高; 若 nice > 0:PRI 变大,优先级降低。

3.3.2 查看与修改优先级

查看:ps -l 输出中 PRI 和 NI 列。

修改方式:

  1. 命令行:nice / renice

  2. 系统调用:getpriority() / setpriority()

cpp 复制代码
#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
  1. 交互式:top 命令 → 按 r → 输入 PID → 输入新 nice 值。

3.3.3 并发与并行

并发(Concurrent):单个 CPU 上,多个进程通过时间片轮转切换执行,宏观上同时推进。

并行(Parallel):多个 CPU 核心上,多个进程同时执行。

竞争与独立:

竞争:进程争夺有限 CPU 资源,因此需要优先级调度。

独立:多进程独占资源,运行期间互不干扰。

3.4 进程切换与调度队列

3.4.1 上下文切换(Context Switch)

本质:CPU 寄存器状态的保存与恢复。

过程:

  1. 内核将当前进程的寄存器数据保存到其 task_struct 栈中;

  2. 从下一个进程的 task_struct 中恢复寄存器数据到 CPU;

  3. 切换页表,完成地址空间切换,开始执行新进程。

时间片:每个进程分配固定时间片,时间片耗尽则被强制切出 CPU。

3.4.2 Linux 2.6 内核 O(1) 调度队列

核心结构:每个 CPU 对应一个 runqueue(运行队列),包含:

活跃队列(active):时间片未耗尽的进程,按优先级分组。

过期队列(expired):时间片耗尽的进程。

prio_array:优先级数组,包含 queue[140](140 个优先级队列)和 bitmap[5](快速查找非空队列)。

调度流程:

  1. 遍历 bitmap 找到第一个非空优先级队列;

  2. 取出该队列队首进程执行;

  3. 时间片耗尽后,进程移入过期队列;

  4. 活跃队列空时,交换 active 和 expired 指针。

优势:查找下一个可运行进程的时间复杂度为 O(1),不随进程数量增加而变化。

四、命令行参数与环境变量

4.1 环境变量基础

定义:操作系统中用于指定运行环境的全局参数,具有全局性,可被子进程继承。

常见环境变量:

PATH:命令搜索路径(Shell 据此查找可执行文件)

HOME:用户主目录

SHELL:当前 Shell 类型(通常为 /bin/bash)

4.2 操作环境变量

查看:

echo NAME:查看单个变量(如 echo PATH)

env:查看所有环境变量

set:查看本地变量 + 环境变量

设置:export NAME=value(导出为环境变量,子进程可见)

清除:unset NAME

4.3 代码中访问环境变量

方式 1:通过 main 函数第三个参数

cpp 复制代码
#include <stdio.h>

int main(int argc, char *argv[], char *envp[]) {
    int i = 0;
    for (; envp[i]; i++) {
        printf("%s\n", envp[i]); // 遍历环境变量表
    }
    return 0;
}

方式 2:通过全局变量 environ

cpp 复制代码
#include <stdio.h>

extern char **environ; // 声明libc全局变量

int main() {
    int i = 0;
    for (; environ[i]; i++) {
        printf("%s\n", environ[i]);
    }
    return 0;
}

方式 3:通过 getenv() / putenv() 函数

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    char *env = getenv("MYENV"); // 获取环境变量
    if (env) {
        printf("%s\n", env);
    }
    return 0;
}

注意:仅在 Shell 中定义 MYENV="hello" 时,子进程不可见;必须 export MYENV="hello" 才能继承。

五、进程地址空间与虚拟内存

5.1 程序地址空间布局(32 位 Linux)

总大小:4GB,其中用户空间 3GB,内核空间 1GB。

用户空间分布(从低到高):

  1. 正文代码段(Text):存放可执行指令,只读。

  2. 初始化数据段(Data):存放已初始化的全局变量、静态变量。

  3. 未初始化数据段(BSS):存放未初始化的全局变量、静态变量(默认 0)。

  4. 堆(Heap):动态内存分配(malloc/new),向高地址增长。

  5. 共享区(Memory Mapping Segment):加载动态库、文件映射等。

  6. 栈(Stack):存放局部变量、函数调用栈,向低地址增长。

  7. 命令行参数与环境变量:栈顶之上的只读区域。

地址验证代码

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_unval;       // 未初始化全局变量 -> BSS段
int g_val = 100;    // 初始化全局变量 -> Data段

int main(int argc, char *argv[], char *envp[]) {
    const char *str = "helloworld"; // 字符串常量 -> 只读代码段
    static int test = 10;           // 静态变量 -> Data段

    // 堆内存分配
    char *heap_mem = (char*)malloc(10);
    char *heap_mem1 = (char*)malloc(10);
    char *heap_mem2 = (char*)malloc(10);
    char *heap_mem3 = (char*)malloc(10);

    // 打印各段地址
    printf("code addr: %p\n", main);
    printf("init global addr: %p\n", &g_val);
    printf("uninit global addr: %p\n", &g_unval);
    printf("test static addr: %p\n", &test);
    printf("heap addr: %p\n", heap_mem);
    printf("stack addr: %p\n", &heap_mem);
    printf("read only string addr: %p\n", str);

    return 0;
}

5.2 虚拟地址与物理地址

核心现象:父子进程中,同一变量的虚拟地址相同,但内容独立(写时复制)。

示例:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_val = 0;

int main() {
    pid_t id = fork();
    if (id < 0) {
        perror("fork");
        return 0;
    } else if (id == 0) { // 子进程
        g_val = 100;
        printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    } else { // 父进程
        sleep(3);
        printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    return 0;
}

输出:child[3046]: 100 : 0x80497e8,parent[3045]: 0 : 0x80497e8

结论:地址相同是虚拟地址,内容不同是因为映射到了不同物理地址。

虚拟地址空间的意义

安全风险:直接操作物理内存时,进程可读写任意内存,易被恶意程序破坏系统。

地址不确定:程序每次加载到物理内存的地址不同,导致代码无法重定位。

效率低下:内存不足时需换出整个进程,I/O 开销大。

虚拟地址解决:

  1. 隔离进程地址空间,保护物理内存安全;

  2. 程序使用统一虚拟地址,由 OS 负责映射到物理内存;

  3. 支持延迟分配、页交换,提升内存利用率。

5.3 内核数据结构

task_struct:进程描述符,包含指向 mm_struct 的指针。

mm_struct:内存描述符,描述整个用户地址空间,包含:

代码段、数据段、堆、栈的起始/结束地址;

mmap 指针:指向虚拟内存区域(VMA)链表/红黑树。

vm_area_struct:虚拟内存区域(VMA)描述符,每个 VMA 代表一段连续的虚拟地址空间(如代码段、堆、栈)。

组织方式:VMA 少时用链表,多时用红黑树,方便快速查找。

六、总结

这部分内容完整覆盖了操作系统核心原理和Linux 进程管理的关键知识点:

  1. 从冯诺依曼体系到 OS 抽象,理解硬件与软件的交互。

  2. 深入进程本质:task_struct 是进程在内核的化身,fork 是创建进程的核心系统调用。

  3. 掌握进程状态、优先级、调度与切换,理解 Linux 如何高效管理多任务。

  4. 学习环境变量与地址空间,理解进程间数据隔离与资源继承的底层机制。

相关推荐
计算机安禾2 小时前
【数据结构与算法】第20篇:二叉树的链式存储与四种遍历(前序、中序、后序、层序)
c语言·开发语言·数据结构·c++·学习·算法·visual studio
顶点多余2 小时前
POSIX信号量+生产消费模型应用+环形缓冲区实现
linux·c++
Qinti_mm2 小时前
Linux NUMA自动优化机制全解析
linux·服务器·numa balancing
开开心心就好2 小时前
桌面图标乱了怎么办,一键恢复固定位置工具
运维·服务器·windows·pdf·excel·3dsmax·houdini
大地的一角2 小时前
(计算机网络)传输层协议原理
网络协议·计算机网络·udp
zb200641202 小时前
自己编译RustDesk,并将自建ID服务器和key信息写入客户端
运维·服务器
Agent产品评测局2 小时前
企业采购自动化落地,供应商全生命周期管控实现方案:智能体驱动下的全链路提效与合规治理
运维·人工智能·ai·chatgpt·自动化
雨声不在2 小时前
IP路由表(ip rule)修改
网络·网络协议·tcp/ip
刘若里2 小时前
【论文阅读】自适应稀疏自注意力——可直接用!
论文阅读·人工智能·笔记·深度学习·计算机视觉