printk内核框架简介
printk是内核用于输出调试信息的接口,这些调试信息可以帮助我们获取系统或程序中的一些关键信息,在系统出现问题后也能第一时间将错误信息记录下来,以帮助开发人员定位问题原因。
而对于稳定性同学经常接触到的,内核oops或者panic时的fulldump,从这些dump中导出内核日志就尤为的重要。在高通平台可以使用Linux ramdump parser解析出来。就是为了实现以上这些需求,内核设计了如下图的printk模块
从上图可看出,其核心是一个叫做log buffer的循环缓冲区,printk作为生产者将消息存入该缓冲区,右边的log服务模块作为消费者可从log buffer中读取消息。这样设计有以下几个优点:
控制台和日志模块初始化前,内核的启动日志可以暂存到log buffer中。待它们初始化完成后,再输出相应信息
在printk log输出太快,而log服务的处理速度不足时,防止log信息丢失
将log输入模块和log输出模块解耦,增加设计的灵活性
在log输入模块中,除了printk外还有devkmsg_write接口可以向log buffer写入消息。该接口通过/dev/kmsg设备节点导出,可以在用户态通过echo命令将信息重定向到该设备节点的方式,将消息写入log buffer中。
在log输出模块中,可以通过/dev/kmsg、/proc/kmsg、klogd读取log信息。也可以将串口、网络等注册为console,通过它们输出log。pstore和mtdoops机制在系统发生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核个数信息都是未知的,它带来以下两个问题:
内存信息被解析之前,系统只映射了内核镜像所在位置内存的虚拟地址,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;
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
最顶层的数据结构
功能
整合描述符环和数据环:协调元数据和文本数据的存储。
统计失败操作:记录因缓冲区满等原因导致的日志丢失情况。
关键成员解析
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;
};
描述符管理环
功能
管理日志元数据:每个描述符对应一条日志记录,存储其状态、文本位置等信息。
控制日志生命周期:通过状态机(保留→提交→终结)确保数据一致性。
关键成员解析
struct prb_data_ring
struct prb_data_ring {
unsigned int size_bits;
char *data;
atomic_long_t head_lpos;
atomic_long_t tail_lpos;
};
功能
存储实际日志文本内容:以字节流形式保存内核打印的原始日志消息。
管理数据空间:通过头尾指针实现环形缓冲区,动态分配和回收数据块。
关键成员解析
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
的位域结构
plaintext
| 63...62 | 61...0 |
|---------|--------|
| 状态位 | 描述符ID |
状态位(2 bits):
00
:空闲(未使用)01
:保留(RESERVED
,数据写入中)10
:已提交(COMMITTED
,数据可读)11
:已终结(FINALIZED
,数据持久化后)
描述符ID(62 bits):单调递增的唯一标识,防止并发修改冲突。
struct prb_data_blk_lpos
结构体的两个成员
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
中)分离,实现高效管理。
成员解析
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 开始)。
示例:
进程 PID 1234 调用
printk
:返回
1234
。
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_reserve
和 printk_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_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;
}
关键设计思想总结
无锁并发:
通过原子操作+中断禁用实现多核安全。
描述符状态机(预留→提交→可读)保证一致性。
空间管理:
描述符环与数据环分离,支持动态扩展。
text_blk_lpos
实现数据块循环复用。
可靠性保障:
序列号跨环递增,避免重复。
空记录提交维护序列连续性。
性能优化:
快速失败路径减少临界区停留时间。
延迟最终化(
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
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在内存中的增长方向,可以让我们对于环形缓冲区的整体印象更加的清晰!
感谢您观看本篇文章!
文章内若有不妥之处,欢迎在评论区交流指正!
✒️ 记于山外,落笔 / 林渡