前言

作为开发者,我们每天都在写代码、运行程序。但你是否认真想过:当你敲下回车执行一个程序时,机器内部到底发生了什么?你写的代码是如何在操作系统的调度下,真正在硬件上跑起来的?

要回答这些问题,就绕不开操作系统的核心概念——进程。进程是操作系统资源分配和调度的基本单位,理解了它,你才能真正看懂操作系统的运作机制。但在直接去背“什么是进程”、“PCB 是什么”之前,我们需要先把地基打好。

这篇作为“Linux 进程概念”系列的开篇,内容比较长,跨度也比较大。我们将从最底层的冯·诺依曼体系结构讲起,理解硬件设备的物理限制;接着,探讨操作系统是如何管理这些硬件的;随后,弄清楚系统调用与库函数的关系;最后,正式切入主题,看看进程到底是什么,它背后的 PCB(task_struct) 又在起着什么作用,以及如何查看系统中的进程。

1 冯·诺依曼体系结构:从硬件底层理解数据流动

在真正进入操作系统的核心世界之前,我们必须先搞懂计算机硬件的基础骨架——冯·诺依曼体系结构。

为什么学底层技术要先学硬件?因为软件本质上就是硬件资源的调度者。如果不理解硬件是怎么运作的,不知道 CPU 和外设之间的物理限制,你对操作系统和进程的理解就只能停留在表面,无法深入到内核的底层逻辑中。

1.1 冯·诺依曼体系的核心约束

现代计算机(无论是你的手机、PC 还是服务器)几乎全部遵循冯·诺依曼体系结构。它主要由五大部件组成:

  1. 输入设备(Input):如键盘、鼠标、麦克风、网卡。

  2. 输出设备(Output):如显示器、音响、打印机、网卡。

  3. 存储器(Memory):也就是我们常说的内存(RAM)。

  4. 运算器(ALU):负责算术和逻辑运算。

  5. 控制器(CU):负责指挥协调各个部件。

其中,运算器和控制器通常被封装在一起,被称为中央处理器(CPU)。

你可以结合下面这张图来理解(注意看红色和黑色的箭头方向):
冯诺依曼体系结构
从这张图中,我们可以得出一个极其重要的硬性约束(划重点):

  • 红色的数据信号:你可以清楚地看到,输入设备的数据只能先流进存储器(内存);CPU 运算完的数据,也是先流向存储器,然后再流向输出设备。

  • 黑色的控制信号:控制器会发出控制信号给所有设备,但 CPU 内部(运算器)和存储器之间的数据交互是直接且频繁的。

结论就是:CPU 不能直接与输入/输出设备(外设)打交道,CPU 只能直接和内存打交道。 同样,外设产生的数据也不能直接给 CPU,必须先写入内存。

所有的数据流动,都必须经过内存这个“中转站”。这就好比 CPU 是个大老板,内存是老板的秘书,外设(键盘、网卡)是外面的快递员。快递员不能直接冲进办公室找老板,必须把东西先交给秘书,老板再从秘书那里拿。

1.2 为什么数据非要在内存中转,不能直接给 CPU?

看到这里,很多初学者会有一个疑问:既然网卡、硬盘都能产生数据,为什么不能直接绕过内存,直接把数据塞给 CPU 呢?这样不是少了一道流程,速度更快吗?

答案在于硬件速度的巨大差异。

我们可以列出一组令人绝望的数据对比:

  • CPU 的运算速度:通常在纳秒级别(10-9 秒)级别。

  • 内存的读写速度:通常在几十到上百纳秒级别(勉强能跟上 CPU)。

  • 硬盘 / 网卡的读写速度:通常在毫秒(10-3 秒)甚至微秒级别。

这三者的速度,相差了整整几个数量级! 如果把 CPU 比作一个每小时能跑 300 公里的超级跑车,那内存就是一辆自行车,而硬盘和网卡就是在泥地里爬行的蜗牛。

如果让 CPU 直接去和硬盘或网卡打交道(去等蜗牛爬过来),那么 CPU 99.99% 的时间都会被白白浪费在等待上,整个系统的性能将退化到无法忍受的地步。

内存充当了“速度缓冲剂”:

  1. 外设(如网卡、硬盘)按自己的慢速节奏,慢慢把数据先搬运到内存里。

  2. 在这个过程中,CPU 根本不需要干等,它可以转头去处理其他任务(比如运行别的进程)。

  3. 等外设把数据都在内存里准备好了,CPU 再以极快的速度去内存里取用。

因此,冯·诺依曼体系结构强制规定“数据必须经过内存中转”,不是没有道理的。它本质上是为了匹配 CPU 与外设之间巨大的速度鸿沟,从而最大化地压榨 CPU 的性能。

(注:在现代计算机中,为了进一步提速,CPU 内部还集成了比内存更快的“高速缓存 Cache”,但那是更深一层的话题了。我们这里暂时不讨论。)

1.3 从概念走向实战:QQ 聊天的数据流

很多教材讲完上面“速度鸿沟”的理论就结束了,但如果不把它深入到真实的软件数据流中,你很难有切身的体会。

现在,想象一个最日常的场景:你坐在电脑前,打开 QQ,给远在天边的小伙伴发了一句“我们是冠军!”。

在这个过程中,数据是怎么在冯·诺依曼体系的各个部件中流转的?我们分阶段来看:

阶段一:你发送消息(本机数据流转)

  1. 输入:你敲击键盘,键盘作为输入设备,将物理敲击转化为电信号(产生数据信号)。

  2. 写入内存:这些电信号(数据)通过总线,先被写入了你电脑的内存中(此时显示在 QQ 的输入框里)。

  3. CPU 处理:QQ 软件被 CPU 调度。**CPU(控制器+运算器)**从内存中读取你输入的文字,进行数据封装(加上网络协议头、你的账号信息等)。

  4. 输出到网卡:封装好的数据,再次从内存经由总线,被送到输出设备——网卡。网卡将这些数字信号转化为光/电信号,发往互联网。

阶段二:网络传输与对方接收(远端数据流转)

  1. 数据经过复杂的路由器、光缆,跨越千山万水,来到了你小伙伴电脑的网卡。

  2. 输入到内存:小伙伴的网卡(输入设备)接收到信号,将其还原为数字信号,立刻写入小伙伴电脑的内存。

  3. CPU 处理:小伙伴的 CPU 从内存中读取这些数据包,解封装,发现是 QQ 消息。

  4. 输出到屏幕:CPU 告诉系统,将这段文字交给显卡/显示器(输出设备)渲染。于是,小伙伴的屏幕上弹出了“我们是冠军!”。

阶段三:如果是发送一张图片呢?
如果发送的不是文字,而是一张 10MB 的图片,过程也是一样的:

  1. 图片文件原本静静躺在你的硬盘(外部存储)里。

  2. 你点击发送,图片数据先被读取到内存。

  3. CPU 处理打包后,再从内存传给网卡发出去。

  4. 对方的网卡收到后,先存入内存,CPU 处理后再显示到显示器上(或者对方保存时,再从内存写入硬盘)。

2 深入理解操作系统:什么是“管理”?

在上一部分,我们搞懂了冯·诺依曼体系:所有的设备都只能直接和内存打交道,而 CPU 是极其宝贵的资源。

但现在问题来了:键盘、鼠标、硬盘、网卡都在疯狂地往内存里塞数据,CPU 到底该先处理谁的数据?如果两个程序同时想用网卡发数据,该听谁的?如果有人在写代码,另一个人在听歌,它们是怎么“同时”运行的?

为了解决这些混乱,必须有一个“大管家”来统筹全局,这就是操作系统(Operating System, OS)。

2.1 操作系统的核心定位

从定义上来说,操作系统是一款搞管理的软件。

如果把计算机硬件比作一个庞大的工厂,那么 CPU 就是厂长,内存是仓库,各种外设是流水线工人。而操作系统,就是这个工厂里的管理层。它不直接参与搬砖(不直接处理具体的业务逻辑),它的主要职责是:

  1. 对上:为上层用户和应用程序提供良好的、稳定的运行环境(也就是各种软件)。

  2. 对下:管理好底层的硬件资源(CPU、内存、硬盘、外设等),让它们高效、有序地工作。

💡 一个极其重要的认知:
我们在电脑上写的 C/C++ 代码、Java 代码,本质上只是文件。即使是编译出来的可执行文件,如果不经过操作系统的调度,扔在硬盘里也只是一堆二进制数据。
所有的软件,都跑在操作系统之上。 软件想要访问硬件(比如读写文件、发送网络请求),都必须经过操作系统这一关。

2.2 什么是“管理”?

理解操作系统的关键,在于理解“管理”这两个字。

在计算机科学中,“管理”的本质,可以用六个字来概括:先描述,再组织。

  1. 先描述(定义属性)
    假设你要管理一个班级的学生,你得先知道每个学生的信息,比如姓名、学号、性别、成绩等。在计算机里,我们通过**结构体(Struct)或类(Class)**来“描述”一个对象。

  2. 再组织(数据结构)
    光有描述是不够的,几百个学生如果散落各处,你根本没法管。你需要把他们的结构体通过链表、数组或树等数据结构串起来,形成一个高效的整体。

💡 举个通俗的例子:
如果操作系统是一个校长,他要管理全校 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 呢?

这主要是因为系统调用太底层、太繁琐,且跨平台兼容性极差。

  1. 使用门槛高:系统调用的参数非常底层,稍有不慎就会导致程序崩溃。比如你要用 write 打印一句话,你需要自己计算字符串长度,还要指定文件描述符。

  2. 性能开销大:每次系统调用,CPU 都要从用户态切换到内核态,处理完再切回来。这个“切换”的过程(上下文切换)是需要消耗时间的。

  3. 不具备可移植性:Linux 的系统调用和 Windows 的系统调用完全不同。如果你直接写了 Linux 的系统调用,代码放到 Windows 上就跑不起来了。

为了解决这些问题,编程语言的开发者们就对系统调用进行了“封装”,这就产生了库函数(Library Functions)。

  • 比如 C 标准库(libc)中的 printf 函数,底层其实就是调用了 Linux 的 write 系统调用。但它帮你处理了格式化字符串、计算长度等繁琐工作。

  • 比如 fopen 函数,底层封装了 open 系统调用。而且 fopen 还维护了一个缓冲区,攒够了一定量的数据再一次性调用 write,从而大大减少了系统调用的次数,提升了性能。

它们之间的关系可以总结为:
库函数(如 printf) 是系统调用(如 write) 的“外衣”和“升级版”。 库函数运行在用户态,系统调用运行在内核态。库函数最终一定要通过系统调用,才能让操作系统内核帮你干活。

(注:并不是所有的库函数都会调用系统调用。比如 C 语言中的 strlen 函数只是计算字符串长度,完全在用户态算完就行,不需要操作系统插手。)

3.3 一张图看懂计算机的软件层级

结合下面这张图,我们现在可以把整个计算机的软硬件层级,从顶层到底层彻底梳理一遍(注意图右侧的分割线,它将系统分为了用户部分和系统软件部分):
在这里插入图片描述

  1. 用户部分(位于分割线之上,运行在用户态)

    • 用户(顶层):也就是使用电脑的人,进行指令操作、开发操作或管理操作。

    • 用户操作接口:包括 shell(命令行外壳)、lib(C 标准库等)以及部分指令。这就是我们前面提到的“库函数层”(如 printf、fopen 等)。

  2. 系统软件部分(位于分割线之下,运行在内核态)

    • 系统调用接口(system call):这是用户态与内核态的绝对边界。不管你在 shell 里敲命令,还是在代码里调 lib 库,最终都必须通过这层接口,向操作系统发起请求。

    • 操作系统内核:接收到系统调用后,操作系统开始干活。它包含了内存管理、进程管理、文件管理和驱动管理。

    • 驱动程序:操作系统内核不会直接去操作硬件,而是通过驱动程序(如网卡驱动、硬盘驱动)来控制硬件。

  3. 底层硬件(最底层):如网卡、硬盘等实际的物理设备。

整个过程是如何流转的?
假设你在终端里敲下了一行命令(或者运行了你写的 C 程序):

  1. 你的请求先到达用户操作接口(比如 shell 或 lib 库)。

  2. 接口将你的请求转化为系统调用(比如 write)。

  3. 系统调用进入操作系统内核,内核的相应模块(如文件管理)接手处理。

  4. 内核通过驱动程序,最终将数据发送给底层硬件(比如网卡或硬盘)。

4 进程的基本概念与 PCB

通过前面的学习,我们知道了操作系统是硬件和软件之间的“大管家”。在这个大管家的众多工作(内存管理、文件管理、驱动管理等)中,有一项最核心的任务,那就是进程管理。

那么,到底什么是进程?操作系统又是怎么管理它的呢?

4.1 从“程序”到“进程”的蜕变

我们在前面提过一个通俗的比喻:

  • 程序(Program):是一堆静态的、保存在硬盘上的可执行文件(比如你编译出来的 a.out)。它是死的,占用的只是硬盘空间。

  • 进程(Process):是程序在内存中动态运行的过程。它是活的,需要占用 CPU、内存等系统资源。

程序是如何变成进程的?
当你双击一个图标,或者在终端输入 ./a.out 按下回车时,操作系统会做一系列复杂的动作:

  1. 将程序的代码和数据从硬盘读取到内存中(因为 CPU 只能直接访问内存)。

  2. 为这个程序分配必要的系统资源(如 CPU 时间片、内存空间等)。

  3. 在操作系统的内部,建立一个数据结构来记录这个程序的运行状态。

完成这一步后,原本静静躺在硬盘里的死文件,就摇身一变,成了一个活生生的进程。

4.2 操作系统是如何管理进程的?

回忆一下我们在 2.2 节讲过的操作系统的管理哲学:先描述,再组织。

操作系统管理成千上万个进程,用的也是这个思路。

  1. 先描述:操作系统会为每一个进程创建一个极其复杂的结构体,用来记录这个进程的所有信息(如进程 ID、状态、优先级、占用的内存地址等)。这个结构体在 Linux 内核中被称为 task_struct,在操作系统理论中,我们统称它为 PCB(Process Control Block,进程控制块)。

  2. 再组织:操作系统会利用各种数据结构(如双向链表、红黑树等),将内存中成百上千个 PCB 组织起来。这样,操作系统只要拿到链表的头指针,就能对所有进程了如指掌,实现增、删、查、改。

一句话总结:在操作系统眼里,一个进程就是一个 PCB 结构体(task_struct)。 所有的进程管理操作,本质上都是在对这个结构体进行修改。

4.3 拆解 Linux 的 PCB:task_struct 里有什么?

task_struct 是 Linux 内核中最庞大、最复杂的结构体之一(包含几百个成员),它就像是一个超级档案袋,记录了进程的方方面面。这里我们挑几个最核心的模块来看看:

  1. 标识符(PID / PPID):进程的唯一身份证(PID)以及它的父进程 ID(PPID)。

  2. 状态(State):记录进程当前是正在运行、睡眠、还是停止(下一篇文章我们会详细讲)。

  3. 优先级(Priority):决定进程谁先获得 CPU 的使用权。

  4. 程序计数器(Program Counter, PC):记录程序当前执行到了哪一行代码。这是上下文切换的关键!

  5. 内存指针:指向该进程的代码段、数据段、堆栈等内存区域。

  6. 上下文数据(Context Data):保存 CPU 寄存器里的各种值。当进程被切走时,必须把寄存器里的数据存到 PCB 里;下次切回来时,再从 PCB 恢复,这样程序才能接着往下跑。

  7. I/O 状态信息:记录进程打开了哪些文件,使用了哪些外设。

  8. 记账信息:记录进程占用了多少 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 小结

这一篇我们从底层硬件开始,一步步推导到了进程的核心结构,整个脉络可以浓缩为以下几点:

  1. 冯·诺依曼体系:CPU 只能直接和内存打交道,外设与 CPU 的巨大速度差异,决定了数据必须经过内存中转。

  2. 操作系统的管理哲学:管理的本质是“先描述,再组织”。操作系统不直接操作硬件,而是通过维护内存中的数据结构来管理它们。

  3. 跨越用户态与内核态:应用程序不能直接访问硬件,必须通过系统调用陷入内核态。而我们常用的 printf 等库函数,本质上是对底层系统调用的封装。

  4. 进程的本质:进程是程序动态运行的过程。在 Linux 中,操作系统通过 task_struct(PCB) 这个庞大的结构体来描述进程的所有信息(如 PID、状态、优先级等)。

  5. 代码实战:通过 getpid() 和 getppid() 我们验证了每次运行程序,操作系统都会为其分配新的 PID 和对应的 PCB。

理解了 PCB 的概念,我们就掌握了操作系统的“透视眼”。下一篇,我们将去挖掘 task_struct 中极其重要的一环——进程状态,去看看进程在操作系统中都有哪些“前世今生”。我们下篇见!

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐