嵌入式面试真题第 01 题:低速 Flash 与 1MB SRAM 下的通用缓存架构设计

在这里插入图片描述

问题

在一款资源受限的嵌入式设备中,系统只有 1MB SRAM 和一块读延迟不稳定的低速 SPI Flash。业务需要从 Flash 中持续或按需读取资源包、配置表、字库、语音片段、模型分块、日志回放数据或其他离线资源,并供应用线程实时或准实时消费。

如果应用线程直接等待 Flash 读取,系统可能因为低速 I/O、随机访问、文件碎片、擦写干扰或后台任务抢占而出现卡顿、超时或数据供应不连续。你会如何设计一套通用 Buffer/Cache 机制,使应用线程尽量读取 SRAM 中已经准备好的数据,而不是直接阻塞等待 Flash?

回答

结论:不要让应用线程直接等待低速 Flash。更合理的设计是在应用和 Flash 之间增加一层 SRAM Cache/Buffer Manager:应用优先读 SRAM 中已经准备好的数据;后台 I/O Worker 根据访问模式、缓存水位和请求压力提前把 Flash 数据搬入 SRAM;缓存层再用 Ring Buffer、Block Cache 或 Segment Cache 组织数据。

这类问题的关键不是“缓存越大越好”,而是把低速存储的长尾延迟、随机访问开销、请求碎片化和后台 I/O 抖动从应用实时路径中隔离出来。Buffer 容量和水位线必须按应用消费速率、Flash 最坏读取延迟、后台 I/O 调度抖动和安全余量量化,而不是按平均读速率拍脑袋。

总体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
flowchart LR
A[应用线程 / 业务模块] --> B[Cache API]
B --> C{SRAM Cache/Buffer Manager}
C --> D[Ring Buffer\n顺序数据流]
C --> E[Block Cache\n随机块访问]
C --> F[Segment Cache\n顺序窗口 + 随机块缓存]
C --> G[Watermark Controller\n高/目标/低/危险/空水位]
G --> H[Request Queue\n预读/紧急读/后台读]
H --> I[I/O Worker]
I --> J[请求合并\n对齐读取\n访问模式判断]
J --> K[低速 Flash / 块设备 / 文件]
K --> I
I --> C
C --> B

这个架构里,应用看到的是缓存 API,而不是 Flash API。应用线程不直接调用底层 flash_read(),也不把低速 I/O 放进关键路径。Cache/Buffer Manager 负责命中判断、预读窗口、随机块缓存、请求合并、淘汰策略和水位线控制。

机制与 Linux/开源实现的对应关系

本题机制 Linux 或开源机制 能否直接使用 主要参考价值
Ring Buffer Linux kfifo, io_uring SQ/CQ ring Linux 内核/用户态可直接使用对应接口;小系统多为参考 单生产者/单消费者 FIFO、共享 ring、提交队列/完成队列。
Block Cache Linux page cache, XArray, dm-cache, bcache Linux 下 page cache 是默认机制;dm-cache/bcache 可作为块层缓存使用 offset 到缓存页/块的映射、缓存块状态、淘汰、脏块、块级 promotion/demotion。
Segment Cache readahead(2), posix_fadvise(2), page cache Linux 用户态可通过 readahead()/posix_fadvise()影响内核预读;自定义缓存可参考 顺序窗口、访问模式提示、顺序读扩大窗口、随机读关闭预读、已用数据释放。
水位线机制 Linux zone watermarks、vm.min_free_kbytes、dirty writeback thresholds 不能直接作为业务缓存组件使用,但机制高度类似 高/低水位触发后台回收或写回,危险水位触发同步回收或限流。
缓存容量量化 page cache/readahead 的动态策略、块设备吞吐与尾延迟建模 Linux 已动态处理大部分场景;受限 SRAM 仍需显式计算 用消费速率、最坏延迟、调度抖动、吞吐余量反推缓存大小。

应用线程应该如何读数据

应用线程不建议直接调用底层 Flash 读接口,而应通过缓存层访问数据,例如:

1
2
3
4
cache_read(handle, offset, buf, len, timeout)
cache_acquire(handle, offset, len, &ptr, &actual_len, timeout)
cache_release(handle, ptr, actual_len)
cache_prefetch(handle, offset, len, priority)

cache_read() 适合普通拷贝式读取;cache_acquire() / cache_release() 适合零拷贝或少拷贝读取;cache_prefetch() 用于应用提前告诉缓存层“接下来大概率会读这段数据”。

应用线程遇到缓存不命中时,不应无限期阻塞。应按业务类型选择策略。

场景 推荐策略 说明
实时数据流 优先从缓存读;缓存不足则短超时等待;超时后降级 持续渲染、连续资源读取、协议流式处理等。
普通资源读取 允许等待,但必须有超时 字库、图标、配置表、索引表等。
后台任务 异步提交请求并等待事件或回调 日志扫描、资源遍历、后台校验等。

Linux 用户态对应的思想是:普通 read() 本身通常会经过 page cache;如果应用知道访问模式,可以通过 posix_fadvise() 提示 POSIX_FADV_SEQUENTIALPOSIX_FADV_RANDOMPOSIX_FADV_WILLNEEDPOSIX_FADV_DONTNEED;如果要显式预热某段文件,可以调用 readahead()。这些接口不等价于自己写 Cache Manager,但它们体现了同一个原则:应用声明访问模式,让缓存层提前或减少 I/O。

Buffer 形态如何选

通用 Buffer 不应只用一种结构。应根据访问模式分成三类:顺序数据流使用 Ring Buffer,随机访问资源使用 Block Cache,混合访问使用 Segment Cache。

顺序数据流使用 Ring Buffer

如果访问模式是顺序消费,例如连续资源读取、日志回放、语音资源流、模型分块读取,最适合使用 Ring Buffer。

Ring Buffer 的特点是读写指针单调推进,内存固定,不产生碎片。后台 I/O Worker 负责把后续数据写入 Ring Buffer;应用线程负责从 Ring Buffer 读取并消费。只要底层存储的长期有效吞吐高于应用消费速率,并且 Ring Buffer 能覆盖底层 I/O 最坏延迟,应用就不会频繁卡在 Flash 上。

1
2
3
4
flowchart LR
W[Writer / I/O Worker] -->|append| R[(Ring Buffer)]
R -->|consume| C[Reader / 应用线程]
R --> M[write index / read index\nfree space / readable bytes]

高吞吐场景下,Ring Buffer 最好支持 claim/finish 语义:

1
2
3
4
5
ring_put_claim()  -> 取得可写 SRAM 地址
io_read_async() -> I/O 直接写入该地址或写入 staging buffer
ring_put_finish() -> I/O 完成后提交有效长度
ring_get_claim() -> 应用取得可读地址
ring_get_finish() -> 应用消费后释放空间

可参考的 Linux/开源实现

  1. Linux kfifo

    kfifo 是 Linux 内核通用 FIFO/ring buffer 工具。它提供 kfifo_in()kfifo_out()kfifo_put()kfifo_get() 等接口。官方文档明确说明,在只有一个并发读者和一个并发写者时,相关宏不需要额外锁;这正好对应“一个预读生产者 + 一个应用消费者”的典型 Ring Buffer 模式。

    可参考点:

    • 固定内存 FIFO,不做运行期频繁分配。
    • 单生产者/单消费者可以简化锁设计。
    • 多生产者或多消费者需要外部锁或带锁变体。
    • in/out 指针推进与 wrap-around 处理可以作为 Ring Buffer 实现参考。
  2. io_uring

    io_uring 不是普通数据缓存,但它的提交队列 SQ 和完成队列 CQ 是共享 ring buffer 模型。用户态把 I/O 请求写入 SQ,内核把完成结果写入 CQ。这个机制非常适合作为“请求队列 + 完成队列”的参考。

    可参考点:

    • 数据路径与控制路径分离。
    • SQ 表示应用提交的 I/O 请求,CQ 表示后台 I/O 完成事件。
    • 多个请求可以异步在途,完成顺序不一定等于提交顺序,因此每个请求必须有 user_data 或 request id。
    • 批量提交和批量完成可以减少上下文切换和系统调用成本。

随机访问资源使用 Block Cache

如果访问模式是随机读取,例如字库、图标、配置表、索引、模型权重片段,应使用定长块缓存。

Block Cache 把底层数据按固定粒度切块,例如 512B、1KB、2KB、4KB 或更大的资源块。每个缓存块记录逻辑地址和状态。应用读取某个 offset 时,缓存层先把 offset 映射到 block id,再查块表;命中则直接返回 SRAM 数据,未命中则提交块读取请求。

1
2
3
4
5
6
7
8
9
10
11
12
flowchart TD
A[read resource_id + offset + len] --> B[计算 block key]
B --> C{Block Table 命中?}
C -->|命中| D[pin block]
D --> E[返回块内数据]
E --> F[release / unpin]
C -->|未命中| G[选择 victim block]
G --> H[状态置为 LOADING]
H --> I[提交底层块读取]
I --> J[校验完成状态]
J --> K[状态置为 VALID]
K --> D

每个缓存块建议至少记录:

1
2
3
4
5
6
7
resource id
logical block index
logical offset
valid length
state: EMPTY / LOADING / VALID / DIRTY / ERROR
last access tick or access generation
pin count

随机访问缓存通常需要 LRU、clock、2Q 或更简单的近似淘汰策略。对关键块要支持 pin count,避免应用正在使用的块被回收。只读资源可以不考虑 DIRTY;如果缓存层也承接写入,则必须增加 DIRTY 状态、写回策略和掉电一致性设计。

可参考的 Linux/开源实现

  1. Linux page cache

    Linux 文档说明,page cache 是用户和内核与文件系统交互的主要方式,普通 reads、writes 和 mmaps 都会经过 page cache,除非使用类似 O_DIRECT 的方式绕过。它本质上就是文件 offset 到内存页/folio 的缓存映射。

    可参考点:

    • 文件数据先进入内存缓存,再供读写路径使用。
    • 缓存粒度以页/folio 为单位,而不是任意小字节。
    • 普通读路径不一定每次都打到底层块设备。
    • 在内存压力下,干净页可以回收,脏页要先写回。
  2. Linux XArray

    XArray 是 Linux 内核里的稀疏数组/索引结构,文档说明它像一个很大的指针数组,也能满足 hash 或动态数组的部分需求,并且 page cache 是 XArray 最重要的使用者之一。

    可参考点:

    • 用逻辑页号或块号作为索引,查找缓存对象。
    • 适合密集或局部密集的整数索引。
    • 支持无锁读侧查找思路,即读多写少路径要尽量轻。
    • 比链表遍历更适合 Block Cache 的按 offset 查找。
  3. dm-cache

    dm-cache 是 Linux device-mapper 的块缓存 target,用较小较快设备缓存较大较慢设备的数据。文档中它把 origin device 划成固定大小块,并由策略模块决定哪些块 promotion 到 cache device、哪些块 demotion 回 origin device。

    可参考点:

    • 固定块大小。
    • 缓存数据和元数据分离。
    • 缓存策略插件化。
    • promotion/demotion 与 Block Cache 的装入/淘汰类似。
  4. bcache

    bcache 是 Linux 块层缓存。文档说明它使用混合 btree/log 跟踪 cached extents,并且会识别大顺序 I/O,默认倾向跳过大顺序流,把缓存资源留给随机 I/O。

    可参考点:

    • 缓存不应该机械缓存所有数据。
    • 大顺序流可能绕过随机块缓存,避免污染缓存。
    • block/extents 的元数据索引和错误处理机制值得参考。
    • 顺序 I/O 与随机 I/O 要采用不同策略。

混合访问使用 Segment Cache

如果资源大体顺序读取,但中间偶尔跳转,例如媒体文件、资源包、数据库式索引、模型权重分段加载,可以使用 Segment Cache。它不是单一固定数据结构,而是 Ring/Window + Block Cache 的组合:一部分 SRAM 做顺序预读窗口,另一部分 SRAM 做小型随机块缓存。

1
2
3
4
5
6
7
8
9
10
11
flowchart TD
A[应用读取 offset] --> B{访问模式判断}
B -->|连续递增| C[Sequential Window]
B -->|小范围回跳/索引读取| D[Random Block Cache]
B -->|大跳转| E[重置顺序窗口]
C --> F[预读 offset 后续区间]
D --> G[缓存索引块/热点块]
E --> H[丢弃旧窗口或降权]
F --> I[返回数据]
G --> I
H --> I

典型拆法如下:

SRAM 区域 作用 典型比例
Sequential Window 顺序预读窗口,保障连续吞吐 50% ~ 75%
Random Block Cache 缓存索引、字库、图标、跳转块 15% ~ 35%
I/O Staging Buffer 处理对齐、回绕和非连续目标 5% ~ 15%
Cache Metadata 块表、请求队列、统计信息 1% ~ 5%

Segment Cache 的核心是“不要让顺序流污染随机热点块,也不要让随机块缓存承担连续吞吐”。顺序窗口负责吞吐,Block Cache 负责跳转和热点。

可参考的 Linux/开源实现

  1. readahead(2)

    readahead() 会把指定文件区域提前读入 page cache,使后续读取尽量从缓存满足,而不是阻塞在磁盘 I/O 上。它对应 Segment Cache 里的 Sequential Window:提前把将要顺序消费的数据放进内存。

  2. posix_fadvise(2)

    posix_fadvise() 允许应用声明访问模式。Linux 下 POSIX_FADV_SEQUENTIAL 会扩大 readahead window,POSIX_FADV_RANDOM 会关闭文件 readahead,POSIX_FADV_WILLNEED 会发起非阻塞预读,POSIX_FADV_DONTNEED 可请求释放不再需要的缓存页。

    这几个 advice 与 Segment Cache 的策略直接对应:

    advice 对应策略
    POSIX_FADV_SEQUENTIAL 顺序窗口扩大,积极预读。
    POSIX_FADV_RANDOM 不做盲目顺序预读,转向 Block Cache。
    POSIX_FADV_WILLNEED 提前装入即将访问的 segment。
    POSIX_FADV_DONTNEED 已消费 segment 降权或释放。
  3. bcache

    bcache 文档明确提到,它会检测顺序 I/O 并跳过缓存,因为大顺序 I/O 通常不值得进入随机缓存。这是 Segment Cache 里“顺序流和随机热点分开处理”的典型参考。

预读线程与应用线程之间的机制

推荐使用“请求队列 + 后台 Worker + 完成事件 + 水位线控制”的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
participant App as 应用线程
participant Cache as Cache/Buffer Manager
participant Q as Request Queue
participant Worker as I/O Worker
participant Store as Flash/文件/块设备

App->>Cache: cache_read(offset, len, timeout)
Cache->>Cache: 查询 Ring/Block/Segment Cache
alt 命中
Cache-->>App: 返回 SRAM 数据
else 未命中
Cache->>Q: 提交 read request
Cache->>Worker: 唤醒或提升预读服务能力
Worker->>Q: 合并相邻请求 / 排序 / 对齐
Worker->>Store: 发起底层读取
Store-->>Worker: 返回数据或完成事件
Worker->>Cache: 更新缓存块状态
Cache-->>App: 唤醒等待者 / 返回数据
end

应用线程负责提出需求和消费数据,不直接访问 Flash,不直接操作底层 I/O,不在实时路径里无限等待。后台 Worker 平时可以休眠;被低水位、缓存 miss 或显式预取请求唤醒后,合并相邻请求,转换为对齐读,把数据搬到 SRAM 缓存区或 staging buffer。完成后,Worker 更新缓存块状态并唤醒等待的应用线程。

对应 Linux 机制可以参考 io_uring:应用通过提交队列投递请求,内核异步处理后通过完成队列返回结果。即使不用 io_uring,这种“提交队列/完成队列/请求 id/异步完成”的结构也适合抽象后台预读。

优先级与资源仲裁

预读 Worker 不应简单固定为最低优先级。更合理的策略是动态服务能力或资源仲裁。

状态 预读策略
缓存高水位 Worker 休眠或降低预读强度。
缓存目标水位 维持稳定预读窗口。
缓存低水位 唤醒 Worker,合并读取,增大单次 read size。
缓存危险水位 暂停非关键读写,优先补充缓存。
应用实时读取即将超时 urgent request 插队,但要避免长期饿死普通请求。
底层存储正在写入或擦除 实时读优先;非关键写入延迟。

Linux 里类似的思想不是“把某个线程提优先级”,而是全局资源管理。例如:

  • page allocator 通过 min/low/high watermark 决定是否触发直接回收、唤醒 kswapd 或停止回收。
  • dirty writeback 通过 dirty_background_bytesdirty_bytes 等阈值决定后台写回和同步写回压力。
  • bcache 会根据缓存设备延迟做拥塞控制,延迟过高时降低相关流量。

因此,在本题里,“优先级”不应只理解成线程优先级,而应理解成资源仲裁:谁可以占用 Flash/I/O 通道,谁可以触发预读,谁必须被限流,谁可以插队。

水位线机制

水位线应同时管理可用数据量、空闲空间和请求压力。

1
2
3
4
5
6
7
8
9
10
stateDiagram-v2
[*] --> Target
Target --> High: 可读数据 >= 高水位
High --> Target: 应用消费后回落
Target --> Low: 可读数据 < 低水位
Low --> Danger: 补充速度仍低于消费速度
Danger --> Empty: 缓存耗尽或等待超时
Low --> Target: 预读补足
Danger --> Target: 紧急预读成功
Empty --> Target: 重新装载成功
水位 触发条件 动作
高水位 缓存数据已足够多,空闲空间不足 停止或降低预读,释放底层 I/O 给其他任务。
目标水位 正常稳定水位 维持预读窗口,保持吞吐平衡。
低水位 应用消费快于补充 唤醒 Worker,合并读取,增大单次 read size。
危险水位 即将不够应用消费 暂停非关键 I/O,允许紧急请求插队,优先补缓存。
空水位 发生 underrun 或 cache miss timeout 返回错误、阻塞到超时、填默认数据或进入业务降级。

水位线不能只按百分比配置,还应按时间配置。比如缓存中还有 64KB 数据并不一定安全;如果应用消费 2MB/s,它只能支撑约 32ms。如果底层存储最坏延迟可能达到 80ms,64KB 就不够。

可参考的 Linux/开源实现

  1. Linux physical memory watermarks

    Linux 的 struct zone 里有 _watermark。官方文档说明:空闲页低于 min watermark 时可能触发 direct reclaim/compaction;低于 low watermark 时唤醒 kswapd;高于 high watermark 时 kswapd 停止回收。这个机制和缓存水位线非常接近:低水位触发后台补救,高水位停止后台动作,危险水位触发同步或强制动作。

  2. vm.min_free_kbytes

    vm.min_free_kbytes 用于让 Linux VM 保留最低空闲内存,并据此计算各 lowmem zone 的 WMARK_MIN。这对应本题里的“危险水位/最低保底容量”:不是看到缓存快空了才动作,而是必须保留能覆盖最坏延迟的安全垫。

  3. dirty writeback thresholds

    dirty_background_bytes 表示达到多少脏内存后后台 flusher 开始写回;dirty_bytes 表示进程自己开始参与写回的阈值。它对应“高水位/低水位触发后台动作”的另一类例子:阈值不是为了显示百分比,而是为了改变系统行为。

缓存容量如何量化

最小缓存容量应基于最坏情况,而不是平均情况。

1
2
3
4
5
6
R_app      = 应用最大连续消费速率,单位 B/s
T_io_tail = 底层 I/O 最坏读取延迟,包括命令、寻址、文件/块映射、等待和搬运
T_sched = 后台 Worker 调度抖动和高优先级任务占用时间
T_jitter = 应用消费抖动、请求合并等待、锁竞争等额外不确定性
T_margin = 安全余量
B_min = 最小可用缓存容量

至少满足:

1
B_min >= R_app * (T_io_tail + T_sched + T_jitter + T_margin)

同时还要满足长期吞吐条件:

1
R_io_effective > R_app_peak_average

如果底层 I/O 的长期有效吞吐小于应用平均消费速率,单纯加 Buffer 只能推迟卡顿,不能从根本解决问题。此时应降低应用消费速率、压缩数据、改变资源布局、减少随机访问或换更快的存储。

顺序 Ring Buffer 容量

Ring Buffer 主要覆盖连续消费过程中的 I/O 空窗期。

1
2
B_ring_min = R_stream * T_cover
T_cover = T_io_tail + T_sched + T_margin

若应用消费速率为 2MB/s,底层最坏读延迟为 80ms,调度抖动 20ms,安全余量 50ms:

1
2
3
B_ring_min = 2MB/s * (80ms + 20ms + 50ms)
= 2MB/s * 150ms
= 300KB

这说明 64KB 缓存即使看起来不小,也只能支撑约 32ms 的 2MB/s 连续消费,无法覆盖 80ms 的长尾延迟。

Block Cache 容量

Block Cache 的容量不能只按时间算,还要看工作集大小和命中率。

1
2
B_block = N_blocks * block_size
N_blocks >= hot_working_set_blocks + pinned_blocks + miss_inflight_blocks

其中:

  • hot_working_set_blocks 是短时间内会重复访问的热点块数量。
  • pinned_blocks 是应用正在持有、不能淘汰的块数量。
  • miss_inflight_blocks 是同时在途读取的未命中块数量。

如果随机访问没有局部性,Block Cache 命中率会很差。此时应该优化资源布局或建立索引,而不是盲目扩大缓存。

Segment Cache 容量

Segment Cache 需要在顺序窗口和随机块缓存之间分配预算。

1
2
3
B_total_cache = B_seq_window + B_random_cache + B_staging + B_meta
B_seq_window >= R_stream * T_cover
B_random_cache >= hot_random_working_set

经验上,应先保证顺序窗口能覆盖最坏延迟,再把剩余空间分给随机块缓存。否则连续流会先卡住;而随机缓存不足通常只是命中率下降,不一定立即造成连续性失败。

Linux 机制里的容量参考

Linux page cache 会根据系统内存压力动态增减,不需要应用显式指定固定大小;但它仍然遵循相同的本质约束:page cache 命中时读路径快,未命中时要等待底层 I/O。readahead() 的作用是提前把文件区域读入 page cache;posix_fadvise() 的作用是告诉内核访问模式,让内核调整 readahead window 或缓存回收倾向。

对于受限 SRAM 的自定义缓存,不能假设系统会自动帮你保留足够 page cache,因此容量必须显式计算。

Flash 读取策略

底层读取应尽量顺序、大块、对齐、可合并。

推荐策略如下:

  1. 顺序预读,不要让应用每次读几十字节都打到底层 Flash。
  2. 读请求按 Flash 页、逻辑块、资源包块和 I/O 对齐要求对齐。
  3. 合并相邻小请求,减少命令开销和片选切换。
  4. 资源索引、目录表、偏移表在打开资源时提前加载。
  5. 避免在实时读取期间做长时间写入或擦除。
  6. 若必须读写共存,建立 I/O Arbiter:实时读优先,非关键写入延迟或降级。
  7. 对于只读资源,优先使用连续布局,减少碎片和跨块跳转。

Linux 里的对应机制包括:

  • readahead():提前把后续数据放进 page cache。
  • POSIX_FADV_SEQUENTIAL:告诉内核顺序访问,扩大 readahead window。
  • POSIX_FADV_RANDOM:告诉内核随机访问,避免错误的顺序预读。
  • bcache 的 sequential bypass:大顺序 I/O 不一定值得进入随机缓存。

缓存解决的是底层存储长尾延迟,不是长期带宽不足。因此读块大小和水位线要按最坏读延迟配置,而不是按实验室平均读速率配置。

I/O 搬运与完成路径

I/O 搬运不应被设计成“应用线程同步读到底”。更好的方式是异步提交、后台完成、完成后提交缓存状态。

1
2
3
4
5
6
7
flowchart LR
A[应用提交读取意图] --> B[Request Queue]
B --> C[I/O Worker]
C --> D[底层 read / async read / DMA]
D --> E[Completion Event]
E --> F[更新缓存状态]
F --> G[唤醒等待应用]

对应 Linux 实现里,io_uring 的 SQ/CQ ring 是很好的参考:请求与完成解耦,完成事件带结果和 request id;多个请求可以在途,完成顺序不保证等于提交顺序。即使系统没有 io_uring,这个模型仍然适合作为缓存层的内部结构。

最终回答组织方式

回答这类题时,可以按以下顺序展开:

  1. 先讲核心原则:应用线程不要直接等 Flash,要读 SRAM 缓存。
  2. 再讲分层架构:应用接口、Cache Manager、后台 Worker、底层 I/O。
  3. 接着讲 Buffer 选型:顺序流用 Ring Buffer,随机读用 Block Cache,混合读用 Segment Cache。
  4. 然后讲线程协作:应用提交请求,Worker 合并请求并异步读取,完成后提交缓存状态。
  5. 再讲水位线:高水位停预读,低水位启动预读,危险水位优先补缓存并限制非关键 I/O。
  6. 最后讲容量计算:按应用消费速率、底层最坏延迟、调度抖动和安全余量反推缓存容量。

一句话概括:Flash 上的数据只是存储形态,应用真正依赖的是 SRAM 缓存层;缓存层通过 Ring/Block/Segment 三类结构、预读、水位线、异步 I/O 和资源仲裁,把低速 Flash 的不确定性挡在应用实时路径之外。

参考链接