[RFC][PATCH 1/4] printk: introduce printing kernel thread

From: Sergey Senozhatsky
Date: Mon Mar 06 2017 - 07:52:02 EST


printk() is quite complex internally and, basically, it does two
slightly independent things:
a) adds a new message to a kernel log buffer (log_store())
b) prints kernel log messages to serial consoles (console_unlock())

while (a) is guaranteed to be executed by printk(), (b) is not, for a
variety of reasons. Unlike log_store(), console_unlock() comes at
a price:

1) console_unlock() attempts to flush all pending kernel log messages
to the console. Thus, it can loop indefinitely.

2) while console_unlock() is executed on one particular CPU, printing
pending kernel log messages, other CPUs can simultaneously append new
messages to the kernel log buffer.

3) the time it takes console_unlock() to print kernel messages also
depends on the speed of the console -- which may not be fast at all.

4) console_unlock() is executed in the same context as printk(), so
it may be non-preemptible/atomic, which makes 1)-3) dangerous.

As a result, nobody knows how long a printk() call will take, so
it's not really safe to call printk() in a number of situations,
including atomic context, RCU critical sections, interrupt context,
and more.

This patch introduces a dedicated printing kernel thread - printk_kthread.
The main purpose of this kthread is to offload printing to a non-atomic
and always scheduleable context, which eliminates 4) and makes 1)-3) less
critical. printk() now just appends log messages to the kernel log buffer
and wake_up()s printk_kthread instead of locking console_sem and calling
into potentially unsafe console_unlock().

This, however, is not always safe on its own. For example, we can't call
into the scheduler from panic(), because this may cause deadlock. That's
why printk() switches back to the old behaviour (console_unlock() from
vprintk_emit()) in those cases. In general, this switch happens
automatically once a EMERG log level message appears in the log buffer.
Another cases when wake_up() might not work as expected are suspend,
hibernate, etc. For those situations we provide two new functions:
-- console_printing_thread_off()
Disables printk offloading. All printk() calls will attempt
to lock the console_sem and, if successful, flush kernel log
messages.

-- console_printing_thread_on()
Enables printk offloading.

Signed-off-by: Sergey Senozhatsky <sergey.senozhatsky@xxxxxxxxx>
Signed-off-by: Jan Kara <jack@xxxxxxx>
---
include/linux/console.h | 3 ++
kernel/printk/printk.c | 101 +++++++++++++++++++++++++++++++++++++++++++++---
2 files changed, 99 insertions(+), 5 deletions(-)

diff --git a/include/linux/console.h b/include/linux/console.h
index 5949d1855589..bea775609249 100644
--- a/include/linux/console.h
+++ b/include/linux/console.h
@@ -187,6 +187,9 @@ extern bool console_suspend_enabled;
extern void suspend_console(void);
extern void resume_console(void);

+extern void console_printing_thread_off(void);
+extern void console_printing_thread_on(void);
+
int mda_console_init(void);
void prom_con_init(void);

diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 2984fb0f0257..1c4232ca2e6a 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -48,6 +48,7 @@
#include <linux/sched/clock.h>
#include <linux/sched/debug.h>
#include <linux/sched/task_stack.h>
+#include <linux/kthread.h>

#include <linux/uaccess.h>
#include <asm/sections.h>
@@ -437,6 +438,46 @@ static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
static char *log_buf = __log_buf;
static u32 log_buf_len = __LOG_BUF_LEN;

+static struct task_struct *printk_kthread __read_mostly;
+/* When `true' printing thread has messages to print. */
+static bool printk_kthread_need_flush_console;
+/*
+ * We can't call into the scheduler (wake_up() printk kthread), for example,
+ * during suspend/kexec. This temporarily switches printk to old behaviour.
+ */
+static int printk_kthread_disable __read_mostly;
+/*
+ * Switch printk to old behavior permanently. Unlike `oops_in_progress'
+ * it doesn't go back to 0.
+ */
+static bool printk_emergency __read_mostly;
+
+static inline bool printk_kthread_enabled(void)
+{
+ return !printk_emergency &&
+ printk_kthread && READ_ONCE(printk_kthread_disable) == 0;
+}
+
+/*
+ * This disables printing offloading and instead attempts
+ * to do the usual console_trylock()->console_unlock().
+ *
+ * Note, this does not stop the printk_kthread if it's already
+ * printing logbuf messages.
+ */
+void console_printing_thread_off(void)
+{
+ printk_kthread_disable++;
+ barrier();
+}
+
+/* This re-enables printk_kthread offloading. */
+void console_printing_thread_on(void)
+{
+ barrier();
+ printk_kthread_disable--;
+}
+
/* Return log buffer address */
char *log_buf_addr_get(void)
{
@@ -1710,6 +1751,14 @@ asmlinkage int vprintk_emit(int facility, int level,
in_sched = true;
}

+ /*
+ * Emergency level indicates that the system is unstable and, thus,
+ * we better stop relying on wake_up(printk_kthread) and try to do
+ * a direct printing.
+ */
+ if (level == LOGLEVEL_EMERG)
+ printk_emergency = true;
+
boot_delay_msec(level);
printk_delay();

@@ -1762,12 +1811,28 @@ asmlinkage int vprintk_emit(int facility, int level,
/* If called from the scheduler, we can not call up(). */
if (!in_sched) {
/*
- * Try to acquire and then immediately release the console
- * semaphore. The release will print out buffers and wake up
- * /dev/kmsg and syslog() users.
+ * Under heavy printing load/slow serial console/etc
+ * console_unlock() can stall CPUs, which can result in
+ * soft/hard-lockups, lost interrupts, RCU stalls, etc.
+ * Therefore we attempt to print the messages to console
+ * from a dedicated printk_kthread, which always runs in
+ * schedulable context.
*/
- if (console_trylock())
- console_unlock();
+ if (printk_kthread_enabled()) {
+ printk_kthread_need_flush_console = true;
+
+ printk_safe_enter_irqsave(flags);
+ wake_up_process(printk_kthread);
+ printk_safe_exit_irqrestore(flags);
+ } else {
+ /*
+ * Try to acquire and then immediately release the
+ * console semaphore. The release will print out
+ * buffers and wake up /dev/kmsg and syslog() users.
+ */
+ if (console_trylock())
+ console_unlock();
+ }
}

return printed_len;
@@ -1874,6 +1939,9 @@ static size_t msg_print_text(const struct printk_log *msg,
bool syslog, char *buf, size_t size) { return 0; }
static bool suppress_message_printing(int level) { return false; }

+void console_printing_thread_off(void) {}
+void console_printing_thread_on(void) {}
+
#endif /* CONFIG_PRINTK */

#ifdef CONFIG_EARLY_PRINTK
@@ -2681,6 +2749,29 @@ static DEFINE_PER_CPU(struct irq_work, wake_up_klogd_work) = {
.flags = IRQ_WORK_LAZY,
};

+static int printk_kthread_func(void *data)
+{
+ while (1) {
+ set_current_state(TASK_INTERRUPTIBLE);
+ if (!printk_kthread_need_flush_console)
+ schedule();
+
+ __set_current_state(TASK_RUNNING);
+ /*
+ * Avoid an infinite loop when console_unlock() cannot
+ * access consoles, e.g. because console_suspended is
+ * true. schedule(), someone else will print the messages
+ * from resume_console().
+ */
+ printk_kthread_need_flush_console = false;
+
+ console_lock();
+ console_unlock();
+ }
+
+ return 0;
+}
+
void wake_up_klogd(void)
{
preempt_disable();
--
2.12.0