Linux 进程核心原理精讲:从体系结构到实战操作
Linux 进程是操作系统资源调度的基本单位,由内核 PCB 数据结构及代码数据组成。从冯诺依曼体系结构入手,讲解操作系统管理软硬件资源的职责,深入剖析进程控制块(task_struct)、进程状态转换(运行/阻塞/僵尸/孤儿)、以及基于优先级的调度机制。结合 ps、top 等指令实操与 fork 系统调用示例,阐述进程并发、上下文切换及内存分配原理,帮助开发者理解底层逻辑以优化性能。

Linux 进程是操作系统资源调度的基本单位,由内核 PCB 数据结构及代码数据组成。从冯诺依曼体系结构入手,讲解操作系统管理软硬件资源的职责,深入剖析进程控制块(task_struct)、进程状态转换(运行/阻塞/僵尸/孤儿)、以及基于优先级的调度机制。结合 ps、top 等指令实操与 fork 系统调用示例,阐述进程并发、上下文切换及内存分配原理,帮助开发者理解底层逻辑以优化性能。

进程是操作系统的核心骨架,所有程序的运行本质都是进程的调度与执行。理解进程的底层逻辑,不仅能打通操作系统、硬件与应用程序的关联,更能为排查性能问题、编写高效代码打下基础。
本文将从冯诺依曼体系结构出发,逐步拆解操作系统的核心职责,再深入进程的定义、PCB 结构、状态转换、优先级调度等核心知识点,同时搭配 ps/top/fork 等实操指令与代码示例,兼顾理论深度与实战性。无论是刚接触 Linux 系统的初学者,还是想夯实底层基础的开发者,都能通过本文理清进程的完整逻辑链,掌握从认识进程到操控进程的核心能力。

这里指的存储器是内存,是硬件级别的缓存空间(外存不算在这里)
外设分为输入设备和输出设备
输入设备:鼠标,键盘,磁盘,网卡…
输出设备:显示器,播放器硬件,磁盘,网卡
由此看出:有些设备既有输入功能,又有输出功能
中央处理器 (CPU):由运算器和控制器组成
运算器:对数据进行计算任务(算数运算,逻辑运算等)
控制器:对计算硬件流程进行一定的控制
他们都是独立的个体!
不考虑缓存情况的话,这个中央处理器是不会跟输入输出设备直接联系的,都是通过存储器进行联系的
这样设计的好处:提高 CPU 的工作效率(不用等输入输出设备慢慢输,可以趁这个时间搞其他的)
一个程序要运行,就必须先加载到内存中运行的原因:冯诺依曼体系结构的规定

概念:操作系统是一款进行管理的软件(可以管理软件和硬件)
要管理这么多东西——注定了操作系统里面存在大量的数据结构(因为先描述再组织)
为什么要有操作系统:
可以帮助用户管理好下面的软硬件资源
为用户提供一个良好(也就是稳定,高效,安全)的运行环境
也就是操作系统通过管理好底层的软硬件资源(手段),为用户提供一个良好的执行环境(目的)
操作系统里面由大量的数据,但是不想被用户获取
所以:为了保证自己的数据安全,也为了能够给用户提供服务,操作系统以接口的方式给用户提供调用的入口,来使用操作系统内部的数据
这个接口是操作系统自己提供的,用 C 语言实现的,像这种操作系统自己内部进行的函数调用,称为系统调用
–所有访问操作系统的行为,都只能通过系统调用来完成
操作系统管理数据的方法–先描述再组织(6 个字很重要)
(也就是先描述数据属性,再把数据串起来)
在操作系统中,管理任何对象,最终都可以转化成对某种数据结构的增删查改
库函数和系统调用:
库函数是通过系统调用提供的接口实现的
这俩个在写代码的时候都可以用(但是要编程语言支持这个)
一个加载到内存中的程序(也就是正在运行的程序),叫做进程–这个说法不准确哈
进程 = 内核 PCB 数据结构对象 + 代码和数据
(代码和数据一直在,只有在运行的时候才会产生 PCB)
PCB(进程控制块):进程属性的集合,描述了这个进程的所有属性
在 Linux 内核里,进程有时候也叫做任务
大多数 Linux 的指令都是 bash 的子进程
一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程
任何一个'进程'在加载到内存形成真正的进程时,操作系统会先创建描述进程的结构体对象–也就是 PCB
对进程做管理其实也就是对 PCB 做管理
"进程的执行有先后"其实是 PCB 在排队等执行,而不是代码和数据
具体到 Linux 是怎么处理进程的:
- pcb 在其中是
task_struct结构体,里面包含进程的所有属性(先描述)(也就是在 Linux 中描述进程的结构体叫做 task_struct)
- Linux 中组织进程:用双向链表组织
如何通过链表节点(令为 start)反向定位 task_struct,从而访问其他 PCB 的数据:
比如 task_struct 里面跟 start 同位置的是 link
那么定位 start 的 task_struct 的方法:
// 假设 start 是链表节点指针
struct task_struct *ts = container_of(start, struct task_struct, link);
task_struct 内容分类
标识符:描述本进程的唯一标识符,用来区别其他进程。
状态:任务状态,退出代码,退出信号等。
优先级:相对于其他进程的优先级。
程序计数器(也叫做 PC 指针):程序中即将被执行的下一条指令的地址。
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据(此处通常配合 CPU 寄存器示意图说明)。
I/O 状态信息:包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
引申:1. 如果想要干掉一个进程的话,可以
kill -9 PID 值(ctrl+c 干不掉时用此可以试试)
语法:
ls /proc查看当前所有的进程信息
语法:
ps ajx | head -1就是查看系统中进程的状态信息(只用第一行,所以 -1)PPID 是当前进程的父进程的 ID 值 --PPID 一般不变,在同一终端下的话
PID 是进程的 ID 值(是唯一的),如果进程结束,再重新开始,PID 大概率会变
–PID 值也是个目录其实
COMMAND 是进程对应的'命令名称或启动路径'
语法:
ps ajx | grep 文件名可以查询这个文件对应的这些信息ps 一般这样用:
ps ajx | head -1 ; ps ajx | grep 文件名
比如:getpid 和 getppid 这俩个系统调用接口(具体的用 man 指令去查)
竞争性
独立性:多进程运行期间互不干扰
并行:多个进程在多个 CPU 下分别同时进行运行
并发:多个进程在一个 CPU 下采用进程切换的方式,在一段时间之内,让多个进程都得以推进
并发的逻辑:用的是基于进程切换和基于时间片进行轮换的调度算法
相同优先级的时间片用完之后就会放入 waiting 队列里面–到时 run 和 wait 交换一下,相同优先级的不同进程间的顺序跟之前一样–这几个名字借鉴的下面一个问题的
上一个优先级时间片全用完了就轮到下一个优先级开始了
注意:进程在被切换时需要保存上下文和恢复上下文
–所以,进程在从 CPU 上离开时,要将自己的上下文数据保存好甚至带走
进程切换时,内核会把 CPU 里的寄存器数据(即进程上下文)拷贝到该进程 task_struct 关联的内存结构里
引申:函数返回值被外部拿到,是通过的 CPU 寄存器
–寄存器的作用:提高效率,进程高频数据会被放入寄存器中
–CPU 寄存器中保存的是进程相关的数据(其实就是进程的临时数据–进程的上下文)
–寄存器的种类也有很多,比如通用寄存器:eg: eax,ebx,ecx,edx
fork 也是一个系统调用 可以通过 man fork 去查他的用法
引申:系统调用接口不能在命令行那里用
简述一下用法:
返回值:(是 pid_t 类型的)
函数声明:
#include <unistd.h>
pid_t fork(void);
fork 之后会产生一个子进程,原来那个变成父进程
父子进程的代码是共享的,但是数据和 PCB 不是
数据的话,在子进程想修改数据(修改的跟父进程不一样时)时,就会进行写时拷贝,把那一点想修改的数据单独开辟空间,其他的数据还是共享的 --父进程也同理
关于 fork 引申出的几个问题:
- 为什么成功时,fork 给子进程返回 0,给父进程返回的是子进程的 pid 值
原因:一个父进程可以有多个子进程,返回 pid 值的话才能分清楚是哪个
- 一个函数是如何做到返回两次的(说的 fork)
原因:fork 函数内部的进程复制机制
一个变量怎么会有不同的内容
pid_t id = fork();--以后再给出原因如果父子进程用 fork 搞好了之后,谁先运行
这个由调度器决定,不确定的–兄弟进程谁先运行也是由调度器决定
bash 创建子进程就是用了 fork(执行外部命令,比如 ls 时,bash 就会创建子进程)
进程状态分为运行状态,阻塞状态和挂起状态等(还有比如阻塞挂起状态啥的)
运行态:会有个运行队列把进程穿起来,在运行队列里的进程都处于运行态(没给 CPU 的也算)
阻塞状态:每个设备都会有一个等待队列,在这个里面的处于阻塞状态,等设备写入到进程的数据里面,就会被交给运行队列(此时进程整体还是都在内存里的)
挂起状态:在阻塞状态的基础上,操作系统内部的内存资源严重不足了,就会把等待队列里面 PCB 对应的代码和数据先放在外存里面(PCB 还是在内存里),此时这个进程就处于挂起状态(此时进程依然存在)
一个进程只要开始运行就会执行完毕才会停止吗–不是的
进程有个时间片的概念–时间片是操作系统分配给每个进程在 CPU 上运行的一段有限时间
在一个时间段内,所有的进程代码都会被执行(也叫做并发执行)
把进程从 CPU 上放上去,在取下来的操作叫做进程切换
R(running):是运行态
S(sleeping): 是阻塞状态中的浅度睡眠–eg: 进程等键盘写入数据
D(disk sleep): 是阻塞状态中的深度睡眠
T(stopped):停止状态–跟阻塞状态的区别就是 T 可能是停下来等写入的,也有可能是单纯停下来的
t(tracing stop): 也是停止状态–跟 T 差不多–比如像 gdb 遇到断点停下时时就会出现 t
X(dead): 死亡状态
Z(zombie): 僵尸状态
关于挂起状态的话,操作系统是不会跟你说这个状态的
像这种状态里面出现 + 的话是表示进程在前台运行,此时用不了命令行解释
在后台运行的进程要杀掉的话,ctrl+c 没用,要用 kill 那种方法
想让他在后台运行的话,要在运行时加 eg: ./text & 或者把进程用 kill 暂停再运行之后,也会成在后台运行的
子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入 Z 状态
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码
僵尸状态的危害:
会导致内存一直被占用(尤其是 PCB 不能被释放)–导致内存泄漏
如果父进程比子进程早结束的话,子进程的父进程就会被改成 PID 为 1 的那个进程(init 进程)(也就是操作系统),此时这个子进程就叫做孤儿进程
如果不这样搞的话,孤儿进程就没人管了,没人将他从 Z 状态搞出去了
(孤儿进程本身不是一直是 Z 状态哈,进 Z 状态的条件跟其他人一样)
cpu 资源分配的先后顺序,就是指进程的优先级
必要性:进程要去抢 CPU,CPU 有限–所以要有优先级
优先级要跟权限区分:
优先级是谁先谁后 权限是谁能谁不能
如果进程长期得不到 CPU 运行的话,该进程就一直止步不前–这是进程的饥饿问题
用 ps -l 查看这个终端的进程
ps -al 查看拥有这个终端的用户底下所有终端的所有进程
UID : 代表执行者的身份
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的 nice 值–是进程优先级的修正数据
nice 取值范围是 [-20,19]
PRI 的取值范围一般是 [60,99]
加入 nice 值后,将会使得 PRI 变为:PRI(new)=PRI(old)+nice–上图显示的就是 PRI(new)
–注意:PRI(old) 永远是 80
top 要超级用户或者 sudo 才行哈 --一般不建议改优先级!
使用方法:
输入 top,然后按 r,就会这样
然后再输入想改的进程的 PID 值,回车
输入想要的 nice 值,回车,然后就 OK 了
每个 task_struct 里面又有双向链表的特性,把 PRI 值相同的进程给串起来
想要快速查找最高优先级进程的话:用到位图
–eg: 优先级为 60 的进程加一个,就在 100 下标那个位图给 +1
–在找时,就要位图等不等于 0,不等于的话就说明有这个优先级的
–这也就是 Linux 的 O(1) 级别的调度算法
–如果嫌弃位图太多,还可以 eg: 一个位图代表 8 个优先级–每四个二进制表示一个优先级这样

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online