本文档整理了操作系统内核、数据结构、存储系统、分布式系统等底层核心技术的精华要点。
📚 目录
一、内存管理
1.1 伙伴系统
核心机制
将内存划分为大小均为 2 的幂 的块,通过分裂与合并管理空闲块。
分配流程
- 请求大小向上取整到 2 的幂
- 从对应空闲链表取块
- 若无则分裂更大块,将伙伴插入低一级链表
释放流程
- 将块放回链表
- 检查伙伴是否空闲
- 若是则合并并继续向上合并
优缺点
| 优势 | 局限 |
|---|---|
| 分配/释放速度快(位运算) | 存在内部碎片(向上取整浪费) |
| 外部碎片较少 | 无法完全避免外部碎片 |
1.2 Slab 分配器
定位
构建于伙伴系统之上的对象缓存管理器。
核心结构
Cache(管理器)
- 为特定大小/类型对象创建的管理器
- 维护三个链表:
slabs_full:完全使用的 slabslabs_partial:部分使用的 slabslabs_free:完全空闲的 slab
Slab(内存块)
- 连续内存块(通常一个或多个页)
- 划分为等大小对象
- 包含空闲链表等元数据
工作流程
分配
- 优先从
slabs_partial取 slab - 再从 slab 空闲链表取对象
- 若无则从
slabs_free取或向伙伴系统申请新 slab
释放
- 对象放回 slab 空闲链表
- 根据空闲数量调整 slab 所在链表
- 若 slab 完全空闲可能归还伙伴系统
优势
- 减少伙伴系统调用
- 降低内部碎片
- 显著提升内核对象分配性能
1.3 伙伴系统与 Slab 的协同
┌─────────────────────────────────────┐│ 伙伴系统 ││ 管理大块内存(2 的幂) │└──────────────┬──────────────────────┘ │ 提供整页 ▼┌─────────────────────────────────────┐│ Slab 分配器 ││ 细分为对象,精确匹配对象大小 │└─────────────────────────────────────┘- 外部碎片由伙伴系统控制
- 内部碎片由 Slab 大幅降低(但仍存在页内尾部分片)
1.4 内存利用率最大化技术
| 技术 | 说明 |
|---|---|
| 内存压缩 | 移动已分配对象,合并空闲空间 |
| 内存去重 (KSM) | 合并内容相同的物理页 |
| 精细分配器 | jemalloc、tcmalloc:细粒度 size class,多级缓存,自适应调整 |
| 其他优化 | 对象池化、元数据内嵌、大页支持、生命周期分区等 |
1.5 概念澄清
Slab 分配器 ≠ Redis 缓存
- Slab:内核内存管理组件,缓存内核对象
- Redis:用户态数据库,其内部可能借鉴 Slab 思想,但层次与目的不同
Cache 与 Slab 的关系
- Cache(如
kmem_cache):是管理器,管理多个 slab - Slab:是被管理的实际内存块
1.6 代码核心逻辑
| 操作 | 核心逻辑 |
|---|---|
| 分裂循环 | 从高阶向低阶分裂,每次将伙伴插入低阶空闲链表 |
| 合并循环 | 从释放块阶开始,检查伙伴是否空闲,是则移除伙伴并合并,继续向上尝试 |
| Slab 分配 | 通过空闲链表取对象,调整 slab 在三个链表中的位置 |
| Slab 释放 | 对象归还空闲链表,必要时移动 slab 链表归属 |
1.7 核心思想总结
- 伙伴系统利用二进制特性快速分裂/合并,控制外部碎片
- Slab 通过对象缓存,在性能与空间利用率间取得平衡
- 二者组合是操作系统内核内存管理的基础
- 极致利用率需结合多种技术,但受硬件对齐、元数据等限制无法达到 100%
二、数据结构
2.1 LRU Cache
LRU(Least Recently Used)Cache 是一种容量受限的缓存结构,当空间占满时优先淘汰最长时间未被访问的数据。
核心要求
get和put操作均达到 O(1) 时间复杂度- 准确维护数据的访问顺序
数据结构设计
采用双向链表 + 哈希表的组合:
┌────────────────────────────────────────┐│ 双向链表(存储数据) ││ ┌──────┐ ┌──────┐ ┌──────┐ ││ │ Head │◄──►│ Node │◄──►│ Tail │ ││ └──────┘ └──────┘ └──────┘ ││ ▲ ││ │ 索引 ││ ▼ ││ 哈希表(快速定位) ││ key ──► node 指针 │└────────────────────────────────────────┘- 双向链表:每个节点存储键值对,
prev和next指针串联- 头部:最近使用的节点
- 尾部:最久未使用的节点
- 哈希表:数组 + 链地址法,快速定位键对应的链表节点
核心操作流程
get(key)
- 通过哈希表定位节点
- 若存在:将节点移动到链表头部,返回值
- 若不存在:返回 -1
put(key, value)
- 检查键是否已存在
- 存在:更新值,移动到头部
- 不存在:创建新节点插入头部,存入哈希表
- 若超过容量:移除尾部节点,从哈希表删除
关键代码逻辑
| 函数 | 作用 |
|---|---|
remove_tail() | 摘除尾部节点、从哈希桶移除、释放内存 |
move_to_head() | 将节点剥离并插入头部 |
常见误区澄清
| 误区 | 澄清 |
|---|---|
| 数据存储位置 | 所有数据实体都保存在双向链表的节点中,哈希表只是指针索引 |
| 与普通存储的区别 | 普通存储没有容量限制和淘汰策略,LRU 的”缓存”特性体现在有限容量和自动淘汰 |
| Go map 是否自带 LRU | Go 的 map 仅提供哈希表功能,不维护顺序也无淘汰机制 |
2.2 跳表 (Skip List)
数据结构本质
跳表是一种概率平衡的多层有序链表:
- 底层(第 0 层)包含所有节点
- 向上每一层是下一层的随机子集
- 节点层数由几何分布随机决定(常用概率 p=0.5)
Level 3: ┌─────┐ │ 4 │Level 2: ├─────┤────────┐ │ 4 │ 7 │Level 1: ├─────┤───┐ ├─────┐ │ 4 │ 6 │ │ 9 │Level 0: ┌───┼─────┼───┼────┼─────┼─────┐ │ 1 │ 4 │ 6 │ 7 │ 9 │ 10 │ └───┴─────┴───┴────┴─────┴─────┘核心操作
查找
- 从最高层开始
- 在当前层向右移动直到下一节点 key ≥ 目标
- 下降一层,重复至底层
- 路径确定,无回溯
插入
- 查找确定每层的前驱节点(update 数组)
- 随机生成新节点层数 k
- 将新节点插入第 0 到第 k 层
- 若 k 超过当前层数,提升跳表层数
删除
- 类似查找定位节点
- 在各层将其从链表中移除
- 可能降低跳表层数
随机性作用
- 仅用于插入时决定节点层数,保证高层节点稀少,形成”快速通道”
- 查找过程完全确定,依靠高层稀疏性实现跳跃
- 平均时间复杂度 O(log n)
- 随机化替代了平衡树复杂的旋转操作
关键设计细节
| 设计点 | 说明 |
|---|---|
| 节点层数连续性 | 节点若出现在第 k 层,则必须出现在所有 0..k 层 |
| update 数组 | 记录每层插入位置的前驱节点,简化指针调整 |
| 头节点 | 层数为最大层数,其 forward 数组作为各层链表起点 |
性能与特点
| 特性 | 说明 |
|---|---|
| 时间复杂度 | 平均 O(log n),最坏 O(n)(概率极低) |
| 空间复杂度 | O(n),每个节点平均约 2 个前向指针(p=0.5 时) |
| 优势 | 支持有序遍历、范围查询,代码量少,不易出错 |
2.3 布隆过滤器 (Bloom Filter)
核心原理
数据结构
- 长度为 m 的位数组(初始全 0)
- k 个独立的哈希函数
添加元素
- 对元素计算 k 个哈希值
- 将位数组对应位置设为 1
查询元素
- 计算同样的 k 个哈希位置:
- 若所有位均为 1 → 可能存在(存在假阳性)
- 若任意一位为 0 → 一定不存在(无假阴性)
实现关键点
哈希函数设计
- 通常用双哈希法(如 FNV-1a + DJB2)生成 k 个独立哈希值
参数优化
m = -n·ln(p) / (ln2)² # 位数组大小k = (m/n)·ln2 # 哈希函数个数其中 n 为预期元素数,p 为可接受的误判率
位操作
- 使用
[]byte存储位 - 通过
idx/8定位字节,1 << (idx%8)定位位掩码
本质作用
- 极简布尔判断:返回”可能存在”或”一定不存在”
- 用极小的空间(每个元素仅占数比特)实现海量数据的存在性过滤
- 允许假阳性,但杜绝假阴性
优缺点
| 优势 | 局限 |
|---|---|
| 空间效率极高(比哈希表节省 90%+) | 不支持删除(标准版) |
| 时间恒定 O(k) | 存在误判率 |
| 无需存储元素本身 | 参数需预先估计 |
典型应用场景
- 缓存穿透防护
- 数据库查询优化
- 网络爬虫 URL 去重
- 垃圾邮件/恶意 URL 过滤
- 分布式系统(HBase、Cassandra、Redis)
三、存储系统
3.1 WAL (预写日志)
核心思想
先写日志,后写数据
- 所有修改操作必须先在日志文件中记录完整信息
- 强制落盘(fsync)后才能应用到内存/数据存储
崩溃恢复
- 系统重启后,通过重放日志文件中的操作
- 使数据恢复到崩溃前的一致状态
原子性与持久性
- 保证事务的持久性(已提交的操作不会丢失)
- 保证原子性(部分写入的日志在恢复时会被丢弃)
设计要点
日志记录格式
- 序列号、操作类型(SET/DELETE)、键、值(仅 SET)、校验和
- 采用固定长度结构,方便直接读写
校验和
- 使用简单的 XOR 对记录关键字段计算
- 用于检测日志是否完整(防止部分写入)
写入流程
wal_append:追加记录到文件缓冲区wal_sync:调用fflush + fsync强制落盘- 日志落盘成功后,更新实际存储
恢复流程
- 顺序读取日志文件
- 对每条记录重新计算校验和
- 校验通过的记录通过回调函数应用到存储(重放操作)
- 遇到校验失败则停止恢复
3.2 SSTable 与 LSM 树
SSTable 基础设计
文件结构
┌─────────────┬─────────────┬─────────────┐│ 数据区 │ 索引区 │ 尾部元数据 │└─────────────┴─────────────┴─────────────┘- 数据区:键长度(4B) + 键数据 + 值长度(4B) + 值数据
- 索引区:键长度(4B) + 键数据 + 偏移量(4B)
- 尾部元数据:index_offset(索引区起始偏移)、num_entries(记录数)
查找流程
- 读取尾部元数据
- 将索引区全部加载到内存
- 二分查找定位键
- 根据偏移量读取键值对
写入流程(两阶段)
sstable_writer_add:顺序追加数据区sstable_writer_finish:构建索引并写入索引区和元数据
结合跳表与 WAL:LSM 树引擎
写入路径
写入请求 → WAL(保证持久性) → 跳表(MemTable) ↓ Flush(后台写入 SSTable)读取路径
查询请求 → MemTable → Immutable MemTable → SSTable 文件(多层) ↓ 合并结果返回最新版本持久化与恢复
- 系统启动时,重放 WAL 中的日志
- 重建内存中的 MemTable
- 使用 fsync 控制持久化时机
后台合并 (Compaction)
- 定期将多个 SSTable 文件合并
- 清除过期数据,降低读放大和空间放大
四、并发与同步
4.1 RCU (Read-Copy-Update)
核心思想
读写不对称
- 读者不阻塞写者
- 写者通过原子替换将新数据”发布”
- 旧数据被隔离,等待所有可能仍在访问旧数据的读者完成(宽限期)
- 最后安全释放旧数据
宽限期 (Grace Period)
C 语言实现
- 全局 epoch 计数器与每线程的活跃标志
- 写者递增 epoch,遍历所有线程
- 等待旧 epoch 内进入临界区的读者全部退出
Go 语言实现
- 两个原子计数器分别记录两个 epoch 的活跃读者数
- 写者等待对应 epoch 的计数器归零
核心约束
- 读者在临界区内不能阻塞或睡眠
- 否则会导致宽限期无限延长
内存模型与内存屏障
C11 标准
- 显式使用
atomic_thread_fence或带有内存顺序的原子操作(acquire/release) rcu_read_lock:acquire 屏障确保临界区内读操作不会提前到锁之前rcu_read_unlock:release 屏障防止读操作延迟到锁之后rcu_assign_pointer:全屏障保证新数据初始化完成后再发布指针
Go 语言
sync/atomic包自动插入必要的内存屏障- 开发者仍需理解语义,正确编排逻辑顺序
C vs Go 实现差异
| 特性 | C 实现 | Go 实现 |
|---|---|---|
| 线程管理 | 手动管理线程列表 | 利用 goroutine 轻量调度 |
| 内存屏障 | 显式调用 | 原子操作内置屏障 |
| 延迟回收 | 手动管理 | 垃圾回收自动延迟回收 |
| 本质 | 一致:通过 epoch 划分读者世代 |
4.2 CFS 调度算法
公平性本质
CFS 摒弃传统优先级队列,引入虚拟运行时间 (vruntime) 作为唯一调度依据。
nice → 权重映射
| nice 值 | 权重 | 说明 |
|---|---|---|
| -10 | 9548 | 高优先级 |
| 0 | 1024 | 基准 |
| 10 | 70 | 低优先级 |
vruntime 更新公式
inc = delta_ms × BASE_WEIGHT / weight- 高权重进程的 inc 小,vruntime 增长缓慢
- 低权重进程增长快
最小堆实现调度队列
- 所有就绪进程按 vruntime 组织成最小堆
- 堆顶始终是 vruntime 最小的进程
- 调度时直接取出堆顶运行
- 运行后重新计算 vruntime 并插入堆中
优先级隐式体现
代码中没有显式的优先级比较,而是通过:
- 权重影响 vruntime 增长速率
- 堆选择 vruntime 最小的进程
- 最终实现高权重进程被频繁调度
五、网络与 I/O
5.1 Epoll 机制
核心机制
epoll 是 Linux 下高效的 I/O 多路复用技术,通过三个系统调用实现:
| 系统调用 | 作用 |
|---|---|
epoll_create1 | 创建 epoll 实例 |
epoll_ctl | 添加或删除被监视的文件描述符 |
epoll_wait | 阻塞等待事件就绪 |
可以同时管理成千上万个连接,只在事件发生时通知应用程序,避免了轮询开销。
C 语言实现要点
- 将监听 socket 和客户端 socket 设为非阻塞
- 选择边缘触发 (ET) 模式以获得更高效率
- 事件循环中,
epoll_wait返回后遍历就绪事件 - 对监听 socket 调用
accept接受新连接 - 对客户端 socket 循环读写直到返回
EAGAIN - 处理
EPOLLERR和EPOLLHUP事件,及时关闭异常连接
Go 语言实现路径
直接使用 syscall
- 使用
syscall或golang.org/x/sys/unix包 - 代码结构与 C 版本类似,语法更简洁
- 借助
defer方便资源释放
推荐使用标准库 net 包
- 在 Linux 底层同样基于 epoll
- 封装成同步阻塞风格的 goroutine-per-connection 模型
- 每个连接启动一个 goroutine
- 调用
conn.Read/Write时,Go 运行时自动挂起 goroutine - 开发者无需手动处理非阻塞、事件循环和 EAGAIN
手动 epoll vs net 包
| 对比项 | 手动 epoll | net 包 |
|---|---|---|
| 性能 | 理论最优,无运行时开销 | 对绝大多数场景绰绰有余 |
| 编程复杂度 | 高,需精细管理状态机 | 低,代码可读性和可维护性高 |
| 适用场景 | 极致性能要求的基础设施 | 一般业务场景 |
| 本质 | ”直接操纵引擎" | "驾驶封装好的汽车” |
事件触发与处理流程
网卡数据到达 ↓协议栈处理 → socket 接收缓冲区 ↓内核检查 socket 是否被 epoll 监控 ↓满足注册事件 → 加入就绪队列 → 唤醒等待线程 ↓epoll_wait 返回 → 遍历就绪事件数组 ↓根据 fd 类型分发:监听 socket / 客户端 socket六、分布式系统
6.1 一致性哈希
核心思想
把哈希空间想象成一个数轴(0 ~ 2³²-1):
- 所有真实节点通过虚拟节点在数轴上各自占据一些位置
- 把这些位置按数值从小到大排序,得到一个有序的数组
查找 key 的归属
- 给定 key,算出哈希值 h
- 在有序数组里,找到第一个哈希值 ≥ h 的虚拟节点
- 若 h 比所有哈希值都大,则回到数组的第一个元素(数轴首尾相连)
偏移量描述
- 计算 h 与每个虚拟节点哈希值的差值(偏移量)
- 找到差值最小且 ≥0 的节点
- 若所有差值 <0,则取绝对值最小的差值(对应第一个节点)
节点增减的影响
加入新节点
- 生成一批虚拟节点,在数轴上插入新的哈希位置
- 原来落在这些新位置附近的 key,会”偏移”到新节点上
- 其他 key 的归属保持不变
移除节点
- 原本指向该节点虚拟位置的 key 会偏移到顺时针方向的下一个节点
虚拟节点的作用
- 让每个真实节点在数轴上拥有多个”锚点”(多个哈希值)
- 就像给每个真实节点分配一组均匀分布的偏移位置
- 使整个数轴被划分成更小的区间
- 每个真实节点管理的区间总长度大致相等,实现负载均衡
6.2 Gossip 协议
核心思想
基于概率传播的去中心化信息扩散机制:
- 每个节点周期性随机选择少量其他节点交换自身状态
- 通过冗余通信实现最终一致性
与全广播的对比
| 特性 | 全广播 | Gossip |
|---|---|---|
| 每轮总消息量 | O(N²) | O(N) |
| 收敛所需轮数 | O(1) | O(log N) |
| 总消息量 | O(N²) | O(N log N) |
| 单节点负载 | 随 N 增长 | 恒定 |
| 可扩展性 | 差 | 极高 |
Gossip vs Raft
| 特性 | Raft | Gossip |
|---|---|---|
| 一致性 | 强一致 | 最终一致 |
| 架构 | 依赖领导者 | 无中心节点 |
| 单轮消息 | O(N) | O(1) 每节点 |
| 应用场景 | 日志复制 | 成员管理、服务发现 |
| 关系 | 互补:Raft 处理强一致操作,Gossip 维护成员关系 |
反熵机制
- 每次通信交换完整或增量状态
- 接收方将新信息合并到本地成员表
- 信息在节点间单向增长,直至所有节点掌握全集
- 超时机制剔除故障节点,使信息量有界且自适应
6.3 Raft 共识算法
C 语言实现难点
状态机与并发
- Raft 节点有 Follower、Candidate、Leader 三种角色
- 状态转换需要严格保护
- 采用单线程事件驱动模型,通过 epoll 统一管理网络、定时器和用户输入
网络通信
- 使用 UDP 作为传输层
- 自行处理消息的序列化、粘包与拆包
- 消息格式采用简单的二进制结构
- 使用
htonl/ntohl保证字节序一致
持久化
- 日志、任期、投票信息必须持久化
- 使用
open + write + fsync保证数据落盘 - 通过临时文件 + rename 实现原子更新
- 快照机制用于压缩日志
定时器与选举
- 选举超时和心跳定时器通过 Linux
timerfd实现 - 与 epoll 集成,实现高精度、非阻塞的计时
- 每个节点独立随机生成选举超时时间
C vs Go 实现对比
| 对比项 | C 语言 | Go 语言 |
|---|---|---|
| 抽象层次 | 手动调用系统 API,管理非阻塞 I/O 和事件循环 | net 包封装 socket、epoll,goroutine 处理并发 |
| 开发效率 | 需更多代码处理内存、序列化、错误处理 | 简洁语法、垃圾回收、丰富标准库 |
| 性能 | 极致性能场景有优势 | 运行时经过高度优化,对大多数场景足够 |
| 适用场景 | 嵌入式、与 C 代码库集成、绝对性能 | 快速开发、稳定性和可维护性 |
七、其他核心技术
7.1 TCMalloc
TCMalloc(Thread-Caching Malloc)是 Google 开发的高性能内存分配器,专为多线程环境设计。
核心设计
线程本地缓存
- 每个线程独立拥有一组空闲对象链表
- 分别对应不同的大小类
- 绝大多数分配和释放操作仅在本地缓存进行,完全无需加锁
- 只有本地缓存为空或过满时,才与中央缓存交互
分层缓存结构
┌─────────────────────────────────────┐│ 线程本地缓存 │ ← 无锁,单链表存储空闲对象├─────────────────────────────────────┤│ 中央缓存 │ ← 全局互斥锁保护,Span 为单位├─────────────────────────────────────┤│ 页堆 │ ← 向操作系统申请/释放虚拟内存└─────────────────────────────────────┘大小类与对齐策略
- 小对象尺寸划分为一系列大小类(8, 16, 32, 64, 128, …)
- 请求分配时向上取整到最近的大小类
- 减少内部碎片
Span 与对象管理
- Span:一组连续的物理页
- 一个 Span 可被切分为多个相同大小的小对象
- 中央缓存以 Span 为单位存储空闲对象
- 当 Span 内所有对象都空闲时,可归还给页堆
地址映射 (PageMap)
- 从内存地址到所属 Span 的快速查找结构
- 通过任意指针可以常数时间定位到 Span
- 实现高效释放的关键
性能特点
- 多核可伸缩性:数百线程场景下依然保持低锁争用
- 吞吐量随核心数线性扩展
- 支持将空闲内存主动归还给操作系统
7.2 LZ77 压缩
核心思想
利用数据中的重复模式,将重复出现的字符串替换为一个匹配对:
- 偏移量 (offset):指向历史窗口中相同字符串的距离
- 长度 (length):重复字符串的长度
滑动窗口与哈希表加速
- 维护固定大小的滑动窗口(历史缓冲区)
- 使用环形缓冲区避免数据移动
- 引入哈希表:将窗口中每个可能匹配位置的三字节哈希值记录在表中
- 压缩时对当前位置计算哈希,直接在表中查找可能匹配的位置
偏移量的关键角色
| 阶段 | 作用 |
|---|---|
| 压缩时 | 记录重复字符串在历史窗口中的距离 |
| 解压时 | 根据偏移量从已输出数据中复制内容,重建原始序列 |
与文法压缩的联系
- “八个1”的表达方式与形式语言中的重复规则相似
- LZ77 实际上是基于有限上下文的引用
- 文法压缩(如 Sequitur)能够提取更复杂的重复模式
- 揭示了数据压缩与计算理论(形式语言)之间的内在关联
7.3 MVCC (多版本并发控制)
MVCC vs xv6 COW 的异同
| 特性 | 相似点 | 本质区别 |
|---|---|---|
| 核心思想 | 都利用”多版本”思想 | - |
| 写操作 | 都创建新副本 | - |
| xv6 COW | 内存管理层面的优化 | 版本数通常为 2,不涉及事务语义 |
| MVCC | 数据库事务并发控制协议 | 维护长版本链,支持快照读、冲突检测、ACID |
MVCC vs Git 的类比
| 类比点 | MVCC | Git |
|---|---|---|
| 版本历史 | 每个键的版本链表 | 一个文件的线性提交历史 |
| 快照 | 事务的开始时间戳 | 基于某个提交的快照 |
| 提交操作 | 生成新的提交节点 | 生成新的提交节点 |
本质区别
- Git:有向无环图(DAG),支持分支、合并、人工冲突解决,适用于离线协作
- MVCC:单调递增的全局提交时间戳,版本历史呈线性,通过写冲突检测自动回滚事务
线性历史 vs DAG
| 特性 | 线性 MVCC | DAG (Git) |
|---|---|---|
| 适用场景 | 高并发、自动事务管理 | 版本控制、分布式协作 |
| 冲突处理 | 自动回滚 | 显式分支合并、人工解决 |
| 复杂度 | 简单且高效 | 支持更复杂的协作模式 |
| 扩展 | 可扩展为分布式版本控制数据库(如 Dolt) | - |
总结
目前只是阅读了底层代码并且进行了学习,在未来将尝试手写这些技术
本文档涵盖了从操作系统内核到分布式系统的核心技术:
- 内存管理:伙伴系统、Slab 分配器、TCMalloc
- 数据结构:LRU Cache、跳表、布隆过滤器
- 存储系统:WAL、SSTable、LSM 树
- 并发同步:RCU、CFS 调度
- 网络 I/O:Epoll 机制
- 分布式系统:一致性哈希、Gossip、Raft
- 其他技术:LZ77 压缩、MVCC
这些技术构成了现代计算机系统的底层基石,理解它们对于系统设计和性能优化至关重要。
部分信息可能已经过时








