printk内核框架简介

printk是内核用于输出调试信息的接口,这些调试信息可以帮助我们获取系统或程序中的一些关键信息,在系统出现问题后也能第一时间将错误信息记录下来,以帮助开发人员定位问题原因。

而对于稳定性同学经常接触到的,内核oops或者panic时的fulldump,从这些dump中导出内核日志就尤为的重要。在高通平台可以使用Linux ramdump parser解析出来。就是为了实现以上这些需求,内核设计了如下图的printk模块

 从上图可看出,其核心是一个叫做log buffer的循环缓冲区,printk作为生产者将消息存入该缓冲区,右边的log服务模块作为消费者可从log buffer中读取消息。这样设计有以下几个优点:

  1. 控制台和日志模块初始化前,内核的启动日志可以暂存到log buffer中。待它们初始化完成后,再输出相应信息

  2. 在printk log输出太快,而log服务的处理速度不足时,防止log信息丢失

  3. 将log输入模块和log输出模块解耦,增加设计的灵活性

在log输入模块中,除了printk外还有devkmsg_write接口可以向log buffer写入消息。该接口通过/dev/kmsg设备节点导出,可以在用户态通过echo命令将信息重定向到该设备节点的方式,将消息写入log buffer中。

  在log输出模块中,可以通过/dev/kmsg、/proc/kmsg、klogd读取log信息。也可以将串口、网络等注册为console,通过它们输出log。pstoremtdoops机制在系统发生oops或panic时,可以将log buffer中的log保存到指定的设备中,如块设备、内存等。

节我们可以很清晰的了解到内核printk的框架,输入和输出的模型。而其中连接两者的就是这个log buffer

在 Linux 内核中,printk() 是打印内核日志的核心函数。在 ARM64 架构下,其最终会将日志写入 printk_ringbuffer(新版)或 log_buf(旧版)缓冲区。

注意:如果本篇文章没有特别说明,则默认log_buffer为printk_ringbuffer。我们也会以kernel-6.1代码中的源码进行讲解。

log buffer的初始化

在内核启动初期devicetree解析之前,系统的内存布局信息和cpu核个数信息都是未知的,它带来以下两个问题:

  1. 内存信息被解析之前,系统只映射了内核镜像所在位置内存的虚拟地址,memblock和buddy都没有初始化,因此此时无法通过动态方式分配内存。为了支持printk,此时内核可以通过全局变量方式定义一个log buffer(全局变量被定义在数据段中,会在内核镜像映射过程中被映射),其定义如下:

    /* 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;
    static u32 log_buf_len = __LOG_BUF_LEN;
  2. cpu信息被解析之前,系统无法知道smp系统的cpu数量,因此也无法计算需要增加的log buffer size,因此需要在cpu init之后再调整log buffer的size。当smp系统的cpu数量为n时,其调整公式如下图:

log_buffer的数据结构

由图可见,其主要由以下三部分组成

struct printk_ringbuffer

/*
 * The high level structure representing the printk ringbuffer.
 *
 * @fail: Count of failed prb_reserve() calls where not even a data-less
 *        record was created.
 */
struct printk_ringbuffer {
	struct prb_desc_ring	desc_ring;
	struct prb_data_ring	text_data_ring;
	atomic_long_t		fail;
};

log buffer最顶层的数据结构

功能

  • 整合描述符环和数据环:协调元数据和文本数据的存储。

  • 统计失败操作:记录因缓冲区满等原因导致的日志丢失情况。

关键成员解析

成员

类型

作用

desc_ring

struct prb_desc_ring

管理日志元数据的描述符环。

text_data_ring

struct prb_data_ring

存储日志文本内容的数据环。

fail

atomic_long_t

统计 prb_reserve() 失败的次数(甚至无法创建无数据记录的情况)。

struct prb_desc_ring

/* A ringbuffer of "struct prb_desc" elements. */
struct prb_desc_ring {
	unsigned int		count_bits;
	struct prb_desc		*descs;
	struct printk_info	*infos;
	atomic_long_t		head_id;
	atomic_long_t		tail_id;
	atomic_long_t		last_finalized_id;
};

描述符管理环

功能

  • 管理日志元数据:每个描述符对应一条日志记录,存储其状态、文本位置等信息。

  • 控制日志生命周期:通过状态机(保留→提交→终结)确保数据一致性。

关键成员解析

成员

类型

作用

count_bits

unsigned int

描述符数量的对数(实际数量为 1 << count_bits)。

descs

struct prb_desc*

描述符数组,每个元素记录日志的状态和文本位置(text_blk_lpos)。

infos

struct printk_info*

日志元信息数组(时间戳、调用者ID、文本长度等),与 descs 一一对应。

head_id

atomic_long_t

消费者指针:读取位置,指向最旧的未处理描述符。

tail_id

atomic_long_t

生产者指针:写入位置,指向下一个可分配的描述符。

last_finalized_id

atomic_long_t

最后一个终结的描述符ID,用于确保数据持久化。

struct prb_data_ring

struct prb_data_ring {
	unsigned int	size_bits;
	char		*data;
	atomic_long_t	head_lpos;
	atomic_long_t	tail_lpos;
};

功能

  • 存储实际日志文本内容:以字节流形式保存内核打印的原始日志消息。

  • 管理数据空间:通过头尾指针实现环形缓冲区,动态分配和回收数据块。

关键成员解析

成员

类型

作用

size_bits

unsigned int

数据环大小的对数(实际大小为 1 << size_bits),支持动态调整容量。

data

char*

指向数据存储区的指针,存储所有日志文本的原始字节流。

head_lpos

atomic_long_t

生产者指针:表示下一个写入位置的逻辑偏移(Logical Position)。

tail_lpos

atomic_long_t

消费者指针:表示已提交数据的最旧位置,用于空间回收。

struct prb_desc

/* Specifies the logical position and span of a data block. */
struct prb_data_blk_lpos {
	unsigned long	begin;
	unsigned long	next;
};

/*
 * A descriptor: the complete meta-data for a record.
 *
 * @state_var: A bitwise combination of descriptor ID and descriptor state.
 */
struct prb_desc {
	atomic_long_t			state_var;
	struct prb_data_blk_lpos	text_blk_lpos;
};

功能

  • 存储单条日志记录的元数据:包括状态、ID 和文本位置信息。

  • 实现无锁同步:通过 state_var 的原子操作管理描述符的生命周期。

成员解析

成员

类型

作用

state_var

atomic_long_t

状态与ID的复合字段
• 高位:描述符状态(RESERVED/COMMITTED/FINALIZED
• 低位:唯一ID(用于避免ABA问题)。

text_blk_lpos

struct prb_data_blk_lpos

文本块位置:指向 prb_data_ring 中存储的日志文本。

state_var 的位域结构

plaintext

| 63...62 | 61...0 |
|---------|--------|
| 状态位   | 描述符ID |
  • 状态位(2 bits):

    • 00:空闲(未使用)

    • 01:保留(RESERVED,数据写入中)

    • 10:已提交(COMMITTED,数据可读)

    • 11:已终结(FINALIZED,数据持久化后)

  • 描述符ID(62 bits):单调递增的唯一标识,防止并发修改冲突。

struct prb_data_blk_lpos结构体的两个成员

成员

类型

作用

begin

unsigned long

逻辑起始位置:标识数据块在 data 数组中的开始偏移量。若最低位为 1,表示数据无效(如空间不足时)。

next

unsigned long

逻辑结束位置:标识数据块的下一个可用位置(即 begin + 数据长度)。

struct printk_info

struct printk_info {
	u64	seq;		/* sequence number */
	u64	ts_nsec;	/* timestamp in nanoseconds */
	u16	text_len;	/* length of text message */
	u8	facility;	/* syslog facility */
	u8	flags:5;	/* internal record flags */
	u8	level:3;	/* syslog level */
	u32	caller_id;	/* thread id or processor id */

	struct dev_printk_info	dev_info;
};

功能

struct printk_info 是 Linux 内核环形缓冲区(printk_ringbuffer)中日志记录的元数据,用于存储每条日志的附加信息(如时间戳、优先级等),与实际日志文本(存储在 prb_data_ring 中)分离,实现高效管理。

成员解析

字段名

类型

作用

seq

u64

序列号:日志的唯一递增标识,用于排序和去重。

ts_nsec

u64

时间戳:日志记录的时间(纳秒精度),通常通过 ktime_get_real_fast_ns() 获取。

text_len

u16

文本长度:关联的日志文本的实际长度(字节数),用于安全读取数据。

facility

u8

设施分类:标识日志来源模块(如 LOG_KERNLOG_USER),参考 syslog.h

flags

u8 (5 bits)

内部标志位
LOG_NEWLINE:文本是否以换行符结尾
LOG_CONT:是否为连续消息的一部分

level

u8 (3 bits)

日志级别:控制输出优先级(如 KERN_EMERGKERN_DEBUG),参考 kern_levels.h

caller_id

u32

调用者标识
• 最高位为 1:表示 CPU ID
• 最高位为 0:表示线程 ID(PID)

dev_info

struct dev_printk_info

设备信息:记录设备相关日志的附加信息(如设备名称、子系统)。

printk函数流程简单分析

当我们通过printk打印日志时,实际上就是通过一系列的流程将日志打印到了printk_ringbuffer中。本小节也将printk 开始,梳理日志保存的流程。

#define printk(fmt, ...) printk_index_wrap(_printk, fmt, ##__VA_ARGS__)

#define printk_index_wrap(_p_func, _fmt, ...)				\
	({								\
		__printk_index_emit(_fmt, NULL, NULL);			\
		_p_func(_fmt, ##__VA_ARGS__);				\
	})

printk 会依次执__printk_index_emit_p_func (也就是_printk)。

__printk_index_emit 的实现受CONFIG_PRINTK_INDEX控制,在我们项目中这个没有被打开!所以该函数空跑,啥都不做。

asmlinkage __visible int _printk(const char *fmt, ...)
{
	va_list args;
	int r;

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

	return r;
}
EXPORT_SYMBOL(_printk);

继续执vprintk

asmlinkage int vprintk(const char *fmt, va_list args)
{
#ifdef CONFIG_KGDB_KDB
	/* Allow to pass printk() to kdb but avoid a recursion. */
	if (unlikely(kdb_trap_printk && kdb_printf_cpu < 0))
		return vkdb_printf(KDB_MSGSRC_PRINTK, fmt, args);
#endif

	/*
	 * Use the main logbuf even in NMI. But avoid calling console
	 * drivers that might have their own locks.
	 */
	if (this_cpu_read(printk_context) || in_nmi())
		return vprintk_deferred(fmt, args);

	/* No obstacles. */
	return vprintk_default(fmt, args);
}
EXPORT_SYMBOL(vprintk);
  • 检查是否处于特殊上下文:

    • printk_context 为真(表示当前 CPU 正处于 printk 上下文中)

    • in_nmi() 为真(表示当前处于不可屏蔽中断上下文中)

  • 如果满足任一条件,调用 vprintk_deferred 进行延迟打印,因为:

    • 直接打印可能导致死锁(如持有某些锁时又调用 printk)

    • NMI 上下文需要特别小心处理

默认情况下vprintk_default

int vprintk_default(const char *fmt, va_list args)
{
	return vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, fmt, args);
}


asmlinkage int vprintk_emit(int facility, int level,
			    const struct dev_printk_info *dev_info,
			    const char *fmt, va_list args)
{
	int printed_len;
	bool in_sched = false;

	/* Suppress unimportant messages after panic happens */
	if (unlikely(suppress_printk))      // 全局禁止打印(例如内核崩溃后避免刷屏)
		return 0;

	if (unlikely(suppress_panic_printk) &&
	    atomic_read(&panic_cpu) != raw_smp_processor_id())      // 在 panic 时,只允许 panic CPU 打印消息,避免其他 CPU 干扰
		return 0;

	if (level == LOGLEVEL_SCHED) {
		level = LOGLEVEL_DEFAULT;
		in_sched = true;         // 标记表示当前在调度上下文中
	}

	printk_delay(level);  // 添加延时,避免消息刷屏导致问题难以追踪

	printed_len = vprintk_store(facility, level, dev_info, fmt, args); // 将消息存入 printk 缓冲区(log_buf)

	/* If called from the scheduler, we can not call up(). */
	if (!in_sched) {
		/*
		 * 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自旋锁
			console_unlock();  // 如果成功获取锁,console输出打印
		preempt_enable();  // 使能调度
	}

	if (in_sched)
		defer_console_output(); // 调用 defer_console_output 延迟控制台输出(避免在调度上下文中直接操作控制台)
	else
		wake_up_klogd(); // 调用 wake_up_klogd 唤醒 klogd 守护进程

	return printed_len;
}

备注:在做android项目时(起码我司自己的项目),在用户版本我们是需要关闭内核的串口输出的,原因就是printk会禁用调度,如果内核存在频繁打印日志的情况,如果不关闭,printk会长时间占用调度,不允许其他进程调度,这样会引起内核调度而造成死机。

根据上述的代码分析,接下去vprintk_store ,这个是printk的核心代码

__printf(4, 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)) // 禁用本地中断(local_irq_save),防止并发问题。
		return 0;  //检查递归调用(避免 printk 内部再调用 printk 导致死锁)

	/*
	 * Since the duration of printk() can vary depending on the message
	 * and state of the ringbuffer, grab the timestamp now so that it is
	 * close to the call of printk(). This provides a more deterministic
	 * timestamp with respect to the caller.
	 */
	ts_nsec = local_clock();   // 高精度时间戳(纳秒级),用于记录消息生成时间

	caller_id = printk_caller_id(); // 标识调用来源(通常为调用者的地址或 CPU ID)

	/*
	 * The sprintf needs to come first since the syslog prefix might be
	 * passed in as a parameter. An extra byte must be reserved so that
	 * later the vscnprintf() into the reserved buffer has room for the
	 * terminating '\0', which is not counted by vsnprintf().
	 */
	va_copy(args2, args);
	reserve_size = vsnprintf(&prefix_buf[0], sizeof(prefix_buf), fmt, args2) + 1;
	va_end(args2);

	if (reserve_size > LOG_LINE_MAX)
		reserve_size = LOG_LINE_MAX;

	/* Extract log level or control flags. */
	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 (flags & LOG_CONT) {
		prb_rec_init_wr(&r, reserve_size);
		if (prb_reserve_in_last(&e, prb, &r, caller_id, LOG_LINE_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_NEWLINE) {
				r.info->flags |= LOG_NEWLINE;
				prb_final_commit(&e);
			} else {
				prb_commit(&e);
			}

			ret = text_len;
			goto out;
		}
	}

	/*
	 * Explicitly initialize the record before every prb_reserve() call.
	 * prb_reserve_in_last() and prb_reserve() purposely invalidate the
	 * structure when they fail.
	 */
	prb_rec_init_wr(&r, reserve_size);
	if (!prb_reserve(&e, prb, &r)) {
		/* truncate the message if it is too long for empty buffer */
		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;
	}

	/* fill message */
	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);
	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));

	/* A message without a trailing newline can be continued. */
	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;
}

这段代码是 Linux 内核 printk 系统的核心存储函数 vprintk_store,负责将格式化后的日志消息存入内核的环形缓冲区(printk ring buffer, prb)。

printk_caller_id获取调用者

static inline u32 printk_caller_id(void)
{
	return in_task() ? task_pid_nr(current) :
		0x80000000 + smp_processor_id();
}
  • 如果 in_task() 为真(进程上下文)
    返回当前进程的 PID(进程ID),通过 task_pid_nr(current) 获取。

    • current 是内核宏,指向当前正在执行的进程的 task_struct

    • task_pid_nr() 提取该进程的 PID(一个正整数)。

  • 如果 in_task() 为假(中断上下文)
    返回一个固定偏移值 0x80000000 加上当前 CPU 的核心编号(通过 smp_processor_id() 获取)。

    • 0x80000000 是一个高位掩码,用于区分 PID 和中断上下文的调用者。

    • smp_processor_id() 返回当前代码运行的 CPU 核心编号(从 0 开始)。

示例:

  1. 进程 PID 1234 调用 printk

    • 返回 1234

  2. CPU 1 的中断处理程序调用 printk

    • 返回 0x80000001

这种机制在内核日志中用于追踪日志来源。

计算消息长度

va_copy(args2, args);
reserve_size = vsnprintf(&prefix_buf[0], sizeof(prefix_buf), fmt, args2) + 1;
va_end(args2);
  • va_copy:复制 va_list(避免修改原参数)。

  • vsnprintf:预计算消息长度(不实际格式化到内存)。

    • +1 为预留终止符 \0

  • 最大长度限制

    c

    if (reserve_size > LOG_LINE_MAX)
        reserve_size = LOG_LINE_MAX;
    • LOG_LINE_MAX 是单条消息的最大长度(通常为 1024 或 2048)。

解析日志级别和标志

if (facility == 0)
    printk_parse_prefix(&prefix_buf[0], &level, &flags);

if (level == LOGLEVEL_DEFAULT)
    level = default_message_loglevel;
  • printk_parse_prefix:从消息前缀(如 <3>KERN_ERR)解析日志级别和标志。

  • 如果未指定级别,使用默认值(default_message_loglevel)。

处理连续消息(LOG_CONT

if (flags & LOG_CONT) {
    prb_rec_init_wr(&r, reserve_size);
    if (prb_reserve_in_last(&e, prb, &r, caller_id, LOG_LINE_MAX)) {
        text_len = printk_sprint(&r.text_buf[r.info->text_len], reserve_size,
                     facility, &flags, fmt, args);
        r.info->text_len += text_len;
        ...
    }
}
  • LOG_CONT:表示消息是前一条的续片(如分多行打印)。

  • prb_reserve_in_last:尝试在上一条记录的剩余空间追加内容。

  • 成功时

    • 调用 printk_sprint 格式化消息到缓冲区。

    • 更新长度字段(r.info->text_len)。

    • 根据 LOG_NEWLINE 决定提交方式:

      • 无换行prb_commit(允许后续追加)。

      • 有换行prb_final_commit(标记消息结束)。

新消息存储(非 LOG_CONT

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;
}
  • prb_reserve:在环形缓冲区中预留空间。

    • 如果失败(缓冲区满),尝试截断消息(保留开头部分)。

  • truncate_msg

    • 缩短 reserve_size(减少消息长度)。

    • 记录截断长度 trunc_msg_len(用于追加截断标记,如 "[truncated]")。

格式化并填充消息

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);
  • printk_sprint:将格式化后的消息写入缓冲区。

  • 如果消息被截断,追加截断标记(如 "[truncated]")。


设置元数据

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));
  • 元数据包括

    • 消息长度、设施、级别、标志。

    • 时间戳、调用者 ID、设备信息(可选)。

提交到环形缓冲区

if (!(flags & LOG_NEWLINE))
    prb_commit(&e);      // 可继续追加
else
    prb_final_commit(&e); // 消息结束
  • LOG_NEWLINE 决定提交方式:

    • 无换行prb_commit(允许后续 LOG_CONT 追加)。

    • 有换行prb_final_commit(标记消息完整)。

清理和返回

ret = text_len + trunc_msg_len;
out:
printk_exit_irqrestore(recursion_ptr, irqflags);
return ret;
  • 恢复中断状态(local_irq_restore)。

  • 返回实际存储的字符数(含截断标记)。

printk ring buffer写入的核心

vprintk_store 函数中,写入 printk ring buffer, prb的核心函数是 prb_reserveprintk_sprint,但更准确地说,prb_reserve 是真正分配缓冲区空间的关键函数,而 printk_sprint 是填充格式化消息的核心函数。

这里还需要注意一点的是:prb的实例,下面的printk ring buffer的函数都是操struct printk_ringbuffer prb这个结构体进行。而这个prb则是一个全局的static变量:

static struct printk_ringbuffer *prb = &printk_rb_static;

而这个结构体,我们是可以通过trace32直接读取的!

函数

作用

prb_reserve / prb_reserve_in_last

分配环形缓冲区空间(写入 log_buf 的物理内存)。

printk_sprint

格式化消息并填充到预留的缓冲区(写入实际内容)。

prb_commit / prb_final_commit

提交消息(标记数据有效,可供消费)。

分配缓冲区空间prb_reserve

prb_rec_init_wr(&r, reserve_size);
if (!prb_reserve(&e, prb, &r)) {
    // 处理缓冲区不足(如截断消息)
    ...
}
  • prb_reserve

    • prb(printk ring buffer)中预留一块连续内存。

    • 如果成功,返回 struct prb_reserved_entry e,包含可写的缓冲区指针(r.text_buf)。

    • 这是写入 log_buf 的第一步,决定了消息存储的物理位置。

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;

	/*
	 * Descriptors in the reserved state act as blockers to all further
	 * reservations once the desc_ring has fully wrapped. Disable
	 * interrupts during the reserve/commit window in order to minimize
	 * the likelihood of this happening.
	 */
	local_irq_save(e->irqflags);   // 保存当前中断状态并禁用中断,防止并发修改缓冲区结构

	if (!desc_reserve(rb, &id)) {   // 原子性地抢占一个空闲描述符ID
		/* Descriptor reservation failures are tracked. */
		atomic_long_inc(&rb->fail);
		local_irq_restore(e->irqflags);
		goto fail;
	}

	d = to_desc(desc_ring, id);   // 根据ID获取对应的描述符(struct prb_desc)
	info = to_info(desc_ring, id); // 获取关联的信息头(struct printk_info)

	/*
	 * All @info fields (except @seq) are cleared and must be filled in
	 * by the writer. Save @seq before clearing because it is used to
	 * determine the new sequence number.
	 */
	seq = info->seq;   // 保存旧序列号后,清空信息头(保留 seq 字段用于后续计算)
	memset(info, 0, sizeof(*info));

	/*
	 * Set the @e fields here so that prb_commit() can be used if
	 * text data allocation fails.
	 */
	e->rb = rb;   // 记录环形缓冲区和描述符ID,供后续 prb_commit 使用
	e->id = id;

	/*
	 * Initialize the sequence number if it has "never been set".
	 * Otherwise just increment it by a full wrap.
	 *
	 * @seq is considered "never been set" if it has a value of 0,
	 * _except_ for @infos[0], which was specially setup by the ringbuffer
	 * initializer and therefore is always considered as set.
	 *
	 * See the "Bootstrap" comment block in printk_ringbuffer.h for
	 * details about how the initializer bootstraps the descriptors.
	 */
	if (seq == 0 && DESC_INDEX(desc_ring, id) != 0)   // 确保序列号单调递增,即使缓冲区循环复用
		info->seq = DESC_INDEX(desc_ring, id);
	else
		info->seq = seq + DESCS_COUNT(desc_ring);

	/*
	 * New data is about to be reserved. Once that happens, previous
	 * descriptors are no longer able to be extended. Finalize the
	 * previous descriptor now so that it can be made available to
	 * readers. (For seq==0 there is no previous descriptor.)
	 */
	if (info->seq > 0)
		desc_make_final(desc_ring, DESC_ID(id - 1));  // 将前一个描述符标记为"不可扩展"状态

	r->text_buf = data_alloc(rb, r->text_buf_size, &d->text_blk_lpos, id); // 从文本数据环分配指定大小的空间
	/* If text data allocation fails, a data-less record is committed. */
	if (r->text_buf_size && !r->text_buf) {
		prb_commit(e);
		/* prb_commit() re-enabled interrupts. */
		goto fail;
	}

	r->info = info;  // 使调用方能访问信息头

	/* Record full text space used by record. */
	e->text_space = space_used(&rb->text_data_ring, &d->text_blk_lpos);   // 记录实际占用的文本空间(用于统计)

	return true;
fail:
	/* Make it clear to the caller that the reserve failed. */
	memset(r, 0, sizeof(*r));
	return false;
}

关键设计思想总结

  1. 无锁并发

    • 通过原子操作+中断禁用实现多核安全。

    • 描述符状态机(预留→提交→可读)保证一致性。

  2. 空间管理

    • 描述符环与数据环分离,支持动态扩展。

    • text_blk_lpos 实现数据块循环复用。

  3. 可靠性保障

    • 序列号跨环递增,避免重复。

    • 空记录提交维护序列连续性。

  4. 性能优化

    • 快速失败路径减少临界区停留时间。

    • 延迟最终化(desc_make_final)合并操作。

格式化消息printk_sprint

text_len = printk_sprint(&r.text_buf[0], reserve_size, facility, &flags, fmt, args);
  • printk_sprint

    • 将格式化字符串(fmt + args)写入 r.text_buf

    • 处理日志级别前缀(如 KERN_ERR)、设施号等。

    • 这是填充 printk_ringbuffer实际内容的步骤

__printf(5, 0)
static u16 printk_sprint(char *text, u16 size, int facility,
			 enum printk_info_flags *flags, const char *fmt,
			 va_list args)
{
	u16 text_len;

	text_len = vscnprintf(text, size, fmt, args);

	/* Mark and strip a trailing newline. */
	if (text_len && text[text_len - 1] == '\n') {
		text_len--;
		*flags |= LOG_NEWLINE;
	}

	/* Strip log level and control flags. */
	if (facility == 0) {
		u16 prefix_len;

		prefix_len = printk_parse_prefix(text, NULL, NULL);
		if (prefix_len) {
			text_len -= prefix_len;
			memmove(text, text + prefix_len, text_len);
		}
	}

	trace_console_rcuidle(text, text_len);

	return text_len;
}
  • 作用:将格式化字符串安全地写入缓冲区,并处理日志前缀和标记。

  • 参数

    • text:目标缓冲区指针

    • size:缓冲区大小

    • facility:日志设施(如 LOG_KERN

    • flags:输出参数(记录日志标志,如 LOG_NEWLINE

    • fmt & args:格式化字符串和可变参数

这个函数就会将日志写到缓冲区,函数指向的是r.text_buf ,实际上也就prb_data_ring ,也就struct printk_ringbuffer text_data_ring 。具体可以查data_alloc 函数,分配内存时已经将指针指向确定。

从trace32读取全局printk_ringbuffer 可以看到这text_data_ring 中的数据存放的就是内核日志!

提交消息(prb_commit

if (!(flags & LOG_NEWLINE))
    prb_commit(&e);      // 允许后续追加
else
    prb_final_commit(&e); // 消息终结
  • prb_commit

    • 标记数据有效,使消费者(如 console_unlock)可以读取。

    • 如果是分片消息(LOG_CONT),允许后续追加。

实例:使用trace32读取printk_ringbuffer

作者手上有一份日志,使用trace32恢复现场后,查看变printk_rb_static

2025/08/halo_gdstw2t.png

变量名

变量结构体

地址

prb

struct pringk_ringbuffer

0xFFFFFFC00A1E7660

desc_ring

struct prb_desc_ring

0xFFFFFFC00A1E7660

text_data_ring

struct prb_data_ring

0xFFFFFFC00A1E7690

desc_ring.descs

struct prb_desc

0xFFFFFFC00A1E7668

0xFFFFFF82F2390000

desc_ring.infos

struct printk_info

0xFFFFFFC00A1E7670

0xFFFFFF82F1E10000

desc_ring.tail_id

atomic_long_t

0xFFFFFFC00A1E7680

counter = 0x0000000100121E53

desc_ring.head_id

atomic_long_t

0xFFFFFFC00A1E7678

counter = 0x0000000100131E52

desc_ring.count_bits

unsigned int

0xFFFFFFC00A1E7660

0x10

text_data_ring.size_bits

unsigned int

0xFFFFFFC00A1E7690

0x15

text_data_ring.data

char *

0xFFFFFFC00A1E7698

0xFFFFFF82F2510000

tail_id=0x0000000100121E53

data_ring_count = 1<<count_bits=1<<0x10=1<<16=0x10000 = 65536

desc_index = tail_id % data_ring_count = 0x0000000100121E53 % 0x10000 = 7,763

desc_offset = sizeof(prb_desc) * desc_index = 0x18 x 7763 = 0x2D7C8

那desc_index为7763对应的struct prb_desc的地址为:

desc_ring.descs基地址+0x2D7C8 = 0xFFFFFF82F2390000+0x2D7C8=0xFFFFFF82F23BD7C8

  (struct prb_desc *)0xffffff82f23bd7c8 = 0xFFFFFF82F23BD7C8 -> (
    state_var = (
      counter = 0xC000000100121E53),
    text_blk_lpos = (begin = 0x09569080, next = 0x095690E8))

state = (0xC000000100121E53>> (sizeof('long')*8 -2))&0b11 = (0xC000000100121E53 >> 62)&0b11 = 0xFFFFFFFFFFFFFFFF & 0b11 = 3

state为3,表示当前的日志是committed或finalized,确保只读取 ringbuffer 中已经写完(committed 或 finalized)的记录,跳过正在写入或无效的记录,防止读到半截数据或垃圾内容。如果state=3,则跳过。

计算下一个desc_index,

next desc_index = desc_index+1 = 7764

next state = 3 (这里不重复计算了,还是根据index计算出offset,然后用desc_ring.descs的基地址+offset获取到state_var再取模)

从内存里可以看到这一连串的descs

因为state=3的代表正在写入或者无效的记录,所以不需要打印,打印日志需要一直遍历找到state不为3的。

这里因为跳过的很多,所以忽略这个过程,直接找一个可以输出日志的情况来说明

当desc_index=55876时,此时的prb_desc的地址为:

desc_ring.descs基地址+sizeof(prb_desc)*desc_index =

0xFFFFFF82F2390000+0x18*55876= 0xFFFFFF82F24D7660

  (struct prb_desc *)0xFFFFFF82F24D7660 = 0xFFFFFF82F24D7660 -> (
    state_var = (
      counter = 0x800000010012DA44),
    text_blk_lpos = (
      begin = 0x09B45EE0,
      next = 0x09B46020))

此时state=(0x800000010012DA44 >>62)&0b11= 0xFFFFFFFFFFFFFFFE&0b11=0b10=2

begin = desc_ring.descs[55876].text_blk_lops.begin % (1 << text_data_ring.size_bits )

= 0x09B45EE0 % (1 <<0x15)

= 0x145EE0

而此时desc_index对应的data地址为

data_start = text_data_ring.data基地址+begin+sizeof('long')

= 0xFFFFFF82F2510000 + 0x145EE0 + 0x8

= 0xFFFFFF82F2655EE8

备注:这里要说明一下为何data_start的地址要加上sizeof('long'),我们可以看上图的内存,在data_start前面的0x8:0x000000010012DA44,这个值时id号,也就是对应的prb_desc的state_var成员

  (struct prb_desc *)0xFFFFFF82F24D7660 = 0xFFFFFF82F24D7660 -> (
    state_var = (
      counter = 0x800000010012DA44),   ///////这个!!!

同时我们可以从内存里看到环形缓冲区的复写:

由于这个id是一直递增的,随着内存增长的方向,一直在递增10012DA44->10012DA45->10012DA46->10012DA47......而在10012DA44的上方是100131E52,这就说明后面的日志复写了低地址保存的日志

总结

本篇文章主要介绍了 Linux 内核中的 printk 内核框架,包括 printk 接口的作用、log buffer 的设计和初始化、log buffer 的数据结构、printk 函数流程等内容。文章详细介绍了 log buffer 的初始化过程、数据结构组成,包括 struct printk_ringbuffer、struct prb_desc_ring、struct prb_data_ring、struct prb_desc 和 struct printk_info 等结构体的功能和关键成员解析。此外,文章还分析了 printk 函数的流程,包括 printk、printk_index_wrap、__printk_index_emit、_printk 和 vprintk 等函数的调用关系。

此外利用trace32读取全局的printk_ringbuffer在内存中的数据,通过实际的计算获取日志!以及prink_ringbuffer在内存中的增长方向,可以让我们对于环形缓冲区的整体印象更加的清晰!

感谢您观看本篇文章!
文章内若有不妥之处,欢迎在评论区交流指正!

✒️ 记于山外,落笔 / 林渡