Linux 系统分层架构:从硬件通电到 systemd 进程管理
一句话总结:本文梳理了 Linux 系统从硬件通电到 systemd 系统管理的完整分层架构,核心结论是内核解决"硬件抽象"和"进程运行平台"两个机制问题,systemd 解决"跑什么进程、怎么管理"的策略问题,二者职责边界清晰。
流程图
通电,从固定地址执行
加载内核+DTB到内存,跳转
创建PID 1,执行/sbin/init
按依赖图并行启动
提供IPC/日志/设备管理
svc/syscall特权指令切换
read/write/ioctl/mmap
open/read/fork/exec
硬件层 CPU/内存/Flash/GPIO
引导层 U-Boot 裸机代码
Linux 内核 驱动+进程平台
systemd PID 1 首个用户态进程
守护进程 journald/udevd/networkd
应用层 bmcweb/ipmid/shell
系统调用 用户态↔内核态 唯一入口
内容梳理
一、硬件层:硅芯片
BMC(Baseboard Management Controller - 基板管理控制器)芯片(如 ASPEED AST2500)通电后,CPU 从硬件固定的复位向量地址(通常是 SPI Flash 的 0 地址)取第一条指令执行。此时没有"内核"、没有"进程"、没有"文件"------只有裸硅。
- CPU 内核(ARM / x86)处于最底层,上电后 PC 指针指向固件入口
- 内存控制器(DDR Controller)尚未初始化,DRAM 不可用
- 外设(I2C 控制器、GPIO、SPI Flash 控制器)通过物理地址映射在总线上的寄存器操作
二、引导层:U-Boot(Das U-Boot - 通用引导加载器)
U-Boot 是一个裸机程序,不依赖任何操作系统,跑在 CPU 的特权模式,负责三件事:
- 初始化 CPU 和 DDR 内存(芯片刚通电时内存还不能用,U-Boot 先跑在 CPU 内部 SRAM 里)
- 从 Flash 找到 Linux 内核镜像(Image/zImage)和设备树(DTB, Device Tree Blob - 设备树二进制)
- 把内核加载到内存,设置 r0/r1 寄存器,跳转过去
U-Boot 的边界:内核一旦启动,U-Boot 就从内存里消失了------它的代码段被内核覆盖,再无关联。它是一次性的。
三、Linux 内核层:两个核心职责
内核是持续运行的平台,从开机到关机一直在工作。
启动序列与 PID 初始化
start_kernel()
→ setup_arch() // 根据设备树初始化平台硬件
→ mm_init() // 初始化内存管理子系统
→ sched_init() // 初始化进程调度器
→ rest_init()
→ 手动构造 PID 0 // idle task, 每个CPU一个, CPU空闲时跑halt指令
→ kernel_thread(kernel_init) // 创建 PID 1 (内核线程)
→ 加载驱动, mount 根文件系统
→ run_init_process("/sbin/init")
→ PID 1 从内核线程变为用户态进程 systemd
→ 创建 PID 2 // kthreadd, 后续所有内核线程由其 fork
关键纠正:PID 0 不是"进程",是内核本身的 idle task 化身,永远不退出。PID 1 是第一个用户态进程。所有后续用户态进程由 PID 1 fork,所有内核线程由 PID 2 fork。
职责一:硬件抽象(驱动)
驱动全在内核态,但通过三种接口向用户态暴露能力:
| 接口 | 例子 | 用户态操作 |
|---|---|---|
/dev/ 设备节点 |
/dev/i2c-3 |
open() → ioctl(I2C_RDWR) → 内核操作 I2C 控制器寄存器 |
/sys/ sysfs |
/sys/class/hwmon/hwmon0/temp1_input |
cat 读 → 驱动当场读传感器寄存器 → 返回值 |
/proc/ procfs |
/proc/cpuinfo |
内核汇总信息暴露 |
驱动分层结构:
硬件寄存器 (物理地址 0x1e78a000)
→ 平台驱动 (i2c-aspeed.c, AST2500 I2C 控制器)
→ 驱动框架 (i2c-dev.c 字符设备 / hwmon.c sysfs 框架)
→ 暴露给用户态 (/dev/i2c-3 / /sys/class/hwmon/)
职责二:进程运行平台(调度与隔离)
- 调度器(CFS, Completely Fair Scheduler - 完全公平调度器):单核物理 CPU 通过时间片轮流跑各个进程,一秒切换几十次,看起来像同时运行
- MMU(Memory Management Unit - 内存管理单元):每个进程有独立的虚拟地址空间,0x400000 在进程 A 里和进程 B 里映射到完全不同的物理内存,互相不可见
系统调用:用户态和内核态的唯一合法入口
系统调用不是独立的一层,而是内核层顶部的"门"。用户态进程通过特权指令(ARM: svc / x86: syscall / 旧 x86: int 0x80)触发 CPU 异常,CPU 切换到特权模式,内核处理请求后返回,CPU 切回非特权模式。
用户态: open("/dev/i2c-3", O_RDWR)
→ glibc 翻译 → svc #0 (ARM 特权指令)
→ CPU 切到内核态 → sys_call_table[__NR_openat]
→ 内核找到 inode → 找到驱动 open 函数 → 返回 fd
→ CPU 切回用户态 → 进程拿到 fd
四、systemd 层:系统管理
systemd 是内核启动的第一个用户态进程(PID 1),它的边界是:只管理进程,不管硬件。
| systemd 做 | systemd 不做 |
|---|---|
| 成为 PID 1,是所有用户态进程的祖先 | 不管硬件驱动(那是内核的活) |
| 管理服务启动顺序和依赖(DAG 依赖图并行启动) | 不管理内存页表和虚拟地址(MMU 在内核里) |
| 监控进程状态,崩溃自动重启 | 不调度进程(CFS 调度器在内核里) |
| 收集日志(journald) | 不暴露设备节点(udev 创建 /dev/ 但实际 IO 由内核驱动处理) |
| 管理 cgroup 资源限制 | 不写驱动代码 |
| 提供基于 D-Bus 的 IPC 通信 | socket 机制由内核提供 |
核心设计哲学
- cgroup 集成 :每个服务属于一个 cgroup,能精确跟踪所有子进程,
kill不会漏掉 fork 出来的子进程 - socket 激活:systemd 先创建 socket 监听,有请求才 wake up 真正服务,服务挂了不影响 socket 队列
- 统一状态机:所有 unit(service/socket/mount/timer/device)用同一套状态机(active/inactive/failed/reloading)
五、Init 系统历史演化
SysV init(1983 年)
通过 /etc/rc.d/rc?.d/ 下的 shell 脚本按编号串行启动服务。
- 痛点:完全串行、依赖靠数字编号暗示、启动完就不管、没有 cgroup、shell 脚本脆弱、碎片化严重
Upstart(2006 年,Ubuntu 主导)
引入事件驱动启动,"网络就绪"事件可同时触发 sshd 和 httpd 并行启动,支持崩溃自动重启。
- 遗留问题:依赖表达仍模糊、无 cgroup 集成、配置文件与 Debian/Fedora 不通用、社区分裂
systemd(2010 年,Red Hat)
不是单纯的 init 重写,而是重新定义了 Linux 系统管理层的设计。整合了 init + inetd + crond + syslogd + udev + ConsoleKit + ...,用统一设计哲学替代一堆零散组件。
总结与展望
总结
- 内核提供"机制"(硬件抽象 + 进程平台),systemd 提供"策略"(跑什么、怎么管)
- PID 0 是内核 idle task 化身而非进程,PID 1 是第一个用户态进程,PID 2 创建所有内核线程
- 驱动全在内核态,通过 /dev、/sys、/proc 三种接口向用户态暴露能力
- 系统调用是穿越用户态/内核态边界的唯一合法入口,通过 CPU 特权指令实现
- systemd 的统一整合思路用 cgroup + socket 激活 + 统一状态机解决 SysV init 的根本缺陷
展望/趋势
- systemd 在嵌入式领域的渗透:OpenBMC 全面使用 systemd 管理 BMC 服务,其 socket 激活对资源受限芯片尤为有利(按需启动,省内存)
- 引导层简化:U-Boot 正在被 Linux 内核自带的直接启动方案(如 LinuxBoot)部分替代,减少不必要的引导代码
- 内核态/用户态边界演变:eBPF 模糊了这条边界------允许用户编写的安全代码在内核态运行,未来设备驱动的一部分逻辑可能被推到用户态(DPDK/SPDK 思路)
- systemd 的争议与接受:systemd 的"大一统"哲学至今有争议,但它解决了 Linux 生态碎片化的实际问题,已成为事实标准
深度思考:大一统 vs 小零件的哲学之争
争议不浮于表面,根源在耦合强度
systemd 争议看似是"技术好不好用"的争论,但本质上是两种软件设计哲学的对立。理解这个对立,比记住 systemd 本身更重要。
两种哲学定义
A. 大一统(Monolithic / Integrated --- 单体整合设计)
把相关的事放在一起做,统一设计,统一接口。
systemd 是典型。另一个典型是 Linux 内核本身------驱动、文件系统、网络栈全在内核态同一个地址空间里跑。PostgreSQL(PostgreSQL --- 关系型数据库)也是一个引擎统一处理关系数据、JSON、时序和向量存储。
B. 小零件(Modular / Composable --- 模块化可组合设计)
每个工具只做一件事,通过标准接口组合成复杂系统。
Unix 管道的极致表达:ls | grep | sort | wc,每个工具只做一件事,拼接完成复杂任务。微服务架构也是这个思路------几百个独立的小服务各自部署,通过 API 互相调用。
两种哲学的优缺点对比
| 维度 | 大一统 | 小零件 |
|---|---|---|
| 调试排查 | 同一进程内看调用堆栈,边界清晰 | 问题跨越几十个服务,需要分布式追踪 |
| 理解成本 | 新人要面对一个巨大的整体才能上手 | 每个零件简单,但零件间的配合规则同样要学,总认知负担未必小 |
| 接口稳定性 | 内部可以快速重构,私有边界改动自由 | 零件靠公开接口协作,一改接口就破坏兼容性,不敢轻易动 |
| 创新速度 | 所有改动要经过核心团队,遗留决策会逐渐让系统变重 | 每个零件独立演进,坏掉只影响自己;但版本兼容矩阵会慢慢爆炸 |
| 故障半径 | 核心崩了全崩,边界少所以故障源少 | 一个零件崩了其他还能跑,但零件太多每个都可能是故障源 |
| 标准化 | 一个仓库风格统一 | 每个团队按自己的偏好来,生态碎片化 |
| 强制绑定 | 用 A 就绑死 B/C/D,想换代价大 | 按需自选零件,但也意味着你自己负责组装和验证兼容性 |
谁在什么场景赢了
| 场景 | 赢家 | 原因 |
|---|---|---|
| 操作系统内核(Linux vs 微内核) | 大一统 | 内核组件间调用极频繁,IPC(Inter-Process Communication --- 进程间通信)开销承受不起;微内核(Minix / GNU Hurd)在这个领域失败了 |
| init 系统(systemd vs SysV 脚本) | 大一统 | 碎片化 shell 脚本各自为政,统一管理才解决依赖编排和资源控制的刚需 |
| Web 后端(微服务 vs 单体) | 来回摇摆 | 当前微服务占优(独立扩缩容),但拆太细后调试和事务一致性成新灾难;近年"Modular Monolith"(逻辑模块化但部署一体)开始回归 |
| Unix 命令行(pipe 组合) | 小零件 | 至今未被挑战,设计典范 |
| 数据库(PostgreSQL vs 专用存储引擎) | 大一统 | 一个引擎覆盖多种数据类型,运维成本低 |
| 前端框架(全栈 vs 微前端) | 来回摇摆 | 目前全栈框架在回归 |
本质规律:耦合强度决定正确选择
识别系统组件之间的耦合强度,是判断用哪种哲学的第一性原理。
紧密耦合(调用频繁、共享状态、低延迟要求)
→ 大一统更合适
→ 拆开反而引入 IPC 开销和状态同步问题
→ 例:内核、数据库引擎、init 系统
松散耦合(独立运行、偶尔通信、对延迟不敏感)
→ 小零件更合适
→ 硬绑在一起反而拖慢迭代、放大故障半径
→ 例:命令行管道、微服务、浏览器插件
systemd 争议的真正根,在于 Linux 系统管理的各个组件(日志、网络、设备管理)到底是"紧密耦合"还是"松散耦合"------反对派认为它们是松散的(不该绑在一起),支持派认为它们实际上是紧密的(进程日志必然和进程生命周期挂钩,网络状态必然影响服务启动依赖)。事实证明支持派的判断更准确。
我从中得出的判断框架
- 不要先站队,先问耦合强度。遇到"大一统还是拆零件"的选择,不做风格偏好判断,先画组件间的调用关系图,看频率、看共享状态、看延迟要求
- 在错误的地方用错误的哲学是灾难。把 Linux 内核拆成微服务是灾难,把命令行工具写成大一统框架也是灾难
- 两种哲学不是对错问题,是场景适配问题。一个人的"单体怪兽"是另一个人的"内部重构空间";一个人的"灵活组合"是另一个人的"集成噩梦"
- 争论本身常常浮于表面。systemd 十年论战中,喊"违反 Unix 哲学"的人和喊"解决了真实问题"的人,讨论的经常不是同一层------前者在谈美学,后者在谈实践