前言
本篇笔记完全基于 BIT Linux 进程 PDF 文档整理,站在 Linux 视角,把冯诺依曼体系、操作系统、PCB 进程、进程状态、僵尸 / 孤儿进程、优先级、环境变量、虚拟地址空间、O (1) 调度算法全部拆解。
**目录
1.计算机底层骨架:冯诺依曼体系(电脑零件怎么传数据)
2.系统大管家:操作系统 OS(谁在管理软硬件)
3.进程是什么?PCB/task_struct(内核怎么描述进程)
4.亲手造进程:fork 创建子进程(代码手把手演示)
5.进程的 7 种人生状态(R/S/D/T/X/Z 全解析)
6.两大 “问题宝宝”:僵尸进程 & 孤儿进程(成因 + 危害 + 示例)
7.CPU 排队规则:进程优先级 PRI/NICE(调优先级实操)
8.进程相处规则:并发 / 并行 / 竞争 / 独立
9.系统全局小纸条:环境变量(命令 + 代码全套示例)
10.迷惑大坑:进程虚拟地址空间(fork 写时拷贝原理)
1. 计算机底层骨架:冯诺依曼体系(电脑零件怎么传数据)

所有电脑、服务器都遵循这套结构,分为 5 大块:
1.输入设备:键盘、鼠标、扫描仪、手写板(给电脑传数据)
2.存储器:这里专指内存(核心中转站,所有硬件只能和内存交互)
3.运算器:负责算术、逻辑计算,属于 CPU 内部
4.控制器:指挥所有硬件收发数据,属于 CPU 内部
5.输出设备:显示器、打印机(把内存数据展示给人)
1.2 核心硬性规则(必考)
- CPU不能直接读写外设(键盘 / 显示器 / 网卡),只能操作内存;
- 外设收发数据必须先存入内存、或从内存读取;
- 一句话总结:所有硬件只和内存打交道。
1.3 实操场景数据流拆解(QQ 聊天 / 发文件)
场景 1:QQ 文字聊天数据流
① 你发消息(本机数据流)
1.键盘(输入设备)输入文字,数据先写入内存;
2.QQ 进程从内存读取文字数据,CPU 运算打包;
3.打包后的数据放回内存,网卡驱动从内存取数据;
4.网卡(输出外设)把数据发送到网络。
② 好友接收消息(对方电脑数据流)
1.对方网卡收到网络数据,写入对方电脑内存;
2.对方 QQ 进程读取内存数据,CPU 解析文字;
3.解析完成放回内存,显示器驱动读取内存数据;
4.显示器(输出设备)展示聊天文字。
场景 2:QQ 发送文件
1.硬盘存放文件(外设),文件先加载到内存;
2.QQ 进程读取内存里的文件二进制数据,CPU 分包;
3.分包数据存入内存,网卡驱动读取内存数据上传网络;
4.好友网卡接收数据写入内存,QQ 进程读取内存数据;
5.数据写入对方硬盘(内存→硬盘外设),完成文件接收。
1.4 初学者理解小贴士
很多新手会误以为 CPU 直接读写硬盘、键盘,这是错误的。内存是唯一中转站,所有外设都不能跳过内存和 CPU 直连。
2. 系统大管家:操作系统 OS(谁在管理软硬件)
2.1 OS 完整组成
2.1 OS 完整组成
操作系统 = 内核(核心管理模块) + 配套程序(shell、库函数等)
内核四大核心管理功能:
1.进程管理:管理所有运行程序
2.内存管理:分配 / 回收内存空间
3.文件管理:管理硬盘文件、文件夹
4.驱动管理:对接网卡、硬盘、显示器硬件驱动

2.2 OS 两大设计目的
向下:和硬件交互,统一管理全部软硬件资源;
向上:给用户程序(QQ、编译器、shell)提供安全、稳定的运行环境。
2.3 OS 的本质:管理者
操作系统管理硬件的固定套路(所有硬件通用):
- 描述对象:用结构体 struct 记录硬件 / 进程全部属性;
- 组织对象:用链表、数组等数据结构把多个对象存起来统一管理。
2.4 分层架构讲解(从上到下)
1.用户层:人操作电脑(敲命令、写代码、点软件)
2.用户操作接口:shell 外壳、lib 库、基础指令;
3.系统调用 system call:内核对外暴露的底层接口(库函数封装它);
4.操作系统内核:进程 / 内存 / 文件 / 驱动四大管理模块;
5.驱动程序:网卡驱动、硬盘驱动,承接内核和硬件;
6.底层硬件:网卡、硬盘、显示器等物理设备。
2.5 系统调用 vs 库函数(必区分)
1.系统调用:内核原生接口,功能底层、使用复杂,直接和内核交互;
例:fork()、getpid()、open() 都是系统调用;
2.库函数:开发者对系统调用封装后的工具,简单好用;
例:C 语言printf()底层封装了write()系统调用。
实操示例
写文件时:fputs()(库函数)→ 内部调用write()(系统调用)。
作用:库函数屏蔽底层复杂逻辑,新手不用直接操作内核。
3. 进程是什么?PCB/task_struct(内核怎么描述进程)
3.1 进程基础概念
课本定义:程序的运行实例、正在执行的程序;
内核定义:分配 CPU、内存等系统资源的实体。
区分小白易混点:
**程序:**硬盘上静态二进制文件(静态,不占运行资源);
**进程:**程序加载到内存运行后的实体(动态,占用 CPU / 内存)。
3.2 PCB:进程控制块
内核不会凭空管理进程,会用一个结构体存储进程全部信息,这个结构体统称 PCB;
Linux 系统下 PCB 专属结构体:task_struct,常驻内存 RAM。
3.3 task_struct 存储的 8 大类信息
1.标示符 PID:进程唯一编号,区分系统内所有进程;
2.状态:运行 / 睡眠 / 僵尸 / 停止等状态、退出码、信号;
3.优先级:进程调度先后顺序;
4.程序计数器 PC:下一条要执行代码的内存地址;
5.内存指针:代码、数据、共享内存的虚拟地址指针;
6.上下文数据:CPU 寄存器临时数据(进程切换时保存现场);
7.I/O 状态:进程占用的外设、打开的文件列表;
8.记账信息:CPU 使用时长、资源限制等统计数据。
3.4 内核如何组织全部进程
系统中所有运行进程的task_struct,会被内核串成双向链表,统一遍历管理。
3.5 查看进程的实操命令


3.6 代码获取 PID/PPID
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// 获取当前进程PID
printf("当前进程PID:%d\n", getpid());
// 获取父进程PPID
printf("父进程PPID:%d\n", getppid());
return 0;
}
作用:getpid()/getppid()是系统调用,快速查看进程自身和父进程编号,调试父子进程必备。
4. 亲手造进程:fork 创建子进程
4.1 fork 核心特性( 3 大重点)
1.调用一次,两个返回值;
2.父子进程代码完全共享;
3.数据采用写时拷贝:不修改数据时共用物理内存,一旦修改,子进程单独复制一份数据。
4.2 fork 返回值规则
- ret < 0:fork 创建失败,返回负数;
- ret == 0:当前是子进程;
- ret > 0:当前是父进程,返回值等于子进程 PID。
4.3 基础 fork 示例 1(无分支判断)
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
printf("进程PID:%d,fork返回值:%d\n", getpid(), ret);
sleep(1);
return 0;
}

作用:直观看到 fork 生成两个进程,父进程返回子 PID,子进程返回 0。
4.4 标准 fork 示例 2(if 分流,业务分离)
实际开发中必须用 if 区分父子进程,做不同逻辑:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int ret = fork();
if(ret < 0){
perror("fork创建失败");
return 1;
}
else if(ret == 0){
// 子进程执行逻辑
printf("我是子进程 PID:%d,fork返回:%d\n", getpid(), ret);
}else{
// 父进程执行逻辑
printf("我是父进程 PID:%d,fork返回:%d\n", getpid(), ret);
sleep(1); // 等待子进程打印完毕
}
return 0;
}
运行效果:父子分别打印专属语句,业务逻辑隔离。
4.5 初学者易错点
fork 之后不是先执行父进程,CPU 调度随机,父子进程执行顺序不确定;如果父进程先退出,子进程会变成孤儿进程。
5. 进程的 7 种人生状态(R/S/D/T/X/Z 全解析)
内核源码定义 7 种任务状态,对应标识:
R运行、S可中断睡眠、D不可中断深度睡眠、T暂停、t调试暂停、X死亡、Z僵尸

5.1 逐个状态通俗讲解
1.R (running) 运行态
不是一定占用 CPU,两种情况:正在 CPU 执行 / 在就绪调度队列等待 CPU 时间片。
2.S (interruptible sleep) 可中断浅睡眠
进程等待资源(网络、用户输入),能被信号唤醒。
示例:sleep(10)的进程,用kill发信号可以提前唤醒。
3.D (uninterruptible sleep) 深度磁盘睡眠
等待磁盘 IO 完成,不能被信号打断,只能等 IO 结束自动唤醒。
场景:进程读写硬盘数据时卡死,无法 kill 杀掉。
4.T (stopped) 暂停态
收到SIGSTOP信号暂停,发送SIGCONT恢复运行。
实操示例:

5.t (tracing stop) 调试暂停
gdb 调试程序断点时,进程进入该状态。
6.X (dead) 死亡态
进程资源完全释放,PCB 直接销毁,进程列表看不到。
7.Z (zombie) 僵尸态
子进程退出,但父进程没有读取子进程退出状态,PCB 保留在系统中。
5.2 状态流转核心逻辑
- R 时间片耗尽 → 回到就绪 R 队列;
- R 等待资源 → S/D 睡眠;资源到位唤醒 → R;
- R 收到 SIGSTOP → T 暂停;收到 SIGCONT → R;
- 进程调用 exit 退出 → Z 僵尸;父进程回收 → X 彻底死亡。
6. 两大 “问题宝宝”:僵尸进程 & 孤儿进程(成因 + 危害 + 示例)
6.1 僵尸进程 Z
6.1.1 形成条件
1.子进程执行exit()正常退出;
2.父进程持续运行,没有调用 wait/waitpid 读取子进程退出码;
3.子进程 PCB(task_struct)保留在内核,进程状态标记 Z。
6.1.2 完整代码示例(生成僵尸进程)
#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;
}
6.1.3 观测步骤
1.终端 1 运行程序:./test;
2.5 秒后新开终端执行ps aux | grep test;
3.能看到子进程状态标注Z(僵尸),持续 25 秒直到父进程休眠结束。
6.1.4 僵尸进程巨大危害
PCB 结构体占用内存,父进程持续创建子进程且不回收,会不断产生僵尸进程,占用大量内核内存,造成内存泄漏,严重时系统无法创建新进程。
6.1.5 解决办法
父进程调用wait()/waitpid()主动回收子进程退出状态,销毁 PCB。
6.2 孤儿进程
6.2.1 形成条件
父进程提前退出,子进程还在运行,此时子进程称为孤儿进程。
6.2.2 系统处理机制
Linux 系统 1 号进程(init/systemd)自动领养所有孤儿进程,当孤儿进程退出时,由 1 号进程自动回收,不会产生僵尸进程。
6.2.3 孤儿进程代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 1;
}
else if(id == 0){
// 子进程休眠10秒,父进程3秒就退出
printf("子进程PID:%d,等待父进程退出\n", getpid());
sleep(10);
printf("子进程PPID现在是:%d\n", getppid());
}else{
// 父进程3秒后退出
printf("父进程PID:%d,3秒后退出\n", getpid());
sleep(3);
exit(0);
}
return 0;
}
6.2.4 观测现象
父进程 3 秒结束后,子进程打印getppid()输出 1,证明被 1 号进程收养。
6.2.5 优缺点
孤儿进程无内存泄漏危害,后台服务程序经常利用孤儿进程实现守护进程。
7. CPU 排队规则:进程优先级 PRI/NICE(调优先级实操)
7.1 优先级概念
CPU 资源有限,进程优先级决定 CPU 调度先后顺序,数值越小,优先执行。
7.2 PRI vs NI(nice 值)核心关系
1.PRI:进程原始优先级,内核自动分配;
2.NI(nice 值):PRI 修正值,用户唯一能手动修改的参数;
3.计算公式:新PRI = 原PRI + nice;
4.nice 取值范围:-20 ~ 19,共 40 个档位:
nice=-20:PRI 变小,优先级最高;
nice=19:PRI 变大,优先级最低。
7.3 查看优先级命令

输出字段说明:
- PID:进程 ID
- PPID:父进程 ID
- PRI:内核优先级
- NI:nice 修正值
7.4 修改进程 nice 实操(top 命令)
1.终端输入top进入监控界面;
2.按下 r 快捷键;
3.输入要修改的进程 PID;
4.输入新 nice 值(root 可设 - 20~19,普通用户只能 0~19);
作用:临时调整进程 CPU 权重,编译大型程序时调高优先级,编译速度更快。
7.5 初学者区分误区
nice 值≠优先级,只是修正量;PRI 才是 CPU 真正调度依据,只能通过修改 nice 间接改变 PRI。
8. 进程相处规则:并发 / 并行 / 竞争 / 独立
8.1 竞争性
系统进程数量远大于 CPU 核心,CPU 资源稀缺,所有进程互相竞争 CPU 时间片,因此需要优先级区分调度顺序。
8.2 独立性
多进程运行时资源隔离,一个进程崩溃不会影响其他进程;进程拥有独立虚拟地址空间,互不干扰。
8.3 并发(单核 CPU)
单颗 CPU 通过快速切换进程,在一段时间内让多个进程都向前推进,宏观看起来同时运行,微观同一时刻只有一个进程占用 CPU。
示例:单核电脑一边听歌一边打字,CPU 高速切换音乐进程、输入法进程。
8.4 并行(多核 CPU)
多颗 CPU 核心,同一时刻每个核心独立运行一个进程,真正同时执行。
示例:4 核 CPU 同时跑 4 个编译程序,4 个进程同步运算。
9.系统全局小纸条:环境变量
9.1 环境变量定义
操作系统记录运行环境的全局参数,子进程默认继承父进程环境变量。
9.2 高频常用环境变量
1.PATH:命令搜索路径,输入指令时系统自动在 PATH 目录查找二进制程序;
2.HOME:当前用户家目录,cd ~等价于进入 $HOME;
3.SHELL:当前使用的命令行解释器,默认 /bin/bash。
9.3 操作环境变量 Linux 命令
- echo 查看单个环境变量


9.4 代码读取环境变量三种方式
方式 1:main 函数第三个参数 env
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
int i = 0;
// 循环打印所有环境变量
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}
方式 2:全局外部变量 environ
#include <stdio.h>
// 手动声明全局环境表指针,无头文件包含
extern char **environ;
int main()
{
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}
方式 3:系统调用 getenv(推荐,精准读取单个变量)
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 单独读取PATH环境变量
char *path = getenv("PATH");
printf("PATH路径:%s\n", path);
return 0;
}
9.5 局部变量 vs 环境变量

9.6 PATH 实操小实验(理解为什么./ 才能运行程序)
- 编译 hello.c 生成可执行文件 hello;
- 直接输入hello报错:command not found;
- 执行export PATH=$PATH:./,把当前目录加入 PATH;
- 再次输入hello直接运行,不需要./hello;
- 原理:系统只会在 PATH 记录的目录里查找程序,不在 PATH 内必须写完整路径执行。
10. 迷惑大坑:进程虚拟地址空间(fork 写时拷贝原理)

32 位系统寻址范围 0~4G,分为两大部分:
1.内核空间(高地址 1G):操作系统内核专属,用户进程无权访问;
2.用户空间(低地址 3G):程序代码、数据、栈、堆全部存放于此,细分区域(从低到高):
- 正文代码段:编译后的二进制代码;
- 初始化数据段:全局初始化变量;
- 未初始化数据段:未赋值全局变量;
- 堆:动态内存 malloc/free,自低地址向上增长;
- 共享区:动态库、mmap 映射文件;
- 栈:局部变量、函数调用栈帧,自高地址向下增长;
- 命令行参数 & 环境变量:存放 argv、env 数组。
10.2 核心现象:fork 后父子变量地址相同,值不同
测试代码
#include <stdio.h>
#include <unistd.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id == 0){
// 子进程修改全局变量
g_val = 100;
printf("子进程 val=%d 虚拟地址=%p\n", g_val, &g_val);
}else{
sleep(3);
printf("父进程 val=%d 虚拟地址=%p\n", g_val, &g_val);
}
sleep(1);
return 0;
}

现象解释

1.C语言代码打印的所有地址都是虚拟地址,不是内存条真实物理地址;
2.每个进程拥有独立虚拟地址空间,父子进程虚拟地址编号完全一致;
3.MMU 内存管理单元通过页表,把虚拟地址映射到不同物理内存块;
4.写时拷贝机制:
- 不修改数据时,父子进程页表指向同一块物理内存,节省内存;
- 一旦任意一方修改数据,内核复制一份独立物理内存给子进程,更新子进程页表映射,实现数据隔离。
10.3 初学者易错点
1.程序文件(硬盘静态)≠ 进程地址空间(内存动态);
2.地址相同不代表同一块内存,虚拟地址只是逻辑编号,物理地址由 OS 隐藏管理。
1630



被折叠的 条评论
为什么被折叠?



