RFC-0079:检测调试日志数据丢失

RFC-0079:检测调试日志数据丢失
状态已接受
区域
  • 内核
  • 诊断
说明

改进了用于检测调试日志数据丢失的机制。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-02-17
审核日期(年-月-日)2021-03-25

摘要

本文档提出了一种更新机制,用于检测内核的调试日志对象中丢弃的消息。

背景和动机

内核调试日志子系统是一种简单的日志记录设施,可让用户模式程序读取和写入日志消息。从逻辑上讲,此系统提供单个 FIFO 日志缓冲区,可供多个写入器或读取器写入或读取。

调试日志可能会丢失数据。假设读取速度可以跟上写入速度,那么所有读取器都会按写入顺序看到所有写入者写入的所有消息。不过,如果读取器速度缓慢且无法跟上,则会错过消息。

用户模式程序可以通过 zx_debuglog_write 将消息写入调试日志,内核也可以通过 printf 将消息写入调试日志。可通过 zx_debuglog_read 读取消息。此外,内核还有一个专用线程(称为 debuglog_dumper),用于从调试日志中读取消息并将其写入调试串行端口。

调试日志缓冲区具有固定的容量。达到该容量时,系统可能会舍弃最近写入的消息,以便为新消息腾出空间。任何未“赶上”的读者都不会看到已丢弃的消息。

知道日志已完整,有助于推理某些事件的缺失,因此检测丢弃的日志消息是日志系统的一项重要功能。调试日志读取器需要能够检测日志消息何时被丢弃。

目前,调试日志提供了一种机制,供读取器检测已丢弃的日志数据的时间和字节数(zx_log_record_tuint32_t rolled_out 字段)。

使用 zx_debuglog_read 读取 zx_log_record_t 后,rolled_out 字段将包含自该读取器上次成功读取以来从调试日志中丢弃的日志消息的字节数。该值同时包含丢弃的日志标头的字节数和丢弃的日志正文的字节数。

rolled_out 机制的实现方式是让每个读取器都维护一个指向调试日志缓冲区的指针,该指针指向其尚未读取的下一消息。调试日志会维护一个写入指针,该指针指向下一个消息将要写入的位置。如果读取器发现写入指针已超出读取指针,则表示它错过了一条或多条消息。通过减去指针值,读取器可以确定它错过了多少字节的日志数据(包括标头)。

rolled_out 机制目前未使用。

提案

此 RFC 提议...

  1. 将以字节为导向的数据丢失检测替换为以记录为导向的检测。

    为了更贴近调试日志读取器(尤其是 debuglog_dumper)的预期,现有的以字节为导向的 rolled_out 机制将替换为可用于检测数据丢失(序列中存在空白)的每个记录序列号。

    debuglog_dumper 必须将读取的每条消息写入调试串行端口。由于串行端口可能不是特别快,因此 debuglog_dumper 通常无法跟上 debuglog,这会导致丢失消息。在这种情况下,我们希望向串行端口输出一条消息,指明发生了数据丢失以及丢失了多少条消息,类似于 Linux 的 printk 输出。

  2. 使用 64 位值消除数据丢失未检测到的可能性。

    由于 rolled_out 字段的大小为 32 位,并且统计的是字节数而非记录数,因此如果在两次调用 zx_debuglog_read 之间写入了 4GB 的日志数据,则可能会导致值溢出,从而导致数据丢失而无法检测到。这在实践中不太可能发生,但最好能完全排除这种可能性。如果我们将 32 位字节序列字段替换为 32 位每记录序列字段,则创建溢出所需的数据量会增至大约 128GB。通过使用 64 位序列字段,我们可以完全忽略溢出可能性,即使在日志记录速率非常高的情况下也是如此。

  3. 支持未来的实现优化。

    未来可能会进行一些优化,具体取决于是否允许多个调试日志读取器“共享”单个 zx_log_record_t

    如果没有缓慢的读取器,所有调试日志读取器都应以相同的顺序看到完全相同的日志数据。除 rolled_out 外,zx_log_record_t 的所有字段在记录写入调试日志时都会固定。rolled_out 的不同之处在于,它是在从调试日志中读取记录时按读取器计算的。如果我们能够通过其他方式检测数据丢失,而无需使用可能会因读取器而异的字段,我们就可以在未来实现一些潜在的优化,即在所有读取器之间共享单个记录。

设计

当记录写入调试日志时,调试日志会为每个记录分配一个 64 位序列号(从 0 开始)。

每个记录的序列号都将比前一个记录大 1。移除 zx_log_record_trolled_out 字段,并将其替换为记录的序列号 uint64_t sequence

然后,zx_debuglog_read 的调用方可以检测序列中的空缺,并计算丢弃了多少消息。

实现

zx_debuglog_readzx_log_record_t 不会在树外使用。虽然不需要完整的 Fuchsia 大规模更改 (LSC) 流程,但我们会提交一个 FYI LSC bug,并分阶段完成实现。

rolled_out 字段未使用,但包含的结构体 zx_log_record_t 在 fuchsia.git 中的几个位置被使用。需要注意,不要破坏现有代码。zx_log_record_t 不会在树外使用。

zx_debuglog_read 的系统调用定义和文档实际上并未指定它会返回 zx_log_record_t。而是指定 void*size_t,并且调用方必须知道如何在结果上投射或“叠加”zx_log_record_t。向 zx_log_record_t* 转换容易出错,因此 void* 系统调用参数将更改为 zx_log_record_t*,并且调用方将更新。

目前没有与 zx_log_record_t 等效的 Rust 函数,Rust 调用方使用硬编码偏移量来访问字段,因此更改其字段的大小或偏移量可能会静默破坏这些调用方。在实现过程中,我们将创建等效于 zx_log_record_t 的 Rust 函数,并更新 Rust 调用方以使用该函数。

相关步骤如下:

  1. 向调试日志消息的私有内部表示法 (dlog_header_t) 添加了 64 位序列号。

  2. 将调试日志使用方更改为使用 zx_log_record_t(或语言等效项),而不是使用硬编码字段偏移量。具体而言,请创建一个 Rust 等效类型,并更新 Rust 代码以使用该类型。

  3. zx_debuglog_readvoid* 参数更改为 zx_log_record_t*

  4. zx_log_record_trolled_out 字段更改为填充零的 unused 字段。

  5. zx_log_record_tunused 字段替换为 uint64_t sequence,并确保不会创建任何隐式结构体内边距。请对所有 zx_log_record_t 等效类型执行相同的操作,无论语言如何。

  6. 更新了 zx_debuglog_read 文档,说明了调用方可以如何使用新的序列字段检测数据丢失。

第 1、第 2 和第 3 步各有自己的 CL。第 4、第 5 和第 6 步将在单个 CL 中执行。

性能

管理序列计数器的运行时开销

在保持锁定状态时执行调试日志操作,因此我们可以使用常规 uint64_t 值来生成序列。预计不会有任何可衡量的性能影响。

每个记录序列值对大小的影响

内核的私有记录实现 zx_log_record_tdlog_header_t 的大小都会增加。zx_log_record_t 的 32 位 rolled_out 字段将替换为 64 位记录序列字段,从而净增 4 个字节。

dlog_header_t 大小更改更有趣,因为它是日志记录存储在 FIFO 中的原生形式。dlog_header_t 的大小为 32 个字节,且没有 rolled_out 字段,因此其净增量将是完整的 8 个字节。debuglog 对象中的 FIFO 空间有限,因此将每个日志记录增加 8 个字节会减少可存储在 FIFO 中的记录数量上限,同时也会减少消息大小上限(从 224 字节减少到 216 字节)。

FIFO 可以存储 128KB 的标头和消息。对消息进行采样后发现,平均大小约为 100 字节。假设平均大小为 32 字节的标头,FIFO 可以存储约 971 条消息。如果使用每个记录的序列号,则该数量会减少到大约 917。

安全注意事项

该提案不会改变系统的安全性。调试日志读取器是特权组件。如果没有数据丢失,调试日志读取器已经可以准确地合成日志记录序列。

隐私注意事项

对隐私没有影响。

测试

内核内单元测试将验证底层调试日志实现,调试日志核心测试将验证在系统调用层可观察到的行为。

文档

zx_log_record_t 的文档将会更新。

缺点、替代方案和未知情况

不进行任何操作

由于 rolled_out 尚未使用,因此目前只需相对较少的工程工作即可实现该提案。一旦下游代码使用了 rolled_out,实现此建议或类似建议的开销就会更高。

通过将 UINT32_MAX 的语义记录为“UINT32_MAX 或更多”,可以稍微缓解 32 位循环问题。或者,我们可以将 rolled_out 的类型更改为 uint64_t

记录序列和字节序列

如果有空闲空间,我们可以在每个 zx_log_record_t 中同时放置记录序列值和字节序列值。然后,调试日志读取器可以通过记录数或丢失的字节数来衡量数据丢失情况。不过,这会进一步增加 dlog_header_t 的大小,而且我们尚不清楚是否有丢弃字节的使用情形。

在先技术和参考文档

Linux 的 printk 会报告丢弃/抑制的消息数量,而不是丢弃/抑制的消息数据字节数。