作者介绍

大家好,我是 CodeStats

一个在底层技术上“考古”了四年的硬核爱好者,也是 WWAIC(全周项目AI编程) 范式的提出者和实践者。我曾手写过一个完整的 Java Web 框架(从 IoC 容器到嵌入式 Tomcat,代码全开源),也喜欢用通俗的语言拆解 CPU、JVM、操作系统的运行本质

写这篇文章的初衷很简单:我发现太多开发者只停留在“会用”层面,却很少追问“为什么这么设计”。 我希望用一篇文章,帮你把从CPU指令到高级语言运行时的这条认知链彻底打通。

📖 你将从本文获得什么?

  • 一套从CPU物理执行到操作系统管理的完整认知框架

  • 编译型 vs 解释型 vs JVM型语言运行机制的本质理解

  • 对 Rust内存安全、Go调度器设计等现代语言核心特性的深度剖析

  • 面试官问“程序怎么跑起来的”时,能答出别人答不出来的那一层

📑 问题目录

  • 问题一:程序运行的本质是什么?操作系统是如何调度进程的?

  • 问题二:操作系统内核是C语言写的,为什么C语言需要依赖操作系统库文件?

  • 问题三:Python和Java虚拟机都是C/C++实现的,为什么Python环境在Linux通常需要源码编译,而JDK通常解压安装就行?

  • 问题四:Rust/C++/C都是编译型语言,Rust编译解决内存安全问题,为什么无法取代C/C++?

  • 问题五:Go语言也是编译型高级语言,本质是用户态解决多线程切换提高操作系统调度并发效率问题,为什么不像JVM一样,而是GC和调度器都打包进程序里面?

  • 总结:50+种编程语言的分类图谱

问题一:程序运行的本质是什么?操作系统是如何调度进程的?

1.1 CPU眼中的程序:就是一条条二进制指令

程序运行的终极真相极其简单:CPU只认识二进制指令

你写的每一行代码,最终都会被编译(或解释)成CPU能够执行的机器码。CPU内部有一个程序计数器(PC / IP寄存器),它永远指向内存中下一条待执行指令的地址。控制单元根据PC的指向,将主存中的指令装载到指令寄存器,然后经过取址 → 译码 → 执行的循环,一条一条地把指令“吃”进去。

text

┌──────────────────────────────────────────────────────────────┐
│  你写的代码:int a = 10; int b = 20; printf("%d", a+b);    │
│                          ↓ 编译                             │
│  CPU执行的二进制:01001011 11010010 00101101 ...           │
│                          ↓ 逐条执行                        │
│  取址 → 译码 → 执行 → 取址 → 译码 → 执行 → ...            │
└──────────────────────────────────────────────────────────────┘

关键认知: 程序不是“活”的,它是一张被编排好的乐谱。CPU是那个无情的演奏机器,按部就班地弹完每一个音符。

1.2 操作系统眼中的程序:进程与调度

程序在磁盘上只是一个文件(.exe / ELF)。操作系统把它加载到内存后,给它分配一个“身份”——进程(Process),并建立一个进程控制块(PCB)来记录它的所有信息。

操作系统的核心任务之一,就是让多个进程“同时”运行。但CPU核心数量有限,怎么办?

答案:时间片轮转 + 上下文切换。

每个进程都会被分配到CPU的一个时间片(通常是几十毫秒)。当一个进程用完时间片,或者被更高优先级的进程抢占时,操作系统会:

  1. 保存当前进程的状态(寄存器、程序计数器、栈指针等)到PCB中

  2. 从就绪队列中选出一个新进程

  3. 恢复新进程的PCB中的状态到CPU寄存器

这个过程叫做上下文切换(Context Switch)

text

┌─────────────────────────────────────────────────────────────────┐
│  进程A(运行中)  →  时间片用完  →  保存状态到PCB_A            │
│                                        ↓                       │
│                              操作系统调度器选择进程B            │
│                                        ↓                       │
│  进程B(恢复运行)  ←  从PCB_B恢复状态  ←  加载进程B           │
└─────────────────────────────────────────────────────────────────┘

上下文切换是有成本的——每次切换都要保存和恢复大量寄存器数据,这就是为什么操作系统线程切换(内核态)比用户态协程切换(如Go的Goroutine)慢得多的根本原因。

🧠 思考总结

我当年手写框架时,第一个“顿悟”时刻就是发现:不管代码写得多么花哨,CPU根本不关心你是Java还是C++,它只认二进制指令。这个认知一旦建立,你就不会再被“运行时”、“虚拟机”、“解释器”这些概念唬住了——它们都只是在这条指令流上加了不同层级的“包装”。

问题二:操作系统内核是C语言写的,为什么C语言需要依赖操作系统库文件?

这个问题看似矛盾,实则揭示了C语言与操作系统之间微妙的分层关系。

2.1 操作系统内核 vs C标准库:不是一回事

操作系统内核(如Linux内核)是用C语言写的,它运行在CPU的Ring 0(内核态),拥有对硬件的完全控制权。内核本身不依赖任何外部库——它自己就是“库”的提供者。

你写的C语言程序,运行在Ring 3(用户态)。它不能直接操作硬件,必须通过系统调用(System Call) 来请求内核帮它做事。

而C标准库(如glibc)就是用户态程序和内核之间的“翻译官”

text

┌─────────────────────────────────────────────────────────────────┐
│  你的C程序:printf("Hello")                                    │
│       ↓                                                        │
│  C标准库(glibc):将printf翻译成write()系统调用              │
│       ↓                                                        │
│  操作系统内核:执行write(),把数据写入终端                     │
└─────────────────────────────────────────────────────────────────┘

2.2 为什么C程序依赖库文件?

因为C标准库提供了大量封装好的函数(printf、malloc、fopen等),让你不用每次都用汇编去触发系统调用。

而这些库函数有两种链接方式:

链接方式 文件后缀 特点
静态链接 .a 库代码复制进你的exe,程序独立但体积大
动态链接 .so / .dll 运行时加载库,程序体积小但依赖外部库文件

关键认知: 操作系统内核不依赖任何库,但你的C程序依赖libc——因为libc是你和内核之间的桥梁。用ldd命令查看任意C编译的程序,几乎都能看到对libc.so的依赖。

🧠 思考总结

我在手写框架的经历中有一个很深的体会:分层越清晰,系统越健壮。 操作系统和用户程序之间,正是通过“系统调用”这一层明确的边界来完成分工。C标准库不是“多余”的,它是用户程序能够安全、便捷地使用操作系统功能的必要抽象。没有它,你写每一行代码都得手动触发系统调用——那会倒退到汇编时代。

问题三:Python和Java虚拟机都是C/C++实现的,为什么Python环境在Linux通常需要源码编译,而JDK通常解压安装就行?

这个问题触及了“分发策略”“运行依赖”的核心差异。

3.1 相同点:底层实现都是C/C++

Python的主流实现CPythonJava虚拟机(JVM),底层都是用C/C++写的。两者都是“在操作系统之上又搭了一层软件环境”。

3.2 不同点:分发的是什么?

维度 Python(源码编译安装) JDK(解压即用)
分发内容 源代码(.c文件 + .py文件) 已编译好的二进制文件(javac、java、.so/.dll)
依赖关系 依赖系统编译器(gcc)、开发头文件 不依赖编译器,只依赖操作系统的基础C库
安装方式 ./configure → make → make install 解压 → 配置PATH → 即用

3.3 为什么Python常需要源码编译?

原因一:系统自带版本太老。 很多Linux发行版默认带的是Python 2.7或3.6,而你需要的是3.12的新特性。

原因二:预编译包缺失。 某些Linux发行版的软件仓库没有提供最新版Python的预编译包。

原因三:定制需求。 你需要修改Python源码、启用/禁用某些模块、或者指定安装路径。

3.4 为什么JDK通常解压即用?

因为Oracle/OpenJDK官方直接提供了针对各平台的预编译二进制包

JDK的二进制文件(java、javac)已经是编译好的本地机器码,只需解压到任意目录、配置JAVA_HOMEPATH即可使用。

🧠 思考总结

我从零手写Java Web框架时,深度体验了Java生态的“开箱即用”。当时我写了一个IoC容器和一个简化的Spring MVC,跑在嵌入式Tomcat上,整个过程没有任何“编译C代码”的步骤。Java这种“一次编译,到处运行”的体验,本质上是各平台预编译好的二进制分发策略带来的。 而Python之所以需要源码编译,不是因为它“更底层”,而是因为它的分发方式更依赖于你所在的操作系统环境。

问题四:Rust/C++/C都是编译型语言,Rust编译解决内存安全问题,为什么无法取代C/C++?

Rust通过所有权系统、借用规则和生命周期检查,在编译期就杜绝了悬垂指针、数据竞争等内存安全问题。它不需要垃圾回收(GC),却能保证内存安全。

那为什么Rust无法取代C/C++?

4.1 原因一:历史代码的“天文数字”

全球有数十亿行C/C++代码在关键系统中运行——Linux内核、Windows内核、数据库引擎、游戏引擎、浏览器内核。

重写这些代码的成本是天价。不是技术问题,是经济问题。

4.2 原因二:ABI(应用程序二进制接口)不稳定性

Rust 没有稳定的ABI。这意味着:

  • 用Rust 1.70编译的库,无法直接链接给Rust 1.80使用

  • Rust无法成为操作系统的系统动态链接库标准

  • 而C/C++的ABI在特定编译器下相对稳定

4.3 原因三:学习曲线陡峭

Rust的所有权和生命周期概念需要完全重塑编程思维。C/C++程序员习惯了“手动管理内存的自由”,切换到Rust的“编译器管一切”模式,学习成本极高。

4.4 原因四:编译速度

Rust的编译速度远慢于C/C++。因为编译器要执行大量的借用检查、生命周期分析和泛型单态化。

4.5 原因五:生态成熟度

在某些领域(如特定GUI框架、游戏引擎),Rust的库和工具链还不如C++丰富和稳定。

关键认知: Rust不是在“取代”C/C++,而是在“围剿”C/C++的应用边界——新项目可以用Rust,但存量系统会继续用C/C++。

🧠 思考总结

我手写框架的过程中,有一个深刻的体会:真正约束技术选型的,往往不是技术本身,而是历史惯性。 你写的框架再优雅,如果它依赖的底层库只提供C/C++ API,你就只能用Java/JNI去调它。Rust的内存安全再强大,它撼动不了那几十亿行已经跑在生产环境上的C/C++代码。技术演进不是“推倒重来”,而是在存量基础上做增量创新。

问题五:Go语言也是编译型高级语言,本质是用户态解决多线程切换提高操作系统调度并发效率问题,为什么不像JVM一样,而是GC和调度器都打包进程序里面?

5.1 Go vs JVM:设计哲学的根源差异

维度 JVM(Java) Go Runtime
运行形态 独立的虚拟机进程,需要预先安装 嵌入在二进制文件中的代码库
调度模型 操作系统线程(1:1映射) G-P-M用户态调度器(M:N映射)
部署方式 需要安装JRE/JDK + 配置环境变量 一个二进制文件,拷过去就能跑
启动速度 慢(JVM初始化 + 类加载) 极快(毫秒级)
内存占用 大(JVM本身占几百MB) (运行时嵌入,无额外进程)

5.2 为什么Go要把Runtime打包进二进制?

原因一:部署即运行,零依赖。 Go编译出的二进制文件是静态链接的,不依赖任何外部库(甚至不依赖libc)。在容器化时代,这意味着更小的镜像、更快的启动。

原因二:没有“中间层”的性能损耗。 Go直接编译为本地机器码,运行时没有字节码解释或JIT编译的额外开销。

原因三:调度器与程序“共生”。 Go的调度器(G-P-M模型)直接与操作系统线程协作,调用cloneepoll等系统调用。它不是“寄生”在操作系统之上,而是与操作系统协同工作

5.3 Go Runtime vs JVM:一张图说清

text

┌─────────────────────────────────────────────────────────────────────┐
│  JVM模式(Java)                                                  │
│  ┌─────────┐    ┌──────────────┐    ┌─────────────────────────┐  │
│  │ 你的代码 │ → │ .class字节码 │ → │ JVM(需预先安装)       │  │
│  └─────────┘    └──────────────┘    │  - 解释器/JIT编译器    │  │
│                                      │  - GC                   │  │
│                                      │  - 线程调度器           │  │
│                                      └─────────────────────────┘  │
│                                                 ↓                 │
│                                          操作系统 + CPU           │
├─────────────────────────────────────────────────────────────────────┤
│  Go模式(静态编译)                                               │
│  ┌─────────────────────────────────────────────────────────────┐  │
│  │              一个独立的二进制文件(.exe / ELF)            │  │
│  │  ┌───────────┐  ┌───────────┐  ┌─────────────────────┐   │  │
│  │  │ 你的业务  │ + │ 调度器   │ + │ GC + 内存分配器    │   │  │
│  │  │ 代码      │   │ (G-P-M)  │   │ + netpoll          │   │  │
│  │  └───────────┘  └───────────┘  └─────────────────────┘   │  │
│  └─────────────────────────────────────────────────────────────┘  │
│                              ↓                                    │
│                       操作系统 + CPU                              │
└─────────────────────────────────────────────────────────────────────┘

关键认知: Go不是“不想做成JVM”,而是“不需要做成JVM”。Go的设计目标是云原生时代的服务端语言——快速部署、低内存占用、高并发。把Runtime打包进二进制,牺牲了“跨平台字节码”的灵活性,换来了极致的部署简单性和启动速度

🧠 思考总结

我手写框架的经验让我明白了:没有“最好”的设计,只有“最适合场景”的设计。 我当年选择用Java写框架,就是因为Java的生态和跨平台能力最适合“一次编写,到处跑”的Web场景。而Go选择把Runtime打包进二进制,是因为它瞄准的是云原生场景——在那里,极致的部署速度和低内存占用比“跨平台字节码”重要得多。设计语言不是在追求“完美”,而是在做“取舍”。

总结:50+种编程语言的分类图谱

至此,我们已经从CPU指令一路讲到了五种主流语言的运行机制。现在把这些知识整理成一张完整的分类图谱:

📊 编程语言运行模式分类

text

                    ┌─────────────────────────────────────────────┐
                    │              编程语言分类                   │
                    └─────────────────────────────────────────────┘
                                      │
          ┌───────────────────────────┼───────────────────────────┐
          │                           │                           │
    ┌─────▼─────┐             ┌──────▼──────┐            ┌───────▼───────┐
    │ 编译型     │             │ 虚拟机型     │            │  解释型       │
    │ (AOT)     │             │ (VM-Based)  │            │  (Interpreted)│
    └─────┬─────┘             └──────┬──────┘            └───────┬───────┘
          │                          │                           │
    ┌─────┴─────┐              ┌─────┴─────┐               ┌─────┴─────┐
    │           │              │           │               │           │
┌───▼───┐ ┌───▼───┐      ┌───▼───┐ ┌───▼───┐       ┌───▼───┐ ┌───▼───┐
│C/C++  │ │Rust   │      │Java   │ │C#    │       │Python │ │Ruby   │
│Rust   │ │Go     │      │Scala  │ │Kotlin│       │PHP    │ │JS     │
│Zig    │ │       │      │Groovy │ │F#    │       │Perl   │ │Lua    │
└───────┘ └───────┘      └───────┘ └───────┘       └───────┘ └───────┘
    │          │              │           │               │           │
    └────┬─────┘              └─────┬─────┘               └─────┬─────┘
         │                          │                           │
    直接编译为                  编译为字节码                逐行解释执行
    本地机器码                  + JIT编译                  无编译过程
    无运行时依赖               需安装VM/运行时             需安装解释器

📋 各语言运行机制速查表

语言 执行模式 编译产物 运行时依赖 内存管理 并发模型
C 编译型 本地机器码 libc(动态/静态) 手动管理 OS线程
C++ 编译型 本地机器码 libstdc++ 手动/RAII OS线程
Rust 编译型 本地机器码 无(静态链接) 所有权系统 OS线程 + async
Go 编译型 本地机器码 无(Runtime内嵌) GC G-P-M协程
Java VM型 字节码(.class) JVM GC OS线程 + 虚拟线程
C# VM型 IL字节码 .NET CLR GC OS线程 + Task
Python 解释型 无(.pyc缓存) CPython解释器 引用计数+GC OS线程(GIL限制)
JavaScript 解释/JIT V8/SpiderMonkey等 GC 事件循环

🔑 四条核心认知

  1. 程序运行的本质没有变——无论什么语言,最终都是CPU在逐条执行二进制指令。所有“运行时”、“虚拟机”、“解释器”,都只是在这条指令流上加了不同层级的“包装”。

  2. 语言选择的本质是“场景匹配”——没有“最好”的语言,只有“最适合”的场景。C/C++统治操作系统和嵌入式,Java统治企业应用,Go统治云原生基础设施,Rust开始统治高性能系统软件。

  3. “打通”认知的价值——真正理解从CPU指令到高级语言运行时的全链路,不是为了炫技,而是为了在遇到性能问题、内存问题、并发问题时,能准确判断问题出在哪一层

  4. 技术演进不是“推倒重来”,而是在存量基础上做增量创新——这就是Rust取代不了C/C++的根本原因,也是Go选择“把Runtime打包进二进制”而非“做成JVM”的现实考量。

世界上有50多种主流编程语言,但CPU只有一种执行方式——逐条执行二进制指令。 理解了这一点,你就理解了所有语言的本质。

🤔 互动

如果这篇文章对你有帮助,欢迎:

  • 👍 点赞:让更多开发者看到这篇硬核内容

  • ⭐ 收藏:随时回顾从CPU到应用的完整认知框架

  • 💬 评论:分享你对某个语言运行机制的独特见解

  • 🔗 分享:转发给正在学习编程语言底层原理的朋友

Logo

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

更多推荐