news 2026/10/7 10:27:40

Linux底层原理:从冯诺依曼体系结构到进程调度

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux底层原理:从冯诺依曼体系结构到进程调度

这段时间我把 Linux 底层的基础知识重新梳理了一遍,重点放在冯诺依曼体系结构、操作系统和进程这三条线上。刚开始学的时候总觉得它们是三个独立的知识点,后来才发现其实是同一条链路:硬件架构决定了操作系统怎么管理资源,操作系统通过进程这个抽象去调度 CPU,最终让用户写的程序真正跑起来。这篇笔记就是把这条链路完整地捋一遍,顺便附上我实际在 Linux 里观察进程、排查状态的一点经验,适合刚接触 Linux 或者想回头看底层原理的朋友参考。

1. 冯诺依曼体系结构:Linux 进程的硬件地基

1.1 存储程序原理:指令和数据为什么要先进内存

冯诺依曼体系结构最核心的一句话是“存储程序”。意思是,程序本身和程序要处理的数据,都要以二进制形式存放在同一个存储器里,CPU 按地址从中取出指令并执行。这个设计在今天看起来理所当然,但在当时是革命性的:在此之前,程序是靠外部设备连接的物理线路来“固定”的,改功能等于改电路,而存储程序让“换程序”变成了“换数据”。

放到 Linux 的场景下理解,你会发现整个进程模型都建立在这个基础上。你在终端敲下./a.out之后,内核要做的事情就是把磁盘上的可执行文件读入内存,然后让 CPU 指向这段内存的第一条指令开始执行。没有“指令和数据都在内存里”这个前提,进程这个词压根就不存在。所以学习进程之前,一定得先把存储程序原理想明白:内存不是 CPU 的缓存,而是指令和数据共同的“工作台”。

1.2 五大部件与数据通路:CPU 为什么不直接读写硬盘

这个体系结构把计算机分成输入设备、输出设备、存储器、运算器、控制器五大部件。输入设备把外界信息变成二进制,输出设备把结果变成人能感知的形式,存储器保存指令和数据,运算器负责加减逻辑运算,控制器负责取指令、解释指令、指挥其他部件协同工作。

关键点在于数据通路:CPU 不直接和外设打交道,所有数据都要先经过存储器。我最早不理解这个设计,总觉得“CPU 直接读硬盘不就更快吗”?实际上,外设速度差异太大,内存的访问速度比机械硬盘快好几个数量级,CPU 又比内存快几个数量级。如果让 CPU 直接等外设,那大部分时间 CPU 都闲着。所以中间必须有一块高速缓冲区域,就是内存。这个差异也催生了后续的 DMA 技术、缓冲区机制,以及在 Linux 里随处可见的 cache 和 buff。

部件作用Linux 中的对应物
输入设备向计算机输入数据和程序键盘、网卡、磁盘读取等由驱动管理的设备
输出设备输出结果屏幕、网卡、磁盘写入
存储器存储指令和数据物理内存、swap、page cache
运算器执行算术与逻辑运算CPU 的核心计算单元
控制器取指、译码、控制流程指令流水线与内核调度逻辑

1.3 冯诺依曼瓶颈:理解 Linux 性能问题的一把钥匙

“CPU 和内存之间的通路只有一条,指令和数据共用同一条总线”这个设计在性能上有一个著名代价,叫冯诺依曼瓶颈。意思是,CPU 要频繁地从内存取指令和数据,而总线带宽有限,当 CPU 运算速度远超内存传输速度时,CPU 的能力就被“饿”住了。

在 Linux 里实际表现为:你跑一个大任务,CPU 使用率却不饱和,反而vmstat里显示 wa(I/O wait)很高。我遇到过不少刚入门的同学,看到 CPU 利用率 50% 就以为机器性能不好,其实瓶颈可能根本不在 CPU 上,而在内存带宽、磁盘 I/O 或者锁竞争上。理解了总线这个共享通道之后,再看一些性能优化手段就顺理成章了:加大缓存、调整内核的预读参数、用简单数据结构减少内存访问次数,本质上都是想绕过或者缓解这个瓶颈。

2. 操作系统:连接硬件与进程的中间层

2.1 操作系统到底解决了什么问题

如果没有操作系统,一个程序想往硬盘写数据,就得自己控制磁盘控制器,知道固件指令怎么发、DMA 怎么设置、中断怎么处理。而现实是你只需要调一个write,剩下的脏活累活全由操作系统干了。这就是操作系统的本质:对上给用户提供一个方便、安全的使用接口,对下统一管理硬件资源。

从硬件角度看,操作系统主要管四类资源:CPU、内存、设备、文件。CPU 管理要解决“下一个轮到谁用 CPU”、内存管理要解决“程序和数据放在哪”“怎么让进程之间不会互相踩踏”、设备管理要解决“鼠标键盘网卡这些乱七八糟的硬件怎么统一访问”、文件管理要解决“数据怎么持久化在磁盘上”。进程的概念就诞生在这四类资源管理之中:每一个进程,本质是操作系统为用户程序分配的一组资源的集合。

我学习的时候有个很管用的类比:操作系统像一个大管家,进程像住在公寓里的租客。租客不会自己去开水闸、拉电网,他只需要按开关,管家就保证水电到位。租客之间互不干扰,是因为管家在中间做了隔离和分配。

2.2 用户态与内核态:系统调用是唯一的门

为了保护系统安全,CPU 把执行级别分成了内核态和用户态。操作系统内核跑在内核态,可以访问所有硬件和任意内存;普通进程跑在用户态,很多敏感操作被禁止。用户程序想要做打开文件、创建进程、申请内存这种事,就必须触发系统调用,陷入内核态,让内核替它完成,再返回用户态。

这就像银行柜台:你可以在大厅里自由看宣传册、填单子,但想取钱就必须通过柜员窗口,不能自己闯进金库。也就是说,内核把“危险操作”统一封装成了有限几个窗口。在 Linux 上,这些窗口就是open、read、write、fork、mmap等系统调用。很多面试题里问“系统调用和库函数的区别”,核心就是:库函数是用户态的封装,底层可能调用多个系统调用,也可能根本不涉及系统调用。

2.3 进程的诞生是操作系统运行的起点

Linux 启动之后,内核做的第一件大事就是创建一个编号为 0 的进程,之后又创建了编号为 1 的进程,通常称为init或systemd。系统上所有进程都是这个 1 号进程的子孙。你可以打开终端跑一下pstree -p,立刻就能看到一棵从 systemd 延伸出来的进程树。

所以“进程”对操作系统来说不是附加功能,而是它工作的基本单位。操作系统把 CPU 时间切成一段一段,分配给不同的进程;把内存分成一块一块,映射给不同的进程;把文件描述符、信号、终端信息都记在进程头上。理解了操作系统是“管理进程的管家”,再回头看内核源码里那如山的进程调度器、内存管理代码,主线就清晰了:一切为了服务进程。

3. 进程:内核眼中的程序执行流

3.1 程序是静态的,进程是动态的

程序是躺在磁盘上的一个可执行文件,它的体积、内容都是固定的,什么也不做。进程则是这个文件被加载到内存之后,正在执行的动态过程。同一个程序启动两次,会得到两个完全独立的进程,它们有各自的地址空间、各自的文件描述符,互不干扰。这就是为什么你可以同时开两个终端都去执行vim,它们各编各的。

换句话说,进程 = 程序 + 执行状态 + 系统资源。程序给出了指令序列,但真正在执行的是进程。这个“静态和动态”的区别,是我建议所有 Linux 初学者最先记住的一句话。

3.2 PCB 与 task_struct:进程的一切都记在内核里

内核要管理那么多进程,总得有个地方记录每个进程的“档案”。这个档案在操作系统原理里叫 PCB(进程控制块),在 Linux 里则是一个具体的结构体task_struct。它非常庞大,包含了进程的基本信息:pid、父进程 pid、进程状态、优先权、文件描述符表、内存描述符、信号处理函数、工作目录、终端信息等。

我不建议新手一开始就钻进源码,但建议养成看/proc的习惯。每个进程在/proc下都有一个以 pid 命名的目录,比如cat /proc/1/status就能看到 1 号进程的档案。这里面的字段和task_struct字段是一一对应的。当你亲手看到 Name、State、PPid、VmRSS、Threads 这些字段,会比背十遍概念都管用。

3.3 进程的一生:创建、运行、等待、消亡

进程不会凭空出现,它由另一个进程复制而来。Linux 上常见的创建方式是fork:调用fork之后,内核会复制一份几乎一模一样的进程,父进程继续跑自己的,子进程从这个调用点开始跑。这里有一个非常违反直觉的点:fork之后代码里会“出现”两个返回现场,一次返回 0,一次返回子进程 pid。

进程随后的命运大致分成三类:占用 CPU 正在运行、因为等待资源而睡眠、暂时被停止。关于状态细节,我在第 5 部分再展开。进程退出的时候,如果父进程没来得及收尸,它会变成僵尸进程;父进程通过wait系列系统调用把子进程的退出状态收走,这个“尸体”才会真正从系统里消失。这个机制我第一次知道的时候觉得特别有意思:系统连“收尸”都有专门的状态和调用,可见进程模型有多讲究。

3.4 调度与上下文切换:多个进程共用一颗 CPU

单核 CPU 同一时刻只能执行一条指令流,但系统上却有几十个进程,怎么做到“同时运行”?答案是快速切换:每个进程各分得一小段时间片,跑完自己的时间片就被换下去,下一个进程换上来。这种切换频率非常高,快到让人感觉不到,这就是多任务的秘密。

切换的过程叫上下文切换。内核要把当前进程的寄存器值、程序计数器、栈指针等保存起来,再把下一个进程之前保存的状态恢复出来。这个过程有代价,所以进程切换不能太频繁。Linux 默认的调度策略是 CFS(完全公平调度器),它维护一棵红黑树,核心思想是让每个进程按照优先级分摊 CPU 时间。理解这个机制之后,你就能明白为什么一个死循环程序会让系统变卡:它吃掉了太多调度实体,把别人的时间片挤占了。

4. 实战:在 Linux 终端里看见进程的全貌

4.1 先学会这几条命令,胜过背十遍概念

进程的知识如果只停留在概念层面,很快就会忘。我强烈建议你打开终端,把下面这些命令挨个跑一遍,并且对照着第 3 小节的内容去理解输出。

  • ps aux:查看所有进程的快照,A表示所有用户,U表示按用户维度展示,X表示列出没有控制终端的进程。
  • ps -ef:另一种格式的全量进程列表,重点是能沿 PPID 找到父子关系。
  • top或htop:动态刷新查看当前系统资源占用和进程状态。
  • pstree:以树的形状展示进程家族,直观看到谁是祖先谁是子嗣。
  • pgrep -a name:按名称查找进程 pid,比ps aux | grep name更干净。
  • pidof name:直接输出某个程序的所有 pid。

我重点说一下ps输出里那几列到底怎么读。PID 是进程编号,PPID 是父进程编号,%CPU 和 %MEM 是瞬时采样值,不是从开机到现在的平均值。STAT 是状态码,R 表示正在运行,S 表示睡眠,D 表示不可中断睡眠,Z 表示僵尸。TIME 是进程累计占用 CPU 的时间,这个值我不会只看大小,因为有些进程虽然存活很久,但绝大部分时间在睡觉,TIME 很小反而说明它没怎么消耗资源。

4.2 /proc:操作系统把“内幕”全摆成了文件

Linux 有一个非常独特的风格:一切皆文件,进程的状态也不例外。/proc是一个虚拟文件系统,它不占用磁盘空间,只是内核对外展示数据的一个窗口。进入/proc,你会看到一堆数字目录,每个数字就是一个进程的 pid。拿ls /proc/1来说,1是 systemd 的 pid,这个目录里放着一堆文件,其中最有价值的是:

cat /proc/1/status # 进程的详细档案 cat /proc/1/cmdline # 启动命令,注意用 xxd 或 tr 转一下 \0 分隔符 ls /proc/1/fd # 这个进程当前打开的所有文件描述符 cat /proc/self/status # 你当前这条命令自己的进程档案

我学进程的时候,最喜欢做一件事:写一个sleep 1000的后台任务,拿到它的 pid,再在/proc/pid/status里看 VmRSS(物理内存占用)、 voluntary_ctxt_switches(自愿上下文切换次数)这些指标,然后趁它睡眠或运行时反复查看,看数值怎么变。比任何教科书都直观。注意/proc里的文件是“即时生成”的,你用cat读它时,内核现场抓数据,所以文件大小经常显示为 0,这是正常的。

4.3 写一个小程序,亲手验证 fork 的行为

理论和观察都做了,最后亲手动动手。下面这段 C 代码,我建议你自己创建文件编译跑一遍,它是理解进程创建绕不开的经典例子:

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { pid_t pid = fork(); if (pid < 0) { perror("fork"); return 1; } else if (pid == 0) { printf("子进程:pid=%d,父进程=%d\n", getpid(), getppid()); } else { printf("父进程:pid=%d,子进程=%d\n", getpid(), pid); wait(NULL); } return 0; }

编译运行:

gcc -o fork_demo fork_demo.c ./fork_demo

你大概率会看到两行输出,一行来自父进程,一行来自子进程。这里有个关键的坑:fork之前,这两个进程还不存在,fork返回值区分了父子两条执行流。子进程在fork返回时得到的返回值为 0,父进程得到的是子进程的 pid。所以pid == 0的分支只有子进程会走,在这个分支里如果再调用getpid(),看到的就是子进程自己的编号。

我再提醒一点:这段程序在管道输出时可能看起来顺序混乱,因为printf有缓冲区,父子进程各自复制了这个缓冲区,退出时可能反复刷新。如果想要严格的输出顺序,可以在每次printf后加fflush(stdout)。这种“缓冲区被复制”的坑,在真实的多进程程序里也很常见。

4.4 用 wait 给子进程收尸

上面的例子我在父进程分支加了wait(NULL),它的作用是父进程阻塞等待子进程退出,并回收子进程的资源。如果不加,子进程退出时父进程还在运行,或者直接退出了,子进程会变孤儿或者僵尸。孤儿进程会被 1 号进程收养,僵尸则要等待父进程调用wait才清除。

你可以故意删掉wait(NULL),然后连续启动几次程序,用ps -ef看输出,会发现有一小段时间进程状态是 Z。亲手制造一次僵尸进程再把它清理掉,你对进程状态的记忆绝对深刻。清理僵尸进程的办法很简单:杀掉它的父进程,让 1 号进程接管并回收,不过更标准的做法是让父进程正常调用wait或搭配信号处理。

5. 常见问题与避坑指南

5.1 进程状态速查表:top 里 O、S、D、Z 分别代表什么

很多新手第一次用top看到一大排 S 状态进程,就疑惑“它们是不是不工作了”。其实 S 是睡眠状态,表示进程正在等待某个条件满足,比如等网络数据、等用户输入,绝大多数服务型进程长时间处于 S 状态是正常的。真正要警惕的是 D 状态,也叫不可中断睡眠,进程正在执行内核态磁盘 I/O,不能被杀掉甚至不能被信号打断。如果你看到一堆进程卡在 D 状态,通常说明磁盘 I/O 出问题了,比如外接存储掉线。

我做过一张速查表,遇到状态异常直接对照就行:

状态码含义常见原因与处理思路
RRunning 或 Runnable正在 CPU 上跑或排队,属于正常
SSleeping(可中断)等待事件,多数进程长时间如此,正常
D不可中断睡眠内核 I/O 等待,磁盘故障、NFS 挂载异常都可能引发
T暂停(Stopped)被Ctrl+Z或SIGSTOP暂停,可用fg/bg恢复
Z僵尸(Zombie)子进程退出但父进程未回收,杀掉父进程或修源码
I空闲(Idle)内核线程,比如kthreadd的子线程,正常

遇到 Z 状态,我的处理习惯是先找它的 PPID,确认哪个父进程没回收,再根据实际情况看:临时清除就杀父进程,长期解决得改父进程的逻辑,加上wait或者信号处理。如果有大量 D 状态,我会立刻查磁盘、看系统日志、检查挂载的存储,而不是直接重启,重启通常解决不了根因。

5.2 学习顺序:为什么先硬件、再操作系统、最后进程

我现在回头看自己的学习经历,最大的弯路就是上来就死磕命令行和“进程概念”,结果越学越散。后来我重新调整顺序,思路立刻清晰了:第一层先理解硬件框架,第二层看操作系统怎么管理硬件,第三层才看进程如何被创建、调度、销毁。这三层就像盖楼,进程是最上面的那一层,下面没有硬件和操作系统的认知,进程永远是空中楼阁。

我建议的学习路线可以这样走:先花一周把冯诺依曼体系结构的五个部件和总线关系弄清楚,再花一周看操作系统“四大资源管理”的框架,最后用两到三周专门跟进程过不去——每天用ps、top、/proc观察,再配合fork、exec、wait写几个小程序。等到你看到ps aux的输出能下意识说出每个字段对应的内核数据结构时,就算真正过关了。

5.3 关于 Linux 发行版的延伸:内核才是根本

经常有初学者纠结“该装哪个发行版”“国产发行版和国外发行版有什么区别”。这里我想说一个容易被忽略的事实:发行版千差万别,但大家跑的其实是同一个 Linux 内核。Ubuntu、CentOS、Debian、Arch,以及我们常见的麒麟、统信 UOS 等系统,差异主要在软件包管理、默认工具链、桌面环境上,进程模型、调度器、虚拟文件系统这些核心机制是一致的。

所以你在一个发行版上学会的进程知识,换到另一个发行版照样成立。我用过好几个发行版,最深的感受是:与其纠结选哪个系统,不如把精力放在理解内核和系统接口的通用机制上。今天你在 Ubuntu 上学到的ps怎么看、进程怎么调度,明天换到国产系统上依然有效,因为这些底层逻辑是操作系统这门学科的核心,而不是某个厂商的配置差异。

5.4 排查进程问题的一个实用套路

最后分享一个我自己的排查流程,希望对你有用。系统出现“卡顿、延迟、CPU 飙高”这类现象时,我一般按下面的顺序走:

top # 第一步:看全局,谁在消耗 CPU/内存 ps -eo pid,stat,wchan:32 # 第二步:看进程状态,筛选 R 和 D pidstat # 第三步:按进程看待 CPU 占比和上下文切换 strace -p PID # 第四步:跟踪某个进程正在做什么系统调用

第四步的strace是个神兵利器,它可以看到进程在内核门口做了什么:是网络连接等待、文件读写、还是陷入了死循环。我在定位“进程假死”问题时就靠它,一眼看出是卡在read等待 I/O 还是卡在某个锁上。这些工具要结合着用,单独看任何一个输出都容易误判,因为系统状态是动态变化的,尤其是性能问题,一定要连续观察几次而不是只看一个瞬间。

回到最初那三个关键词,我最想说的是:冯诺依曼体系结构让你理解硬件边界,操作系统让你理解资源管理,进程让你理解程序如何被系统对待。三者串起来,Linux 就不再是一堆命令的集合,而是一个逻辑自洽的系统。你在终端里敲下的每一条命令,背后都有一条完整的链路在撑着你。希望这篇笔记能帮你在 Linux 学习的路上少走一点弯路,哪怕只是把一个概念想透彻,也值得了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:27:35

SpringBoot2+Vue3在线教学平台全栈实战:从架构设计到部署踩坑

“在线教学平台”这个方向的 Java Web 项目&#xff0c;我前后看过不少&#xff0c;也自己动手改过几套。说实话&#xff0c;大多数所谓的“完整源码”要么后端只挂了几个 demo 接口&#xff0c;要么前端还停留在 jQuery 时代&#xff0c;真正能把 SpringBoot2 Vue3 MyBatis-…

作者头像 李华
网站建设 2026/10/7 10:26:01

鸿蒙Flutter适配json_rpc_2:双向通信与RPC协议实践

1. 为什么 json_rpc_2 是鸿蒙场景下绕不开的那个库我最早接触 json_rpc_2&#xff0c;是因为在鸿蒙 Flutter 应用里做设备与后台的双向实时通信&#xff0c;来回换了好几套方案&#xff0c;最后发现真正好用的还是这个在纯 Dart 层就能闭环的库。先说结论&#xff1a;json_rpc_…

作者头像 李华
网站建设 2026/10/7 10:25:49

喀斯特岩溶空间分布矢量数据集:SHP处理、裁剪统计与格式转换实战

简介&#xff1a;这份中国喀斯特岩溶空间分布矢量数据集面向GIS、地理学与地质环境领域的研究人员和学生&#xff0c;用于岩溶地貌区域划分、溶蚀作用模拟及地质灾害监测等空间分析场景。资源包共8个文件&#xff0c;约1.2MB&#xff0c;以SHP格式为核心&#xff0c;配套shx与s…

作者头像 李华
网站建设 2026/10/7 10:25:29

成渝城市群矢量数据实战:shp体检、POI空间连接与坐标处理

简介&#xff1a;资源聚焦成渝城市群空间数据应用场景&#xff0c;面向地理信息系统、城市规划与科研项目中需要学校、医院等POI及道路、建筑轮廓等基础矢量数据的开发者和研究人员。数据年限为2019年&#xff0c;POI信息经网络地图爬取并完成导出、裁剪等处理&#xff0c;统一…

作者头像 李华
网站建设 2026/10/7 10:24:14

TaoToken 实战:js获取页面光标坐标(x轴y轴)的3种可靠方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 10:23:11

Windows .cpl命令全攻略:一条命令直达系统设置

实不相瞒&#xff0c;我这几年帮人远程修电脑&#xff0c;用得最多的反而不是那些花哨的第三方工具&#xff0c;而是按下Win R&#xff0c;输入几个字母加.cpl。前几天朋友说新买的笔记本找不到耳机输出选项&#xff0c;我让他敲了一行mmsys.cpl&#xff0c;播放设备列表立刻弹…

作者头像 李华