
文章目录
引言
在Linux操作系统学习过程中,进程与内存管理作为这门知识体系中最核心的部分,也最容易让初学者感到困惑。本文系统拆解了进程与内存管理的核心知识点:从冯诺依曼体系的底层逻辑出发,逐步深入操作系统的管理本质、进程的创建与状态流转、优先级调度、上下文切换,再到环境变量的特性与虚拟地址空间的实现。帮助读者建立起对进程与内存管理的体系化认知。
1.冯诺伊曼体系结构
所有现代计算机(如笔记本、服务器等)都遵循冯诺伊曼体系结构,核心是"所有设备只和内存打交道",CPU不能直接访问外设。

我们通过上图结合"快递驿站"来理解:
- 内存:相当于小区快递站,所有快递(数据)都先存放在这里;
- CPU:相当于我们自己,只去驿站取或寄快递,不直接从快递员那里拿;
- 输入设备:相当于快递员,他负责把快递送到驿站;
- 输出设备:相当于我们拆快递后使用的商品,它是由驿站交给我们的,经过我们处理后使用。
那么数据流动的过程是怎样的呢?
以QQ聊天为例:
(1)我们用键盘输入消息(输入设备)-> 数据写入内存;
(2)CPU从内存中读取消息,处理后(编码等) -> 写回内存;
(3)网卡(外设)从内存读取数据,发送给好友;
(4)好友网卡接收数据 -> 写入好友内存;
(5)CPU读取内存数据,处理后 -> 输出到显示器(输出设备)。
如果是QQ传文件呢?流程与发消息一致,文件先从本地磁盘读入内存,再经CPU调度、网卡发送;对方接收后先存入内存,再写入磁盘。
2.操作系统
操作系统 = 内核 + 其他程序:

操作系统(OS)是"管理软硬件资源的软件",对上为用户程序提供一个良好的运行环境,对下管理硬件,核心是"先描述,后组织"。

💡我们可以结合学校中的管理系统来理解:
(1)校长(内核):管理核心资源(辅导员、教室等),负责制定规则;
(2)辅导员(管理模块):负责具体执行(进程调度、内存分配等),管理自己班的学生;
(3)学生(应用程序):需要资源(内存、CPU时间等),向辅导员申请。
管理的核心逻辑:先描述,再组织
- 描述:用结构体记录被管理对象的各种属性(如学生信息结构体);
- 组织:用数据结构(如链表、队列等)管理结构体(如学生名单链表)。
在OS中,用PCB(task_struct结构体)描述进程,通过双链表链接所有PCB来组织进程。
3.进程
3.1.核心概念
进程 = 内核数据结构(PCB)+ 程序代码和数据,是OS分配资源的基本单位 。
PCB(task_struct):相当于进程的"身份证"和"状态卡",包含以下成员:
- 标识符(PID/PPID):描述当前进程的唯一标识,用来区别于其他进程;
- 状态(运行/睡眠/僵尸等):任务状态,退出代码,退出信号等;
- 优先级(CPU调度顺序):相对于其他进程的优先级;
- 内存指针:包括代码和进程相关数据的指针,还有和其他进程共享的内存块指针;
- 上下文数据:进程执行时处理器的寄存器中的数据。
💡我们以生活中"外卖小哥"送外卖的过程为例:
程序 = 外卖配送规则(怎么取餐、怎么配送);
进程 = 正在配送的骑手(有自己的账户、配送状态等);
PCB = 骑手的工牌 + 配送APP(记录所有相关信息)。
3.2.查看进程
可以使用ps命令查看进程:
bash
# 查看所有进程详细信息
ps aux
# 查看进程树
pstree
# 实时监控进程
top
# 查看PID=1的进程详情(系统初始化进程)
ls /proc/1
以test.c代码为例:
- 编译运行:
gcc test.c -o test && ./test; - 查看进程:
ps aux | grep test。
3.3.获取进程ID(PID/PPID)
调用getpid() / getppid()函数获取当前进程/父进程的ID(该函数返回值类型本质上是整型):
c
#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.4.创建进程
可以通过调用fork()函数来创建进程,它有两个返回值:父进程返回子进程PID,子进程返回0:
c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t ret = fork(); // 创建子进程
if (ret < 0) {
perror("fork失败");
return 1;
} else if (ret == 0) {
// 子进程执行逻辑
printf("我是子进程,PID:%d,父进程PPID:%d,ret:%d\n", getpid(), getppid(), ret);
} else {
// 父进程执行逻辑
printf("我是父进程,PID:%d,子进程PID:%d,ret:%d\n", getpid(), ret, ret);
}
sleep(2); // 防止进程提前退出
return 0;
}
/*
运行结果:
我是父进程,PID:12345,子进程PID:12346,ret:12346
我是子进程,PID:12346,父进程PPID:12345,ret:0
*/
fork()的关键特性:写时拷贝
fork()后父子进程代码共享,数据私有:
- 初始时数据共享;
- 当任一进程修改数据时,OS才为其分配独立空间(拷贝数据)。
💡生活事例:
当你和朋友共用一本书时(代码共享),平时可以一起看;如果朋友想在书上记笔记(修改数据),就会自己复印一本来用(数据私有)。
4.进程状态
4.1.核心逻辑
在Linux内核中,进程有7中状态,核心变化逻辑如下:

4.2.状态详解:
| 状态 | 含义 | 触发方式 |
|---|---|---|
| R(运行) | 正在CPU运行或在运行队列 | 正在执行的代码 |
| S(可中断睡眠) | 等待事件完成(可被信号唤醒) | sleep(10) |
| D(不可中断睡眠) | 等待IO完成(不可唤醒) | 磁盘读写时 |
| T(停止) | 被信号暂停 | kill -SIGSTOP PID |
| Z(僵尸) | 子进程退出,父进程未回收 | 子进程exit,父进程不处理 |
| X(死亡) | 进程终止 | 程序return 0 |
4.3.创建僵尸进程
c
#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) {
// 父进程睡眠30秒,不回收子进程
printf("父进程PID:%d,睡眠30秒\n", getpid());
sleep(30);
} else {
// 子进程睡眠5秒后退出
printf("子进程PID:%d,5秒后退出\n", getpid());
sleep(5);
exit(EXIT_SUCCESS);
}
return 0;
}
编译运行后,同时在命令行监控:
bash
while :; do ps aux | grep 程序名 | grep -v grep; done
此时子进程状态变为Z+(僵尸进程)。
4.4.创建孤儿进程
父进程提前退出,子进程被1号init进程收养:
c
#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) {
// 子进程睡眠10秒
printf("子进程PID:%d,父进程PPID:%d\n", getpid(), getppid());
sleep(10);
// 醒来后父进程已退出,PPID变为1
printf("子进程醒来,父进程PPID:%d\n", getppid());
} else {
// 父进程睡眠3秒后退出
printf("父进程PID:%d,3秒后退出\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
编译运行后,同时在命令行监控:
bash
while :; do ps aux | grep 程序名 | grep -v grep; done
子进程醒来后,PPID变成1号(被init进程收养)。
4.5.僵尸进程的危害
- 子进程退出状态保存在PCB中,父进程不回收,PCB一直占用内存;
- 大量僵尸进程会导致内存泄露,消耗系统资源。
5.进程优先级
5.1.核心概念
- 优先级决定进程获取CPU资源的顺序,Linux中用
PRI(实际优先级)和NI(修正值)控制:PRI(new) = PRI(old) + NI。 - NI取值范围:-20(最高优先级修正值)~ 19(最低优先级修正值)。
💡以医院挂号为例:
- PRI = 病情紧急程度(急诊 > 门诊);
- NI = 额外调整(老人、孕妇、军人优先,它们的NI值更小);
- CPU = 医生(优先处理病情紧急,且有额外优待的病人)。
5.2.调整进程优先级
- 命令行调整:
bash
# 1. 用top命令调整已运行进程
top → 按r → 输入进程PID → 输入NI值(比如-5)
# 2. 启动进程时设置NI值
nice -n 5 ./test # NI=5,优先级降低
nice -n -10 ./test # NI=-10,优先级升高
# 3. 修改已运行进程的NI值
renice -5 12345 # 把PID=12345的进程NI设为-5
- C语言函数调整:
c
#include <stdio.h>
#include <sys/resource.h>
int main() {
// 获取当前进程优先级(PRI)
int prio = getpriority(PRIO_PROCESS, getpid());
printf("当前进程优先级:%d\n", prio);
// 设置优先级(NI=-5 → PRI降低5)
int ret = setpriority(PRIO_PROCESS, getpid(), -5);
if (ret == -1) {
perror("设置优先级失败");
return 1;
}
prio = getpriority(PRIO_PROCESS, getpid());
printf("设置后优先级:%d\n", prio);
return 0;
}
5.3.并行与并发
- 并行:多个CPU同时运行多个进程(比如两个主治医生同时看病);
- 并发:一个CPU通过切换,让多个进程"看起来同时运行"(比如一个医生同时给多个病人看病)。
6.进程切换
6.1.核心概念
进程切换:就是上下文切换,即保存当前进程的CPU寄存器状态,加载下一个进程的状态。

💡以接力赛为例:
- 运动员A跑一段后(时间片耗尽),把接力棒(上下文数据)交给运动员B;
- 运动员B接棒后,继续从接棒位置按之前的速度跑(恢复上下文)。
6.2.Linux2.6的O(1)调度算法
- 核心:用两个队列(active队列 + expired队列)管理进程;
active队列:时间片未用完的进程,按优先级排列;expired队列:时间片用完的进程;- 切换逻辑:active队列空缺时,交换两个队列的指针,时间复杂度为O(1)。

7.环境变量
7.1.核心概念
- 环境变量:是操作系统的全局参数 ,用于指定运行环境 (如搜索路径、用户主目录),可被子进程继承。
- 常见环境变量:
| 环境变量 | 含义 | 实操命令 |
|---|---|---|
| PATH | 命令搜索路径 | echo $PATH |
| HOME | 用户主目录 | echo $HOME |
| SHELL | 当前Shell | echo $SHELL |
7.2.命令行操作环境变量
bash
# 1. 查看所有环境变量
env
# 2. 查看单个环境变量
echo $PATH
# 3. 添加环境变量(临时有效)
export PATH=$PATH:/你的程序目录
# 4. 清除环境变量
unset MYENV
# 5. 永久添加环境变量(修改配置文件)
echo 'export PATH=$PATH:/你的程序目录' >> ~/.bashrc
source ~/.bashrc
7.3.C语言获取环境变量
c
#include <stdio.h>
#include <stdlib.h>
int main() {
// 方法1:通过main函数第三个参数
// int main(int argc, char *argv[], char *env[]) {
// for (int i = 0; env[i]; i++) {
// printf("%s\n", env[i]);
// }
// 方法2:通过environ全局变量
extern char **environ;
for (int i = 0; environ[i]; i++) {
printf("%s\n", environ[i]);
}
// 方法3:获取指定环境变量
char *path = getenv("PATH");
printf("PATH:%s\n", path);
return 0;
}
7.4.环境变量的继承性
父进程的环境变量会被子进程继承,如:
bash
# 1. 父进程设置环境变量
export MYENV="hello"
# 2. 运行C程序(子进程),能获取到MYENV
./test
8.虚拟地址空间
8.1.核心概念
-
我们写代码时看到的地址都是虚拟地址 ,OS通过页表将虚拟地址映射到物理地址,实现 "地址隔离 + 高效内存管理"。
-
虚拟地址空间布局(32位系统):

💡我们以地图导航为例:
- 虚拟地址 = 地图上的坐标(我们看到的地址);
- 物理地址 = 实际地理位置(真实内存地址);
- 页表 = 地图的"坐标映射表"(OS维护)。
8.2.验证虚拟地址布局
c
#include <stdio.h>
#include <stdlib.h>
// 初始化全局变量(DATA区)
int g_val = 100;
// 未初始化全局变量(BSS区)
int g_unval;
int main(int argc, char *argv[], char *env[]) {
// 代码区(TEXT)
printf("代码区地址:%p\n", main);
// 数据区
printf("初始化全局变量地址:%p\n", &g_val);
printf("未初始化全局变量地址:%p\n", &g_unval);
// 静态变量(DATA/BSS区)
static int s_val = 200;
printf("静态变量地址:%p\n", &s_val);
// 堆区(向上增长)
char *heap1 = malloc(10);
char *heap2 = malloc(10);
char *heap3 = malloc(10);
printf("堆区地址1:%p\n", heap1);
printf("堆区地址2:%p\n", heap2);
printf("堆区地址3:%p\n", heap3);
// 栈区(向下增长)
int a = 10;
int b = 20;
int c = 30;
printf("栈区地址a:%p\n", &a);
printf("栈区地址b:%p\n", &b);
printf("栈区地址c:%p\n", &c);
// 命令行参数+环境变量
printf("argv[0]地址:%p\n", argv[0]);
printf("env[0]地址:%p\n", env[0]);
return 0;
}
运行结果:
bash
代码区地址:0x55f876a79149
初始化全局变量地址:0x55f876c7a010
未初始化全局变量地址:0x55f876c7a018
静态变量地址:0x55f876c7a014
堆区地址1:0x55f878f0c2a0
堆区地址2:0x55f878f0c2c0
堆区地址3:0x55f878f0c2e0
栈区地址a:0x7ffeefbff68c
栈区地址b:0x7ffeefbff688
栈区地址c:0x7ffeefbff684
argv[0]地址:0x7ffeefbff838
env[0]地址:0x7ffeefbff858
不难发现:堆地址递增(向上长),栈地址递减(向下长),符合布局规律。
8.3.验证虚拟地址≠物理地址
c
#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 1;
} else if (id == 0) {
// 子进程修改全局变量
g_val = 100;
printf("子进程:g_val=%d,地址=%p\n", g_val, &g_val);
} else {
// 父进程延迟3秒,等待子进程修改
sleep(3);
printf("父进程:g_val=%d,地址=%p\n", g_val, &g_val);
}
sleep(1);
return 0;
}
运行结果:
bash
子进程:g_val=100,地址=0x80497e8
父进程:g_val=0,地址=0x80497e8
(虚拟)地址相同,但值不同,证明被映射到不同物理地址。
8.4.虚拟地址空间的优势
- 安全隔离:进程不能直接访问物理内存,避免恶意修改;
- 地址独立:每个进程看到的地址布局一致,编译时无需考虑物理地址;
- 高效内存利用:采用"延迟分配",申请内存时只占虚拟地址,实际使用时才分配物理内存。
结语
进程与内存管理作为操作系统的核心,其本质是 "资源的合理分配与高效调度"。
技术的提升从来不是一蹴而就的,本文的知识点只是操作系统体系的冰山一角。后续不妨尝试将所学知识融会贯通:用进程调度原理优化多任务程序性能,用虚拟地址空间的认知排查内存泄漏问题,在实际项目中检验学习成果。
如果本文对你有帮助,欢迎分享给身边的朋友;若有疑问、补充或不同见解,也期待在评论区与你交流探讨。
愿你在技术之路上,始终保持对底层原理的探索欲,扎稳基础、稳步前行,用扎实的核心能力应对各类技术挑战!