进程是什么
1. 进程
进程(Process) 是操作系统中对程序一次运行过程 的抽象描述,是操作系统进行资源分配 和CPU 调度的基本单位。
从本质上讲,进程不是程序本身,也不是单纯的内存空间,而是:
程序在操作系统管理下的一次动态执行实体
其"动态性"体现在:
-
有明确的生命周期(创建、运行、阻塞、结束)
-
有不断变化的执行状态
-
会被操作系统调度、挂起、唤醒和回收
因此,进程是操作系统为了管理并发执行而引入的一种抽象对象。
2. 进程的本质:进程 = 内部数据结构 + 程序代码与数据
结论性表述
从操作系统(尤其是内核)的视角看,可以将进程概括为:
**进程 = 内核中的进程描述数据结构
- 该进程所运行的程序代码与数据**
这一定义同时涵盖了:
-
操作系统如何"认识"进程
-
进程实际"执行"的内容
为什么必须有"内部数据结构"?
操作系统需要对进程进行管理,而管理的前提是:
进程必须是"可描述、可定位、可操作"的对象
为此,操作系统为每个进程维护一组内部数据结构,用于记录和管理该进程的运行环境,例如:
-
进程的唯一标识(PID)
-
当前执行状态(运行、就绪、阻塞等)
-
CPU 上下文信息(用于切换)
-
内存使用情况
-
打开的文件和其他系统资源
-
与其他进程的父子关系
这些信息并不属于程序本身,而是操作系统为了管理进程而额外维护的状态。
👉 没有这些内部数据结构,操作系统就无法调度、暂停、恢复或终止一个进程。
因此,从内核视角看:内部数据结构是"进程存在的前提"
为什么还必须有"程序代码与数据"?
另一方面,如果只有内部数据结构,而没有程序代码与数据:进程将"无事可做",不具备任何计算意义
进程所运行的程序代码与数据包括:可执行程序的指令(代码段),程序使用的全局数据、堆、栈,运行时依赖的共享库
这些内容决定了:进程"要做什么",进程"如何计算"
👉 程序代码与数据赋予进程"行为和功能"。
二者的分工与关系
可以从职责上区分这两部分:
| 组成部分 | 作用 |
|---|---|
| 内部数据结构 | 操作系统管理进程的依据(调度、资源、状态) |
| 程序代码与数据 | 进程实际执行的计算内容 |
二者缺一不可:
-
只有程序代码,没有内部数据结构 → 程序无法运行
-
只有内部数据结构,没有程序代码 → 进程没有意义
因此:
进程并不是"程序"
而是"被操作系统管理并正在执行的程序实例"
推论
由"进程 = 内部数据结构 + 程序代码与数据"可以自然推出:
-
同一程序可以对应多个进程
-
代码可以共享
-
内部数据结构彼此独立
-
-
进程是操作系统的管理对象,而程序不是
-
操作系统"看见"的是进程
-
程序只是进程运行的内容之一
-
-
线程本质上是共享代码与数据、但拥有独立执行状态的执行实体
为后续理解"线程为何被视为轻量级进程"奠定基础
好,这一节正好是**"进程概念 → 内核设计方法论"** 的桥梁。我按你写笔记的方式来,先给结论,再回答「为什么」,最后解释「怎么样」,逻辑是可递进、可展开的。
为什么以及怎么样:先描述,后组织
1 结论性说明
Linux 对进程的管理遵循一个非常核心的设计思想:
先对进程进行完整、统一的描述,
再根据不同的管理需求对这些描述对象进行组织。
也可以概括为:
描述是基础,组织是手段;
描述面向"进程是什么",组织面向"如何管理进程"。
2 为什么要"先描述,后组织"?
操作系统首先需要"认识进程"
操作系统要管理进程,必须先回答一个最基本的问题:
"一个进程在系统中到底由哪些信息构成?"
如果没有统一的描述:无法判断两个进程是否是不同实体,无法保存和恢复进程状态,无法在调度、信号、内存管理等子系统之间共享信息
在操作系统中管理进程之前,内核首先必须解决一个根本问题:进程在内核中到底以什么形式存在。进程并不是一个抽象的概念标签,而是一个需要被保存、被查找、被调度、被切换的实体。为了做到这一点,内核必须为每一个进程建立一个稳定且完整的"描述",用以记录该进程当前所处的状态、所占用的资源以及与系统中其他进程的关系。只有当进程被明确地描述出来,它才可能成为操作系统内部一个可以被操作和管理的对象。
因此,内核必须为每个进程建立一个完整、统一的描述,作为所有管理行为的基础。
👉 描述的本质:定义"进程是什么"
不同管理需求需要不同的"视角"
进程需要被管理的方面是多样的:
-
调度器关心:哪些进程可以运行?
-
信号子系统关心:信号该发给谁?
-
进程控制关心:父子关系如何维护?
-
用户态工具关心:系统中有哪些进程?
这些问题:关注点不同,使用场景不同,数据访问方式也不同
如果把所有管理逻辑都混进一个"统一结构",会导致:结构臃肿,子系统强耦合,扩展和维护困难
👉 因此,"描述"和"如何使用描述"必须分离
先描述,才能被多次、灵活地组织
一旦每个进程都有了稳定的描述对象:
-
同一个进程
→ 可以同时参与多种管理结构
-
不同的子系统
→ 可以用各自最合适的数据结构来管理进程
这种"先描述"的做法,本质上是为了让进程在内核中具有明确而统一的存在形式。无论是调度器、内存管理、信号处理,还是进程控制与回收,所有子系统在面对同一个进程时,都必须基于同一份事实来源。如果没有一个统一的描述,各个子系统就不得不维护各自的进程信息副本,这不仅会造成信息冗余,还会引入一致性和同步上的复杂问题。因此,内核通过集中式的进程描述,将"进程是什么"这一问题先行固定下来。
这使得内核可以做到:
"一个进程,多种组织方式,多种管理视角"
3 "描述"和"组织"各自解决什么问题?
描述解决的问题
描述关注的是单个进程自身的完整性:
-
进程是谁(身份)
-
在做什么(状态)
-
用了什么资源(内存、文件)
-
当前执行到哪里(上下文)
在完成对单个进程的描述之后,内核才进一步考虑如何同时管理大量进程。系统中往往存在成百上千个进程,不同的管理任务对进程的"关注方式"并不相同。有的场景需要根据进程标识快速定位某一个进程,有的场景需要遍历系统中所有进程,有的场景只关心当前可运行的进程集合,还有的场景需要维护进程之间的父子层级关系。这些需求在访问模式、效率要求和结构形式上都存在明显差异,因此不适合用单一的数据结构来统一解决。
👉 描述的目标是:
让一个进程在内核中成为一个"自洽、独立、可保存"的对象
组织解决的问题
组织关注的是进程之间的关系和管理效率:
-
如何快速找到某个进程
-
如何遍历所有进程
-
如何选择下一个运行的进程
-
如何表达进程之间的层级关系
在这一背景下,"后组织"的思想自然产生。内核并不重新定义新的进程对象,而是以已经存在的进程描述为基础,根据不同的管理目的,将这些描述对象组织进不同的数据结构中。这些组织结构并不包含进程的完整信息,而只是保存对进程描述的引用或链接,从而以最低的成本服务于各自的管理需求。同一个进程描述可以同时出现在多种组织结构中,而这些组织方式彼此独立、互不干扰。
👉 组织的目标是:
让大量进程在系统中"可管理、可扩展、高性能"
4 怎么样做到"先描述,后组织"?
每个进程对应一个独立的描述对象
Linux 中的基本做法是:
-
每创建一个进程
-
就分配一个独立的进程描述对象
-
该对象完整记录该进程的所有关键信息
这个描述对象:
-
生命周期与进程一致
-
不依附于某一种管理方式
-
可以被多个子系统同时引用
Linux 在实现"先描述,后组织"这一思想时,核心做法是将进程本身的定义 与进程的管理方式严格分离。内核在创建进程时,首先为其分配并初始化一个专门用于描述进程的内部对象,该对象完整记录进程的身份、状态、执行上下文以及所占用的系统资源。这一描述对象在进程生命周期内保持唯一性和权威性,内核中所有关于该进程的操作都以它为事实基础,而不会再生成其他等价的进程副本。
👉 这是"先描述"的具体体现
通过"引用"而不是"复制"来组织进程
在此基础上,内核采用的组织方式是:
用不同的数据结构保存对进程描述对象的引用
而不是:
-
为调度复制一份
-
为父子关系再建一份
-
为 PID 管理再建一份
这样做的结果是:
-
进程信息只有一份真实来源
-
各种组织结构只关心"如何使用它"
在此基础上,内核并不把管理逻辑直接写死在进程描述之中,而是通过引用和链接的方式,将同一个进程描述对象纳入不同的管理结构。每一种管理需求------例如查找、调度、层级关系维护或资源控制------都有各自合适的数据结构,而这些结构中保存的并不是进程的完整信息,而是指向进程描述对象的关联关系。这样一来,组织结构只负责"如何使用进程",而不负责"进程是什么"。
进程描述在设计时具有高度的稳定性和完整性。它对外暴露的并不是某一种特定用途的视角,而是能够被多个子系统同时理解和使用的通用信息集合。调度器、内存管理子系统和信号子系统虽然关注点不同,但它们都可以基于同一个进程描述读取或更新自己关心的部分,而无需关心该进程在其他管理结构中的存在方式。
👉 这是"后组织"的关键手段
不同组织结构服务于不同目的
同一个进程描述对象,可以同时:
-
被放入"按 ID 快速查找"的结构中
-
被挂入"进程层级关系"的结构中
-
在"调度相关结构"中等待运行
-
参与资源控制、命名空间等管理结构
这些组织方式之间:
-
相互独立
-
互不干扰
-
但操作的都是同一个进程描述
当进程状态发生变化时,内核通过维护这些组织结构与进程描述之间的一致性来完成管理。例如,一个进程从不可运行状态转为可运行状态时,其描述对象本身并不会改变身份,而只是被相应地加入或移出某些管理结构。进程的"存在"始终由描述对象保证,而进程的"可管理性"则由组织结构动态体现。这使得进程的本质信息与管理策略在实现层面自然分离。
最终,Linux 通过这种方式实现了"一个进程描述,多种组织方式"的结构:进程先以描述对象的形式存在于内核中,再根据需要被挂接到不同的管理体系中。描述决定进程的身份与状态,组织决定进程在系统中的管理路径和处理方式。
5 抽象层面的类比
可以这样理解:
描述进程,类似于定义一个"人"的完整档案
组织进程,则是把这些人:
按家庭关系分组
按部门分组
按权限分组
按当前是否在岗排队
档案只有一份,但组织方式可以有很多种
6 小结
Linux 对进程的管理遵循"先描述,后组织"的思想。
先通过统一的描述对象,完整刻画单个进程的状态和资源;
再根据调度、查找、层级关系等不同需求,
将这些描述对象组织进不同的数据结构中。
这种设计使得进程描述稳定、管理方式灵活,
也是 Linux 内核可扩展、高性能的重要原因之一。
认识pcb(Process Control Block,进程控制块)
PCB 里保存了:进程是谁、现在跑到哪了、用什么资源、该怎么调度、和谁有关系。
PCB 里通常包含哪些内容?
PCB(Process Control Block,进程控制块)是操作系统内核用于描述和管理进程的核心数据结构。在 Linux 系统中,PCB 主要由 task_struct 结构体实现。内核并不直接操作"进程"这一抽象概念,而是通过 PCB 来感知进程的存在、状态以及所占用的系统资源,因此可以认为 PCB 是进程在内核中的唯一表示。
1、进程的"身份信息"
PCB 中首先包含进程的基本标识信息,用于唯一确定一个进程及其权限属性。这些信息包括进程 ID、父进程 ID、所属用户和用户组等。通过这些字段,内核能够区分不同进程,并进行权限检查和进程层级管理。
2、进程状态(非常重要)
PCB 还记录了进程的当前状态,用以描述进程在生命周期中所处的阶段。常见的状态包括运行态、就绪态、睡眠态、停止态以及僵尸态。进程状态是进程调度和状态转换的依据,系统工具如 ps 和 top 所显示的进程状态信息,正是来源于 PCB 中的状态字段。
常见状态包括:
-
TASK_RUNNING:正在运行 / 就绪 -
TASK_INTERRUPTIBLE:可中断睡眠 -
TASK_UNINTERRUPTIBLE:不可中断睡眠(如 IO) -
TASK_STOPPED:被暂停 -
TASK_ZOMBIE:僵尸进程
📌 ps, top 看到的状态,就来自 PCB。
3、CPU 相关信息(上下文)
为了支持进程的切换与恢复,PCB 中保存了与 CPU 执行相关的上下文信息。这些内容包括程序计数器、栈指针以及通用寄存器的值。当发生进程切换时,内核会将当前进程的 CPU 上下文保存到其 PCB 中,同时从下一个进程的 PCB 中恢复对应的上下文,从而实现进程在时间上的并发执行。
这部分决定了进程切换是否可行:
-
各种 寄存器的值
-
PC(程序计数器)
-
SP(栈指针)
-
通用寄存器
-
-
内核栈指针
-
用户栈指针
🔁 进程切换 = 保存旧进程 PCB + 恢复新进程 PCB
4、调度信息
在进程调度方面,PCB 中还包含调度策略和优先级等信息。调度器根据 PCB 中的调度参数决定进程何时获得 CPU 以及能够运行多长时间。Linux 的进程调度机制正是围绕 PCB 中的调度相关字段进行工作的。
-
调度策略:
-
SCHED_NORMAL -
SCHED_FIFO -
SCHED_RR
-
-
优先级(priority / nice 值)
-
时间片信息
-
调度队列指针
💡 Linux 调度器(CFS)就是围绕 PCB 工作的。
5、内存管理信息
PCB 还维护了进程的内存管理信息,用于描述进程所拥有的虚拟地址空间。通过指向内存描述结构体的指针,PCB 与进程的代码段、数据段、堆和栈等内存区域建立联系。进程创建时,内核会为其建立相应的内存管理结构,并在进程运行期间通过 PCB 进行统一管理。
-
指向
mm_struct -
虚拟地址空间:
-
代码段
-
数据段
-
堆
-
栈
-
-
页表信息
📌 fork() 后父子进程的 PCB 会复制 + 写时拷贝(COW)。
6、文件和 IO 相关
在文件与输入输出管理方面,PCB 中保存了进程已打开的文件信息以及文件系统相关的上下文。进程在运行过程中打开的文件描述符表、当前工作目录等内容,都可以通过 PCB 间接访问,这使得操作系统能够正确地进行文件访问控制和资源回收。
-
打开的文件描述符表(fd table)
-
当前工作目录(cwd)
-
根目录(root)
-
文件系统上下文
比如你写:
int fd = open("a.txt", O_RDONLY);
👉 fd 就被记录在 PCB 关联的数据结构里。
7、进程关系(组织结构)
此外,PCB 还记录了进程之间的组织关系。每个进程在 PCB 中都保存了指向父进程和子进程的引用,从而在内核中形成一棵进程树。这种结构为进程的创建、终止以及作业控制提供了基础支持。
-
父进程
-
子进程链表
-
兄弟进程
-
进程组 ID
-
会话 ID
📌 shell、后台进程、作业控制都靠这套关系。
8、信号相关
在信号处理方面,PCB 中包含了与信号相关的数据,用于描述进程能够接收哪些信号、当前是否存在未处理的信号以及对应的处理方式。信号机制正是通过修改和检查 PCB 中的相关字段来实现对进程的异步控制。
-
信号掩码(blocked signals)
-
待处理信号集合
-
信号处理函数表
kill -9 背后就是往 PCB 里塞信号。
注:信号就是操作系统给进程发的紧急通知,
PCB 里记录了哪些通知被屏蔽、哪些已经收到但没处理、以及每种通知该怎么应对。
像 kill -9 这种命令,本质上就是内核往进程的 PCB 里塞一个"必须立刻死"的信号,
进程连反抗的机会都没有
9、资源使用统计
最后,PCB 还保存了进程在运行过程中产生的资源使用统计信息,例如占用的 CPU 时间、上下文切换次数以及内存使用情况。这些数据既可用于系统调度决策,也为系统监控和性能分析工具提供了基础。
-
用户态 CPU 时间
-
内核态 CPU 时间
-
上下文切换次数
-
使用的内存量
top / htop 的数据来源之一。
一个简化版 PCB 结构(示意)
struct task_struct {
pid_t pid;
long state;
int priority;
struct mm_struct *mm; // 内存
struct files_struct *files; // 打开的文件
struct signal_struct *signal;// 信号
struct task_struct *parent;
struct list_head children;
struct thread_struct thread; // CPU 上下文
};
⚠️ 实际 Linux 的 task_struct 非常大,但核心思想就是这些。
好,给你一版只保留核心、好记不绕弯的总结 👇
(面试 / 实操都够用)
常用的进程查看命令
ps ------ 看"快照"
看某一时刻的进程状态
ps aux
ps -ef
-
看进程号(PID)
-
看用户
-
看 CPU / 内存
-
看命令行
ps 看"这一刻有哪些进程"
top ------ 实时监控
动态看进程变化
top
-
实时 CPU / 内存
-
排序(
P按 CPU,M按内存) -
可直接
k杀进程
top 是动态版 ps
二、按"目标"查进程(实用)
pgrep ------ 按名字找 PID
pgrep nginx
不知道 PID,用它找
ps + grep ------ 经典组合
ps aux | grep nginx
老派但通用
pstree ------ 看父子关系
pstree -p
看谁 fork 了谁(后台进程 / 守护进程必用)
三、和信号 / 控制相关(你前面那章的延伸)
kill ------ 发信号
kill PID # 默认 SIGTERM
kill -9 PID # SIGKILL(强制)
本质是"往 PCB 里塞信号"
jobs / fg / bg ------ shell 作业控制
jobs
fg %1
bg %1
只管当前 shell 启动的进程