我们知道,linux内核为了支持在各种位置都能使用printk,做了不少的工作,这篇文章简单介绍一下printk的一些并发处理。
本文基于linux内核4.19.195.
printk最终会调用到vprintk_func函数。
__printf(1, 0) int vprintk_func(const char *fmt, va_list args)
{
/*
* Try to use the main logbuf even in NMI. But avoid calling console
* drivers that might have their own locks.
*/
if ((this_cpu_read(printk_context) & PRINTK_NMI_DIRECT_CONTEXT_MASK) &&
raw_spin_trylock(&logbuf_lock)) { //看注释,以及这里是raw_spin_trylock
int len;
len = vprintk_store(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args);
raw_spin_unlock(&logbuf_lock);
defer_console_output();
return len;
}
// nmi和vprintk_safe的分支都走的printk_safe_log_store,只是传入的buffer不一样
/* Use extra buffer in NMI when logbuf_lock is taken or in safe mode. */
if (this_cpu_read(printk_context) & PRINTK_NMI_CONTEXT_MASK)
return vprintk_nmi(fmt, args);
/* Use extra buffer to prevent a recursion deadlock in safe mode. */
if (this_cpu_read(printk_context) & PRINTK_SAFE_CONTEXT_MASK)
return vprintk_safe(fmt, args);
/* No obstacles. */
return vprintk_default(fmt, args);
}
可以看到,这个函数有三个分支:vprintk_nmi、vprintk_safe、vprintk_default。其中,vprintk_default是正常走的分支,vprintk_nmi是在nmi中断中调用printk走的分支,vprintk_safe是在不安全的上下文中调用printk走的分支。下面我们主要以vprintk_nmi为例分析。
我们知道,printk最终会将输出信息保存在一个buffer中。如果多核同时调用printk,则最简单的情况,都走到vprintk_default分支,最终是由logbuf_lock_irqsave以及logbuf_unlock_irqrestore来完成并发处理的。
#define logbuf_lock_irqsave(flags) \
do { \
printk_safe_enter_irqsave(flags); \
raw_spin_lock(&logbuf_lock); \
} while (0)
asmlinkage int vprintk_emit(int facility, int level,
const char *dict, size_t dictlen,
const char *fmt, va_list args)
{
****
/* This stops the holder of console_sem just where we want him */
logbuf_lock_irqsave(flags);
curr_log_seq = log_next_seq;
printed_len = vprintk_store(facility, level, dict, dictlen, fmt, args);
pending_output = (curr_log_seq != log_next_seq);
logbuf_unlock_irqrestore(flags);
*****
}
可以看到,这里为了做好并发处理,使用了关中断以及spin_lock实现的。我们知道nmi中断是无法被屏蔽掉的,那么我们如果在nmi中断中使用printk时,怎么保证并发安全呢?
static __printf(1, 0) int vprintk_nmi(const char *fmt, va_list args)
{
struct printk_safe_seq_buf *s = this_cpu_ptr(&nmi_print_seq);
return printk_safe_log_store(s, fmt, args);
}
static __printf(2, 0) int printk_safe_log_store(struct printk_safe_seq_buf *s,
const char *fmt, va_list args)
{
int add;
size_t len;
va_list ap;
again:
len = atomic_read(&s->len);
/* The trailing '\0' is not counted into len. */
if (len >= sizeof(s->buffer) - 1) {
atomic_inc(&s->message_lost);
queue_flush_work(s);
return 0;
}
/*
* Make sure that all old data have been read before the buffer
* was reset. This is not needed when we just append data.
*/
if (!len)
smp_rmb();
va_copy(ap, args);
add = vscnprintf(s->buffer + len, sizeof(s->buffer) - len, fmt, ap);
va_end(ap);
if (!add)
return 0;
/*
* Do it once again if the buffer has been flushed in the meantime.
* Note that atomic_cmpxchg() is an implicit memory barrier that
* makes sure that the data were written before updating s->len.
*/
if (atomic_cmpxchg(&s->len, len, len + add) != len)
goto again;
queue_flush_work(s);
return add;
}
通过代码可以看到,nmi中断并没有直接把printk要打印的东西输出到全局的buffer中,而是通过将内容输出到一个percpu的buffer—nmi_print_seq中,然后调用queue_flush_work(),利用irq_work机制把输出的内容memcpy到全局的buffer中,从而支持了nmi中断中使用printk,具体的memcpy动作在work函数__printk_safe_flush()中完成。
此外,printk的基本原理,可以参考https://github.com/kaka555/KAKAOS/blob/master/C/ubuntu/src/kernel/OS_LIB/myMicroLIB.c中函数ka_printf()的实现