我们知道,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_trylockint 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()的实现