前言
平时按下电脑电源键,几秒钟后就能看到登录界面。
这件事太熟悉了,以至于很少有人继续追问:
- 刚通电时,内存里明明什么都没有,CPU 第一条指令从哪里来?
- 操作系统还没启动,究竟是谁先控制 CPU?
- BIOS 和 UEFI 到底是什么关系?
- Bootloader 为什么能够把 Linux、Windows 这样的系统启动起来?
- 网上经常说 Bootloader 在 MBR,并被加载到
0x7C00,现在的 UEFI 机器还是这样吗? - Linux 内核启动后,为什么普通 Java 程序不能直接操作硬盘、网卡和其他硬件?
- 所谓用户态、内核态和 Ring 0、Ring 3,到底分别是什么?
- 一次
Files.read()、一次 Socket 读取,Java 是怎样跨进内核的? - “进入内核态”是不是就意味着发生了一次线程上下文切换?
如果这些问题没有串起来,我们很容易把:
BIOS
UEFI
MBR
Bootloader
Kernel
Ring 0
System Call
记成几个孤立的面试名词。
实际上,它们描述的是一条非常完整的控制权转移链:
这篇文章不准备深入到“自己写一个 Bootloader”的程度,而是先把从按下电源一直到 Java 程序调用操作系统的完整路径建立起来。
一、CPU 上电后从哪里执行第一条指令?
操作系统存储在 SSD 或硬盘中。
而 CPU 想执行代码,首先必须知道:
从哪个地址取第一条指令?
问题是,此时操作系统甚至还没有加载到内存。
所以开机最早期必须存在一段:
不依赖已经运行的操作系统,就能够被 CPU 找到并执行的固件代码。
这就是 Firmware 所处的位置。
1. BIOS:传统固件如何接管启动?
在传统 IBM PC 兼容机体系中,最熟悉的固件就是:
BIOS
Basic Input/Output System
它负责完成一系列最早期工作,例如:
- 建立最基本的硬件环境;
- 初始化和检查必要设备;
- 找到可启动设备;
- 把控制权交给下一阶段启动代码。
传统资料经常把开机流程写成:
通电
↓
BIOS
↓
POST 自检
↓
读取磁盘第一个扇区
↓
Bootloader
对于 Legacy BIOS 启动,这个模型非常有价值。
2. UEFI:现代固件改变了什么?
现在 PC 上更常见的是:
UEFI
Unified Extensible Firmware Interface
经常有人把它理解成:
BIOS 是黑白界面,UEFI 是有鼠标、有彩色 GUI 的新版 BIOS。
界面变化只是最表面的一层。
UEFI 真正重要的变化,是它重新定义了固件:
- 怎样描述启动项;
- 怎样加载可执行的 EFI Image;
- 怎样提供 Boot Services;
- 怎样通过 NVRAM 保存
BootOrder、Boot####等启动配置; - 怎样从文件系统中寻找 OS Loader。
所以更准确地说:
BIOS 与 UEFI 都承担“操作系统之前的固件启动环境”角色,但 UEFI 不是给 BIOS 换了一个图形界面,而是一套更现代的固件接口与启动规范。
二、BIOS 与 UEFI 是怎么启动系统的?
这是整篇文章最容易被旧资料误导的地方。
因为:
MBR
0x7C00
第一扇区
都是真的。
但它们主要描述的是Legacy BIOS 时代的启动路径。
1. Legacy BIOS:从启动扇区开始
传统 x86 BIOS 启动模式下,可以用下面的模型理解:
0x7C00 这个地址因此非常经典。
Linux 官方 x86 Boot Protocol 的历史布局里,也仍然明确保留了:
Boot sector entry point 0000:7C00
这个概念。
但是必须注意:
0x7C00 是 Legacy x86 Boot Sector 的经典入口,不是“所有现代操作系统内核固定加载地址”。
例如 Linux 的传统 bzImage 协议中,Protected-Mode Kernel 可以被装载到:
0x100000
也就是 1 MiB 位置。
所以不能把:
Boot Sector Entry = 0x7C00
继续推导成:
Linux Kernel Entry 永远 = 0x7C00
2. MBR:传统磁盘的启动入口
MBR 是:
Master Boot Record
传统 MBR 磁盘布局中,磁盘最开始的位置包含:
- 引导代码;
- 分区信息;
- 签名等数据。
Legacy BIOS 可以读取其中的启动代码,然后交给它继续完成启动。
但这同样属于历史路径。
3. UEFI:从 NVRAM 到 EFI Loader
UEFI 的正常启动流程完全可以不执行传统 MBR Boot Code。
UEFI Firmware 自己拥有 Boot Manager。
启动项保存在 NVRAM 中,例如:
Boot0000
Boot0001
Boot0002
...
BootOrder
某个 Boot#### 项会指向:
硬件设备
↓
分区
↓
某个 EFI 可执行文件
例如 Linux 系统中的引导器可能位于 EFI System Partition。
一个更接近现代机器的流程是:
这和:
BIOS → MBR → 0x7C00
已经是两套不同的启动模型。
三、CMOS、NVRAM 与主板电池是什么关系?
旧 PC 资料里经常会说:
BIOS 配置存储在 CMOS 中,主板电池给 CMOS 供电。
这句话有很强的历史背景。
1. CMOS 为什么需要 RTC 电池?
早期 PC 需要保存:
- 时间;
- 日期;
- 某些硬件配置;
- 启动相关设置。
因此主板上会存在 RTC / CMOS 相关电路,并通过纽扣电池在断电后维持部分状态。
这也是为什么老机器电池没电以后经常出现:
时间归零
BIOS 设置丢失
启动配置恢复默认
2. UEFI 为什么还需要 NVRAM?
现代 UEFI 的:
BootOrder
Boot####
等变量属于 Firmware NVRAM 机制。
所以今天再写:
所有 BIOS/UEFI 设置都存放在 CMOS 芯片中,拔掉主板电池就一定能清空全部密码和配置。
并不严谨。
不同厂商可能采用:
- NVRAM;
- Flash;
- RTC-backed 配置;
- 安全存储;
等不同方式。
因此:
拔主板电池可能重置部分传统固件设置和 RTC,但不能把它当成所有现代 UEFI 密码、Secure Boot 配置和 Firmware Variable 的通用清除方案。
四、Bootloader 如何把系统启动起来?
Firmware 的任务不是把整个 Linux 运行起来。
它需要把控制权继续交给更了解操作系统格式的下一阶段。
这个角色通常由:
Bootloader / OS Loader
承担。
1. Bootloader 负责做什么?
假设磁盘上同时安装:
Windows
Linux
Firmware 并不需要理解:
- Linux 的 Kernel Command Line;
- initramfs;
- Windows 内核的具体启动协议;
- 各种文件系统和启动参数。
Bootloader / OS Loader 可以承担这些更具体的事情。
它大致负责:
2. Kernel 获得控制权后做什么?
当 Linux Kernel 获得控制权以后,还会继续:
所以:
Kernel 启动
和:
用户看到登录界面
中间还有很长一段系统初始化过程。
3. 开机过程中的控制权转移
把前几章连起来:
这条链真正说明的是:
计算机启动不是某个程序突然“出现”,而是每一阶段先完成自己的初始化任务,再把执行权交给下一阶段。
最终,一台刚刚通电、几乎还没有软件运行环境的机器,才逐步变成能够运行 Java、浏览器、数据库等应用程序的完整系统。
五、操作系统与 Kernel 到底管理什么?
Kernel 启动以后,机器真正进入操作系统管理阶段。
操作系统可以先理解成夹在:
Application
↓
Operating System
↓
Hardware
中间的一层。
1. Kernel 如何管理硬件?
Kernel 需要管理:
- CPU 调度;
- 内存;
- 中断;
- 设备驱动;
- 文件系统;
- 网络;
- 权限和隔离。
2. 操作系统如何给应用提供抽象?
应用程序不需要知道:
某块 NVMe SSD 的控制寄存器地址是多少?
Java 只需要:
Files.readAllBytes(path);
浏览器只需要:
打开文件
建立 Socket
分配内存
创建线程
Kernel 负责把这些高级需求映射到真实硬件。
所以操作系统最重要的价值之一,就是把:
完全不同的硬件
封装成:
稳定的软件接口
六、宏内核、微内核与外核有什么区别?
这部分很容易因为各种营销材料而产生误解。
先只讨论架构思想。
1. 宏内核:核心服务集中在内核空间
Linux 属于 Monolithic Kernel 路线。
一个简化模型:
这里的“宏”并不意味着:
所有代码永远焊死在一个文件里,完全不能模块化。
Linux 本身就支持 Loadable Kernel Module。
宏内核真正强调的是:
大量操作系统核心服务运行在同一个特权内核地址空间中,并可以通过直接函数调用高效协作。
优势:
- 调用链短;
- 性能高;
- 内核组件之间协作直接。
代价:
- 大量代码都处在高权限区域;
- 内核 Bug 的影响范围可能更大。
2. 微内核:把更多服务移出内核
Microkernel 的思想则是:
只在内核里保留尽量少的关键机制,把更多服务放到用户空间进程中。
一个概念模型:
不同微内核究竟把哪些功能留在 Kernel 中并不完全一样。
所以“微内核只负责调度”也过于绝对。
更稳定的理解是:
Microkernel 尽量缩小高特权内核,把文件系统、驱动、网络等更多服务迁移到隔离的用户空间组件,通过 IPC 协作。
2.1 微内核不等于分布式系统
这也是一个很容易混淆的点。
Microkernel 的:
IPC + User-space Service
通常首先描述的是同一台机器内部的 OS 架构。
服务是否通过网络跨设备运行,属于:
Distributed System / Distributed OS
层面的设计。
它们可以结合,但:
Microkernel ≠ 天生就是跨设备分布式操作系统
3. OpenHarmony 应该怎么理解?
一些早期资料把鸿蒙简单描述成:
一个纯微内核操作系统。
这个说法今天已经不能这样写。
当前 OpenHarmony 6.0 官方架构文档描述的是分层架构,并且 Kernel Subsystem 使用:
多内核设计,可根据设备选择 Linux Kernel 或 LiteOS。
同时整个系统支持按照设备资源:
裁剪组件
这部分确实体现了面向不同设备进行灵活部署的设计目标。
因此更合理的表述是:
OpenHarmony 是面向多设备场景的分层、可裁剪系统框架,其当前开源架构包含 Linux / LiteOS 多内核支持;不能简单把整个现代 OpenHarmony 等同于“一颗微内核”。
4. 外核:把更多策略交给应用
Exokernel 是更偏研究性的 OS 架构思想。
MIT Exokernel 的核心目标不是:
Kernel 帮应用提供越来越高级的抽象。
反而是:
Kernel 尽量只负责安全地复用和保护原始硬件资源,把更高级的抽象交给应用层的 Library OS。
模型更接近:
这样不同应用可以根据需要自己决定:
- 文件系统抽象;
- 网络策略;
- 内存资源管理方式。
它强调的是:
Protection 留在 Kernel
Policy / Abstraction 尽量交给 LibOS
而不是简单理解成“给 Office 写一个专用 Windows”。
5. VMM / Hypervisor 为什么不是内核分类?
Virtual Machine Monitor,也就是 Hypervisor,解决的是另一件事:
怎样让多个操作系统共享一台物理机器。
所以:
Monolithic Kernel
Microkernel
Exokernel
讨论的是 OS Kernel Architecture。
而:
VMM / Hypervisor
讨论的是 Virtualization。
它们不是同一维度的三个并列“内核类型”。
七、为什么要区分用户态和内核态?
如果每一个普通程序都可以:
随便修改页表
随便关闭中断
随便改磁盘控制器寄存器
随便覆盖 Kernel 内存
系统根本无法稳定运行。
因此现代处理器和操作系统共同建立了:
Privilege Separation
也就是权限隔离。
1. 应用程序为什么不能直接控制硬件?
早期 x86 实模式环境里的程序拥有远高于现代用户程序的直接硬件控制能力。
这种自由的代价也很明显:
一个程序写错地址,就可能把整个系统一起带走。
现代操作系统则把:
Kernel
和:
Application
隔离开。
2. Ring 0 与 Ring 3 分别代表什么?
x86 体系结构定义了多个 Privilege Level,常用“环”表示:
Ring 0
Ring 1
Ring 2
Ring 3
可以建立最基础的直觉:
Ring 0 → 高特权
...
Ring 3 → 低特权
现代 x86 Linux 通常主要使用:
CPL 0 → Kernel
CPL 3 → User Space
Ring 1 和 Ring 2 通常不用来承载普通 Linux 用户进程。
但这里还有两个特别重要的区别。
2.1 CPU 特权级
它回答:
当前 CPU 正以什么特权级执行指令?
2.2 地址空间与保护
它回答:
哪些虚拟地址和资源允许谁访问?
两者高度相关,但不是完全同义词。
3. 用户态到底能直接做什么?
有一种常见说法:
应用程序读内存也必须切换 Ring 0。
这是错误的。
一个 Java 程序正常执行:
int x = array[10];
读取的是当前进程自己合法映射的 User Virtual Memory。
这完全可以在用户态完成。
真正需要 Kernel 参与的是:
- 访问受保护硬件;
- 文件 I/O;
- 网络 I/O;
- 创建或管理 OS 资源;
- 修改受保护的内存映射;
- 执行 Privileged Operation。
可以直接对比:
八、System Call:用户程序怎么进入内核?
现在终于到 Java 和操作系统真正接上的地方。
假设 Java 执行:
Files.readAllBytes(path);
JVM 自己并不会偷偷去控制 NVMe 控制器。
它必须借助操作系统。
1. Java 调用如何一路进入 Kernel?
一个简化流程:
这就是 System Call 的价值:
Kernel 向 User Space 暴露一组受控入口,让普通程序能够请求高权限服务,而不是直接获得高权限。
2. 常见 System Call 有哪些?
Linux 中可以看到很多系统调用,例如:
read
write
openat
mmap
futex
socket
accept
sendfile
clone
...
不要背:
Linux 一共有 200 多个系统调用。
系统调用表会随着:
- Kernel 版本;
- CPU 架构;
- ABI;
变化。
Linux 官方文档甚至在介绍新增 System Call 时,要求直接修改对应架构的:
syscall_64.tbl
syscall_32.tbl
所以系统调用“总数”并不是一个值得记忆的固定常量。
3. fork 为什么不是创建 Java 线程?
这是另一个常见混淆。
fork
传统语义是创建新进程。
Linux 上 Native Thread 的创建通常会经过:
pthread
↓
clone / clone3 等 Kernel 机制
Java Platform Thread 再通过 JVM 与本地线程运行时映射到 OS 线程。
这部分涉及:
进程
线程
调度
上下文切换
虚拟线程
属于下一阶段内容,这里先不展开。
九、Mode Switch 与 Context Switch 有什么区别?
不是。
这是用户态 / 内核态最容易产生的误区之一。
1. System Call 为什么会发生 Mode Switch?
当前线程:
Thread A
执行:
User Mode
↓
System Call
↓
Kernel Mode
↓
处理请求
↓
返回
↓
User Mode
整个过程中仍然可能一直是:
Thread A
只不过 CPU 的执行特权级发生了变化。
所以:
Mode Switch
不等于:
Context Switch
2. 什么情况下才会发生 Context Switch?
假设 Thread A 调用:
read()
数据还没准备好。
Kernel 可能让它进入等待状态:
Thread A
↓
System Call
↓
等待 I/O
↓
Scheduler
↓
切换到 Thread B
这时候才进一步发生真正的线程调度 / Context Switch。
因此完整关系应该是:
这个区别非常重要,因为很多性能文章一看到:
System Call
就直接等同于:
线程切换
这是不准确的。
十、JVM 在操作系统中处于哪一层?
从 Linux 看,JVM 不是特殊的“第二操作系统”。
执行:
java MyApplication
之后,本质上仍然创建了一个普通 User-Space Process。JVM 可以在这个进程内部继续构建 Java 自己的运行时抽象,但它并没有因此越过 Linux 的权限边界。
HotSpot 自己会:
- 管理 Java Heap;
- 管理 Class Metadata;
- JIT 编译代码;
- 管理 GC;
- 管理 Java Thread / Virtual Thread;
- 提供 Java Runtime。
但它没有因此获得:
Ring 0
权限。
可以画成:
当 Java 只是:
int c = a + b;
JIT 编译后的机器码可以直接在 User Mode 执行。
但当 Java 执行:
文件 I/O
Socket I/O
内存映射
线程阻塞 / 唤醒
JVM / Native Runtime 就需要进入 Kernel 提供的接口。
因此这几层关系可以再画成:
JVM 屏蔽了很多操作系统差异,但 JVM 自己仍然建立在操作系统之上。
十一、常见误区
1. 关于 BIOS / UEFI
误区 1:UEFI 就是带图形界面的 BIOS。
不准确。
UEFI 是完整的 Firmware Interface 与 Boot Architecture,GUI 只是表面变化。
误区 2:现代电脑启动时一定先执行 MBR 第一扇区。
错误。
这描述的是 Legacy BIOS 典型路径。原生 UEFI 启动可以通过 NVRAM Boot Option 直接定位 EFI Image。
误区 3:操作系统 Kernel 固定加载到 0x7C00。
错误。
0x7C00 是 Legacy x86 Boot Sector 经典入口地址,不是现代 Kernel 的统一装载地址。
2. 关于 CMOS
误区 4:所有 UEFI 配置都保存在 CMOS,拔电池一定能清空。
不准确。
现代 Firmware 可能使用 NVRAM / Flash 等方式保存变量。主板电池与 RTC、历史 CMOS 配置密切相关,但不能把拔电池当成所有机器的统一清除方案。
3. 关于内核架构
误区 5:Linux 是宏内核,所以所有功能完全不可拆分。
错误。
Linux 属于 Monolithic Kernel 路线,但同时支持大量模块化机制和 Loadable Kernel Module。
误区 6:微内核就是“只有一个调度器”。
过度简化。
不同 Microkernel 会保留不同最小机制,通常包括 IPC、Scheduling、Address-Space / Protection 等核心能力。
误区 7:微内核的服务天然可以跑到冰箱、电视等另一台设备上。
错误。
Microkernel 首先描述单机 OS 内部的服务隔离和 IPC。跨设备协作属于 Distributed System / Distributed OS 的另一层问题。
误区 8:OpenHarmony 就等于纯微内核。
不准确。
当前 OpenHarmony 官方架构采用多内核设计,根据设备类型支持 Linux Kernel 或 LiteOS,并提供分层、组件裁剪等能力。
误区 9:Exokernel 就是给每个应用定制一套普通操作系统。
不准确。
Exokernel 的核心是安全复用原始硬件资源,把高层抽象更多交给 LibOS / Application。
4. 关于用户态与系统调用
误区 10:Java 程序读取自己的内存也要切 Ring 0。
错误。
合法的 User Virtual Memory 可以直接在 User Mode 访问。
误区 11:sudo 让程序直接变成 Ring 0。
错误。
sudo 是操作系统的用户身份 / 权限控制工具。即便 UID 0 的用户程序,普通用户代码依然运行在 User Mode;真正执行特权指令的仍然是 Kernel。
误区 12:一次 System Call 一定等于一次线程 Context Switch。
错误。
System Call 必然涉及受控的用户态 / 内核态转换,但如果内核能立即处理并返回,不一定切换到另一个线程。
误区 13:Linux 固定只有 200 多个系统调用。
错误。
数量随架构、ABI 和 Kernel 版本变化,不需要背固定数字。
十二、从上电到 Java 程序的完整启动链
现在重新看一次完整过程。
这条链里发生了两次非常重要的“接管”。
第一次:
Firmware / Bootloader
↓
Kernel
意味着操作系统正式接管机器。
第二次:
Kernel
↓
User Space
意味着操作系统开始允许普通程序运行,但不会把最高硬件权限一起交给它们。
于是:
浏览器
数据库
JVM
Java 应用
都能安全共存。
十三、总结
开机并不是:
按电源
↓
操作系统突然出现
而是一层层加载、一层层转移控制权:
CPU Reset
↓
Firmware
↓
Boot Manager / Bootloader
↓
Kernel
↓
User Space
↓
JVM
↓
Java Application
Legacy BIOS 时代常见:
MBR + 0x7C00
而现代 UEFI 则更多依赖:
NVRAM Boot Option
+
EFI System Partition
+
EFI OS Loader
Kernel 启动以后,又通过 CPU 特权级和虚拟内存保护,把系统拆成:
Kernel Mode
User Mode
普通程序不能直接拥有硬件最高权限,只能通过:
System Call
进入 Kernel 提供的受控服务入口。
这也解释了 Java 和 Linux 真正的关系:
Java API
↓
JVM / JDK Native Runtime
↓
System Call
↓
Linux Kernel
↓
Driver / Hardware
从按下电源到运行一行 Java 代码,本质上是一场不断交接控制权的过程:Firmware 负责把系统带到 Bootloader,Bootloader 把控制权交给 Kernel,Kernel 建立用户空间和保护边界,而 JVM 最终作为普通用户态进程,通过 System Call 使用操作系统提供的硬件能力。
评论区