[TOC]

kernel/printk.c 内核打印(Kernel Printing) 内核信息输出的基础设施

历史与背景

这项技术是为了解决什么特定问题而诞生的?

kernel/printk.c 及其核心功能 printk() 的诞生,是为了解决一个对于操作系统内核来说最根本的问题:如何从一个没有标准输出(stdout)、没有文件系统、甚至可能没有正常运行环境的受限上下文中,可靠地输出诊断信息。

用户空间的程序可以简单地使用 printf() 将信息打印到终端,但内核无法这样做。内核是所有用户空间程序运行的基础,它需要一个独立于任何用户进程的日志记录机制,以应对以下场景:

  1. 系统启动早期:在 init 进程启动之前,甚至在控制台驱动初始化之前,就需要有方法来报告硬件探测、内存初始化等关键步骤的状态。
  2. 中断和异常上下文:当内核正在处理一个硬件中断或CPU异常时,它处于一个不能睡眠、不能调用大部分内核函数的受限上下文中。此时需要一个足够安全、不会导致死锁的打印函数。
  3. 系统崩溃(Kernel Panic):当系统遭遇无法恢复的致命错误时,printk 通常是内核在“死亡”前留下最后“遗言”的唯一方式,这些信息对于事后分析崩溃原因至关重要。
  4. 常规诊断与调试:为内核开发者和系统管理员提供一个标准的、无处不在的接口来记录驱动程序的状态、警告和错误信息。

它的发展经历了哪些重要的里程碑或版本迭代?

printk 是内核最古老、最核心的组件之一,其发展历程反映了内核自身的成熟过程。

  • 基本实现:最初的 printk 非常简单,可能只是直接将字符写入一个硬编码的串行端口。
  • 环形缓冲区(Ring Buffer)的引入:这是一个决定性的里程碑。内核实现了一个固定大小的内存环形缓冲区(log buffer),printk 将消息写入此缓冲区,而不是直接发送到硬件。这实现了生产者(printk调用者)和消费者(控制台驱动)的解耦。即使没有活动的控制台,消息也能被保存下来,供日后通过 dmesg 命令读取。
  • 日志级别(Log Levels):引入了 KERN_EMERG, KERN_INFO 等日志级别。这允许根据消息的重要性进行过滤,例如,可以配置控制台只显示 KERN_WARNING 及以上级别的严重消息。
  • 控制台抽象(Console Abstraction):内核创建了 struct console 抽象层。任何可以显示字符的驱动(如串行端口驱动 ttyS、VGA文本模式驱动 fgconsole、网络控制台 netconsole)都可以将自己注册为一个“控制台”。当 printk 唤醒消费者时,所有注册的活动控制台都会从环形缓冲区中读取并显示新消息。
  • 并发与性能优化:在多核(SMP)系统上,多个CPU可能同时调用 printk。为了处理并发,引入了自旋锁 (logbuf_lock)。后续为了减少锁竞争,又引入了更复杂的机制,如 per-CPU 缓冲区,以提高性能。
  • 速率限制(Rate Limiting):为了防止有缺陷的驱动程序疯狂打印日志(log flood)导致系统性能下降和日志被冲刷,内核引入了 printk_ratelimit() 机制。
  • 结构化与字典压缩:较新的内核版本正在尝试引入结构化日志和字典压缩技术,以减少日志的体积,并使其更易于被机器解析。

目前该技术的社区活跃度和主流应用情况如何?

printk 是Linux内核中最基础、最稳定、使用最广泛的功能,没有之一。

  • 主流应用:它是所有内核代码(核心、驱动、文件系统等)输出日志信息的标准方式dmesg 命令是每个Linux系统管理员必备的诊断工具,其内容就直接来自 printk 的环形缓冲区。journald, rsyslog 等用户空间日志服务也会从内核读取 printk 的输出。它是内核开发“Hello, World!”的第一步。

核心原理与设计

它的核心工作原理是什么?

printk 的工作流程可以概括为“生产者-消费者”模型:

  1. 生产者(printk 调用)

    • 内核代码调用 printk(KERN_INFO "Device initialized with IRQ %d\n", irq);
    • 该函数首先会解析格式化字符串和参数,生成最终的日志消息。
    • 它会为消息添加一个前缀,包含日志级别、时间戳等元数据。
    • 然后,它会获取保护环形缓冲区的锁(logbuf_lock)。
    • 消息被原子地写入到内核的全局环形日志缓冲区 (log_buf) 中。
    • 写入完成后,释放锁。
  2. 唤醒消费者

    • 写入新消息后,printk 会唤醒所有已注册的控制台(consoles)。
  3. 消费者(控制台驱动)

    • 被唤醒的控制台驱动(例如,串口驱动)会再次获取锁,检查环形缓冲区中是否有自己尚未打印的新消息。
    • 如果有,它会读取这些消息,并将其输出到它所管理的物理硬件上(如通过串口发送出去,或显示在屏幕上)。
  4. 用户空间接口

    • 用户空间程序(如 dmesg)可以通过 /dev/kmsg 接口或 syslog() 系统调用,直接从内核的环形缓冲区中读取所有日志消息,无论它们是否曾被打印到物理控制台。

它的主要优势体现在哪些方面?

  • 可靠性和健壮性printk 被设计为在内核几乎任何状态下都能工作,包括中断处理、系统恐慌等极端情况。
  • 简单性:其 printf-like 的API对C程序员来说非常熟悉,使用门槛极低。
  • 解耦设计:通过环形缓冲区,printk 的调用者无需关心消息最终将如何、以及在何处显示。
  • 通用性:控制台抽象层使得 printk 的输出可以被重定向到多种物理设备。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能开销printk 不是一个“免费”的操作。它涉及字符串格式化、获取全局锁、唤醒其他任务等,开销相对较大。在性能极其敏感的路径(如网络数据包处理路径)中频繁调用 printk 会严重影响系统性能。
  • 日志风暴(Log Flood):一个有 bug 的驱动程序可能会在循环中不停地调用 printk,导致环形缓冲区被迅速填满,淹没掉其他有用的信息,并消耗大量CPU。
  • 信息泄露风险:开发者可能会不慎将内核的敏感信息(如内存地址、数据结构内容)打印出来,构成安全隐患。
  • 非结构化:传统的 printk 输出是纯文本,不利于自动化工具的解析和分析。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

printk 是用于内核向外界报告异步事件、状态和错误的首选和标准方案,特别是对于频率不高的事件。

  • 驱动初始化和探测:报告硬件是否被成功识别,资源(IRQ, I/O地址)是否分配成功。
  • 错误和警告报告:报告硬件故障、非法的操作请求、资源耗尽等异常情况。
  • 重要的状态变更:例如,网络接口的 UP/DOWN,磁盘的挂载/卸载。
  • 调试:在开发和调试阶段,用于追踪代码执行路径和变量值。为了避免在生产环境中产生不必要的输出,通常会使用 pr_debug()dev_dbg() 等宏,它们在内核编译时若未定义DEBUG则会变为空操作。

是否有不推荐使用该技术的场景?为什么?

  • 性能敏感的热路径(Hot Path):在每秒需要执行数百万次的代码路径中(如网络驱动的核心收发包函数、调度器的核心决策逻辑),应绝对避免使用 printk。这种场景应使用Tracepoints,它在关闭时几乎没有开销。
  • 高频事件:不要为每个成功处理的数据包或每个发生的硬件中断调用 printk
  • 用户空间与内核的数据交换printk 是单向的日志通道,不应用作用户空间和内核之间的双向通信机制。应使用 ioctl, netlink, sysfsprocfs

对比分析

请将其 与 其他相似技术 进行详细对比。

特性 printk Tracepoints / trace_printk pr_debug / dev_dbg procfs/sysfs
主要用途 通用日志记录 (错误、警告、信息) 高性能追踪和调试 条件编译的调试打印 导出状态/配置接口
性能开销 中到高。总是会格式化字符串并尝试获取锁。 极低 (当关闭时)。开启时,写入高效的二进制追踪缓冲区。 (当DEBUG未定义时)。宏展开为空,代码被编译掉。 read/write时才有开销。
输出目标 内核环形缓冲区 (dmesg) 和控制台。 ftrace/perf 的二进制追踪缓冲区。 printk相同,但受编译条件限制。 文件系统中的文件。
数据格式 非结构化文本。 结构化二进制数据。 非结构化文本。 结构化或非结构化文本。
运行时控制 可通过控制台日志级别过滤输出。 可通过 ftrace 接口动态开启/关闭/过滤。 编译时决定,运行时无法开启。 总是可读/写。
适用场景 报告低频、重要的系统事件和错误。 调试性能问题,追踪高频事件。 在开发阶段添加调试代码,发布时移除。 向用户空间暴露内核对象的状态和配置。

include/linux/kern_levels.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define KERN_SOH	"\001"		/* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'

#define KERN_EMERG KERN_SOH "0" /* 系统不可用 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取作 */
#define KERN_CRIT KERN_SOH "2" /* 危急情况 */
#define KERN_ERR KERN_SOH "3" /* 错误条件 */
#define KERN_WARNING KERN_SOH "4"/* 警告条件 */
#define KERN_NOTICE KERN_SOH "5" /* 正常但严重的情况 */
#define KERN_INFO KERN_SOH "6" /* 信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试级消息 */

#define KERN_DEFAULT "" /* the default kernel loglevel */

/*
*对日志打印输出的 “continued” 行的注释(仅在没有封闭的 \n 行之后完成)。仅在早期启动期间由 core/arch 代码使用(否则连续行不是 SMP 安全的).
*/
//text 是连续行的片段
#define KERN_CONT KERN_SOH "c"

include/linux/printk.h

printk 等级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define pr_emerg(fmt, ...) \
printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...) \
printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...) \
printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...) \
printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn(fmt, ...) \
printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_notice(fmt, ...) \
printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...) \
printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)
#define pr_cont(fmt, ...) \
printk(KERN_CONT fmt, ##__VA_ARGS__)
#ifdef DEBUG
#define pr_devel(fmt, ...) \
printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_devel(fmt, ...) \
no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

printk_get_level 获取日志级别

  • 例如KERN_EMERG输出反馈为”0”
1
2
3
4
5
6
7
8
9
10
11
static inline int printk_get_level(const char *buffer)
{
if (buffer[0] == KERN_SOH_ASCII && buffer[1]) {
switch (buffer[1]) {
case '0' ... '7':
case 'c': /* KERN_CONT */
return buffer[1];
}
}
return 0;
}

printk_deferred_enter 和printk_deferred_exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*可被 NMI 抢占。 */
void __printk_safe_enter(void)
{
this_cpu_inc(printk_context);
}

/* 可被 NMI 抢占。 */
void __printk_safe_exit(void)
{
this_cpu_dec(printk_context);
}


void __printk_deferred_enter(void)
{
cant_migrate();
__printk_safe_enter();
}

void __printk_deferred_exit(void)
{
cant_migrate();
__printk_safe_exit();
}

/*
* printk_deferred_enter/exit 宏仅作为某些需要延迟所有 printk 控制台打印的代码路径的 hack 可用。必须在延迟持续时间内禁用中断。
*/
#define printk_deferred_enter() __printk_deferred_enter()
#define printk_deferred_exit() __printk_deferred_exit()

kernel/printk/internal.h

printk_get_console_flush_type 确定使用哪些控制台刷新方法

  1. have_nbcon_consolehave_legacy_consoleregister_console时设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/*
* 确定应在调用方的上下文中使用哪些控制台刷新方法。
*/
static inline void printk_get_console_flush_type(struct console_flush_type *ft)
{
memset(ft, 0, sizeof(*ft));

switch (nbcon_get_default_prio()) { //获取nbcon 默认优先级
case NBCON_PRIO_NORMAL:
if (have_nbcon_console && !have_boot_console) {
if (printk_kthreads_running)
ft->nbcon_offload = true;
else
ft->nbcon_atomic = true;
}

/* Legacy consoles are flushed directly when possible. */
if (have_legacy_console || have_boot_console) {
if (!is_printk_legacy_deferred())
ft->legacy_direct = true;
else
ft->legacy_offload = true;
}
break;

case NBCON_PRIO_EMERGENCY:
if (have_nbcon_console && !have_boot_console)
ft->nbcon_atomic = true;

/* 如果可能,将直接刷新旧控制台。 */
if (have_legacy_console || have_boot_console) {
if (!is_printk_legacy_deferred())
ft->legacy_direct = true;
else
ft->legacy_offload = true;
}
break;

case NBCON_PRIO_PANIC:
/*
* In panic, the nbcon consoles will directly print. But
* only allowed if there are no boot consoles.
*/
if (have_nbcon_console && !have_boot_console)
ft->nbcon_atomic = true;

if (have_legacy_console || have_boot_console) {
/*
* This is the same decision as NBCON_PRIO_NORMAL
* except that offloading never occurs in panic.
*
* Note that console_flush_on_panic() will flush
* legacy consoles anyway, even if unsafe.
*/
if (!is_printk_legacy_deferred())
ft->legacy_direct = true;

/*
* In panic, if nbcon atomic printing occurs,
* the legacy consoles must remain silent until
* explicitly allowed.
*/
if (ft->nbcon_atomic && !legacy_allow_panic_sync)
ft->legacy_direct = false;
}
break;

default:
WARN_ON_ONCE(1);
break;
}
}

printk_info_flags

1
2
3
4
5
6
7
/* 单个 printk 记录的标志。*/
enum printk_info_flags {
/* 始终在控制台上显示,忽略console_loglevel */
LOG_FORCE_CON = 1
LOG_NEWLINE = 2/* 以换行符结尾的文本 */
LOG_CONT = 8/* text 是连续行的片段 */
};

console_is_usable 检查控制台是否可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* 检查给定的控制台当前是否能够并允许打印记录。
* 请注意,此函数不考虑当前上下文,该上下文也可以在决定是否可以使用 @con 打印记录时发挥作用。
*/
static inline bool console_is_usable(struct console *con, short flags, bool use_atomic)
{
if (!(flags & CON_ENABLED))
return false;

if ((flags & CON_SUSPENDED))
return false;

if (flags & CON_NBCON) {
/* The write_atomic() callback is optional. */
if (use_atomic && !con->write_atomic)
return false;

/*
* For the !use_atomic case, @printk_kthreads_running is not
* checked because the write_thread() callback is also used
* via the legacy loop when the printer threads are not
* available.
*/
} else {
if (!con->write)
return false;
}

/*
* Console drivers may assume that per-cpu resources have been
* allocated. So unless they're explicitly marked as being able to
* cope (CON_ANYTIME) don't call them until this CPU is officially up.
*/
if (!cpu_online(raw_smp_processor_id()) && !(flags & CON_ANYTIME))
return false;

return true;
}

include/linux/console.h

for_each_console 遍历控制台列表

1
2
3
4
5
6
7
8
9
10
11
12
13
HLIST_HEAD(console_list);

/**
* for_each_console() - 已注册控制台的迭代器
* @con:用作循环光标的结构控制台指针
*
* 控制台列表和 &console.flags 在迭代时是不可变的。
*
* 需要举行console_list_lock。
*/
#define for_each_console(con) \
lockdep_assert_console_list_lock_held(); \
hlist_for_each_entry(con, &console_list, node)

cons_flags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* enum cons_flags - 常规控制台标志
* @CON_PRINTBUFFER:由新注册的控制台使用,以避免重复输出已由引导控制台显示或用户空间通过 syslog() syscall 读取的消息。
* @CON_CONSDEV:表示控制台驱动程序正在备份
* /dev/console 的
* @CON_ENABLED:指示是否允许控制台打印记录。如果为 false,则控制台也不会前进到后面的记录。
* @CON_BOOT:将控制台驱动程序标记为早期控制台驱动程序,在实际驱动程序可用之前,在启动期间使用该驱动程序。除非使用 “keep_bootcon” 参数,否则当注册真正的控制台驱动程序时,它将自动注销。
* @CON_ANYTIME:一个用词错误的历史标志,它告诉核心代码,可以在标记为 OFFLINE 的 CPU 上调用旧版 @console::write 回调。这具有误导性,因为它表明调用回调没有上下文限制。最初的动机是每个 CPU 区域的准备情况。
* @CON_BRL:表示盲文设备由于明显的原因而免于接收 printk 垃圾邮件。
* @CON_EXTENDED:控制台支持 /dev/kmesg 的扩展输出格式,需要更大的输出缓冲区。
* @CON_SUSPENDED:指示控制台是否暂停。如果为 true,则不得调用 printing 回调。
* @CON_NBCON:控制台可以在旧版样式console_lock约束之外运行。
*/
enum cons_flags {
CON_PRINTBUFFER = BIT(0),
CON_CONSDEV = BIT(1),
CON_ENABLED = BIT(2),
CON_BOOT = BIT(3),
CON_ANYTIME = BIT(4),
CON_BRL = BIT(5),
CON_EXTENDED = BIT(6),
CON_SUSPENDED = BIT(7),
CON_NBCON = BIT(8),
};

console_srcu_read_flags 无锁读取可能已注册控制台的标志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* console_srcu_read_flags - 无锁读取可能已注册控制台的标志
* @con: 指向控制台结构体的指针,用于读取标志
*
* 无锁读取 @con->flags 提供了一致的读取值,因为最多只有一个 CPU 修改 @con->flags,
* 并且该 CPU 仅使用读-修改-写操作进行修改。
*
* 需要持有 console_srcu_read_lock,这意味着 @con 可能是一个已注册的控制台。
* 持有 console_srcu_read_lock 的目的是保证控制台状态有效(CON_SUSPENDED/CON_ENABLED),
* 并且如果控制台当前正在注销,确保不会运行退出/清理例程。
*
* 如果调用者持有 console_list_lock 或者 _确定_ @con 未注册且不会被注册,
* 调用者可以直接读取 @con->flags。
*
* 上下文:任何上下文。
* 返回值:@con->flags 字段的当前值。
*/
static inline short console_srcu_read_flags(const struct console *con)
{
WARN_ON_ONCE(!console_srcu_read_lock_is_held());

/*
* READ_ONCE() 与 console_srcu_write_flags() 修改已注册控制台的 @flags 时的
* WRITE_ONCE() 相匹配。
*/
return data_race(READ_ONCE(con->flags));
}

console_is_registered_locked 控制台是否已注册

1
2
3
4
5
6
/* 举行 console_list_lock 时 console_is_registered() 的变体。 */
static inline bool console_is_registered_locked(const struct console *con)
{
lockdep_assert_console_list_lock_held();
return !hlist_unhashed(&con->node);
}

kernel/printk/printk_ringbuffer.h 内核打印环形缓冲区实现

prb_desc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 描述符状态查询的可能响应。*/
enum desc_state {
desc_miss = -1/* ID 不匹配(伪状态) */
desc_reserved = 0x0/* 保留, 正在由编写器 */
desc_committed = 0x1/* 由编写器提交,可以重新打开 */
desc_finalized = 0x2/* 已提交,不允许进一步修改 */
desc_reusable = 0x3/* 免费,尚未被任何编写器使用 */
};

/*
* 描述符:记录的完整元数据。
*
* @state_var:描述符 ID 和描述符状态的按位组合。
*/
struct prb_desc {
//最高几位为desc_state,该变量用于识别ABA问题
atomic_long_t state_var;
/* 指定数据块的逻辑位置和跨度。 */
struct prb_data_blk_lpos text_blk_lpos;
};

_DEFINE_PRINTKRB 定义 ringbuffer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
/**
* DOC: printk_ringbuffer overview
*
* 数据结构
* --------------
* printk_ringbuffer 由三个内部环形缓冲区组成:
*
* desc_ring
* 描述符及其元数据(如序列号、时间戳、日志级别等)的环形缓冲区,
* 以及关于记录的内部状态信息和逻辑位置,指定其他环形缓冲区中文本字符串的位置。
*
* text_data_ring
* 数据块的环形缓冲区。数据块由一个无符号长整型(ID)组成,
* 它映射到 desc_ring 的索引,后跟记录的文本字符串。
*
* 描述符的内部状态信息是允许读写者无锁同步访问数据的关键元素。
*
* 实现
* --------------
*
* 描述符环
* ~~~~~~~~~~~~~~~
* 描述符环是一个描述符数组。描述符包含用于跟踪 printk 记录数据的基本元数据,
* 使用 blk_lpos 结构指向关联的文本数据块(参见下文的“数据环”)。
* 每个描述符都分配了一个 ID,该 ID 直接映射到描述符数组的索引值,并具有一个状态。
* ID 和状态按位组合到一个名为 @state_var 的单个描述符字段中,
* 允许 ID 和状态同步且原子地更新。
*
* 描述符有四种状态:
*
* reserved
* 写入者正在修改记录。
*
* committed
* 记录及其所有数据已写入。写入者可以重新打开描述符(将其转换回 reserved),
* 但在 committed 状态下,数据是一致的。
*
* finalized
* 记录及其所有数据已完成并可供读取。写入者无法重新打开描述符。
*
* reusable
* 记录存在,但其文本和/或元数据可能不再可用。
*
* 查询记录的 @state_var 需要提供要查询的描述符的 ID。
* 这可能会产生第五种(伪)状态:
*
* miss
* 被查询的描述符具有意外的 ID。
*
* 描述符环有一个 @tail_id,包含最旧描述符的 ID,
* 以及一个 @head_id,包含最新描述符的 ID。
*
* 当需要创建一个新的描述符(并且环已满)时,
* 通过首先将尾部描述符转换为 reusable 状态,
* 然后使尾部数据块(对于文本环)无效,直到包括与尾部描述符关联的数据块。
* 然后推进 @tail_id,接着推进 @head_id。
* 最后,新描述符的 @state_var 被初始化为新的 ID 和 reserved 状态。
*
* 只有当新的 @tail_id 处于 committed 或 reusable 查询状态时,@tail_id 才能推进。
* 这使得尾部的有效序列号始终可用。
*
* 描述符最终化
* ~~~~~~~~~~~~~~~~~~~~~~~
* 当写入者调用提交函数 prb_commit() 时,记录数据完全存储并在环形缓冲区内一致。
* 然而,写入者可以重新打开该记录,声明独占访问权(如同 prb_reserve()),并修改该记录。
* 完成后,写入者必须再次提交记录。
*
* 为了使记录可供读者使用(并且也可供写入者回收),它必须被最终化。
* 最终化的记录不能被重新打开,也永远不会变得“未最终化”。
* 记录最终化可以在三种不同的场景中发生:
*
* 1) 写入者可以通过调用 prb_final_commit() 而不是 prb_commit() 来同时提交并最终化其记录。
*
* 2) 当一个新记录被保留并且前一个记录通过 prb_commit() 提交时,
* 该前一个记录会自动最终化。
*
* 3) 当一个记录通过 prb_commit() 提交并且已经存在一个更新的记录时,
* 被提交的记录会自动最终化。
*
* 数据环
* ~~~~~~~~~
* 文本数据环是由数据块组成的字节数组。数据块由 blk_lpos 结构引用,
* 该结构指向数据块的起始逻辑位置和下一个相邻数据块的起始逻辑位置。
* 逻辑位置直接映射到字节数组环形缓冲区的索引值。
*
* 每个数据块由一个 ID 和后续的写入者数据组成。
* ID 是与数据块关联的描述符的标识符。
* 如果满足以下所有条件,则认为给定的数据块有效:
*
* 1) 与数据块关联的描述符处于 committed 或 finalized 查询状态。
*
* 2) 与数据块关联的描述符中的 blk_lpos 结构引用回同一数据块。
*
* 3) 数据块位于 head/tail 逻辑位置范围内。
*
* 如果数据块的写入者数据会超出字节数组的末尾,
* 则仅在逻辑位置存储数据块的 ID,而完整的数据块(ID 和写入者数据)存储在字节数组的开头。
* 引用的 blk_lpos 将指向 wrap 之前的 ID,
* 下一个数据块将位于 wrap 之后完整数据块相邻的逻辑位置。
*
* 数据环有一个 @tail_lpos,指向最旧数据块的起始位置,
* 以及一个 @head_lpos,指向下一个(尚不存在)数据块的逻辑位置。
*
* 当需要创建一个新的数据块(并且环已满)时,
* 首先通过将其关联的描述符置于 reusable 状态并推动 @tail_lpos 超过它们,
* 使尾部数据块无效。然后推动 @head_lpos 并将其与一个新的描述符关联。
* 如果数据块无效,则 @tail_lpos 无法推进超过它。
*
* 信息数组
* ~~~~~~~~~~
* printk 记录的一般元数据存储在 printk_info 结构中,
* 这些结构存储在一个数组中,其元素数量与描述符环相同。
* 每个信息与描述符环中相同索引的描述符相对应。
* 信息的有效性通过在加载信息之前和之后评估相应的描述符来确认。
*
* 用法
* -----
* 以下是一些简单的示例,演示了写入者和读取者的用法。
* 对于示例,提供了一个全局环形缓冲区(test_rb)(这不是 printk 使用的实际环形缓冲区)::
*
* DEFINE_PRINTKRB(test_rb, 15, 5);
*
* 该环形缓冲区最多允许 32768 条记录(2 ^ 15),
* 并且文本数据的大小为 1 MiB(2 ^ (15 + 5))。
*
* 示例写入者代码:
*
* const char *textstr = "message text";
* struct prb_reserved_entry e;
* struct printk_record r;
*
* // 指定要分配的大小
* prb_rec_init_wr(&r, strlen(textstr) + 1);
*
* if (prb_reserve(&e, &test_rb, &r)) {
* snprintf(r.text_buf, r.text_buf_size, "%s", textstr);
*
* r.info->text_len = strlen(textstr);
* r.info->ts_nsec = local_clock();
* r.info->caller_id = printk_caller_id();
*
* // 提交并最终化记录
* prb_final_commit(&e);
* }
*
* 注意,还提供了其他写入者函数,用于在记录已提交但尚未最终化时扩展记录。
* 只要没有保留新记录并且调用者相同,就可以这样做。
*
* 示例写入者代码(记录扩展):
*
* // 上一个示例的替代部分
*
* r.info->text_len = strlen(textstr);
* r.info->ts_nsec = local_clock();
* r.info->caller_id = printk_caller_id();
*
* // 提交记录(但尚未最终化)
* prb_commit(&e);
* }
*
* ...
*
* // 指定额外的 5 字节文本空间以扩展
* prb_rec_init_wr(&r, 5);
*
* // 尝试扩展,但仅当其不超过 32 字节时
* if (prb_reserve_in_last(&e, &test_rb, &r, printk_caller_id(), 32)) {
* snprintf(&r.text_buf[r.info->text_len],
* r.text_buf_size - r.info->text_len, "hello");
*
* r.info->text_len += 5;
*
* // 提交并最终化记录
* prb_final_commit(&e);
* }
*
* 示例读取者代码:
*
* struct printk_info info;
* struct printk_record r;
* char text_buf[32];
* u64 seq;
*
* prb_rec_init_rd(&r, &info, &text_buf[0], sizeof(text_buf));
*
* prb_for_each_record(0, &test_rb, &seq, &r) {
* if (info.seq != seq)
* pr_warn("lost %llu records\n", info.seq - seq);
*
* if (info.text_len > r.text_buf_size) {
* pr_warn("record %llu text truncated\n", info.seq);
* text_buf[r.text_buf_size - 1] = 0;
* }
*
* pr_info("%llu: %llu: %s\n", info.seq, info.ts_nsec,
* &text_buf[0]);
* }
*
* 注意,还提供了其他不太方便的读取者函数,以允许复杂的记录访问。
*
* ABA 问题
* ~~~~~~~~~~
* 为了帮助避免 ABA 问题,描述符通过 ID(数组索引值与计数数组环的标记位组合)引用,
* 数据块通过逻辑位置(数组索引值与计数数组环的标记位组合)引用。
* 然而,在 32 位系统上,标记位的数量相对较小,因此 ABA 事件(至少在理论上)是可能的。
* 例如,如果在 32 位系统上的 NMI 上下文中发生了 400 万条最大大小(1KiB)的 printk 消息,
* 中断的上下文将无法识别 32 位整数完全环绕,因此表示与中断上下文期望的不同的数据块。
*
* 为了帮助应对这种可能性,执行了额外的状态检查(例如,即使 set() 足够,也使用 cmpxchg())。
* 这些额外的检查已被注释,并希望能够捕获 32 位系统可能遇到的任何 ABA 问题。
*
* 内存屏障
* ~~~~~~~~~~~~~~~
* 使用了多个内存屏障。为了简化正确性证明和生成点火测试,
* 与内存屏障相关的代码行(加载、存储和相关的内存屏障)被标记为:
*
* LMM(function:letter)
*
* 注释仅引用“function:letter”部分。
*
* 内存屏障对及其顺序如下:
*
* desc_reserve:D / desc_reserve:B
* 推动描述符尾部(id),然后推动描述符头部(id)
*
* desc_reserve:D / data_push_tail:B
* 推动数据尾部(lpos),然后设置新的描述符为 reserved(状态)
*
* desc_reserve:D / desc_push_tail:C
* 推动描述符尾部(id),然后设置新的描述符为 reserved(状态)
*
* desc_reserve:D / prb_first_seq:C
* 推动描述符尾部(id),然后设置新的描述符为 reserved(状态)
*
* desc_reserve:F / desc_read:D
* 设置新的描述符 id 和 reserved(状态),然后允许写入者更改
*
* data_alloc:A (或 data_realloc:A) / desc_read:D
* 设置旧描述符为 reusable(状态),然后修改新的数据块区域
*
* data_alloc:A (或 data_realloc:A) / data_push_tail:B
* 推动数据尾部(lpos),然后修改新的数据块区域
*
* _prb_commit:B / desc_read:B
* 存储写入者更改,然后设置新的描述符为 committed(状态)
*
* desc_reopen_last:A / _prb_commit:B
* 设置描述符为 reserved(状态),然后读取描述符数据
*
* _prb_commit:B / desc_reserve:D
* 设置新的描述符为 committed(状态),然后检查描述符头部(id)
*
* data_push_tail:D / data_push_tail:A
* 设置描述符为 reusable(状态),然后推动数据尾部(lpos)
*
* desc_push_tail:B / desc_reserve:D
* 设置描述符为 reusable(状态),然后推动描述符尾部(id)
*
* desc_update_last_finalized:A / desc_last_finalized_seq:A
* 存储最终化记录,然后设置新的最高最终化序列号
*/

/*
* 描述符引导
*
* 描述符数组经过最小初始化,以允许读取器和写入器立即使用。描述符数组初始化必须满足的要求:
*
* 要求1
* 尾部必须指向现有的(已提交或可重用的)描述符。这是 prb_first_seq() 的实现所必需的。
*
* 要求2
* 读取器必须看到 ringbuffer 最初是空的。
*
* 要求3
* 写入器保留的第一条记录被分配了序列号 0。
*
* 为了满足 Req1,尾部最初指向一个最小初始化的描述符(没有数据块,即数据块的 lpos @begin和 @next 值设置为 FAILED_LPOS 的无数据)。
*
* 为了满足 Req2,初始尾部描述符被初始化为可重用状态。读者将可重用的描述符识别为现有记录,但会跳过它们。
*
* 为了满足 Req3,数组中的最后一个描述符用作初始头(和尾)描述符。这允许写入器(head 1)保留的第一条记录成为数组中的第一个描述符。(只有数组中的第一个 Descriptor 可以具有有效的序列号 0。
*
* 首次保留描述符时,会为其分配一个序列号,其中包含数组索引的值。可以识别“首次保留”描述符,因为它的序列号为 0,但索引没有 0。(只有数组中的第一个 Descriptor 可以具有有效的序列号 0。在第一次预留之后,所有未来的预留(回收)都只涉及按数组计数递增序列号。
*
* 黑客 #1
* 仅数组中的第一个描述符允许序列号为 0。在这种情况下,无法识别它是第一次保留(设置为索引值)还是之前已保留(按数组计数递增)。这是通过在保留数组中的第一个描述符时_always_ 按数组计数递增序列号来处理的。为了满足 Req3,数组中第一个描述符的序列号被初始化为减去数组计数。然后,在第一次保留时,它递增为 0,从而满足 Req3。
*
* 黑客 #2
* prb_first_seq() 可以由读取器随时调用,以检索尾部描述符的序列号。但是,由于 Req2 和 Req3,最初没有记录来报告其序列号(序列号为 u64,并且不小于 0)。为了处理此问题,初始尾部描述符的序列号初始化为 0。从技术上讲,这是不正确的,因为没有序列号为 0 的记录(目前),并且尾部描述符不是数组中的第一个描述符。但它允许 prb_read_valid() 始终正确报告 _any_ 给定序列号的记录的存在。当第一次推送 tail 时,Bootstrapping 完成,从而最终指向 writer 保留的第一个 Descriptors,该 Descriptors 具有分配的序列号 0。
*/

#define _DATA_SIZE(sz_bits) (1UL << (sz_bits))
#define _DESCS_COUNT(ct_bits) (1U << (ct_bits))
#define DESC_SV_BITS BITS_PER_LONG //描述符状态变量的位数
#define DESC_FLAGS_SHIFT (DESC_SV_BITS - 2) //FLAG所占的位数
#define DESC_FLAGS_MASK (3UL << DESC_FLAGS_SHIFT) //FLAGES掩码
#define DESC_STATE(sv) (3UL & (sv >> DESC_FLAGS_SHIFT)) //描述符状态 FLAG
#define DESC_SV(id, state) (((unsigned long)state << DESC_FLAGS_SHIFT) | id) //描述符状态变量
#define DESC_ID_MASK (~DESC_FLAGS_MASK) //描述符 ID 掩码
#define DESC_ID(sv) ((sv) & DESC_ID_MASK) //描述符 ID
/* 数据环的初始@head_lpos和@tail_lpos。它在索引处
* 0,并且 LPOS 值在第一次换行时将溢出 */
#define BLK0_LPOS(sz_bits) (-(_DATA_SIZE(sz_bits))) //数据块的逻辑位置
/* 0. descbits = 5
1. 初始化时, head_id = DESC0_ID(descbits) = DESC_ID(-(_DESCS_COUNT(ct_bits) + 1)) = DESC_ID(-(1U << (ct_bits)) + 1)
= DESC_ID(-33) = 0b00011111 = 刚好是描述符数组的最后一个索引处的 ID。
初始化=这个值,那么第一次进入+1刚好从0开始,第二次进入+1就会溢出到0xFFFFFFFFF
*/
#define DESC0_ID(ct_bits) DESC_ID(-(_DESCS_COUNT(ct_bits) + 1)) //刚好是描述符数组的最后一个索引处的 ID。
/* 这个设置了最高位的状态默认为无使用 */
#define DESC0_SV(ct_bits) DESC_SV(DESC0_ID(ct_bits), desc_reusable)

/*
* 特殊数据块逻辑位置值(适用于 @prb_desc.text_blk_lpos 的字段)。
*
* - Bit0 用于标识记录是否没有数据块。(在 LPOS_DATALESS() 宏中实现。
*
* - Bit1 指定没有数据块的原因。
*
* 由于数据块的元数据和对齐填充,这些特殊值永远不可能是真正的 lpos 值。(有关详细信息,请参阅 to_blk_size().)
*/
#define FAILED_LPOS 0x1
#define EMPTY_LINE_LPOS 0x3

#define FAILED_BLK_LPOS \
{ \
.begin = FAILED_LPOS, \
.next = FAILED_LPOS, \
}

/*
- 使用外部文本数据缓冲区定义 ringbuffer。与 DEFINE_PRINTKRB() 相同,但需要为文本数据指定外部缓冲区。
*
* 注意:指定的外部缓冲区的大小必须为:
* 2 ^ (描述平均文本位)
*/
#define _DEFINE_PRINTKRB(name, descbits, avgtextbits, text_buf) \
static struct prb_desc _##name##_descs[_DESCS_COUNT(descbits)] = { \
/* 初始头和尾 */ \
[_DESCS_COUNT(descbits) - 1] = { \
/* 可 重用 */ \
.state_var = ATOMIC_INIT(DESC0_SV(descbits)), \
/* 无关联数据块 */ \
.text_blk_lpos = FAILED_BLK_LPOS, \
}, \
}; \
static struct printk_info _##name##_infos[_DESCS_COUNT(descbits)] = { \
/* 这将是写入器保留的第一条记录 */ \
[0] = { \
/* 将在第一次预订时递增为 0 */ \
.seq = -(u64)_DESCS_COUNT(descbits), \
}, \
/* 初始头和尾 */ \
[_DESCS_COUNT(descbits) - 1] = { \
/* 报告 Bootstrap 阶段的第一个 Seq 值 */ \
.seq = 0, \
}, \
}; \
static struct printk_ringbuffer name = { \
.desc_ring = { \
.count_bits = descbits, \
.descs = &_##name##_descs[0], \
.infos = &_##name##_infos[0], \
.head_id = ATOMIC_INIT(DESC0_ID(descbits)), \
.tail_id = ATOMIC_INIT(DESC0_ID(descbits)), \
.last_finalized_seq = ATOMIC_INIT(0), \
}, \
.text_data_ring = { \
.size_bits = (avgtextbits) + (descbits), \
.data = text_buf, \
.head_lpos = ATOMIC_LONG_INIT(BLK0_LPOS((avgtextbits) + (descbits))), \
.tail_lpos = ATOMIC_LONG_INIT(BLK0_LPOS((avgtextbits) + (descbits))), \
}, \
.fail = ATOMIC_LONG_INIT(0), \
}

kernel/printk/printk_ringbuffer.c 内核打印环形缓冲区实现

macros

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define DATA_SIZE(data_ring)		_DATA_SIZE((data_ring)->size_bits)
#define DATA_SIZE_MASK(data_ring) (DATA_SIZE(data_ring) - 1)

#define DESCS_COUNT(desc_ring) _DESCS_COUNT((desc_ring)->count_bits) //获取
#define DESCS_COUNT_MASK(desc_ring) (DESCS_COUNT(desc_ring) - 1)

/* Determine the data array index from a logical position. */
#define DATA_INDEX(data_ring, lpos) ((lpos) & DATA_SIZE_MASK(data_ring))

/* Determine the desc array index from an ID or sequence number. */
#define DESC_INDEX(desc_ring, n) ((n) & DESCS_COUNT_MASK(desc_ring))

/* 确定数据数组已环绕的次数。*/
#define DATA_WRAPS(data_ring, lpos) ((lpos) >> (data_ring)->size_bits)

/* Determine if a logical position refers to a data-less block. */
#define LPOS_DATALESS(lpos) ((lpos) & 1UL)
#define BLK_DATALESS(blk) (LPOS_DATALESS((blk)->begin) && \
LPOS_DATALESS((blk)->next))

/* 获取当前环绕的索引 0 处的逻辑位置. */
#define DATA_THIS_WRAP_START_LPOS(data_ring, lpos) \
((lpos) & ~DATA_SIZE_MASK(data_ring))

/* 获取与给定 ID 相同的上一个环绕索引的 ID。 */
#define DESC_ID_PREV_WRAP(desc_ring, id) \
DESC_ID((id) - DESCS_COUNT(desc_ring))

desc_reopen_last 尝试将最新的描述符从 committed 转换回 reserved

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* 尝试将最新的描述符从 committed 转换回 reserved,以便写入器可以再次修改记录。仅当描述符尚未最终确定且提供的 @caller_id 匹配时,才有可能执行此作。
*/
static struct prb_desc *desc_reopen_last(struct prb_desc_ring *desc_ring,
u32 caller_id, unsigned long *id_out)
{
unsigned long prev_state_val;
enum desc_state d_state;
struct prb_desc desc;
struct prb_desc *d;
unsigned long id;
u32 cid;

id = atomic_long_read(&desc_ring->head_id);

/*
* To reduce unnecessarily reopening, first check if the descriptor
* state and caller ID are correct.
*/
d_state = desc_read(desc_ring, id, &desc, NULL, &cid);
if (d_state != desc_committed || cid != caller_id)
return NULL;

d = to_desc(desc_ring, id);

prev_state_val = DESC_SV(id, desc_committed);

if (!atomic_long_try_cmpxchg(&d->state_var, &prev_state_val,
DESC_SV(id, desc_reserved))) { /* LMM(desc_reopen_last:A) */
return NULL;
}

*id_out = id;
return d;
}

prb_reserve_in_last

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
bool prb_reserve_in_last(struct prb_reserved_entry *e, struct printk_ringbuffer *rb,
struct printk_record *r, u32 caller_id, unsigned int max_size)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
struct printk_info *info;
unsigned int data_size;
struct prb_desc *d;
unsigned long id;

local_irq_save(e->irqflags);

/* 将最新的描述符转换回 reserved 状态. */
d = desc_reopen_last(desc_ring, caller_id, &id);
if (!d) {
local_irq_restore(e->irqflags);
goto fail_reopen;
}

/* 现在 writer 拥有独占访问权限:LMM(prb_reserve_in_last:A)*/

info = to_info(desc_ring, id);

/*
* 在此处设置 @e 字段,以便从现在开始出现任何故障时可以使用 prb_commit()。
*/
e->rb = rb;
e->id = id;

/*
* desc_reopen_last() checked the caller_id, but there was no
* exclusive access at that point. The descriptor may have
* changed since then.
*/
if (caller_id != info->caller_id)
goto fail;

if (BLK_DATALESS(&d->text_blk_lpos)) { //数据块没有数据
if (WARN_ON_ONCE(info->text_len != 0)) {
pr_warn_once("wrong text_len value (%hu, expecting 0)\n",
info->text_len);
info->text_len = 0;
}

if (!data_check_size(&rb->text_data_ring, r->text_buf_size))
goto fail;

if (r->text_buf_size > max_size)
goto fail;
// 分配新的数据块
r->text_buf = data_alloc(rb, r->text_buf_size,
&d->text_blk_lpos, id);
} else {//数据块有数据
//获取数据块的大小
if (!get_data(&rb->text_data_ring, &d->text_blk_lpos, &data_size))
goto fail;

/*
* 增加缓冲区大小以包含原始大小。如果元数据 (@text_len) 不合理,请使用完整的数据块大小。
*/
if (WARN_ON_ONCE(info->text_len > data_size)) {
pr_warn_once("wrong text_len value (%hu, expecting <=%u)\n",
info->text_len, data_size);
info->text_len = data_size;
}
r->text_buf_size += info->text_len;

if (!data_check_size(&rb->text_data_ring, r->text_buf_size))
goto fail;

if (r->text_buf_size > max_size)
goto fail;
// 重新分配数据块
r->text_buf = data_realloc(rb, r->text_buf_size,
&d->text_blk_lpos, id);
}
if (r->text_buf_size && !r->text_buf)
goto fail;

r->info = info;

e->text_space = space_used(&rb->text_data_ring, &d->text_blk_lpos);

return true;
fail:
prb_commit(e);
/* prb_commit() re-enabled interrupts. */
fail_reopen:
/* 向调用方明确表示重新预留失败。 */
memset(r, 0, sizeof(*r));
return false;
}

to_blk_size 分配对齐的区域并添加指针所需的空间

1
2
3
4
5
6
7
8
9
10
11
12
/*
* Increase the data size to account for data block meta data plus any
* padding so that the adjacent data block is aligned on the ID size.
*/
static unsigned int to_blk_size(unsigned int size)
{
struct prb_data_block *db = NULL;

size += sizeof(*db);
size = ALIGN(size, sizeof(db->id));
return size;
}

data_check_size 检查数据大小是否可以存入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* 储备金大小的健全性检查器。ringbuffer 代码假定数据块不超过 ringbuffer 可容纳的最大可能大小。此函数提供基本大小检查,以便假设是安全的。
*/
static bool data_check_size(struct prb_data_ring *data_ring, unsigned int size)
{
struct prb_data_block *db = NULL;

if (size == 0)
return true;

/*
* 确保对齐填充大小可能适合数据数组。尽可能大的数据块仍必须至少为下一个数据块的 ID 留出空间。
*/
size = to_blk_size(size); //分配对齐的块区域并添加指针所需的空间
if (size > DATA_SIZE(data_ring) //_DATA_SIZE((data_ring)->size_bits)
- sizeof(db->id))
return false;

return true;
}

get_desc_state 查询描述符的状态。

1
2
3
4
5
6
7
8
9
/* 查询描述符的状态。 */
static enum desc_state get_desc_state(unsigned long id,
unsigned long state_val)
{
if (id != DESC_ID(state_val))
return desc_miss;

return DESC_STATE(state_val);
}

desc_read 获取指定描述符的副本并返回其查询状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
* 获取指定描述符的副本并返回其查询状态。
* 如果 Descriptor 处于不一致状态(miss 或 reserved),
* 则调用方只能期望 Descriptor 的 @state_var 字段有效。
*
* 可以选择性地检索序列号和caller_id。与所有非state_var数据一样,它们仅在描述符处于一致状态时有效。
*/
static enum desc_state desc_read(struct prb_desc_ring *desc_ring,
unsigned long id, struct prb_desc *desc_out,
u64 *seq_out, u32 *caller_id_out)
{
struct printk_info *info = to_info(desc_ring, id); //&desc_ring->infos[id];
struct prb_desc *desc = to_desc(desc_ring, id); //&desc_ring->descs[id];
atomic_long_t *state_var = &desc->state_var;
enum desc_state d_state;
unsigned long state_val;

/* 检查描述符状态。 */
state_val = atomic_long_read(state_var); /* LMM(desc_read:A) */
d_state = get_desc_state(id, state_val);
if (d_state == desc_miss || d_state == desc_reserved) {
/*
* 描述符处于不一致状态。至少设置 @state_var,以便调用方可以看到不一致状态的详细信息。
*/
goto out;
}

/*
* 保证在复制描述符内容之前加载状态。这样可以避免复制可能不适用于 Descriptor 状态的过时 Descriptor 内容。这与 _prb_commit:B 配对。
*
* 记忆障碍参与:
*
* 如果 desc_read:A 读取 _prb_commit:B,则 desc_read:C 读取 _prb_commit:A。
*
*依赖:
*
* WMB 从 _prb_commit:A 到 _prb_commit:B 匹配
* RWB 从 desc_read:A 到 desc_read:C
*/
smp_rmb(); /* LMM(desc_read:B) */

/*
* Copy the descriptor data. The data is not valid until the
* state has been re-checked. A memcpy() for all of @desc
* cannot be used because of the atomic_t @state_var field.
*/
if (desc_out) {
memcpy(&desc_out->text_blk_lpos, &desc->text_blk_lpos,
sizeof(desc_out->text_blk_lpos)); /* LMM(desc_read:C) */
}
if (seq_out)
*seq_out = info->seq; /* also part of desc_read:C */
if (caller_id_out)
*caller_id_out = info->caller_id; /* also part of desc_read:C */

smp_rmb(); /* LMM(desc_read:D) */

/*
*数据已复制。返回当前描述符状态,该状态自上述加载以来可能已更改。
*/
state_val = atomic_long_read(state_var); /* LMM(desc_read:E) */
d_state = get_desc_state(id, state_val);
out:
if (desc_out)
atomic_long_set(&desc_out->state_var, state_val);
return d_state;
}

data_push_tail 将数据环形缓冲区的尾部推进到至少 @lpos 的位置。此函数会将与尾部推进超出其关联数据块的描述符置为可重用状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/*
* 将数据环形缓冲区的尾部推进到至少 @lpos 的位置。此函数会将与尾部推进超出其关联数据块的描述符置为可重用状态。
*/
static bool data_push_tail(struct printk_ringbuffer *rb, unsigned long lpos)
{
struct prb_data_ring *data_ring = &rb->text_data_ring;
unsigned long tail_lpos_new;
unsigned long tail_lpos;
unsigned long next_lpos;

/* 如果 @lpos 来自无数据块,则无需操作。 */
if (LPOS_DATALESS(lpos))
return true;

tail_lpos = atomic_long_read(&data_ring->tail_lpos); /* LMM(data_push_tail:A) */

/*
* 循环直到尾部 lpos 达到或超过 @lpos。此条件可能已经满足,从而无需执行来自 data_push_tail:D 的完整内存屏障。
* 然而,由于此 CPU 看到了新的尾部 lpos,任何变为可重用状态的描述符状态必须已经可见。
*/
while ((lpos - tail_lpos) - 1 < DATA_SIZE(data_ring)) {
/*
* 将与 @lpos 之前数据块相关联的所有描述符置为可重用状态。
*/
if (!data_make_reusable(rb, tail_lpos, lpos, &next_lpos)) {
smp_rmb(); /* LMM(data_push_tail:B) */

tail_lpos_new = atomic_long_read(&data_ring->tail_lpos
); /* LMM(data_push_tail:C) */
if (tail_lpos_new == tail_lpos)
return false;

/* 另一个 CPU 推进了尾部。重试。 */
tail_lpos = tail_lpos_new;
continue;
}

if (atomic_long_try_cmpxchg(&data_ring->tail_lpos, &tail_lpos,
next_lpos)) { /* LMM(data_push_tail:D) */
break;
}
}

return true;
}

desc_push_tail 推进描述符环的尾部

  1. 读取最后一个节点的状态
  2. 判断最后一个节点的状态.
  3. 如果最后一个节点的状态是desc_miss,则判断ID是否恰好比预期 ID 晚 1 个换行,则它正在由另一个写入器保留,并且必须被视为保留。
  4. 如果最后一个节点的状态是desc_reserved,则返回false,表示描述符正在由编写器
  5. 如果最后一个节点的状态是desc_committed,则返回false,表示描述符已提交,可以重新打开
  6. 如果最后一个节点的状态是desc_finalized,则将描述符设置为可重用状态
  7. 如果最后一个节点的状态是desc_reusable,则表示free,尚未被任何作者使用
  8. desc_finalized 和 desc_reusable 状态的描述符可以被推送到下一个描述符。
  9. 进行数据块的失效操作,使其关联的描述符可供回收。
  10. 在将 tail 推向 next 之前,请检查 @tail_id 之后的下一个 Descriptors,
  11. 如果下一个描述符小于或等于 @head_id因此不存在将尾部推过头部的风险。可以进行推进操作。
  12. 否则,则重新检查尾部 ID。@tail_id后面的描述符未处于允许的尾部状态。但是,如果尾部此后被另一个 CPU 移动,则无关紧要。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
* 推进 desc ring tail。此函数将尾部前进一个描述符,从而使最早的描述符无效。
* 在推进尾部之前,尾部描述符是可重用的,并且所有数据块(包括描述符的数据块)都无效(即数据环尾部被推过描述符的数据块,使其可重用)。
*/
static bool desc_push_tail(struct printk_ringbuffer *rb,
unsigned long tail_id)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
enum desc_state d_state;
struct prb_desc desc;
//读取最后一个节点的状态
d_state = desc_read(desc_ring, tail_id, &desc, NULL, NULL);

switch (d_state) {
case desc_miss: //最后一个节点的状态 ID 不匹配(伪状态
/*
*如果 ID 恰好比预期 ID 晚 1 个换行,则它正在由另一个写入器保留,并且必须被视为保留。
*/
if (DESC_ID(atomic_long_read(&desc.state_var)) ==
DESC_ID_PREV_WRAP(desc_ring, tail_id)) {
return false;
}

/*
* ID 已更改。另一个作者一定已经推动了尾巴并回收了描述符。返回 Success 是因为调用方只对正在推送的指定尾部感兴趣,而事实确实如此。
*/
return true;
case desc_reserved: //描述符正在由编写器
case desc_committed: //描述符已提交,可以重新打开
return false;
case desc_finalized: // committed,不允许进一步修改
desc_make_reusable(desc_ring, tail_id);
break;
case desc_reusable: //free,尚未被任何作者使用
break;
}

/*
* 数据块必须先失效,然后才能使其关联的描述符可供回收。以后无法使它们失效,因为一旦其关联的描述符消失,就无法信任数据块。
*/

if (!data_push_tail(rb, desc.text_blk_lpos.next))
return false;
/*
* 在将 tail 推向 next 之前,请检查 @tail_id 之后的下一个 Descriptors,
* 因为 tail 必须始终处于 finalized 或 reusable 状态。prb_first_seq() 的实现依赖于此。
*
* 成功读取意味着下一个描述符小于或等于 @head_id因此不存在将尾部推过头部的风险。
*/
d_state = desc_read(desc_ring, DESC_ID(tail_id + 1), &desc,
NULL, NULL); /* LMM(desc_push_tail:A) */

if (d_state == desc_finalized || d_state == desc_reusable) {
/*
* 确保在推送尾部 ID 之前存储任何已转换为可重用的描述符状态。这允许验证回收的描述符状态。
* 需要一个完整的内存屏障,因为其他 CPU 可能已使描述符状态可重用。这与 desc_reserve:D 配对。
*/
atomic_long_cmpxchg(&desc_ring->tail_id, tail_id,
DESC_ID(tail_id + 1)); /* LMM(desc_push_tail:B) */
} else {
smp_rmb(); /* LMM(desc_push_tail:C) */

/*重新检查尾部 ID。@tail_id后面的描述符未处于允许的尾部状态。但是,如果尾部此后被另一个 CPU 移动,则无关紧要。
*/
if (atomic_long_read(&desc_ring->tail_id) == tail_id) /* LMM(desc_push_tail:D) */
return false;
}

return true;
}

desc_reserve 返回新描述符,必要时使最旧的描述符失效。

  1. 原子读取描述符环的头部 ID。
  2. 判断上一次环绕索引的 ID 是否与当前尾部 ID 一致,则尝试将尾部 ID 向前推进以为新描述符腾出空间。
  3. 尝试xchg写入head_id为新值
  4. 这时候分配了未被使用的desc
  5. 判断desc->state_var不是未使用状态,则证明程序出现了问题,发出警告
  6. 写入desc->state_var为DESC_SV(id, desc_reserved) 也就是将描述符的状态设置为保留状态。
  7. 给出id_out可以被进行使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/* 保留新描述符,必要时使最旧的描述符失效。 */
static bool desc_reserve(struct printk_ringbuffer *rb, unsigned long *id_out)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
unsigned long prev_state_val;
unsigned long id_prev_wrap;
struct prb_desc *desc;
unsigned long head_id;
unsigned long id;
/* "Litmus Memory Model",是一种用于描述内存屏障行为的标记。 */
head_id = atomic_long_read(&desc_ring->head_id); /* LMM(desc_reserve:A) */

do {
/* 每次进入+1,获取下一个描述符 ID
当descbits = 5时
第一次进入head_id = DESC_ID(32 + 1) = 0 */
id = DESC_ID(head_id + 1);
/* 获取与给定 ID 相同的上一个环绕索引的 ID。
例如第一次进入id_prev_wrap = -32 = 0*/
id_prev_wrap = DESC_ID_PREV_WRAP(desc_ring, id);

/*
* 确保在读取尾部 ID 之前读取头部 ID。由于尾部 ID 在头部 ID 之前更新,因此可以保证 @id_prev_wrap 永远不会领先于尾部 ID。这与 desc_reserve:D 配对。
*
* 内存屏障参与:
*
* 如果 desc_reserve:A 读取 desc_reserve:D,则 desc_reserve:C 读取 desc_push_tail:B。
*
*依赖:
*
* 从 desc_push_tail:B 到 desc_reserve:D 匹配的 MB
* RMB从 desc_reserve:A 到 desc_reserve:C
*
* 注意:desc_push_tail:B 和 desc_reserve:D 可以是不同的 CPU。但是,desc_reserve:D CPU(执行完整内存屏障)之前必须见过 desc_push_tail:B。
*/
smp_rmb(); /* LMM(desc_reserve:B) */
/* 初始化为32 与head_id一致*/
if (id_prev_wrap == atomic_long_read(&desc_ring->tail_id
)) { /* LMM(desc_reserve:C) */
/*
* 通过前进尾部为新描述符腾出空间。
*/
if (!desc_push_tail(rb, id_prev_wrap))
return false;
}

/*
* 1.保证在验证回收的描述符状态之前读取尾部 ID。读取内存屏障就足够了。这与 desc_push_tail:B 配对。
*
* 记忆障碍参与:
*
* 如果 desc_reserve:C 从 desc_push_tail:B 读取,则 desc_reserve:E 从 desc_make_reusable:A 读取。
*
*依赖:
*
* MB 从 desc_make_reusable:A 到 desc_push_tail:B 匹配
* RMB 从 desc_reserve:C 到 desc_reserve:E
*
* 注意:desc_make_reusable:A 和 desc_push_tail:B 可以是不同的 CPU。但是,desc_push_tail:B CPU(执行完整内存屏障)之前必须见过 desc_make_reusable:A。
*
* 2.确保在存储头部 ID 之前存储尾部 ID。这与 desc_reserve:B 配对。
*
* 3.确保在回收描述符之前存储任何数据环尾部更改。数据环尾部更改可以通过 desc_push_tail()->data_push_tail() 进行。需要一个完整的内存屏障,因为另一个 CPU 可能已经推送了数据环尾部。这与 data_push_tail:B 配对。
*
* 4.确保在回收描述符之前存储新的尾部 ID。需要一个完整的内存屏障,因为另一个 CPU 可能已经推送了尾部 ID。这与 desc_push_tail:C 配对,这也与 prb_first_seq:C 配对。
*
* 5.确保在尝试完成上一个描述符之前存储 head ID。这与 _prb_commit:B 配对。
*/
} while (!atomic_long_try_cmpxchg(&desc_ring->head_id, &head_id, id)); /* LMM(desc_reserve:D) */
/* 再次读取desc_ring->head_id,判断与head_id是否一致,一致则将id付给desc_ring->head_id
true是发生交换,否则@false */

desc = to_desc(desc_ring, id); //&desc_ring->descs[id];

/*
* 如果描述符已被回收,请验证旧状态 val。请参阅“ABA 问题”,了解执行此验证的原因。
ABA 问题是并发编程中的一个经典问题,通常发生在使用无锁数据结构时。具体来说:

一个线程(线程 A)读取了某个共享变量的值(例如 val)。
在线程 A 进行进一步操作之前,另一个线程(线程 B)修改了该变量的值,并随后将其恢复为线程 A 最初读取的值。
线程 A 再次检查该变量时,发现值没有变化(仍然是 A),因此错误地认为变量的状态没有被修改。
这种情况下,线程 A 无法察觉到变量经历了从 A -> B -> A 的变化过程,可能导致数据不一致或逻辑错误。

验证旧状态的目的
通过验证描述符的旧状态,可以检测到描述符是否已经被回收并重新使用。这种验证通常结合某种机制(例如版本号或序列号)来解决 ABA 问题。例如:

在描述符中附加一个版本号,每次回收时递增版本号。
在线程操作描述符时,同时检查描述符的值和版本号是否匹配。
这种方法可以有效避免 ABA 问题,因为即使描述符的值恢复为原始值,版本号的变化仍然可以被检测到。

应用场景
这种验证机制在以下场景中尤为重要:

环形缓冲区:描述符可能被多个生产者和消费者线程并发访问。
无锁队列或栈:这些数据结构依赖于原子操作和状态验证来确保线程安全。
内核开发:在操作系统内核中,描述符通常用于管理资源(如内存块或 I/O 请求),需要特别注意并发访问问题

*/
prev_state_val = atomic_long_read(&desc->state_var); /* LMM(desc_reserve:E) */
if (prev_state_val &&
get_desc_state(id_prev_wrap, prev_state_val) != desc_reusable) {
WARN_ON_ONCE(1);
return false;
}

/*
* 为描述符分配一个新 ID,并将其状态设置为 reserved。
* 请参阅“ABA 问题”,了解为什么使用 cmpxchg() 而不是 set()。
*
* 确保在进行任何其他更改之前存储新的描述符 ID 和状态。一个写内存屏障就足够了。这与 desc_read:D 配对。
*/
if (!atomic_long_try_cmpxchg(&desc->state_var, &prev_state_val,
DESC_SV(id, desc_reserved))) { /* LMM(desc_reserve:F) */
WARN_ON_ONCE(1);
return false;
}

/* 现在可以修改 @desc 中的数据:LMM(desc_reserve:G) */
*id_out = id;
return true;
}

get_next_lpos 确定数据块的结尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 确定数据块的结尾。 */
static unsigned long get_next_lpos(struct prb_data_ring *data_ring,
unsigned long lpos, unsigned int size)
{
unsigned long begin_lpos;
unsigned long next_lpos;
//根据当前数据块的起始位置(lpos)和大小(size),计算数据块的结束位置
begin_lpos = lpos;
next_lpos = lpos + size;

/* 首先检查数据块是否没有环绕。
如果数据块没有跨越缓冲区的边界,则直接返回计算的结束位置*/
if (DATA_WRAPS(data_ring, begin_lpos) == DATA_WRAPS(data_ring, next_lpos))
return next_lpos;

/*环绕数据块在开始时存储其数据。
如果数据块跨越了缓冲区的边界(发生了“环绕”),则调整结束位置,使其指向缓冲区的起始位置加上数据块的大小 */
return (DATA_THIS_WRAP_START_LPOS(data_ring, next_lpos) + size);
}

data_alloc 分配一个新的数据块,如果有必要会使最旧的数据块失效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/*
* 分配一个新的数据块,如果有必要会使最旧的数据块失效。
* 此函数还会将数据块与指定的描述符关联起来。
*/
static char *data_alloc(struct printk_ringbuffer *rb, unsigned int size,
struct prb_data_blk_lpos *blk_lpos, unsigned long id)
{
struct prb_data_ring *data_ring = &rb->text_data_ring;
struct prb_data_block *blk;
unsigned long begin_lpos;
unsigned long next_lpos;

if (size == 0) {
/*
* 对于空行不会创建数据块。相反,读取器将识别这些特殊的 lpos 值并适当地处理它。
*/
blk_lpos->begin = EMPTY_LINE_LPOS;
blk_lpos->next = EMPTY_LINE_LPOS;
return NULL;
}

size = to_blk_size(size);

begin_lpos = atomic_long_read(&data_ring->head_lpos);

do {
next_lpos = get_next_lpos(data_ring, begin_lpos, size); //确定数据块的结尾

if (!data_push_tail(rb, next_lpos - DATA_SIZE(data_ring))) {
/* 分配失败,指定一个无数据块。 */
blk_lpos->begin = FAILED_LPOS;
blk_lpos->next = FAILED_LPOS;
return NULL;
}

/*
* 1. 确保任何已转换为可重用状态的描述符状态在修改新分配的数据区域之前已存储。
* 需要一个完整的内存屏障,因为其他 CPU 可能已将描述符状态设置为可重用。
* 请参阅 data_push_tail:A 了解为什么可重用状态是可见的。这与 desc_read:D 配对。
*
* 2. 确保任何更新的尾部 lpos 在修改新分配的数据区域之前已存储。
* 另一个 CPU 可能正在 data_make_reusable() 中并从该区域读取块 ID。
* data_make_reusable() 可以处理读取垃圾块 ID 值,但随后它必须能够加载新的尾部 lpos。
* 需要一个完整的内存屏障,因为其他 CPU 可能已更新了尾部 lpos。这与 data_push_tail:B 配对。
*/
} while (!atomic_long_try_cmpxchg(&data_ring->head_lpos, &begin_lpos, next_lpos)); /* LMM(data_alloc:A) */

blk = to_block(data_ring, begin_lpos);
blk->id = id; /* LMM(data_alloc:B) */

if (DATA_WRAPS(data_ring, begin_lpos) != DATA_WRAPS(data_ring, next_lpos)) {
/* 环绕的数据块将其数据存储在开头。 */
blk = to_block(data_ring, 0);

/*
* 为了一致性,在环绕块上存储 ID。
* printk_ringbuffer 实际上并不使用它。
*/
blk->id = id;
}

blk_lpos->begin = begin_lpos;
blk_lpos->next = next_lpos;

return &blk->data[0];
}

prb_commit 将数据状态设置为提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* 提交数据(可能完成数据)并恢复中断。 */
static void _prb_commit(struct prb_reserved_entry *e, unsigned long state_val)
{
struct prb_desc_ring *desc_ring = &e->rb->desc_ring;
struct prb_desc *d = to_desc(desc_ring, e->id);
unsigned long prev_state_val = DESC_SV(e->id, desc_reserved);

/* Now the writer has finished all writing: LMM(_prb_commit:A) */

if (!atomic_long_try_cmpxchg(&d->state_var, &prev_state_val,
DESC_SV(e->id, state_val))) { /* LMM(_prb_commit:B) */
WARN_ON_ONCE(1);
}

/*restore interrupts,则 reserve/commit 窗口已完成。*/
local_irq_restore(e->irqflags);
}

/**
* prb_commit() - 将(以前保留的)数据提交到环形缓冲区。
*
* @e:包含保留数据信息的条目。
*
* 这是 writer 可用于提交数据的公共函数。
*
* 请注意,在最终确定之前,读者无法获得数据。当为下一条记录预留空间时,会自动进行定稿。
*
* 请参阅 prb_final_commit() 以获取此函数的立即完成的版本。
*
* 上下文:任何上下文。启用本地中断。
*/
void prb_commit(struct prb_reserved_entry *e)
{
struct prb_desc_ring *desc_ring = &e->rb->desc_ring;
unsigned long head_id;

_prb_commit(e, desc_committed);

/*
* 如果此 descriptor 不再是 head (即已分配新记录),则不再允许扩展此记录的数据,因此必须完成该记录。
*/
head_id = atomic_long_read(&desc_ring->head_id); /* LMM(prb_commit:A) */
if (head_id != e->id)
desc_make_final(e->rb, e->id);
}

prb_reserve 在环形缓冲区中保留空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/**
* prb_reserve() - 在环形缓冲区中保留空间。
*
* @e:要设置的入口结构。
* @rb:用于保留数据的 ringbuffer。
* @r:要为其分配缓冲区的记录结构。
*
* 这是写入器可用于保留数据的公共函数。
*
* 写入器通过设置 @r 的 @text_buf_size 字段来指定要保留的文本大小。为了确保 @r 的正确初始化,应使用 prb_rec_init_wr()。
*
* 上下文:任何上下文。成功时禁用本地中断。返回:如果至少可以分配文本数据,则为 true,否则为 false。
*
* 成功后,@r 的 @info 和 @text_buf 字段将由此函数设置,并应由 writer 在提交前填写。同样在成功时,可以在 @e 上使用 prb_record_text_space() 来查询用于文本数据块的实际空间。
*
* 重要提示:写入器需要正确设置 @info->text_len 才能读取和/或扩展数据。其值初始化为 0。
*/
bool prb_reserve(struct prb_reserved_entry *e, struct printk_ringbuffer *rb,
struct printk_record *r)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
struct printk_info *info;
struct prb_desc *d;
unsigned long id;
u64 seq;

if (!data_check_size(&rb->text_data_ring, r->text_buf_size))
goto fail;

/*
* 保留状态的描述符在desc_ring完全环绕后充当所有进一步预留的阻止程序。在 reserve/commit 窗口期间禁用中断,以最大程度地减少发生这种情况的可能性。
*/
local_irq_save(e->irqflags);

if (!desc_reserve(rb, &id)) { //返回新描述符,必要时使最旧的描述符失效。
/* 跟踪描述符预留失败. */
atomic_long_inc(&rb->fail);
local_irq_restore(e->irqflags);
goto fail;
}

d = to_desc(desc_ring, id);
info = to_info(desc_ring, id);

/*
* 所有@info字段(@seq 字段除外)都将被清除,并且必须由编写器填写。在清除之前保存@seq,因为它用于确定新的序列号。
*/
seq = info->seq;
memset(info, 0, sizeof(*info));

/*
* 在此处设置 @e 字段,以便在文本数据分配失败时可以使用 prb_commit()。
*/
e->rb = rb;
e->id = id;

/*
* 如果序列号 “从未设置” ,则初始化序列号。否则,只需将其增加 full wrap 即可。
*
* 如果 @seq 的值为 0,则认为 @infos[0] 的值为 _except_,则认为该值为 0,这是由 Ringbuffer 初始值设定项专门设置的,因此始终被视为已设置。
*
* 请参阅 printk_ringbuffer.h 中的 “Bootstrap” 注释块,了解有关初始值设定项如何引导描述符的详细信息。
*/
if (seq == 0 && DESC_INDEX(desc_ring, id) != 0)
info->seq = DESC_INDEX(desc_ring, id);
else
info->seq = seq + DESCS_COUNT(desc_ring);

/*
* 新数据即将被保留。一旦发生这种情况,以前的描述符将无法再扩展。现在完成前面的描述符,以便读者可以使用它。(对于 seq==0,没有前面的描述符。
*/
if (info->seq > 0)
desc_make_final(rb, DESC_ID(id - 1));

r->text_buf = data_alloc(rb, r->text_buf_size, &d->text_blk_lpos, id);
/* 如果文本数据分配失败,则状态修改为提交无数据记录。 */
if (r->text_buf_size && !r->text_buf) {
prb_commit(e);
/* prb_commit() 重新启用中断。 */
goto fail;
}

r->info = info; //这里将描述符的信息结构体赋值给r->info,用于将外面的信息传递给内核

/* 记录 record 使用的全文空间。 */
//返回数据块使用的字节数
e->text_space = space_used(&rb->text_data_ring, &d->text_blk_lpos);

return true;

fail:
/* 向调用方明确表示预留失败。*/
memset(r, 0, sizeof(*r));
return false;
}

prb_rec_init_wr 初始化用于写入记录的缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* prb_rec_init_wr() -初始化用于写入记录的缓冲区。
*
* @r: The record to initialize.
* @text_buf_size: The needed text buffer size.
*/
static inline void prb_rec_init_wr(struct printk_record *r,
unsigned int text_buf_size)
{
r->info = NULL;
r->text_buf = NULL;
r->text_buf_size = text_buf_size;
}

prb_first_seq 获取尾部描述符的序列号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 获取尾部描述符的序列号。 */
u64 prb_first_seq(struct printk_ringbuffer *rb)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
enum desc_state d_state;
struct prb_desc desc;
unsigned long id;
u64 seq;

for (;;) {
id = atomic_long_read(&rb->desc_ring.tail_id); /* LMM(prb_first_seq:A) */

d_state = desc_read(desc_ring, id, &desc, &seq, NULL); /* LMM(prb_first_seq:B) */

/*
* T他的循环不会是无限的,因为 tail is_always_处于 finalized 或 reusable 状态。
*/
if (d_state == desc_finalized || d_state == desc_reusable)
break;
smp_rmb(); /* LMM(prb_first_seq:C) */
}

return seq;
}

__ulseq_to_u64seq 将 32 位序列号转换为 64 位序列号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline u64 __ulseq_to_u64seq(struct printk_ringbuffer *rb, u32 ulseq)
{
u64 rb_first_seq = prb_first_seq(rb); //获取尾部描述符的序列号
u64 seq;

/*
* 提供的序列只是 ringbuffer 序列的低 32 位。它需要扩展到 64 位。从 ringbuffer 中获取第一个序列号并将其折叠。
*
* 在控制台中具有 32 位表示就足够了。如果控制台在 ringbuffer 后面获得超过 2^31 条记录,那么这是最少的问题。
*
* 此外,对环形缓冲区的访问始终是安全的。
*/
seq = rb_first_seq - (s32)((u32)rb_first_seq - ulseq);

return seq;
}

desc_last_finalized_seq 获取最后一个已完成的序列号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* @last_finalized_seq 的值保证了所有记录直到并包括此序列号都已完成并可以读取。
* 唯一的例外是那些已经被覆盖的过旧记录。
*
* 同时保证 @last_finalized_seq 只会递增。
*
* 请注意,紧随未完成记录之后的已完成记录不会被报告,因为它们尚未对读取器可用。
* 例如,通过 printk() 存储的新记录如果紧随未完成记录之后,将不会对打印机可用。
* 然而,一旦该未完成记录变为完成状态,@last_finalized_seq 将被适当地更新,
* 并且完整的已完成记录集将对打印机可用。而且,由于每个 printk() 调用者
* 要么直接打印,要么触发所有可用未打印记录的延迟打印,因此所有 printk() 消息
* 都会被打印。
*/
static u64 desc_last_finalized_seq(struct printk_ringbuffer *rb)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
unsigned long ulseq;

/*
* 保证在加载关联记录之前加载序列号,以确保该记录可以被此 CPU 看到。
* 这与 desc_update_last_finalized:A 配对。
*/
ulseq = atomic_long_read_acquire(&desc_ring->last_finalized_seq
); /* LMM(desc_last_finalized_seq:A) */

return __ulseq_to_u64seq(rb, ulseq); //将 32 位序列号转换为 64 位序列号
}

desc_read_finalized_seq 获取指定描述符的副本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 这是 desc_read() 的扩展版本。它获取指定描述符的副本。但是,它还会验证记录是否已完成并且序列号是否@seq。成功后,返回 0。
*
* 错误返回值:
* -EINVAL:序列号为 @seq 的最终记录不存在。
* -ENOENT:序列号为 @seq 的最终记录存在,但其数据
* 不可用。这是一个有效的记录,因此读者应该
* 继续下一条记录。
*/
static int desc_read_finalized_seq(struct prb_desc_ring *desc_ring,
unsigned long id, u64 seq,
struct prb_desc *desc_out)
{
struct prb_data_blk_lpos *blk_lpos = &desc_out->text_blk_lpos;
enum desc_state d_state;
u64 s;

d_state = desc_read(desc_ring, id, desc_out, &s, NULL);

/*
* 意外的@id (desc_miss) 或 @seq 不匹配意味着记录不存在。
处于 reserved 或 committed 状态的描述符表示该记录对于读取器尚不存在。
*/
if (d_state == desc_miss ||
d_state == desc_reserved ||
d_state == desc_committed ||
s != seq) {
return -EINVAL;
}

/** 处于可重用状态的描述符可能不再具有其数据可用;将其报告为存在但丢失了数据。或者,该记录实际上可能是丢失数据的记录。
*/
if (d_state == desc_reusable ||
(blk_lpos->begin == FAILED_LPOS && blk_lpos->next == FAILED_LPOS)) {
return -ENOENT;
}

return 0;
}

get_data 给定@blk_lpos,从数据块返回指向写入器数据的指针,并计算数据部分的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/*
- 给定@blk_lpos,从数据块返回指向写入器数据的指针,并计算数据部分的大小。如果 @blk_lpos 指定永远不合法的值,则返回 NULL 指针。
*
- 此函数 (由读取器使用) 对 lpos 值执行严格验证,以可能检测编写器代码中的 bug。如果检测到内部错误,则会触发 WARN_ON_ONCE()。
*/
static const char *get_data(struct prb_data_ring *data_ring,
struct prb_data_blk_lpos *blk_lpos,
unsigned int *data_size)
{
struct prb_data_block *db;

/* 无数据块描述。 */
if (BLK_DATALESS(blk_lpos)) {
/*
* 只是空行的记录也是有效的,即使它们没有数据块。对于此类记录,显式返回空字符串数据以表示成功。
*/
if (blk_lpos->begin == EMPTY_LINE_LPOS &&
blk_lpos->next == EMPTY_LINE_LPOS) {
*data_size = 0;
return "";
}

/* 数据丢失、无效或不可用。*/
return NULL;
}

/* 常规数据块:小于 @next 的 @begin 且采用相同的换行. */
if (DATA_WRAPS(data_ring, blk_lpos->begin) == DATA_WRAPS(data_ring, blk_lpos->next) &&
blk_lpos->begin < blk_lpos->next) {
db = to_block(data_ring, blk_lpos->begin);
*data_size = blk_lpos->next - blk_lpos->begin;

/* 环绕数据块:@begin 是 @next 后面的一个环绕。 */
} else if (DATA_WRAPS(data_ring, blk_lpos->begin + DATA_SIZE(data_ring)) ==
DATA_WRAPS(data_ring, blk_lpos->next)) {
db = to_block(data_ring, 0);
*data_size = DATA_INDEX(data_ring, blk_lpos->next);

/* 非法区块描述。*/
} else {
WARN_ON_ONCE(1);
return NULL;
}

/* 有效的数据块将始终与 ID 大小保持一致。*/
if (WARN_ON_ONCE(blk_lpos->begin != ALIGN(blk_lpos->begin, sizeof(db->id))) ||
WARN_ON_ONCE(blk_lpos->next != ALIGN(blk_lpos->next, sizeof(db->id)))) {
return NULL;
}

/*有效的数据块将始终至少具有一个 ID。*/
if (WARN_ON_ONCE(*data_size < sizeof(db->id)))
return NULL;

/* 从 size 中减去块 ID 空间以反映数据大小。 */
*data_size -= sizeof(db->id);

return &db->data[0];
}

copy_data 给定@blk_lpos,将预期的数据@len复制到提供的缓冲区中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* 给定@blk_lpos,将预期的数据@len复制到提供的缓冲区中。
* 如果提供了 @line_count,则计算数据中的行数。
*
* 此函数(由读取器使用)对数据大小执行严格验证,以可能检测到写入器代码中的 bug。如果检测到内部错误,则会触发 WARN_ON_ONCE()。
*/
static bool copy_data(struct prb_data_ring *data_ring,
struct prb_data_blk_lpos *blk_lpos, u16 len, char *buf,
unsigned int buf_size, unsigned int *line_count)
{
unsigned int data_size;
const char *data;

/* 调用方可能不需要任何数据。 */
if ((!buf || !buf_size) && !line_count)
return true;
//给定@blk_lpos,从数据块返回指向写入器数据的指针,并计算数据部分的大小
data = get_data(data_ring, blk_lpos, &data_size);
if (!data)
return false;

/*
* 实际值不能低于预期。由于尾随对齐填充,它可能超出预期。
*
* 请注意,可能会出现无效的 @len 值,因为调用方在允许的数据争用期间加载值。
*/
if (data_size < (unsigned int)len)
return false;

/* 呼叫者对行数感兴趣?*/
if (line_count)
*line_count = count_lines(data, len);

/* Caller interested in the data content? */
if (!buf || !buf_size)
return true;

data_size = min_t(unsigned int, buf_size, len);

memcpy(&buf[0], data, data_size); /* LMM(copy_data:A) */
return true;
}

prb_read 将带有 @seq 的记录中的 ringbuffer 数据复制到提供的 @r 缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* 将带有 @seq 的记录中的 ringbuffer 数据复制到提供的 @r 缓冲区。成功后,返回 0。
*
* 有关错误返回值,请参阅 desc_read_finalized_seq()。
*/
static int prb_read(struct printk_ringbuffer *rb, u64 seq,
struct printk_record *r, unsigned int *line_count)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
struct printk_info *info = to_info(desc_ring, seq);
struct prb_desc *rdesc = to_desc(desc_ring, seq);
atomic_long_t *state_var = &rdesc->state_var;
struct prb_desc desc;
unsigned long id;
int err;

/*提取 ID,用于指定要读取的描述符。 */
id = DESC_ID(atomic_long_read(state_var));

/* 获取正确描述符的本地副本(如果可用)。 */
err = desc_read_finalized_seq(desc_ring, id, seq, &desc);

/*
* 如果 @r 为 NULL,则调用方仅对记录的可用性感兴趣。
*/
if (err || !r)
return err;

/* 如果需要,请复制元数据。 */
if (r->info)
memcpy(r->info, info, sizeof(*(r->info)));

/* 复制文本数据。如果失败,则为无数据记录。 */
//给定@blk_lpos,将预期的数据@len复制到提供的缓冲区中
if (!copy_data(&rb->text_data_ring, &desc.text_blk_lpos, info->text_len,
r->text_buf, r->text_buf_size, line_count)) {
return -ENOENT;
}

/* 确保记录仍处于最终状态并具有相同的@seq. */
return desc_read_finalized_seq(desc_ring, id, seq, &desc);
}

prb_next_reserve_seq 获取最近保留记录之后的序列号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/**
* prb_next_reserve_seq() - 获取最近保留记录之后的序列号。
*
* @rb: 要从中获取序列号的环形缓冲区。
*
* 这是提供给读取器的公共函数,用于查看将分配给下一个保留记录的序列号。
*
* 请注意,根据情况,此值可能等于或高于 prb_next_seq() 返回的序列号。
*
* 上下文:任何上下文。
* 返回:将分配给下一条记录的序列号。
*/
u64 prb_next_reserve_seq(struct printk_ringbuffer *rb)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
unsigned long last_finalized_id;
atomic_long_t *state_var;
u64 last_finalized_seq;
unsigned long head_id;
struct prb_desc desc;
unsigned long diff;
struct prb_desc *d;
int err;

/*
* 可能无法读取 @head_id 的序列号。
* 因此,使用 @last_finalized_seq 的 ID 来计算 @head_id 的序列号。
*/

try_again:
last_finalized_seq = desc_last_finalized_seq(rb);

/*
* @head_id 在 @last_finalized_seq 之后加载,以确保它指向具有
* @last_finalized_seq 或更新的记录。
*
* 内存屏障参与:
*
* 如果 desc_last_finalized_seq:A 从 desc_update_last_finalized:A 读取,
* 则 prb_next_reserve_seq:A 从 desc_reserve:D 读取。
*
* 依赖:
*
* RELEASE 从 desc_reserve:D 到 desc_update_last_finalized:A 匹配
* ACQUIRE 从 desc_last_finalized_seq:A 到 prb_next_reserve_seq:A
*
* 注意:desc_reserve:D 和 desc_update_last_finalized:A 可以是不同的 CPU。
* 然而,执行释放操作的 desc_update_last_finalized:A CPU 必须之前已经看到 desc_read:C,
* 这意味着 desc_reserve:D 可以被看到。
*/
head_id = atomic_long_read(&desc_ring->head_id); /* LMM(prb_next_reserve_seq:A) */

d = to_desc(desc_ring, last_finalized_seq);
state_var = &d->state_var;

/* 提取 ID,用于指定要读取的描述符。 */
last_finalized_id = DESC_ID(atomic_long_read(state_var));

/* 确保 @last_finalized_id 是正确的。 */
err = desc_read_finalized_seq(desc_ring, last_finalized_id, last_finalized_seq, &desc);

if (err == -EINVAL) {
if (last_finalized_seq == 0) {
/*
* 尚未有记录被最终确定或保留。
*
* @head_id 被初始化为使第一次递增将生成第一条记录 (seq=0)。
* 单独处理它以避免下面的负 @diff。
*/
if (head_id == DESC0_ID(desc_ring->count_bits))
return 0;

/*
* 一个或多个描述符已经被保留。使用第一个描述符的 ID (@seq=0)
* 作为下面 @diff 的计算依据。
*/
last_finalized_id = DESC0_ID(desc_ring->count_bits) + 1;
} else {
/* 记录可能已被覆盖。重试。 */
goto try_again;
}
}

/* 已知描述符 ID 的差值,用于计算相关的序列号。 */
diff = head_id - last_finalized_id;

/*
* @head_id 指向最近保留的记录,但此函数返回将分配给
* 下一个(尚未保留)记录的序列号。因此需要 +1。
*/
return (last_finalized_seq + diff + 1);
}

_prb_read_valid 非阻塞读取记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
* 非阻塞读取记录。
*
* 成功时,@seq 将更新为已读取的记录,并且(如果提供)@r 和 @line_count 将包含读取/计算的数据。
*
* 失败时,@seq 将更新为尚未对读取器可用的记录,但它将是读取器下一个可用的记录。
*
* 注意:当当前 CPU 处于 panic 状态时,此函数将跳过任何不存在/未完成的记录,以允许 panic CPU 打印所有已完成的记录。
*/

static bool _prb_read_valid(struct printk_ringbuffer *rb, u64 *seq,
struct printk_record *r, unsigned int *line_count)
{
u64 tail_seq;
int err;
//将带有 @seq 的记录中的 ringbuffer 数据复制到提供的 @r 缓冲区
while ((err = prb_read(rb, *seq, r, line_count))) {
tail_seq = prb_first_seq(rb); //获取尾部描述符的序列号

if (*seq < tail_seq) {
/*
* 在尾巴后面。赶上并重试。这可能发生在 -ENOENT 和 -EINVAL 情况下。
*/
*seq = tail_seq;

} else if (err == -ENOENT) {
/* 记录存在,但数据丢失。跳。 */
(*seq)++;

} else {
/*
* 不存在/未完成的记录。必须停止。
*
* 在 panic 情况下,不能期望未完成的记录会变为完成状态。
* 但可能存在其他需要在 panic 情况下打印的已完成记录。
* 如果这是 panic CPU,则跳过此不存在/未完成的记录,
* 除非它位于或超过头部,在这种情况下无法继续。
*
* 请注意,在这里时,panic CPU 上打印的新消息是已完成的。
* 唯一的例外可能是没有尾随换行符的最后一条消息。
* 但它的序列号将是 "prb_next_reserve_seq() - 1" 返回的值。
* prb_next_reserve_seq 获取最近保留记录之后的序列号。
*/
if (this_cpu_in_panic() && ((*seq + 1) < prb_next_reserve_seq(rb)))
(*seq)++;
else
return false;
}
}

return true;
}

desc_update_last_finalized 将 @last_finalized_seq 更新为这些记录中的最新记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* 检查是否有紧跟在 @last_finalized_seq 之后的记录已完成。如果是这样,请将 @last_finalized_seq 更新为这些记录中的最新记录。不允许跳过尚未完成的记录。
*/
static void desc_update_last_finalized(struct printk_ringbuffer *rb)
{
struct prb_desc_ring *desc_ring = &rb->desc_ring;
u64 old_seq = desc_last_finalized_seq(rb); //获取最后一个已完成的序列号
unsigned long oldval;
unsigned long newval;
u64 finalized_seq;
u64 try_seq;

try_again:
finalized_seq = old_seq;
try_seq = finalized_seq + 1;

/* 尝试查找以后的最终记录。*/
//非阻塞读取记录
while (_prb_read_valid(rb, &try_seq, NULL, NULL)) {
finalized_seq = try_seq;
try_seq++;
}

/* 如果未找到以后的最终记录,则无需更新。*/
if (finalized_seq == old_seq)
return;

oldval = __u64seq_to_ulseq(old_seq);
newval = __u64seq_to_ulseq(finalized_seq);

if (!atomic_long_try_cmpxchg_release(&desc_ring->last_finalized_seq,
&oldval, newval)) { /* LMM(desc_update_last_finalized:A) */
old_seq = __ulseq_to_u64seq(rb, oldval);
goto try_again;
}
}

prb_final_commit 将(以前保留的)数据提交并完成到环形缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* prb_final_commit() - 将(以前保留的)数据提交并完成到环形缓冲区。
*
* @e:包含保留数据信息的条目。
*
* 这是 writer 可用于提交 finalize 数据的公共函数。
*
* 最终确定后,数据将立即提供给读者。
*
* 仅当无意使用 prb_reserve_in_last() 扩展此数据时,才应使用此函数。
*
* 上下文:任何上下文。启用本地中断。
*/
void prb_final_commit(struct prb_reserved_entry *e)
{
_prb_commit(e, desc_finalized);

desc_update_last_finalized(e->rb); //更新 @last_finalized_seq
}

kernel/printk/printk_safe.c 安全打印

force_con 强制输出到控制台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* printk 消息永远不会被禁止显示的上下文 */
static atomic_t force_con;

void printk_force_console_enter(void)
{
atomic_inc(&force_con);
}

void printk_force_console_exit(void)
{
atomic_dec(&force_con);
}

bool is_printk_force_console(void)
{
return atomic_read(&force_con);
}

kernel/printk/nbcon.c Non-Blocking Console 非阻塞控制台

nbcon_get_cpu_emergency_nesting 获取每个 CPU 的 nbcon 紧急嵌套计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Track the nbcon emergency nesting per CPU. */
static DEFINE_PER_CPU(unsigned int, nbcon_pcpu_emergency_nesting);
static unsigned int early_nbcon_pcpu_emergency_nesting __initdata;

static __ref unsigned int *nbcon_get_cpu_emergency_nesting(void)
{
/*
*__printk_percpu_data_ready 的值在正常上下文中和 SMP 初始化之前设置。因此,在 nbcon 紧急区域内,它永远不会改变.
*/
if (!printk_percpu_data_ready()) //start_kernel 到 setup_log_buf之前不为true
return &early_nbcon_pcpu_emergency_nesting;

return raw_cpu_ptr(&nbcon_pcpu_emergency_nesting);
}

nbcon_get_default_prio 获取nbcon 默认优先级

  1. 初始化setup_log_buf之前为NBCON_PRIO_EMERGENCY,之后为NBCON_PRIO_NORMAL
1
2
3
4
5
6
7
8
9
10
11
12
13
enum nbcon_prio nbcon_get_default_prio(void)
{
unsigned int *cpu_emergency_nesting;

if (this_cpu_in_panic()) //当前 CPU 处于 panic 状态
return NBCON_PRIO_PANIC;

cpu_emergency_nesting = nbcon_get_cpu_emergency_nesting();
if (*cpu_emergency_nesting)
return NBCON_PRIO_EMERGENCY;

return NBCON_PRIO_NORMAL;
}

非阻塞控制台(nbcon)的所有权获取机制

此代码片段是Linux内核非阻塞控制台(nbcon)子系统的心脏, 它实现了一套极其精巧、健壮、且分级的所有权获取(acquire)机制。这套机制的根本原理是允许多个不同优先级、不同上下文(包括可休眠的、中断、NMI、系统恐慌等)的代码, 能够安全、高效地竞争同一个物理控制台(如UART)的”打印权”

这是一个为解决最极端并发情况而设计的微型状态机, 其核心是围绕一个原子的nbcon_state变量进行转换, 以确保在任何时刻只有一个执行上下文可以向硬件写入数据。


核心概念: struct nbcon_state

这是一个打包在单个原子变量中的状态机。它包含了所有权的关键信息:

  • prio: 当前所有者的优先级 (最高的是PANIC, 最低的是NONE)。
  • req_prio: 请求所有权的、更高优先级等待者的优先级。
  • unsafe: 一个标志, 表示控制台硬件可能处于不一致的状态(例如, 在一个原子写操作的中间被抢占)。
  • cpu: 当前所有者所在的CPU核心。
  • unsafe_takeover: 一个标志, 表示发生过一次”不安全的强制接管”。

nbcon_context_try_acquire: 所有权获取的总入口

此函数是所有打印操作的入口点。它按顺序尝试三种不同的策略来获取控制台的所有权。

原理与工作流程: 它是一个三级瀑布模型: 尝试直接获取 -> 失败则尝试礼貌地接管 -> 再失败则尝试强制抢占

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
static bool nbcon_context_try_acquire(struct nbcon_context *ctxt, bool is_reacquire)
{
struct console *con = ctxt->console;
struct nbcon_state cur;
int err;

nbcon_state_read(con, &cur); /* 读取当前的状态 */
try_again:
/* 策略1: 尝试直接获取 */
err = nbcon_context_try_acquire_direct(ctxt, &cur, is_reacquire);
if (err != -EBUSY) /* 如果不是"繁忙"错误, 说明已成功或发生不可恢复错误 */
goto out;

/* 策略2: 如果直接获取因"繁忙"而失败, 尝试礼貌地请求"交接"(handover) */
err = nbcon_context_try_acquire_handover(ctxt, &cur);
if (err == -EAGAIN) /* 如果handover期间状态变化, 重新从策略1开始 */
goto try_again;
if (err != -EBUSY) /* 如果不是"繁忙"错误, 结束 */
goto out;

/* 策略3: 如果handover也因"繁忙"而失败, 尝试最后的手段: "强制抢占"(hostile takeover) */
err = nbcon_context_try_acquire_hostile(ctxt, &cur);
out:
if (err) /* 如果最终有任何错误, 返回false */
return false;

/* 获取成功. */

/* 为当前上下文分配正确的打印缓冲区 (恐慌时使用专用缓冲区) */
if (atomic_read(&panic_cpu) == cpu)
ctxt->pbufs = &panic_nbcon_pbufs;
else
ctxt->pbufs = con->pbufs;

/* 设置本轮打印的起始日志序列号 */
ctxt->seq = nbcon_seq_read(ctxt->console);

return true;
}

策略 2: nbcon_context_try_acquire_handover (礼貌的交接)

当直接获取因控制台繁忙而失败时, 此函数被调用。它适用于一个高优先级上下文需要从一个正在”不安全”区域内打印的低优先级上下文中接管控制台的场景。

原理: “敲门并等待”。它不会粗暴地抢占, 而是先原子地设置req_prio字段, 向当前所有者发出一个”交接请求”。然后它进入一个带超时的自旋等待循环, 等待当前所有者在退出”不安全”区域时检查到这个请求, 并主动释放所有权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int nbcon_context_try_acquire_handover(struct nbcon_context *ctxt,
struct nbcon_state *cur)
{
// ... 省略大量前置检查 ...

/* 步骤1: "敲门" - 设置交接请求.
* 通过cmpxchg原子地将ctxt->prio写入到状态的req_prio字段.
* 如果失败(说明状态已变), 返回-EAGAIN让上层重试.
*/
new.atom = cur->atom;
new.req_prio = ctxt->prio;
if (!nbcon_state_try_cmpxchg(con, cur, &new))
return -EAGAIN;

/* 步骤2: "等待" - 进入带超时的自旋等待循环. */
for (timeout = ctxt->spinwait_max_us; timeout >= 0; timeout--) {
/* 在循环中, 反复调用 _requested 函数尝试获取. */
request_err = nbcon_context_try_acquire_requested(ctxt, cur);
if (!request_err) /* 如果获取成功, 返回0. */
return 0;

if (request_err == -EPERM) /* 如果被更高优先级抢单, 退出. */
break;

udelay(1); /* 短暂延迟, 避免烧尽CPU. */
nbcon_state_read(con, cur); /* 重新读取状态. */
}

/* 步骤3: "清理" - 如果超时或被抢单, 必须撤销自己的请求.
* 这是一个复杂的do-while循环, 确保能原子地将req_prio清零,
* 或者在清理过程中"幸运地"获取到锁.
*/
// ... 省略清理逻辑 ...

return request_err; /* 返回超时或被抢单的错误. */
}

nbcon_context_try_acquire_requested (交接的助手): 此函数在handover的等待循环中被反复调用。它检查当前所有者是否已经释放了锁(cur->prio == NBCON_PRIO_NONE), 并且自己是否仍然是合法的等待者。如果条件满足, 它就执行最终的原子交换来获取所有权。


策略 3: nbcon_context_try_acquire_hostile (强制抢占)

这是最后的、最极端的手段, 只有在系统panic时才被允许。

原理: “破门而入”。它完全无视当前的所有者和状态, 使用一个do-while循环来强行cmpxchg将状态机设置为自己拥有。它会继承并设置unsafeunsafe_takeover标志, 明确记录下这次粗暴的抢占, 并警告系统后续的打印可能是在一个不一致的硬件状态下进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int nbcon_context_try_acquire_hostile(struct nbcon_context *ctxt,
struct nbcon_state *cur)
{
/* 检查是否允许强制抢占, 且优先级是否为PANIC. */
if (!ctxt->allow_unsafe_takeover || WARN_ON_ONCE(ctxt->prio != NBCON_PRIO_PANIC))
return -EPERM;

/* 使用do-while循环, 保证cmpxchg最终能成功. */
do {
new.atom = cur->atom;
new.cpu = cpu;
new.prio = ctxt->prio;
/* 继承并设置所有"不安全"标志. */
new.unsafe |= cur->unsafe_takeover;
new.unsafe_takeover |= cur->unsafe;

} while (!nbcon_state_try_cmpxchg(con, cur, &new));

return 0;
}

printk日志序列号的64位到32位转换与重建

此代码片段展示了Linux内核printk子系统中一个非常精巧的优化设计, 它解决了如何在只存储一个32位”进度标记”的情况下, 可靠地重建出完整的64位日志序列号的问题。

核心原理:
内核的主日志环形缓冲区(printk ring buffer, prb)使用一个永不回绕的64位序列号(u64seq)来唯一标识每一条日志记录。然而, 为每一个控制台(console)都维护一个64位的原子变量来记录其打印进度是非常昂贵的, 尤其是在32位系统上。为了优化, 内核为每个非阻塞控制台(nbcon)只存储了一个32位的原子进度标记(ulseq)。

此代码的核心原理就是基于一个”就近原则”的滑动窗口算法, 从一个32位的局部进度标记, 可靠地推断出它在64位全局序列号空间中的确切位置。这个算法的基石是以下这个非常合理的假设: 任何一个控制台的处理进度, 相对于printk主缓冲区的当前内容, 其滞后或超前的记录数都不会超过2^31 (约21亿条)


__ulseq_to_u64seq: 32位到64位序列号的重建引擎

这是实现上述原理的核心算法。它利用有符号和无符号整数算术的特性来巧妙地解决歧义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* __ulseq_to_u64seq: 将一个32位的 ulseq 重建为64位的 u64seq.
* @rb: 指向 printk 环形缓冲区的指针.
* @ulseq: 从控制台读取的32位进度标记.
*/
static inline u64 __ulseq_to_u64seq(struct printk_ringbuffer *rb, u32 ulseq)
{
/* 获取环形缓冲区中现存的"第一条"(最老)记录的完整64位序列号. */
u64 rb_first_seq = prb_first_seq(rb);
u64 seq;

/*
* 提供的序列号只是环形缓冲区序列号的低32位.
* 它需要被扩展到64位. 我们获取环形缓冲区中的第一个序列号并将其折叠.
*
* 在控制台中保留一个32位的表示就足够了.
* 如果一个控制台真的落后环形缓冲区超过2^31条记录,
* 那么这已经是最小的问题了(意味着大量日志已丢失).
*
* 此外, 对环形缓冲区的访问总是安全的.
*/

/* 这是核心重建算法 */
seq = rb_first_seq - (s32)((u32)rb_first_seq - ulseq);

return seq;
}

算法解析:
seq = rb_first_seq - (s32)((u32)rb_first_seq - ulseq);

  1. (u32)rb_first_seq: 取出缓冲区中最老记录的64位序列号的低32位
  2. ((u32)rb_first_seq - ulseq): 计算这两个32位无符号整数的差值。由于无符号算术的回绕特性, 这个差值正确地表示了它们之间的距离, 即使发生了32位的回绕。
  3. (s32)(...): 这是最关键的一步。它将上一步得到的无符号差值强制转换为一个有符号的32位整数
    • 如果ulseq稍微落后于rb_first_seq的低32位, 这个差值会是一个很大的正数, 转换后会成为一个小的负数(代表”落后了少量”)。
    • 如果ulseq稍微领先于rb_first_seq的低32位, 这个差值会是一个小的正数(代表”领先了少量”)。
  4. rb_first_seq - (s32)(...): 从完整的64位基准序列号中减去这个有符号的32位”偏移量”。
    • 减去一个负数等于加上一个正数, 这就将落后的序列号正确地向前调整。
    • 减去一个正数, 这就将领先的序列号正确地向后调整。

通过这种方式, 函数总能找到距离rb_first_seq“最近”的那个64位序列号, 其低32位与ulseq完全匹配, 从而完成了精确的重建。


nbcon_seq_read: 安全地读取控制台进度

这是一个上层的API函数, 负责从一个给定的控制台安全地读取其32位的进度标记, 并调用重建函数将其转换为完整的64位序列号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* nbcon_seq_read - 读取当前控制台的序列号
* @con: 要读取序列号的控制台
*
* 返回: 在 @con 上将要打印的下一条记录的序列号.
*/
u64 nbcon_seq_read(struct console *con)
{
/*
* 使用 atomic_long_read 原子地读取存储在控制台私有数据中的32位进度标记.
* "原子地"是关键, 它确保了即使在单核抢占式系统上, 读取操作也不会被中断
* 或其他任务的写入操作干扰, 从而避免了读取到不一致的撕裂值.
*/
unsigned long nbcon_seq = atomic_long_read(&ACCESS_PRIVATE(con, nbcon_seq));

/*
* 将读取到的32位值传递给重建函数, 得到完整的64位序列号并返回.
*/
return __ulseq_to_u64seq(prb, nbcon_seq);
}

其他辅助宏

1
2
3
4
5
6
7
8
9
10
11
/*
* __u64seq_to_ulseq: 将64位序列号转换为32位进度标记的简单宏.
* 这在更新控制台进度时使用, 它简单地截取低32位.
*/
#define __u64seq_to_ulseq(u64seq) ((u32)u64seq)
/*
* ULSEQ_MAX: 计算一个32位序列号的最大有效值.
* 它的值是环形缓冲区起始序列号加上2^31. 这可能用于边界检查, 以判断一个控制台
* 是否已经落后得太远, 以至于其状态已不可信.
*/
#define ULSEQ_MAX(rb) __u64seq_to_ulseq(prb_first_seq(rb) + 0x80000000UL)

nbcon_context_release: 安全地释放非阻塞控制台的所有权

此函数是Linux内核非阻塞控制台(nbcon)所有权机制的另一半, 是nbcon_context_try_acquire的配对操作。它的核心原理是提供一个原子性的、有条件的、并且健壮的方法来 relinquishment (放弃) 一个先前获取到的控制台”打印权”

这个函数的设计核心是安全, 它必须能正确处理一种关键的并发情况: 在一个上下文正准备释放所有权时, 一个更高优先级的上下文(如NMI或系统恐慌)可能会突然”抢占”这个所有权。此函数通过一个比较并交换(Compare-and-Swap, CAS)的循环来优雅地处理这种情况。

工作流程详解:

  1. 读取当前状态: 函数首先调用nbcon_state_read来获取控制台当前最新的原子状态cur
  2. 进入CAS循环: 函数进入一个do-while循环。这个循环的目的是保证状态更新的原子性, 如果在循环体内, 当我们准备写入新状态时, 发现原子状态已经被其他上下文修改了, cmpxchg就会失败, 循环会重试。
  3. 验证所有权: 在循环内部, 最关键的第一步是调用nbcon_owner_matches。这个检查会验证”当前这个执行上下文(ctxt)是否仍然是cur状态所记录的那个合法所有者”。
    • 如果不是(返回false), 这意味着在我们读取cur之后, 已经有一个更高优先级的上下文抢占了所有权。在这种情况下, 我们已经无权可放, 也没有必要再做任何操作, 函数会立即break跳出循环。
  4. 准备新状态: 如果所有权被验证, 函数会准备一个新的目标状态new
    • 最重要的改变是 new.prio = NBCON_PRIO_NONE;。这将优先级设置为”无所有者”, 从而向系统宣告该控制台现在可用了。
    • 保留”不安全”标记: 它会执行 new.unsafe |= cur.unsafe_takeover;。这是一个关键的”污点”传播机制。如果此控制台之前经历过一次”不安全的强制接管”, unsafe_takeover标志就会被设置。此代码确保了这个标志一旦被设置, 就会被永久地保留在unsafe标志中。这意味着, 一个曾被粗暴对待过的控制台, 会被永久性地标记为”不安全”, 以防止后续的常规打印上下文在可能不一致的硬件状态上进行操作。
  5. 原子更新: 函数调用nbcon_state_try_cmpxchg(con, &cur, &new)来尝试原子地将控制台的状态从cur更新为new。只有当控制台的当前状态仍然是cur时, 这个操作才会成功。
  6. 清理上下文: 在循环结束后(无论是否成功更新), 函数会将上下文中的打印缓冲区指针pbufs清空(ctxt->pbufs = NULL;), 这是一个良好的实践, 可以防止调用者在此上下文被释放后, 仍然错误地通过它访问缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* nbcon_context_release - 释放控制台
* @ctxt: 从 nbcon_context_try_acquire() 获取到的nbcon上下文
*/
static void nbcon_context_release(struct nbcon_context *ctxt)
{
unsigned int cpu = smp_processor_id();
struct console *con = ctxt->console;
struct nbcon_state cur;
struct nbcon_state new;

/* 读取控制台的当前原子状态. */
nbcon_state_read(con, &cur);

/*
* 进入一个 do-while 循环, 以原子方式更新状态.
* 循环条件是 "当比较并交换失败时, 继续循环".
*/
do {
/*
* 关键检查: 确认我们(当前的cpu和优先级)是否仍然是当前的所有者.
* 如果不是, 说明所有权已经被更高优先级的上下文抢占了, 我们无权释放.
*/
if (!nbcon_owner_matches(&cur, cpu, ctxt->prio))
break; /* 跳出循环, 不做任何事. */

/* 准备要写入的新状态. */
new.atom = cur.atom;
/* 将优先级设置为"无所有者", 表示释放锁. */
new.prio = NBCON_PRIO_NONE;

/*
* 如果 unsafe_takeover 标志被设置了, 它将被保留下来,
* 以便该状态保持永久性的不安全.
* 这是一种"污点"传播机制.
*/
new.unsafe |= cur.unsafe_takeover;

/* 尝试原子地将状态从 cur 更新为 new. 如果失败, 重新读取 cur 并重试. */
} while (!nbcon_state_try_cmpxchg(con, &cur, &new));

/* 清理上下文中的缓冲区指针, 防止悬空引用. */
ctxt->pbufs = NULL;
}

nbcon_context_can_proceed: 非阻塞控制台(nbcon)所有权检查与礼让机制

此函数是Linux内核非阻塞控制台(nbcon)子系统中实现”协作式多任务”(Cooperative Multitasking)的核心决策逻辑。它在nbcon打印流程的各个关键检查点被调用, 其核心原理是回答一个关键问题: “我这个当前的打印上下文, 是否还有权继续向前执行?”

这个函数不仅仅是一个简单的所有权检查, 它还内置了主动礼让(yield)给更高优先级等待者的机制, 这是实现nbcon在复杂抢占场景下既能高效工作又不会死锁的关键。


nbcon_context_can_proceed: 核心决策逻辑

原理与工作流程:
函数按照一个精心设计的决策树来判断是否可以继续:

  1. 基础所有权检查: if (!nbcon_owner_matches(cur, cpu, ctxt->prio))

    • 问题: “我还是不是记录在案的那个所有者?”
    • 逻辑: 这是最基础的检查。如果nbcon_state中记录的所有者CPU和优先级与当前上下文不匹配, 意味着所有权已经被(通常是更高优先级的)上下文强制抢占了。此时必须立即返回false, 表示”停止一切”。
  2. 无等待者检查: if (cur->req_prio == NBCON_PRIO_NONE)

    • 问题: “有人在等我吗?”
    • 逻辑: 如果通过了所有权检查, 并且req_prio字段为NONE, 表示没有其他上下文正在请求”交接”(handover)。在这种最常见的情况下, 当前所有者可以安全地继续执行, 函数返回true
  3. “不安全区域”特权: if (cur->unsafe)

    • 问题: “我是否正处于一个’不安全’的操作序列中?”
    • 逻辑: 即使有更高优先级的上下文在等待(req_prio != NONE), 如果当前所有者正处于”不安全区域”(cur->unsafe == true), 它也被允许继续执行
    • 原理: 这是为了防止活锁(livelock)。如果不允许在不安全区内继续, 那么一个高优先级请求者可能会导致低优先级所有者永远无法完成其原子操作序列(例如, 无法完成一次完整的日志发射), 从而永远无法退出不安全区来释放锁。此规则保证了当前所有者至少能完成其最小的原子工作单元。当它尝试退出不安全区时(nbcon_context_exit_unsafe), can_proceed会被再次调用, 届时它将因为cur->unsafefalse而进入下面的礼让逻辑。
  4. 主动礼让 (Yielding):

    • 场景: 如果代码执行到这里, 意味着: (a)我们是合法所有者, (b)有更高优先级的上下文在等待, (c)我们当前处于”安全”状态。
    • 决策: 这是”礼貌”的体现。在这种情况下, 我们不应该继续执行任何耗时的操作。函数会主动调用nbcon_context_release(ctxt)来释放自己持有的锁, 为更高优先级的等待者让路。
    • 返回值: 在主动释放锁之后, 函数返回false。这告知调用者”你不再拥有所有权, 必须中止当前操作并从头开始”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static bool nbcon_context_can_proceed(struct nbcon_context *ctxt, struct nbcon_state *cur)
{
unsigned int cpu = smp_processor_id();

/* 决策1: 基础所有权检查. 如果我不再是所有者, 立即返回false. */
if (!nbcon_owner_matches(cur, cpu, ctxt->prio))
return false;

/* 决策2: 检查是否有等待者. 如果没有, 可以继续. */
if (cur->req_prio == NBCON_PRIO_NONE)
return true;

/*
* 决策3: 检查是否在不安全区内.
* 即使有等待者, 在不安全区内的所有者也必须被允许继续,
* 以完成其原子操作序列. 它将在退出不安全区时执行交接.
*/
if (cur->unsafe)
return true;

/*
* 如果执行到这里, 说明我们在安全状态, 且有更高优先级的上下文在等待.
* 这是一个让出所有权的"安全点".
*/

/* 健全性检查: 等待者的优先级必须高于当前所有者. */
WARN_ON_ONCE(cur->req_prio <= cur->prio);

/*
* 决策4: 主动礼让.
* 调用 nbcon_context_release() 来释放锁, 为等待者让路.
*/
nbcon_context_release(ctxt);

/*
* 在释放锁之后, 我们不再拥有所有权, 因此返回false,
* 强制上层调用栈中止当前操作.
*/
return false;
}

nbcon_can_proceed: 便捷的API封装

这是一个上层的、导出的API函数, 它为nbcon的使用者(主要是驱动的回调函数)提供了一个简洁的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* nbcon_can_proceed - 检查所有权是否可以继续
* @wctxt: 传递给写函数的回调上下文
*
* (函数注释与 nbcon_context_can_proceed 相同)
*/
bool nbcon_can_proceed(struct nbcon_write_context *wctxt)
{
/* 从写上下文中提取出核心上下文. */
struct nbcon_context *ctxt = &ACCESS_PRIVATE(wctxt, ctxt);
struct console *con = ctxt->console;
struct nbcon_state cur;

/* 读取最新的状态. */
nbcon_state_read(con, &cur);

/* 调用核心决策逻辑并返回其结果. */
return nbcon_context_can_proceed(ctxt, &cur);
}
EXPORT_SYMBOL_GPL(nbcon_can_proceed);

nbcon 上下文的”不安全区域”进出机制

此代码片段揭示了Linux内核非阻塞控制台(nbcon)子系统中一个极其精细的内部同步原语。它的核心原理是nbcon的打印操作定义一个”不安全区域”(unsafe section)的边界, 并提供一个原子性的、可被抢占的进入和退出该区域的方法

“不安全区域”是什么?
nbcon_emit_next_record函数中, 从获取下一条日志记录开始, 到最终调用驱动的write回调函数并将日志输出到硬件为止, 这个过程构成了一个关键的操作序列。在这个序列的执行过程中, 硬件的状态(例如UART的FIFO填充状态)和软件的状态(例如指向缓冲区的指针)可能是暂时不一致的。如果这个序列在中间被打断(被更高优先级的上下文抢占), 那么整个控制台就处于一个”不安全”的状态。

nbcon_context_enter_unsafenbcon_context_exit_unsafe这两个宏所封装的__nbcon_context_update_unsafe函数, 就如同这个”不安全区域”的”门卫”。它不仅负责开关门(设置/清除状态位), 更重要的是, 它在开关门的每一步都会检查是否有人(更高优先级的上下文)正在”敲门”要求进入。


__nbcon_context_update_unsafe: 核心实现

此函数是这个”门卫”的核心逻辑。它负责原子地更新nbcon_state中的unsafe位, 并且在每一步都检查当前上下文是否仍然有权继续操作。

原理与工作流程:
它的核心是一个带有抢占检查的比较并交换(Compare-and-Swap, CAS)循环

  1. 读取当前状态: 函数首先获取控制台最新的原子状态cur
  2. CAS循环: 进入一个do-while循环以保证更新的原子性。
  3. “污点”检查 (Latch Mechanism): if (!unsafe && cur.unsafe_takeover)
    • 这是进入循环后的第一道检查。它实现了一个”污点”或”闩锁”机制。
    • !unsafe为真意味着我们正尝试退出不安全区(即调用exit_unsafe)。
    • 但如果cur.unsafe_takeover为真(表示此控制台曾被粗暴地强制接管过), 那么此函数拒绝清除unsafe标志
    • 原理: 一旦发生过强制接管, 控制台的硬件状态就可能已永久性地损坏或不一致, 内核会将其永久标记为”不安全”, 不再允许任何常规的打印操作进入, 以免造成更大的破坏。
  4. 抢占检查: if (!nbcon_context_can_proceed(ctxt, &cur))
    • 这是最关键的抢占检测点。在尝试修改状态之前, 它会检查: “我这个上下文, 是否仍然是合法的所有者, 并且没有更高优先级的上下文正在请求交接?”
    • 如果can_proceed返回false, 意味着所有权已经被(或即将被)抢占。函数必须立即返回false, 通知调用者(“你已经丢失所有权, 马上停止你正在做的一切!”)。
  5. 原子更新: 如果通过了所有检查, 函数会准备一个新的状态new, 其中new.unsafe被设置为期望的值(进入时为true, 退出时为false), 然后调用nbcon_state_try_cmpxchg尝试原子地更新状态。如果失败(因为状态被其他上下文改变了), CAS循环会重试。
  6. 最终检查: 即使成功更新了unsafe位, 在函数返回前, 它再一次调用nbcon_context_can_proceed并返回其结果。这提供了双重保险, 确保即使在CAS操作成功的瞬间, 所有权也未被抢占。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* __nbcon_context_update_unsafe: 更新 con->nbcon_state 中的 unsafe 位 */
static bool __nbcon_context_update_unsafe(struct nbcon_context *ctxt, bool unsafe)
{
struct console *con = ctxt->console;
struct nbcon_state cur;
struct nbcon_state new;

/* 读取控制台的当前原子状态. */
nbcon_state_read(con, &cur);

/* 使用do-while循环来原子地更新状态. */
do {
/*
* "污点"检查: 如果我们尝试清除unsafe标志(!unsafe),
* 但之前发生过一次"不安全的强制接管", 那么不允许清除.
* 控制台将永久保持不安全状态.
*/
if (!unsafe && cur.unsafe_takeover)
goto out;

/*
* 抢占检查: 在尝试修改之前, 检查我们是否还有权继续.
* 如果有更高优先级的请求者, 我们必须立即中止.
*/
if (!nbcon_context_can_proceed(ctxt, &cur))
return false;

/* 准备要写入的新状态. */
new.atom = cur.atom;
new.unsafe = unsafe; /* 设置新的unsafe值. */
/* 尝试原子地将状态从 cur 更新为 new. 如果失败, 重新读取 cur 并重试. */
} while (!nbcon_state_try_cmpxchg(con, &cur, &new));

/* 如果CAS成功, 更新我们的本地状态副本. */
cur.atom = new.atom;
out:
/*
* 最终检查: 即使我们成功更新了位, 也要最后再检查一次我们是否仍然拥有所有权.
* 这可以捕获在CAS操作期间发生的极细微的竞争.
*/
return nbcon_context_can_proceed(ctxt, &cur);
}

便捷宏封装

这两个宏的作用是提供清晰、易读的API, 让调用代码(nbcon_emit_next_record)的意图一目了然, 就像使用锁一样。

1
2
3
4
5
6
7
8
9
10
/*
* nbcon_context_enter_unsafe: 宏, 用于进入不安全区域.
* 它调用核心函数, 并传递 true 来设置 unsafe 位.
*/
#define nbcon_context_enter_unsafe(c) __nbcon_context_update_unsafe(c, true)
/*
* nbcon_context_exit_unsafe: 宏, 用于退出不安全区域.
* 它调用核心函数, 并传递 false 来尝试清除 unsafe 位.
*/
#define nbcon_context_exit_unsafe(c) __nbcon_context_update_unsafe(c, false)

printk_get_next_message: printk 环形缓冲区的格式化读取器

此函数是Linux内核日志系统(printk)的核心组成部分, 它的根本原理是充当一个从内核主日志环形缓冲区(printk ring buffer, prb)到各个控制台驱动程序之间的”格式化桥梁”。它负责根据请求, 安全地从环形缓冲区中取出下一条有效的原始日志记录, 并根据不同的需求(如是否为扩展格式、是否需要过滤日志级别)将其转换成一段人类可读的、准备发送到硬件的字符串。

这是一个纯粹的软件逻辑函数, 不涉及任何硬件访问, 但它实现了printk流程中几个至关重要的功能:

详细工作流程与原理:

  1. 缓冲区策略 (Buffer Strategy): 函数首先根据is_extended标志决定其工作缓冲区。

    • 标准格式 (is_extendedfalse): 它直接将环形缓冲区中的原始日志文本读入最终的输出缓冲区(outbuf)。后续的格式化(如添加时间戳)将以**原地(in-place)**的方式完成。
    • 扩展格式 (is_extendedtrue): 由于扩展格式的头部信息更复杂, 为了避免在构建头部时覆盖原始文本, 它首先将原始日志文本读入一个临时的”草稿缓冲区”(scratchbuf), 然后再将格式化的头部和来自草稿缓冲区的文本组合成最终结果, 放入outbuf中。
  2. 读取并处理间隙 (prb_read_valid): 这是核心的数据检索步骤。它调用prb_read_valid并传入一个期望的序列号seq

    • 原理: prb_read_valid非常健壮, 它会尝试读取序列号为seq的记录。如果seq指向的记录因为环形缓冲区被覆盖而已经不存在了, 它会自动向前搜索并返回第一条仍然有效的记录
    • 如果没有任何有效的记录可读, 函数返回false, 表示读取结束。
  3. 计算丢弃数 (pmsg->dropped): pmsg->dropped = r.info->seq - seq; 这是一个非常精妙的计算。它用实际读到的记录序列号减去期望读取的序列号, 其差值就精确地等于因为缓冲区被覆盖而丢失的记录数量。nbcon_emit_next_record等上层函数会利用这个值来打印[... dropped ...]信息。

  4. 日志级别过滤 (suppress_message_printing): 如果may_suppress标志为真(意味着调用者允许过滤), 并且记录本身没有被标记为LOG_FORCE_CON(强制输出到控制台), 函数就会调用suppress_message_printing来检查记录的日志级别是否低于当前控制台配置的日志级别。如果需要被过滤, 函数会直接goto out, 导致最终输出的字符串长度为0, 从而有效地跳过了这条消息。

  5. 格式化输出: 如果消息未被过滤, 函数会根据is_extended标志, 调用相应的格式化函数(info_print_ext_header, msg_print_ext_body, record_print_text)来构建最终的输出字符串, 包括时间戳、日志级别、设备信息和消息正文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/*
* 读取并格式化指定的记录 (如果指定记录不可用, 则读取其后的第一条可用记录).
* @pmsg: 指向一个 printk_message 结构体, 将用于存放格式化结果. 其pbufs成员必须指向一个有效的缓冲区.
* @seq: 要读取和格式化的记录的序列号. 如果不可用, 将读取下一个有效的记录.
* @is_extended: 布尔值, 指定是否应将消息格式化为扩展控制台输出.
* @may_suppress: 布尔值, 指定是否可以根据日志级别抑制记录的打印.
* 返回: 如果没有可用的记录, 返回false. 否则返回true, 并且@pmsg的所有字段都有效.
*/
bool printk_get_next_message(struct printk_message *pmsg, u64 seq,
bool is_extended, bool may_suppress)
{
struct printk_buffers *pbufs = pmsg->pbufs;
const size_t scratchbuf_sz = sizeof(pbufs->scratchbuf);
const size_t outbuf_sz = sizeof(pbufs->outbuf);
char *scratchbuf = &pbufs->scratchbuf[0];
char *outbuf = &pbufs->outbuf[0];
struct printk_info info;
struct printk_record r;
size_t len = 0;
bool force_con;

/*
* 缓冲区策略:
* 扩展消息需要一个单独的缓冲区来读取原始文本, 所以使用scratchbuf.
* 普通消息是原地格式化的, 所以直接将原始文本读入outbuf.
*/
if (is_extended)
prb_rec_init_rd(&r, &info, scratchbuf, scratchbuf_sz);
else
prb_rec_init_rd(&r, &info, outbuf, outbuf_sz);

/*
* 读取并处理间隙:
* 从环形缓冲区(prb)中读取序列号>=seq的第一条有效记录.
* 如果没有可读的记录, 返回false.
*/
if (!prb_read_valid(prb, seq, &r))
return false;

/* 记录我们实际读到的序列号. */
pmsg->seq = r.info->seq;
/* 计算从期望的seq到实际读到的seq之间跳过了多少条记录. */
pmsg->dropped = r.info->seq - seq;
/* 检查记录是否带有"强制输出到控制台"的标志. */
force_con = r.info->flags & LOG_FORCE_CON;

/*
* 日志级别过滤:
* 如果记录不是强制输出, 并且允许抑制, 并且其级别高于控制台日志级别,
* 则跳过格式化步骤.
*/
if (!force_con && may_suppress && suppress_message_printing(r.info->level))
goto out;

/*
* 格式化输出:
*/
if (is_extended) {
/* 扩展格式: 先打印头部, 再打印消息体. */
len = info_print_ext_header(outbuf, outbuf_sz, r.info);
len += msg_print_ext_body(outbuf + len, out_sz - len,
&r.text_buf[0], r.info->text_len, &r.info->dev_info);
} else {
/* 普通格式: 原地格式化, 添加时间戳等前缀. */
len = record_print_text(&r, console_msg_format & MSG_FORMAT_SYSLOG, printk_time);
}
out:
/* 记录最终格式化字符串的长度. 如果被过滤, len将为0. */
pmsg->outbuf_len = len;
return true; /* 成功处理一条记录. */
}

nbcon_emit_next_record: 发射单条非阻塞控制台(nbcon)日志记录

此函数是Linux内核非阻塞控制台(nbcon)子系统中负责实际打印工作的引擎。它的核心原理是在一个已获取所有权的上下文中, 安全地从主printk环形缓冲区中取出下一条日志记录, 对其进行必要的格式化(如添加”丢弃”或”重放”标记), 然后调用底层控制台驱动提供的.write_atomic().write_thread()回调函数将其发送到硬件, 最后再安全地更新控制台的打印进度

这是一个极其健壮和复杂的函数, 因为它被设计为在任何时刻都可能被更高优先级的上下文”抢占”打印权。因此, 它的每一步操作都充满了防御性检查, 以确保它不会在丢失所有权后继续进行任何操作。

工作流程详解:

  1. 进入”不安全”区域 & 获取消息: 函数首先调用nbcon_context_enter_unsafe()。这会检查是否有更高优先级的上下文请求交接, 如果有, 函数会立即放弃并返回false。如果安全, 它会调用printk_get_next_message()printk环形缓冲区中获取下一条格式化的日志记录。如果缓冲区中没有更多可处理的记录, 它会正常退出并返回true

  2. 添加”丢弃”标记: 如果printk核心为了到达下一条有效记录而跳过了一些损坏或丢失的记录, 或者此控制台本身之前就丢弃过消息, 此函数会计算出总共丢弃的数量, 并调用console_prepend_dropped()在当前要打印的消息前加上一个类似"[... 42 dropped messages ...]的前缀。这对于用户理解日志的不连续性至关重要。

  3. 处理”重放”情况: 这是一个精妙的容错机制。如果一个低优先级的打印上下文被高优先级的抢占, 打印了一条消息, 然后低优先级上下文又重新获得了所有权, 它可能会尝试打印同一条消息。为了避免重复输出, 函数会检查当前要打印的记录序列号是否与”上一个被打印的序列号”(nbcon_prev_seq)相同。如果相同, 它会调用console_prepend_replay()为消息加上"[replay]"前缀, 而不是重复打印完整消息。

  4. 调用驱动回调: 在完成所有前缀添加后, 函数会根据上下文(use_atomic)决定是调用驱动提供的原子write_atomic()函数, 还是可休眠的write_thread()函数, 将最终的日志内容传递给驱动程序, 由驱动程序负责将其写入物理硬件(如UART的FIFO)。

  5. 所有权丢失检测: 在调用驱动之后, 它会检查wctxt->outbuf是否被驱动清空。这是驱动向nbcon核心发信号的一种方式, 表示驱动在write回调内部自己检测到所有权丢失并重新获取了它。如果发生这种情况, 此函数也会认为所有权已丢失, 并立即中止。

  6. 更新状态: 如果消息被成功发送(或被跳过), 函数会再次进入一个”不安全”区域, 来原子地更新此控制台的两个关键状态:

    • 重置con->dropped计数器, 因为丢弃信息已经被打印出去了。
    • 调用nbcon_seq_try_update()将控制台的进度标记(nbcon_seq)更新为下一条记录的序列号。这是整个流程中最重要的”推进”步骤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
static bool nbcon_emit_next_record(struct nbcon_write_context *wctxt, bool use_atomic)
{
struct nbcon_context *ctxt = &ACCESS_PRIVATE(wctxt, ctxt);
struct console *con = ctxt->console;
bool is_extended = console_srcu_read_flags(con) & CON_EXTENDED;
struct printk_message pmsg = {
.pbufs = ctxt->pbufs,
};
unsigned long con_dropped;
struct nbcon_state cur;
unsigned long dropped;
unsigned long ulseq;

/* 健全性检查: 确保函数被正确调用. */
if (WARN_ON_ONCE((use_atomic && !con->write_atomic) ||
!(console_srcu_read_flags(con) & CON_NBCON))) {
nbcon_context_release(ctxt); /* 释放所有权并中止. */
return false;
}

/*
* 步骤1: 进入"不安全"区域. 这会检查是否有更高优先级的请求者.
* 如果我们被抢占了, 就不能继续.
*/
if (!nbcon_context_enter_unsafe(ctxt))
return false;

/* 从printk环形缓冲区获取下一条消息. */
ctxt->backlog = printk_get_next_message(&pmsg, ctxt->seq, is_extended, true);
if (!ctxt->backlog) /* 如果没有更多消息, 退出不安全区并成功返回. */
return nbcon_context_exit_unsafe(ctxt);

/*
* 步骤2: 处理丢弃的消息.
* 计算总丢弃数, 并在消息前添加"[... dropped ...]"前缀.
*/
con_dropped = data_race(READ_ONCE(con->dropped));
dropped = con_dropped + pmsg.dropped;
if (dropped && !is_extended)
console_prepend_dropped(&pmsg, dropped);

/*
* 步骤3: 处理重放情况.
* 检查我们是否在尝试打印一条已经被更高优先级上下文打印过的消息.
*/
ulseq = atomic_long_read(&ACCESS_PRIVATE(con, nbcon_prev_seq));
if (__ulseq_to_u64seq(prb, ulseq) == pmsg.seq) {
console_prepend_replay(&pmsg); /* 如果是, 只添加"[replay]"前缀. */
} else {
/* 否则, 我们是第一个打印此消息的, 尝试更新"上一条"序列号. */
nbcon_state_read(con, &cur);
if (!nbcon_context_can_proceed(ctxt, &cur))
return false; /* 再次检查所有权. */

atomic_long_try_cmpxchg(&ACCESS_PRIVATE(con, nbcon_prev_seq), &ulseq,
__u64seq_to_ulseq(pmsg.seq));
}

/* 退出第一个不安全区域. */
if (!nbcon_context_exit_unsafe(ctxt))
return false;

/* 如果只是跳过的记录(没有内容), 直接去更新状态. */
if (pmsg.outbuf_len == 0)
goto update_con;

/*
* 步骤4: 调用驱动回调函数.
*/
nbcon_write_context_set_buf(wctxt, &pmsg.pbufs->outbuf[0], pmsg.outbuf_len);
if (use_atomic)
con->write_atomic(con, wctxt);
else
con->write_thread(con, wctxt);

/* 步骤5: 检查驱动是否在回调中丢失并重新获取了所有权. */
if (!wctxt->outbuf) {
nbcon_context_release(ctxt);
return false;
}

update_con:
/*
* 步骤6: 更新状态. 再次进入不安全区域来原子地更新进度.
*/
if (!nbcon_context_enter_unsafe(ctxt))
return false;

/* 如果丢弃信息已打印, 重置控制台的丢弃计数器. */
if (dropped != con_dropped) {
WRITE_ONCE(con->dropped, dropped);
}

/* 尝试将控制台的进度序列号更新到下一条记录. */
nbcon_seq_try_update(ctxt, pmsg.seq + 1);

/* 退出不安全区并返回所有权状态. */
return nbcon_context_exit_unsafe(ctxt);
}

非阻塞控制台(nbcon)的原子刷新: 单个控制台实现

此代码片段深入到Linux内核非阻塞控制台(nbcon)原子刷新机制的最底层。它包含了负责刷新单个指定控制台的核心逻辑。其根本原理是一个高度健壮、具备所有权协商和循环处理能力的微型状态机, 专门用于在不可休眠的原子上下文中, 将日志从内核缓冲区驱动到硬件

这套机制是为应对最严苛的系统环境(如panic, NMI)而设计的, 因此其复杂性体现在对所有权、并发和日志追赶问题的处理上。


__nbcon_atomic_flush_pending_con: 核心执行逻辑

此函数是实际执行刷新工作的”引擎”。它尝试获取控制台的所有权, 然后在一个循环中逐条记录地输出日志。

原理与工作流程:

  1. 上下文初始化: 创建并初始化一个nbcon_write_context。这个结构体包含了刷新操作所需的所有状态, 如目标控制台、优先级、超时等。
  2. 获取所有权: 调用nbcon_context_try_acquire尝试获取该控制台的”打印权”。这是一个非阻塞的锁获取操作。如果失败(返回false), 意味着另一个上下文(可能是更高优先级的NMI, 或另一个CPU核心)正在使用此控制台, 函数会立即返回-EPERM(权限错误), 表示本次尝试失败。
  3. 打印循环: 如果成功获取所有权, 函数进入一个while循环, 只要当前控制台的处理进度还没达到目标序列号stop_seq, 循环就会继续。
  4. 单条记录发射: 在循环内部, 它调用nbcon_emit_next_record。这个函数负责:
    • printk环形缓冲区读取下一条可用的日志记录。
    • 调用控制台驱动提供的.write_atomic()回调函数, 将记录内容发送到硬件。
    • 如果在执行过程中, 打印权被更高优先级的上下文”抢占”(takeover)了, nbcon_emit_next_record会返回false。此时, 循环必须终止, 函数返回-EAGAIN, 告知上层”请重试或放弃, 因为其他人接管了”。
  5. 处理”空洞”: 如果printk缓冲区中没有更多可读的记录了(!ctxt->backlog), 但我们仍未达到目标序列号stop_seq, 这意味着有其他CPU核心预留了一个日志槽位但还没有完全写入数据。在原子上下文中, 我们绝不能等待它完成。因此, 函数会设置-ENOENT(无此条目)错误并跳出循环。
  6. 释放所有权: 无论循环是如何退出的(成功完成、被抢占、遇到空洞), 最后都必须调用nbcon_context_release来释放之前获取的打印权。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* __nbcon_atomic_flush_pending_con: 使用write_atomic()回调刷新指定的nbcon控制台 */
static int __nbcon_atomic_flush_pending_con(struct console *con, u64 stop_seq,
bool allow_unsafe_takeover)
{
struct nbcon_write_context wctxt = { };
struct nbcon_context *ctxt = &ACCESS_PRIVATE(&wctxt, ctxt);
int err = 0;

/* 初始化写操作的上下文. */
ctxt->console = con;
ctxt->spinwait_max_us = 2000; /* 尝试获取锁时的自旋等待超时. */
ctxt->prio = nbcon_get_default_prio(); /* 获取默认的优先级. */
ctxt->allow_unsafe_takeover = allow_unsafe_takeover; /* 是否允许不安全的抢占. */

/* 步骤1: 尝试获取控制台的打印权. */
if (!nbcon_context_try_acquire(ctxt, false))
return -EPERM; /* 如果失败, 返回权限错误. */

/* 步骤2: 循环打印, 直到赶上目标序列号. */
while (nbcon_seq_read(con) < stop_seq) {
/*
* 步骤3: 发射下一条记录. 如果所有权被抢占, 此函数返回false.
*/
if (!nbcon_emit_next_record(&wctxt, true))
return -EAGAIN; /* 返回"请重试", 因为其他人接管了. */

/* 步骤4: 检查是否还有待处理的日志. */
if (!ctxt->backlog) {
/* 如果没有了, 但仍未达到目标, 说明遇到了未写完的"空洞"记录. */
if (nbcon_seq_read(con) < stop_seq)
err = -ENOENT; /* 设置"无此条目"错误. */
break; /* 退出循环. */
}
}

/* 步骤5: 释放打印权. */
nbcon_context_release(ctxt);
return err; /* 返回0或-ENOENT. */
}

nbcon_atomic_flush_pending_con: 带安全封装和重试逻辑的包装器

此函数是__nbcon_atomic_flush_pending_con的一个包装器, 它增加了两个至关重要的特性: 中断安全日志追赶(tail-chasing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* nbcon_atomic_flush_pending_con: 刷新指定的nbcon控制台, 带安全封装. */
static void nbcon_atomic_flush_pending_con(struct console *con, u64 stop_seq,
bool allow_unsafe_takeover)
{
struct console_flush_type ft;
unsigned long flags;
int err;

again: /* 用于日志追赶的goto标签. */
/*
* 关键特性1: 中断安全.
* 原子刷新不使用常规的驱动锁(因为可能休眠), 所以必须禁用本地中断.
* 这可以防止在持有nbcon所有权锁时, 本地CPU又发生中断并尝试打印,
* 从而导致在同一个CPU上对自己持有的锁产生死锁.
*/
local_irq_save(flags);

/* 调用核心执行逻辑. */
err = __nbcon_atomic_flush_pending_con(con, stop_seq, allow_unsafe_takeover);

/* 恢复之前的中断状态. */
local_irq_restore(flags);

/*
* 错误处理:
* 如果返回-EPERM或-EAGAIN, 说明有其他上下文接管了, 我们直接退出即可.
* 如果返回-ENOENT, 我们不能等待那个"空洞"记录, 也要退出, 避免死锁.
*/
if (err)
return;

/*
* 关键特性2: 日志追赶 (Tail-chasing).
* 如果我们成功完成了刷新, 但在我们刷新期间又有新的日志被加入,
* 并且系统中没有一个后台打印线程(nbcon_offload)可以处理它们,
* 那么我们这个原子上下文就有责任继续把这些新日志也打印完.
*/
printk_get_console_flush_type(&ft);
if (!ft.nbcon_offload &&
prb_read_valid(prb, nbcon_seq_read(con), NULL)) {
/* 更新目标序列号到最新的日志末尾. */
stop_seq = prb_next_reserve_seq(prb);
/* 跳回到开头, 重新开始一轮刷新. */
goto again;
}
}

非阻塞控制台(nbcon)的原子刷新机制

此代码片段展示了Linux内核中一种现代化、高性能的控制台输出机制——非阻塞控制台(Non-Blocking Console, nbcon)的原子刷新功能。它的核心原理是提供一种在原子上下文(atomic context, 如中断处理程序、持有自旋锁的代码、系统恐慌panic等)中, 能够安全、快速地将内核日志缓冲区中的待处理消息(backlog)强制输出到硬件的方法

这与传统的、可能休眠的控制台输出机制形成鲜明对比。在紧急情况下(如系统即将崩溃), 内核不能调用任何可能导致睡眠的函数, 因此需要一套完全不同的、”原子”的输出路径。nbcon为此而生, 它要求控制台驱动程序提供一个特殊的write_atomic()回调函数, 这个函数必须保证在任何情况下都不会休眠。

nbcon_atomic_flush_pending系列函数就是这个机制的触发器。它不适用于常规日志输出, 而是专门为内核的紧急路径(如kmsg_dump, panic())设计的。


__nbcon_atomic_flush_pending: 核心实现

这是执行原子刷新的底层工作函数。它负责遍历所有已注册的控制台, 筛选出符合条件的非阻塞控制台, 并对它们逐一执行刷新操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* __nbcon_atomic_flush_pending - 使用 write_atomic() 回调函数刷新所有nbcon控制台
* @stop_seq: 刷新到此序列号的记录为止
* @allow_unsafe_takeover: 布尔值, 是否允许不安全的强制接管
*/
static void __nbcon_atomic_flush_pending(u64 stop_seq, bool allow_unsafe_takeover)
{
struct console *con;
int cookie;

/*
* 使用 SRCU (Sleepable Read-Copy Update) 锁来安全地遍历全局控制台列表.
* SRCU 是一种允许读者在遍历时可以睡眠的读写锁机制.
* 在这里, 虽然我们处于原子上下文, 但这是访问全局控制台列表的标准、安全方式.
*/
cookie = console_srcu_read_lock();
for_each_console_srcu(con) {
/* 读取当前控制台的标志位. */
short flags = console_srcu_read_flags(con);

/* 筛选条件1: 检查是否为非阻塞控制台 (CON_NBCON). 如果不是, 跳过. */
if (!(flags & CON_NBCON))
continue;

/* 筛选条件2: 检查此控制台在原子上下文中是否可用. */
if (!console_is_usable(con, flags, true))
continue;

/* 筛选条件3: 检查此控制台是否已经处理完所有需要刷新的记录. 如果是, 跳过. */
if (nbcon_seq_read(con) >= stop_seq)
continue;

/*
* 对于所有通过筛选的nbcon控制台, 调用其私有的原子刷新函数.
* 这个函数内部会循环调用驱动提供的 write_atomic() 回调,
* 将日志从内核缓冲区输出到硬件, 直到达到 stop_seq.
*/
nbcon_atomic_flush_pending_con(con, stop_seq, allow_unsafe_takeover);
}
/* 遍历完成, 释放 SRCU 读锁定. */
console_srcu_read_unlock(cookie);
}

nbcon_atomic_flush_pending: 便捷的API封装

这是一个上层的、导出的API函数, 它为内核的紧急路径提供了一个简洁的调用接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* nbcon_atomic_flush_pending - 使用 write_atomic() 回调函数刷新所有nbcon控制台
*
* 将积压的日志刷新到当前最新的那条记录. 在刷新期间任何新加入的记录,
* 如果有其他上下文可以处理刷新, 则不会被本次刷新处理. 这是为了避免
* 单个CPU因为其他CPU持续添加记录而无限制地进行打印.
*/
void nbcon_atomic_flush_pending(void)
{
/*
* 调用核心实现函数, 并传入:
* - stop_seq: prb_next_reserve_seq(prb)
* 此函数获取printk环形缓冲区中下一条将要被写入的记录的序列号.
* 这意味着本次刷新操作的目标是"清空当前所有的待处理日志".
*
* - allow_unsafe_takeover: false
* 这表示本次刷新是一个"协作式"的刷新. 如果发现有其他CPU核心
* 也正在刷新这个控制台, 本次调用不会强行"接管"刷新工作.
*/
__nbcon_atomic_flush_pending(prb_next_reserve_seq(prb), false);
}

kernel/printk/printk.c 内核消息打印

oops_in_progress 指示当前是否正在处理内核错误

oops_in_progress 是 Linux 内核中的一个全局变量,用于指示当前是否正在处理内核 “oops”(内核错误)。以下是对其功能和用途的详细解释:

  1. 内核 “oops” 的概念

    • 在 Linux 内核中,”oops” 是一种严重的错误,通常表示内核代码中发生了非法操作,例如访问无效内存地址或触发了断言失败。
    • 当发生 “oops” 时,内核会打印错误信息(通过 printk),记录问题的详细信息,并尝试继续运行(如果可能的话),而不是立即崩溃。
  2. oops_in_progress 的作用

    • oops_in_progress 是一个标志变量,用于指示当前是否正在处理 “oops” 错误。
    • 当内核开始处理 “oops” 时,会将 oops_in_progress 设置为非零值,表示系统处于错误处理状态。
    • 在错误处理完成后,oops_in_progress 会被重置为零。
  3. 防止递归错误

    • 在处理 “oops” 的过程中,内核可能会调用其他函数(例如日志记录或调试工具)。如果这些函数再次触发错误,可能会导致递归 “oops” 或系统崩溃。
    • 通过检查 oops_in_progress 的状态,内核可以避免在处理 “oops” 时再次触发某些操作,从而防止递归错误的发生。
  4. 典型使用场景

    • oops_in_progress 通常与内核日志记录(printk)和调试机制配合使用。在记录 “oops” 信息时,内核会检查该标志,以决定是否需要限制某些操作。
    • 例如,如果系统正在处理 “oops”,某些非关键的日志可能会被跳过,以避免进一步的干扰。
  5. 与系统稳定性相关

    • oops_in_progress 的存在使得内核在处理严重错误时能够更好地控制系统行为,尽量避免系统完全崩溃。
    • 在某些情况下,内核可能会尝试继续运行,即使发生了 “oops”。这对于嵌入式系统或关键任务系统尤为重要,因为它们可能需要尽量保持运行状态。

总的来说,oops_in_progress 是 Linux 内核中一个关键的全局变量,用于管理和协调 “oops” 错误的处理过程。它在提高系统稳定性和防止递归错误方面起到了重要作用。

printk_delay 当CONFIG_BOOT_PRINTK_DELAY配置时 每条消息延时打印配置

  1. 当CONFIG_BOOT_PRINTK_DELAY配置时 每条消息延时打印配置
1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void printk_delay(int level)
{
boot_delay_msec(level);

if (unlikely(printk_delay_msec)) {
int m = printk_delay_msec;

while (m--) {
mdelay(1);
touch_nmi_watchdog();
}
}
}

__printk_recursion_counter 返回指向调用方 CPU 上下文的专用计数器的指针

  1. 根据当前 CPU 是否处于 NMI(非屏蔽中断)上下文来选择使用哪个递归计数器。
  2. 根据当前是否完成了 printk 的每 CPU 数据准备来选择使用哪个递归计数器。
1
2
3
4
5
6
7
8
9
10
11
12
13
static u8 *__printk_recursion_counter(void)
{
#ifdef CONFIG_HAVE_NMI //是否支持 NMI(非屏蔽中断)
if (in_nmi()) { //NMI计数不为0
if (printk_percpu_data_ready())
return this_cpu_ptr(&printk_count_nmi);
return &printk_count_nmi_early;
}
#endif
if (printk_percpu_data_ready())
return this_cpu_ptr(&printk_count);
return &printk_count_early;
}

printk_enter_irqsave 屏蔽中断并检查递归调用次数判断是否允许打印,失败不会禁用

  1. 检测递归调用的次数,超过PRINTK_MAX_RECURSION则返回false。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
* 递归是有限的,以保持输出的理智。
printk() 不应要求超过 1 级递归
(例如,允许 printk() 触发 WARN),但如果存在某些 printk 内部错误,
例如 ringbuffer 验证检查失败,则使用更高的值。
*/
#define PRINTK_MAX_RECURSION 3

#define printk_enter_irqsave(recursion_ptr, flags) \
({ \
bool success = true; \
\
typecheck(u8 *, recursion_ptr); \
/* 返回当前中断状态并禁用中断 */
local_irq_save(flags); \
//返回指向调用方 CPU 上下文的专用计数器的指针
(recursion_ptr) = __printk_recursion_counter(); \
if (*(recursion_ptr) > PRINTK_MAX_RECURSION) { \
//失败恢复中断
local_irq_restore(flags); \
success = false; \
} else { \
(*(recursion_ptr))++; \
} \
success; \
})

printk_parse_prefix 提取日志级别或控制标志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* printk_parse_prefix - 解析级别和控制标志。
*
* @text:已终止的短信。
* @level:指向当前级别值的指针将被更新。
* @flags:指向当前 printk_info 标志的指针将被更新。
*
* 如果调用方对解析的值不感兴趣,则 @level 可能为 NULL。否则,必须将 @level 指向的变量设置为 LOGLEVEL_DEFAULT 才能使用解析的值进行更新。
*
* 如果调用方对解析的值不感兴趣,则 @flags 可能为 NULL。否则,@flags 指向的变量将与解析的值进行 OR 运算。
*
* 返回:解析的电平和控制标志的长度。
*/
u16 printk_parse_prefix(const char *text, int *level,
enum printk_info_flags *flags)
{
u16 prefix_len = 0;
int kern_level;

while (*text) { //传入的text会增加一个/0退出
kern_level = printk_get_level(text); //获取日志等级
if (!kern_level) //没有日志等级退出
break;

switch (kern_level) {
case '0' ... '7':
//传入level需要的话必须设置为LOGLEVEL_DEFAULT
if (level && *level == LOGLEVEL_DEFAULT)
*level = kern_level - '0';
break;
case 'c': /* KERN_CONT */
if (flags)
*flags |= LOG_CONT;
}

prefix_len += 2;
text += 2;
}

return prefix_len;
}

printk_ringbuffer 静态 动态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* record buffer */
#define LOG_ALIGN __alignof__(unsigned long)
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
#define LOG_BUF_LEN_MAX ((u32)1 << 31)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
static char *log_buf = __log_buf;
/*
- 定义平均消息大小。这仅影响可用的描述符数量。低估比高估要好(可用描述符太多总比不够好)。
*/
#define PRB_AVGBITS 5 /* 32 个字符的平均长度 */

#if CONFIG_LOG_BUF_SHIFT <= PRB_AVGBITS
#error CONFIG_LOG_BUF_SHIFT value too small.
#endif
_DEFINE_PRINTKRB(printk_rb_static, CONFIG_LOG_BUF_SHIFT - PRB_AVGBITS,
PRB_AVGBITS, &__log_buf[0]);

static struct printk_ringbuffer printk_rb_dynamic;

struct printk_ringbuffer *prb = &printk_rb_static;

truncate_msg 截断消息

  1. log_buf中仅有一半的缓冲区可用
  2. 如果发生截断,则在消息末尾添加<truncated>,启用警告消息(如果有空间).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
- 定义我们最多可以接受多少 log buffer。该值必须大于 2。请注意,当索引指向中间时,只有一半的缓冲区可用。
*/
#define MAX_LOG_TAKE_PART 4
static const char trunc_msg[] = "<truncated>";

static void truncate_msg(u16 *text_len, u16 *trunc_msg_len)
{
/*
*消息不应占用整个缓冲区。否则,它可能会过早地被删除。
*/
u32 max_text_len = log_buf_len / MAX_LOG_TAKE_PART;

if (*text_len > max_text_len)
*text_len = max_text_len;

/* 启用警告消息(如果有空间) */
*trunc_msg_len = strlen(trunc_msg);
if (*text_len >= *trunc_msg_len)
*text_len -= *trunc_msg_len;
else
*trunc_msg_len = 0;
}

vprintk_store 打印消息到环形缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//4 表示格式字符串是函数的第 4 个参数(从 1 开始计数,包括 this 指针或隐式参数)。
//0 表示该函数没有可变参数列表(即没有类似 ... 的参数)。如果存在可变参数列表,则需要指定其起始位置
int vprintk_store(int facility, int level,
const struct dev_printk_info *dev_info,
const char *fmt, va_list args)
{
struct prb_reserved_entry e;
enum printk_info_flags flags = 0;
struct printk_record r;
unsigned long irqflags;
u16 trunc_msg_len = 0;
char prefix_buf[8];
u8 *recursion_ptr;
u16 reserve_size;
va_list args2;
u32 caller_id;
u16 text_len;
int ret = 0;
u64 ts_nsec;
//屏蔽中断并检查递归调用次数判断是否允许打印,失败不会禁用
if (!printk_enter_irqsave(recursion_ptr, irqflags))
return 0;

/*
* 由于 printk() 的持续时间可能因消息和环形缓冲区的状态而异,因此现在获取时间戳,使其接近 printk() 的调用。这为调用方提供了更具确定性的时间戳。
*/
ts_nsec = local_clock();

/* return in_task() ? task_pid_nr(current) : 0x80000000 + smp_processor_id(); */
caller_id = printk_caller_id(); //0x80000000 + smp_processor_id()[0] 中断调用

/*
* sprintf 需要放在最前面,因为 syslog 前缀可能作为参数传入。必须保留一个额外的字节,以便以后进入保留缓冲区的 vscnprintf() 有空间终止 '\0',这不被 vsnprintf() 计算在内。
*/
va_copy(args2, args);
reserve_size = vsnprintf(&prefix_buf[0], sizeof(prefix_buf), fmt, args2) + 1;
va_end(args2);

if (reserve_size > PRINTKRB_RECORD_MAX)
reserve_size = PRINTKRB_RECORD_MAX;

/* 提取日志级别或控制标志。 */
if (facility == 0)
printk_parse_prefix(&prefix_buf[0], &level, &flags);

if (level == LOGLEVEL_DEFAULT)
level = default_message_loglevel;

if (dev_info)
flags |= LOG_NEWLINE;

if (is_printk_force_console()) //强制输出到控制台
flags |= LOG_FORCE_CON;


if (flags & LOG_CONT) { //text 是连续行的片段 可能需要与之前的日志拼接
prb_rec_init_wr(&r, reserve_size);
if (prb_reserve_in_last(&e, prb, &r, caller_id, PRINTKRB_RECORD_MAX)) {
text_len = printk_sprint(&r.text_buf[r.info->text_len], reserve_size,
facility, &flags, fmt, args);
r.info->text_len += text_len;

if (flags & LOG_FORCE_CON)
r.info->flags |= LOG_FORCE_CON;

if (flags & LOG_NEWLINE) {
r.info->flags |= LOG_NEWLINE;
prb_final_commit(&e);
} else {
prb_commit(&e);
}

ret = text_len;
goto out;
}
}


/*
* 在每次调用 prb_reserve() 之前显式初始化记录 prb_reserve_in_last() 和 prb_reserve() 在失败时故意使 th 结构失效。
*/
prb_rec_init_wr(&r, reserve_size);
if (!prb_reserve(&e, prb, &r)) { //保留空间失败
/*如果消息对于空缓冲区来说太长,则截断消息 */
truncate_msg(&reserve_size, &trunc_msg_len);

prb_rec_init_wr(&r, reserve_size + trunc_msg_len);
if (!prb_reserve(&e, prb, &r))
goto out;
}


/* 填充消息*/
text_len = printk_sprint(&r.text_buf[0], reserve_size, facility, &flags, fmt, args);
if (trunc_msg_len)
memcpy(&r.text_buf[text_len], trunc_msg, trunc_msg_len);
//prb_reserve时将实际的描述符的info传递给了r,这里的赋值将会存储下来
r.info->text_len = text_len + trunc_msg_len;
r.info->facility = facility;
r.info->level = level & 7;
r.info->flags = flags & 0x1f;
r.info->ts_nsec = ts_nsec;
r.info->caller_id = caller_id;
if (dev_info)
memcpy(&r.info->dev_info, dev_info, sizeof(r.info->dev_info));

/* 没有尾随换行符的消息则可以继续添加到当前缓冲区中 */
if (!(flags & LOG_NEWLINE))
prb_commit(&e);
else
prb_final_commit(&e); //完成缓存可以使用了

ret = text_len + trunc_msg_len;
out:
printk_exit_irqrestore(recursion_ptr, irqflags);
return ret;
}

vprintk_emit 打印消息发送

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
asmlinkage int vprintk_emit(int facility, int level,
const struct dev_printk_info *dev_info,
const char *fmt, va_list args)
{
struct console_flush_type ft;
int printed_len;

/* Suppress unimportant messages after panic happens */
if (unlikely(suppress_printk)) //panic函数置一
return 0;

/*
* The messages on the panic CPU are the most important. If
* non-panic CPUs are generating any messages, they will be
* silently dropped.
*/
if (other_cpu_in_panic() && !panic_triggering_all_cpu_backtrace)
return 0;
//初始化时不处理
printk_get_console_flush_type(&ft); //确定使用哪些控制台刷新方法

/*如果从调度器调用,我们不能调用 up()。 */
if (level == LOGLEVEL_SCHED) {
level = LOGLEVEL_DEFAULT;
ft.legacy_offload |= ft.legacy_direct;
ft.legacy_direct = false;
}

printk_delay(level); //当CONFIG_BOOT_PRINTK_DELAY配置时 每条消息延时打印配置

printed_len = vprintk_store(facility, level, dev_info, fmt, args); //打印消息到环形缓冲区
if (ft.nbcon_atomic)
nbcon_atomic_flush_pending();

if (ft.nbcon_offload)
nbcon_kthreads_wake();

if (ft.legacy_direct) {
/*
* The caller may be holding system-critical or
* timing-sensitive locks. Disable preemption during
* printing of all remaining records to all consoles so that
* this context can return as soon as possible. Hopefully
* another printk() caller will take over the printing.
*/
preempt_disable();
/*
* Try to acquire and then immediately release the console
* semaphore. The release will print out buffers. With the
* spinning variant, this context tries to take over the
* printing from another printing context.
*/
if (console_trylock_spinning())
console_unlock();
preempt_enable();
}

if (ft.legacy_offload)
defer_console_output();
else
wake_up_klogd();

return printed_len;
}
EXPORT_SYMBOL(vprintk_emit);

printk 内核日志打印函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define printk(fmt, ...) printk_index_wrap(_printk, fmt, ##__VA_ARGS__)
#define printk_deferred(fmt, ...) \
printk_index_wrap(_printk_deferred, fmt, ##__VA_ARGS__)

int vprintk_deferred(const char *fmt, va_list args)
{
//不是从设备驱动中调用的
return vprintk_emit(0, LOGLEVEL_SCHED, NULL, fmt, args);
}

int _printk_deferred(const char *fmt, ...)
{
va_list args;
int r;

va_start(args, fmt);
r = vprintk_deferred(fmt, args);
va_end(args);

return r;
}

ignore_loglevel

  1. ignore_loglevel 是一个布尔变量,用于指示是否忽略内核的日志级别设置。
  2. 如果设置为 true,则内核将忽略日志级别设置,并将所有内核消息打印到控制台。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
static bool __read_mostly ignore_loglevel;

static int __init ignore_loglevel_setup(char *str)
{
ignore_loglevel = true;
pr_info("debug: ignoring loglevel setting.\n");

return 0;
}

early_param("ignore_loglevel", ignore_loglevel_setup);
module_param(ignore_loglevel, bool, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(ignore_loglevel,
"ignore loglevel setting (prints all kernel messages to the console)");

—————-控制台——————————————-

suppress_message_printing 禁止打印消息

  1. suppress_message_printing 函数用于检查是否应该禁止打印消息。
  2. 如果当前的日志级别大于等于 console_loglevelignore_loglevel 为假,则返回 true,表示禁止打印消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* printk's without a loglevel use this.. */
//内核中日志消息的默认等级
#define MESSAGE_LOGLEVEL_DEFAULT CONFIG_MESSAGE_LOGLEVEL_DEFAULT //4

/*
* 默认曾经是硬编码的 7,安静的曾经是硬编码的 4,
* 我们现在允许从 Kernel Config 中设置两者。
*/
//控制台日志输出等级,只有小于这个值的消息才会输出到控制台
#define CONSOLE_LOGLEVEL_DEFAULT CONFIG_CONSOLE_LOGLEVEL_DEFAULT //7
#define CONSOLE_LOGLEVEL_QUIET CONFIG_CONSOLE_LOGLEVEL_QUIET

#define console_loglevel (console_printk[0])
#define default_message_loglevel (console_printk[1])
#define minimum_console_loglevel (console_printk[2])
#define default_console_loglevel (console_printk[3])

int console_printk[4] = {
CONSOLE_LOGLEVEL_DEFAULT, /* console_loglevel */
MESSAGE_LOGLEVEL_DEFAULT, /* default_message_loglevel */
CONSOLE_LOGLEVEL_MIN, /* minimum_console_loglevel */
CONSOLE_LOGLEVEL_DEFAULT, /* default_console_loglevel */
};
EXPORT_SYMBOL_GPL(console_printk);

static bool suppress_message_printing(int level)
{
return (level >= console_loglevel && !ignore_loglevel);
}

console_list_lock console_list_unlock 控制台列表锁定和解锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* console_list_lock - 锁定控制台列表
*
* 对于控制台列表或控制台>标志更新
*/
void console_list_lock(void)
{
/*
* 在 unregister_console() 和 console_force_preferred_locked() 中,
* synchronize_srcu() 在按住console_list_lock的情况下调用。因此,不允许在持有srcu_lock的情况下进行console_list_lock。
*
* 只有在启用适当的调试选项时,才能检测此上下文是否真的位于读取端关键部分。
*/
WARN_ON_ONCE(debug_lockdep_rcu_enabled() &&
srcu_read_lock_held(&console_srcu));

mutex_lock(&console_mutex);
}
EXPORT_SYMBOL(console_list_lock);

/**
* console_list_unlock - Unlock the console list
*
* Counterpart to console_list_lock()
*/
void console_list_unlock(void)
{
mutex_unlock(&console_mutex);
}
EXPORT_SYMBOL(console_list_unlock);

try_enable_default_console 尝试无条件启用控制台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int console_call_setup(struct console *newcon, char *options)
{
int err;

if (!newcon->setup)
return 0;

/* Synchronize with possible boot console. */
console_lock();
err = newcon->setup(newcon, options);
console_unlock();

return err;
}

/* 尝试无条件启用控制台 */
static void try_enable_default_console(struct console *newcon)
{
if (newcon->index < 0)
newcon->index = 0;

if (console_call_setup(newcon, NULL) != 0)
return;

newcon->flags |= CON_ENABLED;

if (newcon->device)
newcon->flags |= CON_CONSDEV;
}

try_enable_preferred_console 尝试启用首选控制台

  1. 控制台没初始化时,跳过匹配
  2. 控制台使能且不需要用户指定,返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/*
* 由 register_console() 调用,以尝试将新注册的控制台与命令行或 add_preferred_console() 选择的任何控制台进行匹配,并设置/启用它。
*
* 需要注意静态启用的控制台,例如 netconsole
*/
static int try_enable_preferred_console(struct console *newcon,
bool user_specified)
{
struct console_cmdline *c;
int i, err;

for (i = 0, c = console_cmdline;
i < MAX_CMDLINECONSOLES && (c->name[0] || c->devname[0]);
i++, c++) {
/* Console not yet initialized? */
if (!c->name[0])
continue;
if (c->user_specified != user_specified)
continue;
if (!newcon->match ||
newcon->match(newcon, c->name, c->index, c->options) != 0) {
/* default matching */
BUILD_BUG_ON(sizeof(c->name) != sizeof(newcon->name));
if (strcmp(c->name, newcon->name) != 0)
continue;
if (newcon->index >= 0 &&
newcon->index != c->index)
continue;
if (newcon->index < 0)
newcon->index = c->index;

if (_braille_register_console(newcon, c))
return 0;

err = console_call_setup(newcon, c->options);
if (err)
return err;
}
newcon->flags |= CON_ENABLED;
if (i == preferred_console)
newcon->flags |= CON_CONSDEV;
return 0;
}

/*
* Some consoles, such as pstore and netconsole, can be enabled even
* without matching. Accept the pre-enabled consoles only when match()
* and setup() had a chance to be called.
*/
if (newcon->flags & CON_ENABLED && c->user_specified == user_specified)
return 0;

return -ENOENT;
}

get_init_console_seq 返回新注册的控制台的起始序列号

  1. 引导控制台直接使用 @syslog_seq 作为起始序列号。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/*返回新注册的控制台的起始序列号。 */
static u64 get_init_console_seq(struct console *newcon, bool bootcon_registered)
{
struct console *con;
bool handover;
u64 init_seq;

if (newcon->flags & (CON_PRINTBUFFER | CON_BOOT)) {
/* 获取 @syslog_seq 的一致副本。 */
mutex_lock(&syslog_lock);
init_seq = syslog_seq;
mutex_unlock(&syslog_lock);
} else {
/* Begin with next message added to ringbuffer. */
init_seq = prb_next_seq(prb);

/*
* If any enabled boot consoles are due to be unregistered
* shortly, some may not be caught up and may be the same
* device as @newcon. Since it is not known which boot console
* is the same device, flush all consoles and, if necessary,
* start with the message of the enabled boot console that is
* the furthest behind.
*/
if (bootcon_registered && !keep_bootcon) {
/*
* Hold the console_lock to stop console printing and
* guarantee safe access to console->seq.
*/
console_lock();

/*
* Flush all consoles and set the console to start at
* the next unprinted sequence number.
*/
if (!console_flush_all(true, &init_seq, &handover)) {
/*
* Flushing failed. Just choose the lowest
* sequence of the enabled boot consoles.
*/

/*
* If there was a handover, this context no
* longer holds the console_lock.
*/
if (handover)
console_lock();

init_seq = prb_next_seq(prb);
for_each_console(con) {
u64 seq;

if (!(con->flags & CON_BOOT) ||
!(con->flags & CON_ENABLED)) {
continue;
}

if (con->flags & CON_NBCON)
seq = nbcon_seq_read(con);
else
seq = con->seq;

if (seq < init_seq)
init_seq = seq;
}
}

console_unlock();
}
}

return init_seq;
}

register_console 注册控制台 unregister_console_locked 注销控制台

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
static int unregister_console_locked(struct console *console);

/*
* 控制台驱动程序在内核初始化期间调用此例程,以使用 printk() 注册控制台打印过程,并打印在初始化控制台驱动程序之前由内核打印的任何消息。
*
* 这可能发生在启动过程的早期(因为early_printk) - 有时在 setup_arch() 完成之前 - 小心使用的内核功能 - 它们可能还没有初始化。
*
* 有两种类型的控制台 - bootconsoles (early_printk) 和 “real” consoles(所有不是 bootconsole 的东西),它们的处理方式不同。
* - 任何时候都可以注册任意数量的引导控制台。
* - 一旦注册了“真实”控制台,所有引导控制台将自动取消注册。
* - 一旦注册了 “真实” 控制台,任何注册 bootconsole 的尝试都将被拒绝
*/
void register_console(struct console *newcon)
{
bool use_device_lock = (newcon->flags & CON_NBCON) && newcon->write_atomic;
bool bootcon_registered = false;
bool realcon_registered = false;
struct console *con;
unsigned long flags;
u64 init_seq;
int err;

console_list_lock();

for_each_console(con) { //遍历控制台列表
if (WARN(con == newcon, "console '%s%d' already registered\n",
con->name, con->index)) {
goto unlock;
}

if (con->flags & CON_BOOT)
bootcon_registered = true;
else
realcon_registered = true;
}

/* 当已经有真正的引导控制台时,不要注册引导控制台。*/
if ((newcon->flags & CON_BOOT) && realcon_registered) {
pr_info("Too late to register bootconsole %s%d\n",
newcon->name, newcon->index);
goto unlock;
}

if (newcon->flags & CON_NBCON) {
/*
* Ensure the nbcon console buffers can be allocated
* before modifying any global data.
*/
if (!nbcon_alloc(newcon))
goto unlock;
}

/*
* 看看我们是否要默认启用这个控制台驱动程序。
*
* 当命令行、设备树或 SPCR 首选控制台时,不会。
*
* 第一个具有 tty 绑定(驱动程序)的真正控制台获胜。在找到合适的控制台之前,可能会启用更多控制台。
*
* 请注意,具有 tty 绑定的主机将设置 CON_CONSDEV 标志,并且将排在列表中的第一个。
*/
if (preferred_console < 0) {
//链表为空或者第一个控制台不是设备或者第一个控制台是引导控制台
if (hlist_empty(&console_list) || !console_first()->device ||
console_first()->flags & CON_BOOT) {
try_enable_default_console(newcon);
}
}

/* 查看此控制台是否与我们在命令行上选择的控制台匹配 */
err = try_enable_preferred_console(newcon, true);

/* 如果没有,请尝试与平台默认值匹配 */
if (err == -ENOENT)
err = try_enable_preferred_console(newcon, false);

/* printk() 消息不会打印到盲文控制台。 */
if (err || newcon->flags & CON_BRL) {
if (newcon->flags & CON_NBCON)
nbcon_free(newcon);
goto unlock;
}

/*
* 如果我们有一个 bootconsole,并且要切换到真正的控制台,请不要再次打印所有内容,
* 因为当 boot console 和真正的 console 是相同的物理设备时,看到两次开始的 boot 消息很烦人
*/
if (bootcon_registered &&
//控制台不是引导控制台,且具有设备绑定
((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) {
newcon->flags &= ~CON_PRINTBUFFER;
}

newcon->dropped = 0;
init_seq = get_init_console_seq(newcon, bootcon_registered);

if (newcon->flags & CON_NBCON) {
have_nbcon_console = true;
nbcon_seq_force(newcon, init_seq);
} else {
have_legacy_console = true;
newcon->seq = init_seq;
}

if (newcon->flags & CON_BOOT)
have_boot_console = true;

/*
* If another context is actively using the hardware of this new
* console, it will not be aware of the nbcon synchronization. This
* is a risk that two contexts could access the hardware
* simultaneously if this new console is used for atomic printing
* and the other context is still using the hardware.
*
* Use the driver synchronization to ensure that the hardware is not
* in use while this new console transitions to being registered.
*/
if (use_device_lock)
newcon->device_lock(newcon, &flags);

/*
* 将此控制台放在列表中 - 将首选驱动程序放在列表的开头。
*/
if (hlist_empty(&console_list)) {
/* 确保始终为头部设置 CON_CONSDEV. */
newcon->flags |= CON_CONSDEV;
hlist_add_head_rcu(&newcon->node, &console_list);

} else if (newcon->flags & CON_CONSDEV) {
/* Only the new head can have CON_CONSDEV set. */
console_srcu_write_flags(console_first(), console_first()->flags & ~CON_CONSDEV);
hlist_add_head_rcu(&newcon->node, &console_list);

} else {
hlist_add_behind_rcu(&newcon->node, console_list.first);
}

/*
* 无需在此处同步 SRCU!调用方不依赖于非所有上下文都能在 nregister_console() 完成之前看到新控制台。
*/

/* 这个新控制台现已注册. */
if (use_device_lock)
newcon->device_unlock(newcon, flags);

console_sysfs_notify();

/*
* 通过在启用真实控制台后取消注册引导控制台,我们会在所有控制台上收到“console xxx enabled”消息
* - 引导控制台、真实控制台等 - 这是为了确保最终用户知道内核的日志缓冲区中可能有一些内容进入了引导控制台(他们在真实控制台上看不到)
*/
con_printk(KERN_INFO, newcon, "enabled\n"); //打印消息日志
if (bootcon_registered &&
//控制台不是引导控制台,且具有设备绑定
((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&
!keep_bootcon) {
struct hlist_node *tmp;

hlist_for_each_entry_safe(con, tmp, &console_list, node) {
if (con->flags & CON_BOOT)
unregister_console_locked(con);
}
}

/* 已更改控制台列表,可能需要启动/停止打印机线程. */
printk_kthreads_check_locked();
unlock:
console_list_unlock();
}
EXPORT_SYMBOL(register_console);

/* Must be called under console_list_lock(). */
static int unregister_console_locked(struct console *console)
{
bool use_device_lock = (console->flags & CON_NBCON) && console->write_atomic;
bool found_legacy_con = false;
bool found_nbcon_con = false;
bool found_boot_con = false;
unsigned long flags;
struct console *c;
int res;

lockdep_assert_console_list_lock_held();

con_printk(KERN_INFO, console, "disabled\n");
//A11Y_BRAILLE_CONSOLE 盲文控制台支持
res = _braille_unregister_console(console);
if (res < 0)
return res;
if (res > 0)
return 0;

if (!console_is_registered_locked(console))
res = -ENODEV;
else if (console_is_usable(console, console->flags, true)) //检查控制台是否可用
__pr_flush(console, 1000, true); //等待打印完成

/* 无条件禁用它*/
console_srcu_write_flags(console, console->flags & ~CON_ENABLED); //无锁读取可能已注册控制台的标志

if (res < 0)
return res;

/*
* Use the driver synchronization to ensure that the hardware is not
* in use while this console transitions to being unregistered.
*/
if (use_device_lock)
console->device_lock(console, &flags);

hlist_del_init_rcu(&console->node); //从链表上删除当前节点

if (use_device_lock)
console->device_unlock(console, flags);

/*
* <遗留>
* 如果这不是最后一个主机,并且已设置CON_CONSDEV,我们需要在下一个首选主机上进行设置。
* </遗留>
*
* 以上内容毫无意义,因为不能保证下一个控制台连接了任何设备。哦,好吧......
*/
if (!hlist_empty(&console_list) && console->flags & CON_CONSDEV)
console_srcu_write_flags(console_first(), console_first()->flags | CON_CONSDEV);

/*
* 确保所有 SRCU 列表遍历都已完成。所有上下文都必须无法在列表中看到此控制台,以便可以安全地执行任何退出/清理例程。
*/
synchronize_srcu(&console_srcu);

if (console->flags & CON_NBCON)
nbcon_free(console);

console_sysfs_notify();

if (console->exit)
res = console->exit(console);

/*
* 此主机消失后,跟踪已注册主机类型的全局标记可能已更改。更新它们。
*/
for_each_console(c) {
if (c->flags & CON_BOOT)
found_boot_con = true;

if (c->flags & CON_NBCON)
found_nbcon_con = true;
else
found_legacy_con = true;
}
if (!found_boot_con)
have_boot_console = found_boot_con;
if (!found_legacy_con)
have_legacy_console = found_legacy_con;
if (!found_nbcon_con)
have_nbcon_console = found_nbcon_con;

/* 更改了控制台列表,可能需要启动/停止打印机线程。 */
printk_kthreads_check_locked();
}

int unregister_console(struct console *console)
{
int res;

console_list_lock();
res = unregister_console_locked(console);
console_list_unlock();
return res;
}
EXPORT_SYMBOL(unregister_console);

setup_log_buf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void __init setup_log_buf(int early)
{
struct printk_info *new_infos;
unsigned int new_descs_count;
struct prb_desc *new_descs;
struct printk_info info;
struct printk_record r;
unsigned int text_size;
size_t new_descs_size;
size_t new_infos_size;
unsigned long flags;
char *new_log_buf;
unsigned int free;
u64 seq;

/*
*一些 arch 多次调用 setup_log_buf() - 第一次是非常早期的,例如从 setup_arch() 开始,第二次 - 当 percpu_areas 初始化时。
*/
if (!early)
set_percpu_data_ready(); //__printk_percpu_data_ready = true;

if (log_buf != __log_buf)
return;

if (!early && !new_log_buf_len)
log_buf_add_cpu();

if (!new_log_buf_len) {
/* Show the memory stats only once. */
if (!early)
goto out;

return;
}

out:
print_log_buf_usage_stats();
}

console_init

1
2
3
               0x00000000c02a34bc                __con_initcall_start = .
*(.con_initcall.init)
0x00000000c02a34bc __con_initcall_end = .
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* 该函数在系统启动的早期阶段被调用,此时内核的许多子系统尚未完全初始化。
* 因此,console_init 主要负责基础的控制台初始化,以便后续的启动过程能够输出调试信息或错误消息
*/
void __init console_init(void)
{
int ret;
initcall_t call;
initcall_entry_t *ce;

/* 初始化默认的 TTY(终端)行规程.
* 行规程是 TTY 子系统的一部分,用于处理终端输入和输出的基本行为,例如回显字符、行编辑等 */
n_tty_init();

/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
ce = __con_initcall_start;
trace_initcall_level("console");
while (ce < __con_initcall_end) {
call = initcall_from_entry(ce);
trace_initcall_start(call);
ret = call();
trace_initcall_finish(call, ret);
ce++;
}
}

printk_set_kthreads_ready

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
/*
* printk_kthreads_check_locked - 检查并适当地启动nbcon kthreads
* @...
* 必须在持有 console_list_lock() 的情况下调用。
*/
static void printk_kthreads_check_locked(void)
{
/* ... 省略变量定义 ... */

/* 确保锁已被持有,这是一个调试断言。*/
lockdep_assert_console_list_lock_held();

/*
* **门控条件1**: 如果系统还未准备好启动kthreads,直接返回。
* printk_kthreads_ready 这个全局标志由下面的 printk_set_kthreads_ready 设置。
*/
if (!printk_kthreads_ready)
return;

/*
* **逻辑块1: 处理传统(Legacy)控制台线程**
* 如果当前系统中有任何传统控制台或启动控制台...
*/
if (have_legacy_console || have_boot_console) {
/*
* ...并且我们还没有创建传统控制台的专有kthread,
* 并且系统强制要求为传统控制台创建一个kthread(force_legacy_kthread),
* 并且创建这个线程失败了(legacy_kthread_create返回假)...
*/
if (!printk_legacy_kthread &&
force_legacy_kthread() &&
!legacy_kthread_create()) {
/*
* **灾难性失败处理**:如果连为传统控制台创建kthread都失败了,
* 说明系统处于一种不健康的状态。为了安全,必须注销掉所有
* 传统的控制台,因为它们无法再以非阻塞方式工作。
* nbcon控制台不受影响,它们有自己的线程。
*/
hlist_for_each_entry_safe(con, tmp, &console_list, node) {
if (con->flags & CON_NBCON)
continue;
unregister_console_locked(con);
}
}
} else if (printk_legacy_kthread) {
/*
* **清理逻辑**: 如果系统中已经没有任何传统或启动控制台了,
* 但我们之前创建的那个传统kthread还在运行,现在就停止并清理它。
*/
kthread_stop(printk_legacy_kthread);
printk_legacy_kthread = NULL;
}

/*
* **门控条件2: 等待启动控制台退出**
* 只要系统中还有任何启动控制台(have_boot_console),或者一个nbcon都还没注册,
* 我们就绝对不能启动打印线程。这是因为无法同步启动代码和常规驱动
* 对硬件的访问。
*/
if (have_boot_console || !have_nbcon_console) {
/* 如果所有nbcon都注销了,要确保清理这个标志。*/
printk_kthreads_running = false;
return;
}

/* 如果线程已经都在运行了,就没必要再做了。*/
if (printk_kthreads_running)
return;

/*
* **逻辑块2: 为所有nbcon控制台创建线程**
* 走到这里,说明所有条件都已满足:系统准备好了,启动控制台已退场,
* 并且至少有一个nbcon控制台存在。
* 现在遍历所有控制台,为每一个标记为CON_NBCON的控制台创建其专属的打印线程。
*/
hlist_for_each_entry_safe(con, tmp, &console_list, node) {
if (!(con->flags & CON_NBCON))
continue;

/* 如果为某个nbcon创建线程失败,就注销掉这个无法工作的控制台。*/
if (!nbcon_kthread_create(con))
unregister_console_locked(con);
}

/* 所有线程创建完毕,设置全局标志,表示我们现在处于异步打印模式。*/
printk_kthreads_running = true;
}

/*
* printk_set_kthreads_ready - 在启动时设置kthreads已准备好的标志
*/
static int __init printk_set_kthreads_ready(void)
{
/*
* 注册一个系统核心操作集(syscore_ops)。这通常用于系统挂起(suspend)和
* 恢复(resume)时的回调。在这里,它确保了在系统休眠前,
* printk相关的状态可以被正确地挂起,在恢复后又能被正确地恢复。
*/
register_syscore_ops(&printk_syscore_ops);

/* 获取保护全局控制台列表的锁。*/
console_list_lock();

/* **核心动作**: 设置全局标志,表示系统环境已准备好,可以启动kthreads了。*/
printk_kthreads_ready = true;

/*
* 立刻调用一次状态机引擎。因为现在 ready 标志刚被设置,
* 这是一个检查状态并可能启动线程的第一个机会。
*/
printk_kthreads_check_locked();

/* 释放锁。*/
console_list_unlock();

return 0; // 初始化成功
}
/* 确保这个初始化函数在内核启动早期被调用。*/
early_initcall(printk_set_kthreads_ready);

pr_flush: 同步并等待内核日志输出

此函数 (pr_flush) 及其核心实现 (__pr_flush) 提供了一个关键的同步机制, 用于确保所有先前由printk产生的内核日志消息, 已经被系统中所有可用的控制台(console)驱动程序实际处理并发送出去

printk本身是一个非常快速的操作, 它只是将日志数据放入一个内核环形缓冲区(printk ring buffer)中。而真正将这些数据输出到物理设备(如UART串口、显示器、网络控制台)的任务, 是由独立的控制台驱动在后台异步完成的。此函数的核心原理就是作为一个同步点, 阻塞当前的执行流程, 直到所有控制台驱动的处理进度都”追上”了调用pr_flush时的日志缓冲区的位置

这在系统即将关机或崩溃的时刻至关重要。例如, 在kernel_power_off中调用pr_flush, 是为了保证在物理断电前, 像”Powering down…”这样的最后一条日志消息以及任何相关的错误信息, 能够被真实地从UART等接口发送出去, 而不是永远地丢失在内存缓冲区中, 这对于事后调试至关重要。


__pr_flush: 核心实现

此函数包含了等待所有控制台追上进度的核心循环逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/*
* 如果指定了@con, 则只等待那个控制台. 否则等待所有控制台.
* @timeout_ms: 超时时间(毫秒).
* @reset_on_progress: 如果看到有进展, 是否重置超时.
*/
static bool __pr_flush(struct console *con, int timeout_ms, bool reset_on_progress)
{
/* 将毫秒超时转换为内核内部的jiffies计数. */
unsigned long timeout_jiffies = msecs_to_jiffies(timeout_ms);
unsigned long remaining_jiffies = timeout_jiffies;
struct console_flush_type ft;
struct console *c;
u64 last_diff = 0;
u64 printk_seq;
short flags;
int cookie;
u64 diff;
u64 seq;

/* 保护性检查: 在调度器运行之前, 无法睡眠, 故此函数无法工作. */
if (system_state < SYSTEM_SCHEDULING)
return false;

/* 静态检查注解, 表明此函数可能会睡眠. */
might_sleep();

/*
* 获取"目标序列号". 这是printk环形缓冲区中下一条记录的序列号.
* 我们的目标就是等待所有控制台都处理完序列号小于此值的记录.
*/
seq = prb_next_reserve_seq(prb);

/*
* 强制触发一次所有控制台的刷新, 以确保它们开始处理挂起的日志.
* 这段代码处理了现代非阻塞控制台(nbcon)和传统控制台的不同刷新机制.
*/
printk_get_console_flush_type(&ft);
if (ft.nbcon_atomic)
nbcon_atomic_flush_pending();
if (ft.legacy_direct) {
console_lock();
console_unlock();
}

/* 进入主等待循环. */
for (;;) {
unsigned long begin_jiffies;
unsigned long slept_jiffies;

/* 在每轮循环开始时, 重置"差距"计数器. */
diff = 0;

/*
* 获取全局控制台锁, 以安全地访问全局控制台列表和每个控制台的seq成员.
* 在单核抢占式系统上, 这能防止在遍历时被其他任务抢占, 保证数据一致性.
*/
console_lock();

cookie = console_srcu_read_lock();
/* 使用SRC_U安全地遍历所有已注册的控制台. */
for_each_console_srcu(c) {
/* 如果指定了单个控制台, 则跳过其他所有控制台. */
if (con && con != c)
continue;

flags = console_srcu_read_flags(c);

/*
* 如果控制台当前不可用(例如, 未完全初始化或已关闭),
* 我们不能期望它能取得进展, 所以在计算差距时跳过它.
*/
if (!console_is_usable(c, flags, true) &&
!console_is_usable(c, flags, false)) {
continue;
}

/* 读取每个控制台当前已经处理到的日志序列号. */
if (flags & CON_NBCON) {
printk_seq = nbcon_seq_read(c);
} else {
printk_seq = c->seq;
}

/*
* 如果控制台的进度落后于我们的目标序列号,
* 则将差距累加到总差距diff中.
*/
if (printk_seq < seq)
diff += seq - printk_seq;
}
console_srcu_read_unlock(cookie);

/*
* 如果总差距相比上一轮减小了(意味着有控制台取得了进展),
* 并且调用者要求了, 那么就重置超时计时器.
* 这对于慢速控制台很有用, 只要它在工作, 我们就愿意等.
*/
if (diff != last_diff && reset_on_progress)
remaining_jiffies = timeout_jiffies;

/* 释放控制台锁. */
console_unlock();

/*
* 循环退出条件:
* 1. diff == 0: 所有控制台都已追上, 任务完成.
* 2. remaining_jiffies == 0: 超时.
*/
if (diff == 0 || remaining_jiffies == 0)
break;

/*
* 短暂睡眠(1毫秒), 让出CPU, 给控制台驱动的后台任务或中断有机会执行,
* 以便它们能继续处理日志并取得进展.
*/
begin_jiffies = jiffies;
msleep(1);
slept_jiffies = jiffies - begin_jiffies; /* 计算实际睡眠的时间. */

/* 从剩余时间中减去实际睡眠的时间. */
remaining_jiffies -= min(slept_jiffies, remaining_jiffies);

/* 更新上一轮的差距值, 用于下一轮的进展判断. */
last_diff = diff;
}

/* 如果循环退出时总差距为0, 说明所有日志都已刷出, 返回true. 否则返回false. */
return (diff == 0);
}

pr_flush: 便捷的API封装

这是一个上层的、导出的API函数, 它为内核其他部分的调用者提供了一个更简洁的接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* pr_flush() - 等待打印线程追上进度.
*
* @timeout_ms: 等待的最大时间(毫秒).
* @reset_on_progress: 如果看到有进展, 是否重置超时.
*
* timeout_ms为0表示不等待. -1表示无限等待.
*
* 如果@reset_on_progress为true, 只要任何打印机被看到取得了进展,
* 超时就会被重置.
*
* 上下文: 进程上下文. 在获取控制台锁时可能会睡眠.
* 返回: 如果所有可用的打印机都已追上, 返回true.
*/
bool pr_flush(int timeout_ms, bool reset_on_progress)
{
/*
* 这是一个简单的封装, 它调用核心实现函数 __pr_flush,
* 并为第一个参数(要等待的特定控制台)传递 NULL, 表示等待所有控制台.
*/
return __pr_flush(NULL, timeout_ms, reset_on_progress);
}

console_prepend_message: 在日志消息前插入文本

此代码片段展示了Linux内核printk日志系统中一个非常实用的内部工具函数: console_prepend_message。它的核心原理是提供一个安全、高效的方法, 在一段已经存在的日志消息字符串之前, 插入一个新的、格式化的前缀字符串

这个函数是实现printk高级功能的基石, 例如打印”[… dropped messages …]”或”[replay]”等提示信息。它通过一个巧妙的内存操作序列来完成工作, 而不是代价高昂的字符串拼接。


console_prepend_message: 核心实现

工作流程与原理:

  1. 格式化前缀: 函数首先像printf一样工作。它接受一个格式化字符串fmt和可变参数..., 并使用vscnprintf将格式化后的前缀字符串生成到一个临时的”草稿缓冲区”(scratchbuf)中。vscnprintfvsprintf的安全版本, 它能防止缓冲区溢出。

  2. 空间检查与截断: 这是保证缓冲区安全的关键步骤。

    • 它首先检查前缀的长度len加上一个最大的可能前缀(用于应对极端情况)是否会超出整个输出缓冲区outbuf的大小。如果会, 这是一个严重的配置错误, 函数会打印一个警告并直接返回。
    • 接着, 它检查现有消息的长度pmsg->outbuf_len加上前缀的长度len是否会超出输出缓冲区。如果会, 这意味着没有足够的空间同时容纳前缀和完整的原始消息。在这种情况下, 它会截断(truncate)原始消息, 从尾部缩短它, 以便为前缀腾出空间。它确保了在截断后, 原始消息仍然是以\0结尾的合法字符串。
  3. 内存移动 (memmove): 这是整个函数最高效、最核心的操作。它调用memmove, 将outbuf中现有的整个日志消息(包括其结尾的\0)向后移动len个字节memmove是一个可以安全处理源和目标内存区域重叠的内存复制函数, 这正是这里所需的情景。执行后, outbuf的开头就空出了len个字节的”空隙”。

  4. 前缀拷贝 (memcpy): 最后, 它调用memcpy将之前在scratchbuf中生成的前缀字符串, 拷贝到outbuf开头刚刚腾出的”空隙”中。

  5. 更新长度: 它将前缀的长度len加到pmsg->outbuf_len上, 以正确反映新的、更长的消息的总长度。

通过这种”移动-拷贝”的方式, console_prepend_message避免了需要分配新内存或进行多次字符串操作的传统拼接方法, 实现了非常高效的字符串前插功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/*
* 在 @pmsg->pbufs->outbuf 中的消息前插入文本. 这是通过移动现有消息,
* 然后插入 scratchbuf 中的消息来实现的.
*
* @pmsg 是原始的 printk 消息.
* @fmt 是将要前插的消息的 printf 格式.
*
* 如果 @pmsg->pbufs->outbuf 中空间不足, 现有消息文本将被充分截断.
*
* 如果 @pmsg->pbufs->outbuf 被修改, @pmsg->outbuf_len 会被更新.
*/
__printf(2, 3) /* GCC属性, 告知编译器此函数类似printf, 会检查格式化字符串和参数的匹配性. */
static void console_prepend_message(struct printk_message *pmsg, const char *fmt, ...)
{
/* 获取指向各个缓冲区的指针和它们的大小. */
struct printk_buffers *pbufs = pmsg->pbufs;
const size_t scratchbuf_sz = sizeof(pbufs->scratchbuf);
const size_t outbuf_sz = sizeof(pbufs->outbuf);
char *scratchbuf = &pbufs->scratchbuf[0];
char *outbuf = &pbufs->outbuf[0];
va_list args;
size_t len;

/* 步骤1: 使用可变参数, 将要前插的前缀格式化到 scratchbuf 中. */
va_start(args, fmt);
len = vscnprintf(scratchbuf, scratchbuf_sz, fmt, args);
va_end(args);

/*
* 步骤2: 空间检查与截断.
* 这是一个健全性检查, 确保缓冲区至少能容纳前缀和一个最大的标准前缀.
*/
if (WARN_ON_ONCE(len + PRINTK_PREFIX_MAX >= outbuf_sz))
return;

/* 如果(原始消息 + 前缀)的总长度会超出缓冲区. */
if (pmsg->outbuf_len + len >= outbuf_sz) {
/* 截断原始消息, 为前缀和结尾的'\0'留出空间. */
pmsg->outbuf_len = outbuf_sz - (len + 1);
outbuf[pmsg->outbuf_len] = 0;
}

/*
* 步骤3: 内存移动. 将原始消息整体向后移动 len 个字节.
* pmsg->outbuf_len + 1 包括了结尾的'\0'.
*/
memmove(outbuf + len, outbuf, pmsg->outbuf_len + 1);
/* 步骤4: 前缀拷贝. 将 scratchbuf 中的前缀拷贝到 outbuf 开头的空隙中. */
memcpy(outbuf, scratchbuf, len);
/* 步骤5: 更新总长度. */
pmsg->outbuf_len += len;
}

console_prepend_dropped: 便捷的API封装

这是一个上层的、具体的应用函数。它使用console_prepend_message来生成一条关于丢弃消息的特定前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
* 在 @pmsg->pbufs->outbuf 中的消息前插入一条"丢弃消息"的提示.
* @pmsg->outbuf_len 会被相应地更新.
*
* @pmsg 是要被前插的printk消息.
* @dropped 是要在丢弃消息中报告的丢弃计数.
*/
void console_prepend_dropped(struct printk_message *pmsg, unsigned long dropped)
{
/*
* 调用核心前插函数, 并提供一个具体的格式化字符串和参数.
* 这会生成类似 "** 42 printk messages dropped **\n" 的字符串,
* 并将其插入到 pmsg 的消息之前.
*/
console_prepend_message(pmsg, "** %lu printk messages dropped **\n", dropped);
}

printk 日志文本格式化与前缀注入

此代码片段展示了Linux内核printk日志系统中负责最终文本格式化的两个核心内部函数。它们的作用是将从环形缓冲区中取出的、”原始”的日志记录, 转换为最终将在控制台上显示的、带有时间戳和日志级别等前缀信息的、并且正确处理了多行消息的字符串。

record_print_text是这个过程的”引擎”, 而info_print_prefix是它的”零件供应商”。


info_print_prefix: 生成单行前缀

此函数是一个简单、专一的辅助函数。它的原理是根据传入的参数, 将日志记录的元数据(metadata)格式化成一个标准的前缀字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* info_print_prefix: 生成单行的日志前缀. */
static size_t info_print_prefix(const struct printk_info *info, bool syslog,
bool time, char *buf)
{
size_t len = 0;

/* 如果需要syslog格式, 添加 <facility|level> 前缀, 如 "<6>". */
if (syslog)
len = print_syslog((info->facility << 3) | info->level, buf);

/* 如果需要时间戳, 添加 "[ 123.456789] " 格式的时间戳. */
if (time)
len += print_time(info->ts_nsec, buf + len);

/* 添加调用者信息(如果配置了). */
len += print_caller(info->caller_id, buf + len);

/* 如果添加了任何前缀信息, 确保后面有一个空格作为分隔. */
if (IS_ENABLED(CONFIG_PRINTK_CALLER) || time) {
buf[len++] = ' ';
buf[len] = '\0';
}

return len; /* 返回生成的前缀的总长度. */
}

record_print_text: 核心的”原地”文本格式化引擎

这是整个格式化流程中最复杂、最精妙的部分。它的核心原理是在一个单一的缓冲区内, 通过一系列高效的memmove操作, 实现为多行日志的每一行都正确地注入前缀, 同时优雅地处理缓冲区空间不足时的截断问题。这种”原地”(in-place)操作避免了为格式化分配额外内存的开销, 这在资源受限的内核环境中至关重要。

工作流程与原理:

  1. 生成标准前缀: 函数首先调用info_print_prefix生成一个本次日志所有行都将使用的标准前缀(如"<6>[ 123.456789] "), 并将其存储在一个临时的栈上缓冲区prefix中。

  2. 进入多行处理循环 (for(;;)): 函数的核心是一个循环, 它将日志文本视为一个或多个由\n分隔的行。

    • 查找换行符: next = memchr(text, '\n', text_len); 它在剩余的文本中查找下一个换行符, 以确定当前行的边界和长度(line_len)。
    • 空间检查与截断: 在处理每一行之前, 它都会进行一次精确的空间计算, 检查”已格式化长度 + 前缀长度 + 剩余文本长度 + 结尾换行符 + 结尾\0”是否会超出缓冲区。
      • 如果会, 它会截断(truncate)剩余文本的长度(text_len), 仅保留足以容纳当前行和前缀的空间。truncated标志被设置, 循环将在处理完这最后一行后终止。
      • 这个检查保证了缓冲区绝对不会溢出。
    • “原地”前缀注入 (memmove/memcpy): 这是最关键的算法。
      a. memmove(text + prefix_len, text, text_len); : 它将整个剩余的文本(包括多行)向后移动prefix_len个字节, 在当前行的开头腾出一个与前缀等长的”空隙”。
      b. memcpy(text, prefix, prefix_len); : 它将之前生成的标准前缀拷贝到这个”空隙”中。
      • 经过这两步, 当前行就已经被成功地加上了前缀。
    • 更新指针和长度: 函数会更新已格式化总长度len, 并将text指针移动到下一行的起始位置, 同时从text_len中减去已处理的长度, 准备处理下一行。
  3. 处理结尾: 当循环结束时(无论是正常处理完所有行还是因为截断而提前终止):

    • 添加尾部换行符: vprintk_store在存储日志时会去掉用户输入的最后一个\n, 以便内部处理。此函数负责在所有行都被处理完后, 将这个\n重新添加回来, 保证日志输出的格式正确。
    • 添加字符串终结符: 最后, 它在已格式化文本的末尾写入一个\0, 确保它是一个合法的C字符串。

在STM32H750上的意义:

这个函数对于确保在STM32的串口或其他控制台上看到的日志格式正确、可读、且信息完整至关重要。

  • 时间戳和日志级别: 开发者通过配置内核, 可以选择是否显示时间戳和syslog级别。这个函数就是将这些配置转化为实际输出的地方。在调试实时性问题时, 精确到纳秒的时间戳(info->ts_nsec)是无价的。
  • 处理多行日志: 嵌入式开发中, 经常需要打印包含多行的数据结构或状态信息。这个函数保证了即使是多行消息, 每一行的开头都会有正确的前缀, 保持了日志的可读性。
  • 内存效率: “原地”操作的算法设计, 对于像STM32这样内存资源相对宝贵的MCU来说, 避免了不必要的动态内存分配, 降低了内存碎片风险, 提高了系统的稳定性和效率。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
static size_t record_print_text(struct printk_record *r, bool syslog,
bool time)
{
size_t text_len = r->info->text_len;
size_t buf_size = r->text_buf_size;
char *text = r->text_buf;
char prefix[PRINTK_PREFIX_MAX]; // 栈上临时缓冲区, 用于存放前缀
bool truncated = false;
size_t prefix_len;
size_t line_len;
size_t len = 0; // 已格式化的总长度
char *next;

/* 如果原始消息因缓冲区不足而被截断, 则只处理可用的部分. */
if (text_len > buf_size)
text_len = buf_size;

/* 步骤1: 生成本次日志所有行共用的标准前缀. */
prefix_len = info_print_prefix(r->info, syslog, time, prefix);

/* 步骤2: 进入多行处理循环. */
for (;;) {
/* 查找下一行. */
next = memchr(text, '\n', text_len);
if (next) {
line_len = next - text; // 当前行长度 (不含\n)
} else {
if (truncated) /* 如果之前已发生截断, 不再处理最后不完整的行. */
break;
line_len = text_len;
}

/* 空间检查与截断. */
if (len + prefix_len + text_len + 1 + 1 > buf_size) {
if (len + prefix_len + line_len + 1 + 1 > buf_size)
break; /* 连当前行都放不下了, 放弃. */

text_len = buf_size - len - prefix_len - 1 - 1;
truncated = true;
}

/* "原地"前缀注入. */
memmove(text + prefix_len, text, text_len); // 向后移动整个剩余文本.
memcpy(text, prefix, prefix_len); // 在开头插入前缀.

/* 更新已格式化长度. */
len += prefix_len + line_len + 1;
if (text_len == line_len) {
/* 这是最后一行, 添加被vprintk_store移除的尾部换行符. */
text[prefix_len + line_len] = '\n';
break;
}

/* 更新指针和长度, 准备处理下一行. */
text += prefix_len + line_len + 1;
text_len -= line_len + 1;
}

/* 步骤3: 确保字符串以'\0'结尾. */
if (buf_size > 0)
r->text_buf[len] = 0;

return len; /* 返回最终格式化后的字符串长度. */
}