Linux 进程概念(一):从冯·诺依曼体系到 PCB
前言
作为开发者,我们每天都在写代码、运行程序。但你是否认真想过:当你敲下回车执行一个程序时,机器内部到底发生了什么?你写的代码是如何在操作系统的调度下,真正在硬件上跑起来的?
要回答这些问题,就绕不开操作系统的核心概念——进程。进程是操作系统资源分配和调度的基本单位,理解了它,你才能真正看懂操作系统的运作机制。但在直接去背“什么是进程”、“PCB 是什么”之前,我们需要先把地基打好。
这篇作为“Linux 进程概念”系列的开篇,内容比较长,跨度也比较大。我们将从最底层的冯·诺依曼体系结构讲起,理解硬件设备的物理限制;接着,探讨操作系统是如何管理这些硬件的;随后,弄清楚系统调用与库函数的关系;最后,正式切入主题,看看进程到底是什么,它背后的 PCB(task_struct) 又在起着什么作用,以及如何查看系统中的进程。
1 冯·诺依曼体系结构:从硬件底层理解数据流动
在真正进入操作系统的核心世界之前,我们必须先搞懂计算机硬件的基础骨架——冯·诺依曼体系结构。
为什么学底层技术要先学硬件?因为软件本质上就是硬件资源的调度者。如果不理解硬件是怎么运作的,不知道 CPU 和外设之间的物理限制,你对操作系统和进程的理解就只能停留在表面,无法深入到内核的底层逻辑中。
1.1 冯·诺依曼体系的核心约束
现代计算机(无论是你的手机、PC 还是服务器)几乎全部遵循冯·诺依曼体系结构。它主要由五大部件组成:
-
输入设备(Input):如键盘、鼠标、麦克风、网卡。
-
输出设备(Output):如显示器、音响、打印机、网卡。
-
存储器(Memory):也就是我们常说的内存(RAM)。
-
运算器(ALU):负责算术和逻辑运算。
-
控制器(CU):负责指挥协调各个部件。
其中,运算器和控制器通常被封装在一起,被称为中央处理器(CPU)。
你可以结合下面这张图来理解(注意看红色和黑色的箭头方向):

从这张图中,我们可以得出一个极其重要的硬性约束(划重点):
-
红色的数据信号:你可以清楚地看到,输入设备的数据只能先流进存储器(内存);CPU 运算完的数据,也是先流向存储器,然后再流向输出设备。
-
黑色的控制信号:控制器会发出控制信号给所有设备,但 CPU 内部(运算器)和存储器之间的数据交互是直接且频繁的。
结论就是:CPU 不能直接与输入/输出设备(外设)打交道,CPU 只能直接和内存打交道。 同样,外设产生的数据也不能直接给 CPU,必须先写入内存。
所有的数据流动,都必须经过内存这个“中转站”。这就好比 CPU 是个大老板,内存是老板的秘书,外设(键盘、网卡)是外面的快递员。快递员不能直接冲进办公室找老板,必须把东西先交给秘书,老板再从秘书那里拿。
1.2 为什么数据非要在内存中转,不能直接给 CPU?
看到这里,很多初学者会有一个疑问:既然网卡、硬盘都能产生数据,为什么不能直接绕过内存,直接把数据塞给 CPU 呢?这样不是少了一道流程,速度更快吗?
答案在于硬件速度的巨大差异。
我们可以列出一组令人绝望的数据对比:
-
CPU 的运算速度:通常在纳秒级别(10-9 秒)级别。
-
内存的读写速度:通常在几十到上百纳秒级别(勉强能跟上 CPU)。
-
硬盘 / 网卡的读写速度:通常在毫秒(10-3 秒)甚至微秒级别。
这三者的速度,相差了整整几个数量级! 如果把 CPU 比作一个每小时能跑 300 公里的超级跑车,那内存就是一辆自行车,而硬盘和网卡就是在泥地里爬行的蜗牛。
如果让 CPU 直接去和硬盘或网卡打交道(去等蜗牛爬过来),那么 CPU 99.99% 的时间都会被白白浪费在等待上,整个系统的性能将退化到无法忍受的地步。
内存充当了“速度缓冲剂”:
-
外设(如网卡、硬盘)按自己的慢速节奏,慢慢把数据先搬运到内存里。
-
在这个过程中,CPU 根本不需要干等,它可以转头去处理其他任务(比如运行别的进程)。
-
等外设把数据都在内存里准备好了,CPU 再以极快的速度去内存里取用。
因此,冯·诺依曼体系结构强制规定“数据必须经过内存中转”,不是没有道理的。它本质上是为了匹配 CPU 与外设之间巨大的速度鸿沟,从而最大化地压榨 CPU 的性能。
(注:在现代计算机中,为了进一步提速,CPU 内部还集成了比内存更快的“高速缓存 Cache”,但那是更深一层的话题了。我们这里暂时不讨论。)
1.3 从概念走向实战:QQ 聊天的数据流
很多教材讲完上面“速度鸿沟”的理论就结束了,但如果不把它深入到真实的软件数据流中,你很难有切身的体会。
现在,想象一个最日常的场景:你坐在电脑前,打开 QQ,给远在天边的小伙伴发了一句“我们是冠军!”。
在这个过程中,数据是怎么在冯·诺依曼体系的各个部件中流转的?我们分阶段来看:
阶段一:你发送消息(本机数据流转)
-
输入:你敲击键盘,键盘作为输入设备,将物理敲击转化为电信号(产生数据信号)。
-
写入内存:这些电信号(数据)通过总线,先被写入了你电脑的内存中(此时显示在 QQ 的输入框里)。
-
CPU 处理:QQ 软件被 CPU 调度。**CPU(控制器+运算器)**从内存中读取你输入的文字,进行数据封装(加上网络协议头、你的账号信息等)。
-
输出到网卡:封装好的数据,再次从内存经由总线,被送到输出设备——网卡。网卡将这些数字信号转化为光/电信号,发往互联网。
阶段二:网络传输与对方接收(远端数据流转)
-
数据经过复杂的路由器、光缆,跨越千山万水,来到了你小伙伴电脑的网卡。
-
输入到内存:小伙伴的网卡(输入设备)接收到信号,将其还原为数字信号,立刻写入小伙伴电脑的内存。
-
CPU 处理:小伙伴的 CPU 从内存中读取这些数据包,解封装,发现是 QQ 消息。
-
输出到屏幕:CPU 告诉系统,将这段文字交给显卡/显示器(输出设备)渲染。于是,小伙伴的屏幕上弹出了“我们是冠军!”。
阶段三:如果是发送一张图片呢?
如果发送的不是文字,而是一张 10MB 的图片,过程也是一样的:
-
图片文件原本静静躺在你的硬盘(外部存储)里。
-
你点击发送,图片数据先被读取到内存。
-
CPU 处理打包后,再从内存传给网卡发出去。
-
对方的网卡收到后,先存入内存,CPU 处理后再显示到显示器上(或者对方保存时,再从内存写入硬盘)。
2 深入理解操作系统:什么是“管理”?
在上一部分,我们搞懂了冯·诺依曼体系:所有的设备都只能直接和内存打交道,而 CPU 是极其宝贵的资源。
但现在问题来了:键盘、鼠标、硬盘、网卡都在疯狂地往内存里塞数据,CPU 到底该先处理谁的数据?如果两个程序同时想用网卡发数据,该听谁的?如果有人在写代码,另一个人在听歌,它们是怎么“同时”运行的?
为了解决这些混乱,必须有一个“大管家”来统筹全局,这就是操作系统(Operating System, OS)。
2.1 操作系统的核心定位
从定义上来说,操作系统是一款搞管理的软件。
如果把计算机硬件比作一个庞大的工厂,那么 CPU 就是厂长,内存是仓库,各种外设是流水线工人。而操作系统,就是这个工厂里的管理层。它不直接参与搬砖(不直接处理具体的业务逻辑),它的主要职责是:
-
对上:为上层用户和应用程序提供良好的、稳定的运行环境(也就是各种软件)。
-
对下:管理好底层的硬件资源(CPU、内存、硬盘、外设等),让它们高效、有序地工作。
💡 一个极其重要的认知:
我们在电脑上写的 C/C++ 代码、Java 代码,本质上只是文件。即使是编译出来的可执行文件,如果不经过操作系统的调度,扔在硬盘里也只是一堆二进制数据。
所有的软件,都跑在操作系统之上。 软件想要访问硬件(比如读写文件、发送网络请求),都必须经过操作系统这一关。
2.2 什么是“管理”?
理解操作系统的关键,在于理解“管理”这两个字。
在计算机科学中,“管理”的本质,可以用六个字来概括:先描述,再组织。
-
先描述(定义属性)
假设你要管理一个班级的学生,你得先知道每个学生的信息,比如姓名、学号、性别、成绩等。在计算机里,我们通过**结构体(Struct)或类(Class)**来“描述”一个对象。 -
再组织(数据结构)
光有描述是不够的,几百个学生如果散落各处,你根本没法管。你需要把他们的结构体通过链表、数组或树等数据结构串起来,形成一个高效的整体。
💡 举个通俗的例子:
如果操作系统是一个校长,他要管理全校 1 万名学生。
-
校长不可能每天亲自去盯着每一个学生(就像 CPU 不可能直接一直盯着外设)。
-
校长是怎么管理的?他会在办公室里,让教务处建立一个学生信息档案库(这就是数据结构)。每个学生有一份档案(这就是结构体),上面记录了学生的各项信息。
-
当校长需要“管理”某个学生时,他只需要在档案库里修改这个学生的状态(比如记大过一次),然后发指令让学生去执行即可。
把硬件代入这个例子:
操作系统要管理网卡、管理硬盘、管理内存,它绝不会直接去修改硬件的物理电路。相反,它会为每一种硬件,在内存中创建一个结构体(描述硬件的属性,比如状态、速度、容量)。然后,用链表把这些结构体组织起来。
当操作系统需要对某个硬件做操作时,它实际上只是在内存中修改那个对应的结构体! 修改完之后,通过总线下发控制信号给硬件即可。
“先描述,再组织”,这就是操作系统管理一切软硬件资源的核心哲学。理解了这一点,后面我们学习“进程”时,你就能瞬间明白所谓的 PCB(进程控制块) 到底是什么了——它不过是操作系统用来“描述”一个进程的超级结构体罢了!
2.3 操作系统的体系结构:用户态与内核态
既然操作系统是硬件和软件之间的“守门人”,那是不是意味着我们的程序可以随意调用操作系统的代码,随意去修改内存里的那些结构体呢?
绝对不行。 如果谁都能去改,那恶意软件岂不是可以把整个系统搞崩溃?
为了保护操作系统的安全,现代的 CPU 和操作系统引入了权限分级的机制,也就是我们常听说的用户态(User Mode)和内核态(Kernel Mode)。
-
内核态(特权级别高):操作系统的核心代码运行在这个状态下。它可以执行所有的 CPU 指令,访问所有的内存和硬件资源。
-
用户态(特权级别低):我们日常写的应用程序(如 QQ、浏览器、你写的 C 语言程序)运行在这个状态下。它们不能直接访问硬件,也不能随意访问操作系统的核心内存。
可是,用户程序总要读写文件、上网啊,怎么办?
这就涉及到了跨越边界的通道——系统调用(System Call)。
当你的程序想要读取一个文件时,它不能直接对硬盘说“给我数据”,而是必须向操作系统发起“请求”(系统调用)。CPU 接收到请求后,会将特权级别从用户态切换到内核态,由操作系统的内核代码来完成具体的读写操作,完成后再切回用户态,把结果返回给你的程序。
(注:这就像你去银行办业务。你不能直接冲进金库(内核态)自己拿钱,你只能隔着玻璃柜台(用户态),向柜员发起一个请求(系统调用),柜员帮你把钱取出来。)
3 跨越边界:系统调用与库函数
在 2.3 节中我们了解到,为了保护操作系统的安全,应用程序(比如你写的 C 语言代码)是运行在用户态的,而操作系统内核运行在内核态。用户态的程序不能直接访问硬件,也不能直接去改内存中那些描述硬件的结构体。
那问题来了:如果我想在屏幕上打印一行字,或者读取一个文件,该怎么办?
这就必须请出操作系统提供给你的唯一合法通道——系统调用(System Call)。
3.1 什么是系统调用?
系统调用,简单来说,就是操作系统内核提供给应用程序的一组“接口(API)”。
当你的程序需要操作硬件(比如读写文件、分配内存、创建新进程)时,它必须通过系统调用向操作系统“请示”。操作系统接收到请求后,会切换到内核态,替你完成底层的脏活累活,然后再把结果返回给你的程序,并切换回用户态。
生活类比:
-
你就是用户进程(用户态)。
-
银行的金库就是硬件资源(受内核态保护)。
-
你绝对不能直接翻窗户进金库拿钱(不允许直接访问硬件)。
-
你必须填一张单子递给柜员,柜员进金库帮你拿(这就是系统调用)。
在 Linux 系统下,常用的系统调用有 open(打开文件)、read(读取文件)、write(写入文件)、fork(创建进程)等。
3.2 为什么有了系统调用,还要有库函数?
既然直接调系统调用就能完成工作,为什么我们在 C 语言里写代码时,用的都是 printf、fopen、malloc,而不是直接调 write、open、brk 呢?
这主要是因为系统调用太底层、太繁琐,且跨平台兼容性极差。
-
使用门槛高:系统调用的参数非常底层,稍有不慎就会导致程序崩溃。比如你要用
write打印一句话,你需要自己计算字符串长度,还要指定文件描述符。 -
性能开销大:每次系统调用,CPU 都要从用户态切换到内核态,处理完再切回来。这个“切换”的过程(上下文切换)是需要消耗时间的。
-
不具备可移植性:Linux 的系统调用和 Windows 的系统调用完全不同。如果你直接写了 Linux 的系统调用,代码放到 Windows 上就跑不起来了。
为了解决这些问题,编程语言的开发者们就对系统调用进行了“封装”,这就产生了库函数(Library Functions)。
-
比如 C 标准库(
libc)中的printf函数,底层其实就是调用了 Linux 的write系统调用。但它帮你处理了格式化字符串、计算长度等繁琐工作。 -
比如
fopen函数,底层封装了open系统调用。而且fopen还维护了一个缓冲区,攒够了一定量的数据再一次性调用write,从而大大减少了系统调用的次数,提升了性能。
它们之间的关系可以总结为:
库函数(如 printf) 是系统调用(如 write) 的“外衣”和“升级版”。 库函数运行在用户态,系统调用运行在内核态。库函数最终一定要通过系统调用,才能让操作系统内核帮你干活。
(注:并不是所有的库函数都会调用系统调用。比如 C 语言中的
strlen函数只是计算字符串长度,完全在用户态算完就行,不需要操作系统插手。)
3.3 一张图看懂计算机的软件层级
结合下面这张图,我们现在可以把整个计算机的软硬件层级,从顶层到底层彻底梳理一遍(注意图右侧的分割线,它将系统分为了用户部分和系统软件部分):

-
用户部分(位于分割线之上,运行在用户态)
-
用户(顶层):也就是使用电脑的人,进行指令操作、开发操作或管理操作。
-
用户操作接口:包括
shell(命令行外壳)、lib(C 标准库等)以及部分指令。这就是我们前面提到的“库函数层”(如printf、fopen等)。
-
-
系统软件部分(位于分割线之下,运行在内核态)
-
系统调用接口(system call):这是用户态与内核态的绝对边界。不管你在
shell里敲命令,还是在代码里调lib库,最终都必须通过这层接口,向操作系统发起请求。 -
操作系统内核:接收到系统调用后,操作系统开始干活。它包含了内存管理、进程管理、文件管理和驱动管理。
-
驱动程序:操作系统内核不会直接去操作硬件,而是通过驱动程序(如网卡驱动、硬盘驱动)来控制硬件。
-
-
底层硬件(最底层):如网卡、硬盘等实际的物理设备。
整个过程是如何流转的?
假设你在终端里敲下了一行命令(或者运行了你写的 C 程序):
-
你的请求先到达用户操作接口(比如
shell或lib库)。 -
接口将你的请求转化为系统调用(比如
write)。 -
系统调用进入操作系统内核,内核的相应模块(如文件管理)接手处理。
-
内核通过驱动程序,最终将数据发送给底层硬件(比如网卡或硬盘)。
4 进程的基本概念与 PCB
通过前面的学习,我们知道了操作系统是硬件和软件之间的“大管家”。在这个大管家的众多工作(内存管理、文件管理、驱动管理等)中,有一项最核心的任务,那就是进程管理。
那么,到底什么是进程?操作系统又是怎么管理它的呢?
4.1 从“程序”到“进程”的蜕变
我们在前面提过一个通俗的比喻:
-
程序(Program):是一堆静态的、保存在硬盘上的可执行文件(比如你编译出来的
a.out)。它是死的,占用的只是硬盘空间。 -
进程(Process):是程序在内存中动态运行的过程。它是活的,需要占用 CPU、内存等系统资源。
程序是如何变成进程的?
当你双击一个图标,或者在终端输入 ./a.out 按下回车时,操作系统会做一系列复杂的动作:
-
将程序的代码和数据从硬盘读取到内存中(因为 CPU 只能直接访问内存)。
-
为这个程序分配必要的系统资源(如 CPU 时间片、内存空间等)。
-
在操作系统的内部,建立一个数据结构来记录这个程序的运行状态。
完成这一步后,原本静静躺在硬盘里的死文件,就摇身一变,成了一个活生生的进程。
4.2 操作系统是如何管理进程的?
回忆一下我们在 2.2 节讲过的操作系统的管理哲学:先描述,再组织。
操作系统管理成千上万个进程,用的也是这个思路。
-
先描述:操作系统会为每一个进程创建一个极其复杂的结构体,用来记录这个进程的所有信息(如进程 ID、状态、优先级、占用的内存地址等)。这个结构体在 Linux 内核中被称为
task_struct,在操作系统理论中,我们统称它为 PCB(Process Control Block,进程控制块)。 -
再组织:操作系统会利用各种数据结构(如双向链表、红黑树等),将内存中成百上千个 PCB 组织起来。这样,操作系统只要拿到链表的头指针,就能对所有进程了如指掌,实现增、删、查、改。
一句话总结:在操作系统眼里,一个进程就是一个 PCB 结构体(task_struct)。 所有的进程管理操作,本质上都是在对这个结构体进行修改。
4.3 拆解 Linux 的 PCB:task_struct 里有什么?
task_struct 是 Linux 内核中最庞大、最复杂的结构体之一(包含几百个成员),它就像是一个超级档案袋,记录了进程的方方面面。这里我们挑几个最核心的模块来看看:
-
标识符(PID / PPID):进程的唯一身份证(PID)以及它的父进程 ID(PPID)。
-
状态(State):记录进程当前是正在运行、睡眠、还是停止(下一篇文章我们会详细讲)。
-
优先级(Priority):决定进程谁先获得 CPU 的使用权。
-
程序计数器(Program Counter, PC):记录程序当前执行到了哪一行代码。这是上下文切换的关键!
-
内存指针:指向该进程的代码段、数据段、堆栈等内存区域。
-
上下文数据(Context Data):保存 CPU 寄存器里的各种值。当进程被切走时,必须把寄存器里的数据存到 PCB 里;下次切回来时,再从 PCB 恢复,这样程序才能接着往下跑。
-
I/O 状态信息:记录进程打开了哪些文件,使用了哪些外设。
-
记账信息:记录进程占用了多少 CPU 时间、执行了多少条指令等。
4.4 查看系统中的进程
理论讲完了,我们来看看现实中如何查看这些 PCB 转化出来的进程。
在 Linux 终端中,使用 ps 命令就可以查看当前进程的快照:
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 169676 13288 ? Ss Oct31 0:03 /usr/lib/systemd/systemd ...
xc 1234 0.0 0.1 21556 5120 pts/0 Ss 10:00 0:00 -bash
xc 1245 0.0 0.1 12345 1024 pts/0 R+ 10:05 0:00 ps aux
-
USER:启动进程的用户。 -
PID:进程的唯一 ID。 -
%CPU / %MEM:占用的 CPU 和内存百分比。 -
STAT:进程当前的状态。 -
COMMAND:启动该进程的命令。
如果你想知道某个具体的进程,还可以用 ps -ef | grep <进程名> 去精确查找它。
4.5 代码实战:获取进程的 PID 与 PPID
除了在终端里用 ps 命令查看,我们在写代码时,也经常需要获取当前进程的 ID。Linux 系统提供了两个非常常用的系统调用函数:
-
getpid():获取当前进程的 PID。 -
getppid():获取当前进程的父进程的 PID(Parent PID)。
我们来写一段简单的 C 语言代码验证一下:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
// 获取当前进程的 PID 和 父进程的 PPID
pid_t my_pid = getpid();
pid_t my_ppid = getppid();
printf("当前进程的 PID 是: %d\n", my_pid);
printf("当前进程的父进程 PPID 是: %d\n", my_ppid);
return 0;
}
编译并运行这段代码(假设编译出来的可执行文件叫 test_pid):
$ gcc test_pid.c -o test_pid
$ ./test_pid
当前进程的 PID 是: 4939
当前进程的父进程 PPID 是: 25613
结果分析:
-
输出的
4939就是运行这个程序的进程 PID。 -
输出的
25613是它的父进程 PID。这通常是启动这个程序的终端环境(Shell)。 -
为什么 PPID 看起来这么大? 很多新手以为父进程一定是
1(比如 systemd)。其实在 Linux 中,每次打开终端、或者通过 SSH 登录(如 Xshell),系统都会为你分配一个新的 shell 进程。这个进程的 ID(25613)通常是系统当前运行到此刻顺延分配的。如果你在终端里敲下ps命令,你会发现你的终端进程的 PID 刚好也是 25613。
如果我再运行一次
./test_pid,PID 会一样吗?
你可以自己试一下,绝大多数情况下,两次运行输出的 PID 是不一样的。
为什么呢?因为每次执行程序,操作系统都会在内存中为它创建一个全新的task_struct(PCB),并分配一个新的、未被占用的 PID。这就再次印证了:同一个程序,每次运行产生的都是不同的进程。
4.6 小结
这一篇我们从底层硬件开始,一步步推导到了进程的核心结构,整个脉络可以浓缩为以下几点:
-
冯·诺依曼体系:CPU 只能直接和内存打交道,外设与 CPU 的巨大速度差异,决定了数据必须经过内存中转。
-
操作系统的管理哲学:管理的本质是“先描述,再组织”。操作系统不直接操作硬件,而是通过维护内存中的数据结构来管理它们。
-
跨越用户态与内核态:应用程序不能直接访问硬件,必须通过系统调用陷入内核态。而我们常用的
printf等库函数,本质上是对底层系统调用的封装。 -
进程的本质:进程是程序动态运行的过程。在 Linux 中,操作系统通过
task_struct(PCB) 这个庞大的结构体来描述进程的所有信息(如 PID、状态、优先级等)。 -
代码实战:通过
getpid()和getppid()我们验证了每次运行程序,操作系统都会为其分配新的 PID 和对应的 PCB。
理解了 PCB 的概念,我们就掌握了操作系统的“透视眼”。下一篇,我们将去挖掘 task_struct 中极其重要的一环——进程状态,去看看进程在操作系统中都有哪些“前世今生”。我们下篇见!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)