环形缓冲区与日志条目

概述

本文档详细介绍 Log 子系统的两个核心组件:无锁环形缓冲区(GlobalLogBuffer)和日志条目(LogEntry)。环形缓冲区负责高效地缓存日志,采用 MPSC(多生产者单消费者)模型支持多核并发写入;日志条目定义了日志数据的内存布局和序列化方式。

环形缓冲区(GlobalLogBuffer)

结构概览

GlobalLogBuffer 是一个固定大小的环形缓冲区,位于 os/src/log/buffer.rs。其核心设计是无锁 MPSC 模型,通过原子操作和票号系统实现多核并发安全。

内存布局

GlobalLogBuffer 结构体布局:
┌─────────────────────────────────────────────────────────────────┐
│                     GlobalLogBuffer                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  writer: CachePadded64<WriterData>      [64 字节对齐]  │     │
│  │  ┌─────────────────────────────────────────────────┐   │     │
│  │  │  write_seq: AtomicUsize  (写序列号)             │   │     │
│  │  │  当前值表示下一个可用的票号                      │   │     │
│  │  └─────────────────────────────────────────────────┘   │     │
│  │  [填充至 64 字节,避免伪共享]                          │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                   │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  reader: CachePadded64<ReaderData>      [64 字节对齐]  │     │
│  │  ┌─────────────────────────────────────────────────┐   │     │
│  │  │  read_seq: AtomicUsize  (读序列号)              │   │     │
│  │  │  dropped: AtomicUsize   (丢弃计数)              │   │     │
│  │  └─────────────────────────────────────────────────┘   │     │
│  │  [填充至 64 字节,避免伪共享]                          │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                   │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  slots: [MaybeUninit<LogEntry>; MAX_ENTRIES]          │     │
│  │                                                         │     │
│  │  环形槽位数组,每个槽位存储一个 LogEntry              │     │
│  │  MAX_ENTRIES = BUFFER_SIZE / size_of::<LogEntry>()    │     │
│  │             ≈ 16384 / 280 ≈ 58 个槽位                 │     │
│  └────────────────────────────────────────────────────────┘     │
│                                                                   │
│  ┌────────────────────────────────────────────────────────┐     │
│  │  unread_bytes: AtomicUsize  (未读字节计数)             │     │
│  │                                                         │     │
│  │  记录所有未读日志格式化后的总字节数                    │     │
│  │  用于 SIZE_UNREAD 系统调用                             │     │
│  └────────────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────┘

内存对齐说明:
- WriterData 和 ReaderData 各占 64 字节(一个缓存行)
- 避免伪共享:多个 CPU 写 write_seq 不会与读 read_seq 竞争同一缓存行
- slots 数组紧随其后,每个 LogEntry 约 280 字节

定义位于 os/src/log/buffer.rs:54-59

MPSC 并发模型

环形缓冲区采用多生产者单消费者(MPSC)模型,这是日志系统的自然选择:

并发模型示意图:

                    多个生产者(写入者)

        ┌──────┐      ┌──────┐      ┌──────┐      ┌──────┐
        │ CPU0 │      │ CPU1 │      │ CPU2 │      │ CPU3 │
        └───┬──┘      └───┬──┘      └───┬──┘      └───┬──┘
            │             │             │             │
            │             │             │             │
            │ pr_info!()  │ pr_err!()   │ pr_warn!()  │ pr_debug!()
            │             │             │             │
            └─────────────┴──────┬──────┴─────────────┘
                                 │
                                 │ 并发写入(使用原子操作协调)
                                 │
                    ┌────────────▼───────────┐
                    │   GlobalLogBuffer      │
                    │                        │
                    │  write_seq (原子递增)  │
                    │  slots[...]            │
                    │  read_seq              │
                    └────────────┬───────────┘
                                 │
                                 │ 单一读取者(顺序读取)
                                 │
                          ┌──────▼──────┐
                          │  Log Reader │
                          │  (用户态工具) │
                          └─────────────┘

关键特点:
1. 多个 CPU 可以并发调用 pr_* 宏,无需等待锁
2. 写入者通过 write_seq.fetch_add() 获取独占的票号
3. 每个票号对应唯一的槽位,写入者互不干扰
4. 读取者单独访问 read_seq,无竞争
5. Release-Acquire 内存序保证数据可见性

票号分配系统

票号系统是无锁环形缓冲区的核心机制,通过原子递增操作分配唯一的序列号:

票号分配流程:

初始状态:
┌────────────────────────────┐
│  write_seq = 0             │
│  read_seq = 0              │
└────────────────────────────┘

时刻 T1:三个 CPU 同时请求写入
┌────────────────────────────┐
│  CPU0 执行 fetch_add(1)    │ → 返回 0,write_seq 变为 1
│  CPU1 执行 fetch_add(1)    │ → 返回 1,write_seq 变为 2
│  CPU2 执行 fetch_add(1)    │ → 返回 2,write_seq 变为 3
└────────────────────────────┘

结果:
┌────────────────────────────┐
│  CPU0 获得票号 0           │ → 写入 slot[0 % MAX_ENTRIES]
│  CPU1 获得票号 1           │ → 写入 slot[1 % MAX_ENTRIES]
│  CPU2 获得票号 2           │ → 写入 slot[2 % MAX_ENTRIES]
│  write_seq = 3             │
└────────────────────────────┘

票号系统的保证:
1. 原子性:fetch_add 保证每个 CPU 获得唯一的票号
2. 公平性:先到达的 CPU 获得较小的票号
3. 顺序性:票号单调递增,读取者按顺序消费日志
4. 无等待:获得票号后立即写入,无需等待其他 CPU

实现位于 os/src/log/buffer.rs:128

写入流程详解

每次写入日志遵循严格的五步流程,保证并发安全和数据完整性:

五步写入流程:

┌─────────────────────────────────────────────────────────────┐
│  步骤 1:获取票号                                            │
│  ─────────────────────────────────────────────────────────  │
│  seq = write_seq.fetch_add(1, Relaxed)                     │
│                                                             │
│  · 原子地递增 write_seq 并返回旧值                         │
│  · Relaxed 语义足够,因为票号本身是线程私有的              │
│  · 返回的 seq 是该日志的唯一标识                           │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 2:计算槽位                                            │
│  ─────────────────────────────────────────────────────────  │
│  idx = seq % MAX_ENTRIES                                   │
│                                                             │
│  · 将线性序列号映射到环形槽位索引                           │
│  · MAX_ENTRIES 是编译期常量,取模可能优化为位与操作        │
│  · 多个序列号可能映射到同一槽位(当缓冲区绕一圈后)         │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 3:检测并处理溢出                                      │
│  ─────────────────────────────────────────────────────────  │
│  current_write = write_seq.load(Relaxed)                   │
│  current_read = read_seq.load(Relaxed)                     │
│                                                             │
│  if (current_write - current_read) > MAX_ENTRIES:          │
│      // 缓冲区满,需要覆盖旧数据                            │
│      loop:                                                  │
│          old_read = read_seq.load(Relaxed)                 │
│          new_read = current_write - MAX_ENTRIES + 1        │
│          if read_seq.compare_exchange(old_read, new_read): │
│              dropped.fetch_add(new_read - old_read)        │
│              break                                          │
│                                                             │
│  · CAS 循环推进 read_seq,保证只有一个 CPU 成功            │
│  · 增加 dropped 计数记录丢弃的日志数量                     │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 4:拷贝日志数据                                        │
│  ─────────────────────────────────────────────────────────  │
│  slot = &slots[idx]                                         │
│  // 拷贝除 seq 字段外的所有数据:                           │
│  slot.level = entry.level                                   │
│  slot.cpu_id = entry.cpu_id                                 │
│  slot.timestamp = entry.timestamp                           │
│  slot.task_id = entry.task_id                               │
│  slot.length = entry.length                                 │
│  slot.message.copy_from_slice(entry.message)                │
│                                                             │
│  · 注意:seq 字段暂不写入,它是同步标志                    │
│  · 此时数据尚未"发布",读取者看不到                        │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 5:发布数据                                            │
│  ─────────────────────────────────────────────────────────  │
│  slot.seq.store(seq, Release)                              │
│                                                             │
│  · 使用 Release 语义写入 seq 字段                          │
│  · Release 保证之前的所有写入对后续的 Acquire 读取可见     │
│  · 写入 seq 是"发布"操作,标志槽位已就绪                   │
│  · 读取者通过检查 seq 字段判断数据是否可读                 │
└─────────────────────────────────────────────────────────────┘

完整实现位于 os/src/log/buffer.rs:128-177

溢出处理机制

当写入速度超过读取速度时,环形缓冲区会满。此时采用 FIFO 策略覆盖最旧的日志:

溢出处理状态转换:

正常状态:
┌────────────────────────────────────────┐
│  write_seq: 10                         │
│  read_seq: 5                           │
│  容量:MAX_ENTRIES = 58                │
│  已用:10 - 5 = 5 条日志               │
│  可用:58 - 5 = 53 个槽位              │
└────────────────────────────────────────┘

接近满状态:
┌────────────────────────────────────────┐
│  write_seq: 62                         │
│  read_seq: 5                           │
│  已用:62 - 5 = 57 条日志              │
│  可用:58 - 57 = 1 个槽位              │
│  警告:缓冲区即将满                     │
└────────────────────────────────────────┘

溢出检测:
┌────────────────────────────────────────┐
│  write_seq: 64                         │
│  read_seq: 5                           │
│  已用:64 - 5 = 59 > MAX_ENTRIES       │
│  判定:缓冲区溢出!                     │
└────────────────────────────────────────┘
                  │
                  ▼
溢出处理(CAS 循环):
┌────────────────────────────────────────┐
│  计算需要推进的读序列号:               │
│  new_read = 64 - 58 + 1 = 7            │
│                                        │
│  尝试 CAS:                            │
│  read_seq.compare_exchange(5, 7)      │
│  ├─ 成功:read_seq = 7                │
│  │         dropped += (7 - 5) = 2     │
│  │         返回,继续写入               │
│  └─ 失败:其他 CPU 已推进 read_seq     │
│            重新加载 read_seq,重试      │
└────────────────────────────────────────┘
                  │
                  ▼
恢复正常:
┌────────────────────────────────────────┐
│  write_seq: 64                         │
│  read_seq: 7                           │
│  已用:64 - 7 = 57 条日志              │
│  可用:58 - 57 = 1 个槽位              │
│  dropped: 2(记录丢弃了 2 条日志)     │
└────────────────────────────────────────┘

关键点:
1. 只有写入者会推进 read_seq(在溢出时)
2. CAS 保证多个写入者中只有一个成功推进
3. dropped 计数精确记录被覆盖的日志数量
4. 被覆盖的日志是最旧的日志(FIFO 策略)

实现位于 os/src/log/buffer.rs:148-162

读取同步机制

读取者通过检查槽位的 seq 字段判断数据是否就绪:

读取流程:

┌─────────────────────────────────────────────────────────────┐
│  1. 加载当前读序列号                                         │
│     current_read = read_seq.load(Relaxed)                   │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  2. 计算槽位索引                                             │
│     idx = current_read % MAX_ENTRIES                        │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  3. 检查槽位就绪(Acquire 同步点)                           │
│     slot_seq = slot.seq.load(Acquire)                       │
│                                                             │
│     if slot_seq == current_read:                            │
│         // 数据已发布,可以安全读取                         │
│     else:                                                   │
│         // 数据未就绪或已被覆盖,返回 None                  │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  4. 拷贝日志条目                                             │
│     // 从槽位拷贝到栈上的临时变量                            │
│     entry = clone_from_slot(slot)                           │
│                                                             │
│     · Acquire 保证能看到写入者的所有数据                    │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  5. 推进读序列号                                             │
│     read_seq.fetch_add(1, Relaxed)                          │
│                                                             │
│     · 单消费者,无竞争,Relaxed 足够                        │
└─────────────────────────────────────────────────────────────┘

为什么检查 slot_seq == current_read?
┌────────────────────────────────────────┐
│  情况 1:slot_seq == current_read      │
│  → 数据已发布,匹配期望的序列号        │
│  → 可以安全读取                        │
├────────────────────────────────────────┤
│  情况 2:slot_seq < current_read       │
│  → 数据尚未写入(写入者还未到达)      │
│  → 返回 None,等待写入者               │
├────────────────────────────────────────┤
│  情况 3:slot_seq > current_read       │
│  → 数据已被覆盖(缓冲区绕了一圈)      │
│  → 返回 None,日志已丢失               │
└────────────────────────────────────────┘

实现位于 os/src/log/buffer.rs:179-201

缓存行填充优化

WriterDataReaderData 使用 CachePadded64 包装,避免伪共享:

伪共享问题(未优化):

假设缓存行大小为 64 字节:
┌────────────────────────────────────────────────────────────┐
│  缓存行                                                     │
│  ┌──────────────────┐  ┌──────────────────┐              │
│  │  write_seq (8B)  │  │  read_seq (8B)   │  [其他数据]  │
│  └──────────────────┘  └──────────────────┘              │
└────────────────────────────────────────────────────────────┘
     ↑                      ↑
     │                      │
  CPU0 频繁写入         CPU1 频繁读取

问题:
- CPU0 修改 write_seq → 缓存行失效 → CPU1 的缓存行被强制刷新
- CPU1 读取 read_seq → 导致 CPU0 的缓存行失效
- 两个 CPU 互相干扰,性能下降(伪共享)

优化后(CachePadded64):

┌────────────────────────────────────────┐
│  缓存行 1 (64 字节)                     │
│  ┌──────────────────┐                  │
│  │  write_seq (8B)  │  [填充 56 字节]  │
│  └──────────────────┘                  │
└────────────────────────────────────────┘
     ↑
     │
  CPU0 独占此缓存行

┌────────────────────────────────────────┐
│  缓存行 2 (64 字节)                     │
│  ┌──────────────────┐                  │
│  │  read_seq (8B)   │  [填充]          │
│  │  dropped (8B)    │                  │
│  └──────────────────┘                  │
└────────────────────────────────────────┘
     ↑
     │
  CPU1 独占此缓存行

优势:
- write_seq 和 read_seq 位于不同的缓存行
- CPU0 写入 write_seq 不会影响 CPU1 的缓存
- CPU1 读取 read_seq 不会影响 CPU0 的缓存
- 消除伪共享,提升并发性能

定义位于 os/src/log/buffer.rs:40-52

非破坏性读取

除了传统的破坏性读取(read()),环形缓冲区还支持非破坏性读取,允许多次查看相同的日志而不删除它们。

peek 操作

非破坏性读取流程:

┌─────────────────────────────────────────────────────────────┐
│  步骤 1:获取可读范围                                        │
│  ─────────────────────────────────────────────────────────  │
│  start = read_seq.load(Acquire)                            │
│  end = write_seq.load(Acquire)                             │
│                                                             │
│  · 获取当前的读写指针位置                                   │
│  · 可读范围为 [start, end)                                 │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 2:验证索引                                            │
│  ─────────────────────────────────────────────────────────  │
│  if (index < start || index >= end):                       │
│      return None  // 索引越界                              │
│                                                             │
│  if (end >= start + MAX_ENTRIES):                          │
│      oldest_valid = end - MAX_ENTRIES                      │
│      if (index < oldest_valid):                            │
│          return None  // 数据已被覆盖                      │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 3:读取槽位                                            │
│  ─────────────────────────────────────────────────────────  │
│  slot_idx = index % MAX_ENTRIES                            │
│  slot = &slots[slot_idx]                                   │
│                                                             │
│  · 计算环形槽位索引                                         │
│  · 获取槽位指针                                             │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 4:验证序列号                                          │
│  ─────────────────────────────────────────────────────────  │
│  seq = slot.seq.load(Acquire)                              │
│  if (seq != index):                                        │
│      return None  // 数据尚未就绪或已被覆盖                │
└─────────────────────────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  步骤 5:克隆并返回                                          │
│  ─────────────────────────────────────────────────────────  │
│  entry = (*slot).clone()                                   │
│  return Some(entry)                                        │
│                                                             │
│  · 注意:不修改 read_seq,日志保留在缓冲区中               │
└─────────────────────────────────────────────────────────────┘

并发安全性

  • peek() 可以与 write()read() 并发调用
  • 使用 Acquire 内存序保证能看到已发布的数据
  • 不修改任何共享状态,完全无锁

使用场景

  • SyslogAction::ReadAll - 非破坏性读取所有日志
  • 日志监控工具 - 周期性扫描而不删除日志
  • 调试分析 - 反复查看相同的日志条目

实现位置os/src/log/buffer.rs:330-381

精确字节计数

环形缓冲区实时维护未读日志的格式化字节数,用于支持 SyslogAction::SizeUnread 系统调用。

字节计数机制

字节计数维护流程:

写入时:
┌─────────────────────────────────────────────────────────────┐
│  calculate_formatted_length(entry)                         │
│    ↓                                                        │
│  · 计算日志条目格式化后的精确字节长度                       │
│  · 包括 ANSI 颜色代码、时间戳、上下文信息、消息内容        │
│  · 必须与 format_log_entry() 和 direct_print_entry() 一致  │
│    ↓                                                        │
│  unread_bytes.fetch_add(formatted_len, Release)            │
│    ↓                                                        │
│  · 原子地增加未读字节计数                                   │
└─────────────────────────────────────────────────────────────┘

读取时:
┌─────────────────────────────────────────────────────────────┐
│  entry = read_log()                                        │
│    ↓                                                        │
│  calculate_formatted_length(&entry)                        │
│    ↓                                                        │
│  unread_bytes.fetch_sub(formatted_len, Release)            │
│    ↓                                                        │
│  · 原子地减少未读字节计数                                   │
└─────────────────────────────────────────────────────────────┘

查询时:
┌─────────────────────────────────────────────────────────────┐
│  unread_bytes.load(Acquire)                                │
│    ↓                                                        │
│  · O(1) 时间返回结果,无需遍历缓冲区                       │
│  · 用于 syslog SIZE_UNREAD 系统调用                       │
└─────────────────────────────────────────────────────────────┘

格式化长度计算

calculate_formatted_length() 函数精确计算日志条目格式化后的字节长度:

格式: "{color_code}{level} [{timestamp:12}] [CPU{cpu_id}/T{task_id:3}] {message}{reset}\n"

组成部分:
- ANSI 颜色代码: entry.level().color_code().len()  // 开始
- 级别标签: entry.level().as_str().len()  // "[INFO]" 等
- 时间戳: 14 字节  // " [{:12}]" = 2 + 12
- CPU ID: 5 + digit_count(cpu_id)  // " [CPU<digits>"
- 任务 ID: 2 + digit_count_padded(task_id, 3) + 1  // "/T<digits>]"
- 消息内容: entry.message().len()
- ANSI 重置代码: entry.level().reset_color_code().len()  // 结束
- 分隔符和换行: 3 + 1 字节  // 3个空格 + 1个换行

总字节数 = 所有部分之和

重要维护点

如果修改日志输出格式,必须同步更新三处:

  1. buffer::calculate_formatted_length() - 字节长度计算(os/src/log/buffer.rs:18-102
  2. log_core::direct_print_entry() - 控制台输出格式(os/src/log/log_core.rs:223-240
  3. log_core::format_log_entry() - syslog 字符串格式化(os/src/log/log_core.rs:277-286

性能特性

  • 查询字节数:O(1) - 单次原子加载
  • 写入时计算:O(1) - 固定计算,无循环
  • 内存开销:一个 AtomicUsize(8 字节)

实现位置

  • 字节计数:os/src/log/buffer.rs:18-102, os/src/log/buffer.rs:236-238, os/src/log/buffer.rs:302-305
  • 查询接口:os/src/log/buffer.rs:323-326

日志条目(LogEntry)

内存布局

LogEntry 定义了单条日志的内存布局,位于 os/src/log/entry.rs:20-30

LogEntry 内存布局(总大小约 280 字节):

偏移量   字段            类型              大小    对齐    说明
──────────────────────────────────────────────────────────────────
0x0000   seq            AtomicUsize       8 字节  8       同步标志,必须在首位
0x0008   level          LogLevel          1 字节  1       日志级别 (0-7)
0x0009   [padding]      -                 3 字节  -       对齐填充
0x000C   cpu_id         usize             8 字节  8       记录日志的 CPU 核心 ID
0x0014   length         usize             8 字节  8       消息实际长度
0x001C   task_id        u32               4 字节  4       记录日志的任务 ID
0x0020   timestamp      usize             8 字节  8       时间戳(架构相关单位)
0x0028   message        [u8; 256]         256字节 1       消息缓冲区

总计:约 280 字节(实际取决于编译器对齐)

关键设计:
┌────────────────────────────────────────────────────────────────┐
│  seq 字段必须在首位的原因:                                     │
│  ─────────────────────────────────────────────────────────────│
│  1. 作为同步标志,读取者首先检查 seq 判断数据是否就绪          │
│  2. Acquire-Release 语义:                                     │
│     · 写入者最后以 Release 写入 seq                            │
│     · 读取者首先以 Acquire 读取 seq                            │
│     · 保证 seq 之前的所有字段对读取者可见                      │
│  3. 固定偏移量 0,便于汇编优化和理解                           │
└────────────────────────────────────────────────────────────────┘

内存表示(#[repr(C, align(8))]):
┌────────────────────────────────────────────────────────────────┐
│  +0x00  ┌───────────────────────────────────────────────────┐  │
│         │  seq: AtomicUsize                                 │  │
│  +0x08  ├───┬───────────────────────────────────────────────┤  │
│         │lv │ [padding 3 bytes]                             │  │
│  +0x0C  ├───┴───────────────────────────────────────────────┤  │
│         │  cpu_id: usize                                    │  │
│  +0x14  ├───────────────────────────────────────────────────┤  │
│         │  length: usize                                    │  │
│  +0x1C  ├───────────────────────────────────────────────────┤  │
│         │  task_id: u32                                     │  │
│  +0x20  ├───────────────────────────────────────────────────┤  │
│         │  timestamp: usize                                 │  │
│  +0x28  ├───────────────────────────────────────────────────┤  │
│         │  message: [u8; 256]                               │  │
│         │  固定大小的消息缓冲区                              │  │
│         │  UTF-8 编码,超长消息会被截断                      │  │
│  +0x128 └───────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘

消息截断策略

消息缓冲区固定为 256 字节(MAX_MESSAGE_LEN),超过长度的消息会被自动截断:

截断规则

  1. 尊重 UTF-8 边界:截断时检查 UTF-8 字符边界,避免产生无效字符序列

    • 如果第 256 字节是 UTF-8 多字节字符的中间位置,向前查找完整字符的起始位置
    • 保证截断后的字符串是有效的 UTF-8
  2. 记录实际长度length 字段记录消息的实际字节数(截断前的长度)

    • 读取者可以通过比较 lengthMAX_MESSAGE_LEN 判断是否被截断
  3. 静默截断:截断不会报错或警告,保证日志记录的可靠性

    • 设计哲学:宁可记录部分信息,也不能因为消息过长而丢失整条日志

UTF-8 截断示例

假设消息是 "Hello 世界!Extra text"(UTF-8 编码):

字节序列:
H  e  l  l  o     世        界        !        E  x  t  r  a  ...
48 65 6c 6c 6f 20 e4b896 e7958c efbc81 45 78 74 72 61 ...

假设 MAX_MESSAGE_LEN = 20:
- 朴素截断:截取前 20 字节 → 可能在"界"字的中间(e7958c 被截断)
- 智能截断:检测到 0xe7 是三字节字符的开始,向前推到"世"字的结束位置
- 最终截断:"Hello 世界!E" (完整的 UTF-8 字符)

实现位于 os/src/log/entry.rs:96-114

显示格式

日志条目实现了 core::fmt::Display trait,格式化输出为可读的字符串:

标准格式

[时间戳] [级别] [CPU核心/任务ID] 消息内容

示例输出

[000012345678] [INFO ] [CPU0/Task1] Kernel initialized successfully
[000012350123] [ERROR] [CPU2/Task5] Failed to allocate memory
[000012351000] [WARN ] [CPU1/Task3] High memory usage: 95%
[000012352456] [DEBUG] [CPU3/Task0] Entering function foo()

字段说明

字段格式说明
时间戳[%012d]12 位十进制数,左填充零,单位取决于架构(通常是 CPU 周期或纳秒)
级别[%-5s]5 字符宽,左对齐,使用 ANSI 颜色(如果控制台支持)
CPU核心CPU%dCPU 核心 ID
任务IDTask%d任务 ID(如果未设置则为 0)
消息UTF-8 字符串实际的日志消息,可能被截断

颜色映射

级别颜色ANSI 代码
Emergency亮红\x1b[91m
Alert亮红\x1b[91m
Critical亮红\x1b[91m
Error红色\x1b[31m
Warning黄色\x1b[33m
Notice青色\x1b[36m
Info绿色\x1b[32m
Debug默认无颜色

实现位于 os/src/log/entry.rs:116-137

性能特性

环形缓冲区性能

操作时间复杂度说明
写入O(1)fetch_add + 槽位写入,常数时间
读取O(1)槽位读取 + fetch_add,常数时间
溢出处理O(k)k 为 CAS 循环次数,通常 1-3 次
级别检查O(1)原子加载,几纳秒

日志条目性能

操作时间复杂度说明
创建条目O(n)n 为消息长度,需要格式化和拷贝
拷贝条目O(1)固定大小(280 字节),memcpy 优化
格式化显示O(n)n 为消息长度,需要格式化输出

内存使用

环形缓冲区总内存

总大小 = WriterData (64B) + ReaderData (64B) + slots数组

slots数组大小 = MAX_ENTRIES × sizeof(LogEntry)
             ≈ 58 × 280 字节
             ≈ 16240 字节

总大小 ≈ 64 + 64 + 16240 ≈ 16368 字节 ≈ 16 KB

符合配置文件中定义的 BUFFER_SIZE = 16384 字节(os/src/log/config.rs:9)。

设计权衡

固定 vs 动态大小

当前设计:固定大小的消息缓冲区(256 字节)

优势

  • 日志条目大小确定,环形缓冲区容量可静态计算
  • 无需堆分配,适合裸机环境
  • 槽位对齐良好,访问效率高

劣势

  • 长消息会被截断
  • 短消息浪费空间(平均日志可能只有几十字节)

权衡分析:对于内核日志系统,可预测性和可靠性优先于灵活性。固定大小设计更符合实时系统的需求。

MPSC vs MPMC

当前设计:MPSC(多生产者单消费者)

优势

  • 简化读取端同步,read_seq 无竞争
  • 避免多消费者的活锁和优先级反转
  • 性能更好(读取是冷路径,可接受单线程)

劣势

  • 只能有一个读取者
  • 多个工具需要读取日志时需要额外协调

权衡分析:日志的自然特性是多写少读,MPSC 是最佳选择。如需多消费者,可在用户态实现分发。

Release-Acquire vs SeqCst

当前设计:Release-Acquire 语义用于 seq 字段同步

优势

  • 比 SeqCst 更弱的内存序,性能更好(特别是在 ARM/RISC-V 上)
  • 足以保证无锁环形缓冲区的正确性

劣势

  • 需要更仔细的推理和验证
  • 错误使用可能导致难以调试的并发 bug

权衡分析:经过仔细验证,Release-Acquire 是正确的选择。SeqCst 会带来不必要的性能开销。

未来改进方向

动态缓冲区大小

当前缓冲区大小在编译期固定。未来可考虑:

  • 启动参数配置缓冲区大小
  • 运行时动态扩展(需要复杂的迁移逻辑)

持久化支持

当前日志只存在内存中。未来可增加:

  • 慢速持久化层(SPI Flash、磁盘)
  • 后台线程定期刷新内存日志到持久化存储
  • 崩溃恢复时读取持久化日志

压缩和去重

对于重复的日志,可以压缩:

  • 记录重复次数而不是完整消息
  • 使用哈希值检测重复
  • 节省缓冲区空间

多缓冲区分区

针对不同模块使用不同的缓冲区:

  • 减少竞争,提高并发性能
  • 支持按模块过滤和分析
  • 隔离故障(一个模块的日志洪水不影响其他模块)

这些改进在不破坏当前 API 的前提下都是可行的,得益于良好的模块化设计。