カーネルソースを読む ( 4 ) ~ printk() ~

July 11, 2017

printk() を読んでみる

printk() って?
-> カーネル空間にメッセージを出力する。syslog ( /var/log/messages ) やシリアルコンソールにメッセージを出力できる。

早速 kernel/printk.c を見てみよう。

/**
 * printk - print a kernel message
 * @fmt: format string
 *
 * This is printk().  It can be called from any context.  We want it to work.
 *
 * We try to grab the console_sem.  If we succeed, it's easy - we log the output and
 * call the console drivers.  If we fail to get the semaphore we place the output
 * into the log buffer and return.  The current holder of the console_sem will
 * notice the new output in release_console_sem() and will send it to the
 * consoles before releasing the semaphore.
 *
 * One effect of this deferred printing is that code which calls printk() and
 * then changes console_loglevel may break. This is because console_loglevel
 * is inspected when the actual printing occurs.
 *
 * See also:
 * printf(3)
 *
 * See the vsnprintf() documentation for format string extensions over C99.
 */

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

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

	return r;
}

冒頭の “This is printk(). It can be called from any context. We want it to work.” は「printk() はどのコンテキストからも実行できる、つまり割り込みハンドラでも使える」 ということを言っている。 printk(const char *fmt, …) の中身を見ると、vprintk() を呼んでいるだけのことがわかる。

asmlinkage int vprintk(const char *fmt, va_list args)
{
	int printed_len = 0;
	int current_log_level = default_message_loglevel;
	unsigned long flags;
	int this_cpu;
	char *p;

	boot_delay_msec();
	printk_delay();

	preempt_disable();
	/* This stops the holder of console_sem just where we want him */
	raw_local_irq_save(flags);
	this_cpu = smp_processor_id();

boot_delay_msec() と printk_delay() はメイン処理の前に待ちを入れるかの判断をするだけ。 preempt_disable() はカーネル・プリエンプションを無効にする。 最後の行で preempt_enable() で最有効化をしている。これは、printk() の実行中にプリエンプションが発生すると、処理が正しく動かないため。 raw_local_irq_save() は、printk() を実行している CPU からの割り込みを禁止し、他の処理に割り込ませないようにする。 smp_processor_id() は CPU の番号を取得。(コア単位)

	/*
	 * Ouch, printk recursed into itself!
	 */
	if (unlikely(printk_cpu == this_cpu)) {
		/*
		 * If a crash is occurring during printk() on this CPU,
		 * then try to get the crash message out but make sure
		 * we can't deadlock. Otherwise just return to avoid the
		 * recursion and return - but flag the recursion so that
		 * it can be printed at the next appropriate moment:
		 */
		if (!oops_in_progress) {
			recursion_bug = 1;
			goto out_restore_irqs;
		}
		zap_locks();
	}

	lockdep_off();
	spin_lock(&logbuf_lock);
        printk_cpu = this_cpu;

	if (recursion_bug) {
		recursion_bug = 0;
		strcpy(printk_buf, recursion_bug_msg);
		printed_len = strlen(recursion_bug_msg);
	}

unlikely() で CPU 番号のチェックが入っている。
printk() の実行中にカーネルパニックが発生し、その過程で printk() が呼び出されると printk() の無限ループになる。それを防ぐのがこの CPU 番号のチェック。
recursion_bug_msg を printk_bug に格納している。

	/* Emit the output into the temporary buffer */
	printed_len += vscnprintf(printk_buf + printed_len,
				  sizeof(printk_buf) - printed_len, fmt, args);

vscnprintf() では printk_buf[] というバッファに printk() に渡された引数をフォーマットしてコピーしている。

/* kernel.h */
extern int vscnprintf(char *buf, size_t size, const char *fmt, va_list args)
        __attribute__ ((format (printf, 3, 0)));

以降の処理は printk_buf に対して行われる。

	p = printk_buf;

	/* Do we have a loglevel in the string? */
	if (p[0] == '<') {
		unsigned char c = p[1];
		if (c && p[2] == '>') {
			switch (c) {
			case '0' ... '7': /* loglevel */
				current_log_level = c - '0';
			/* Fallthrough - make sure we're on a new line */
			case 'd': /* KERN_DEFAULT */
				if (!new_text_line) {
					emit_log_char('\n');
					new_text_line = 1;
				}
			/* Fallthrough - skip the loglevel */
			case 'c': /* KERN_CONT */
				p += 3;
				break;
			}
		}
	}

バッファの二番目の要素から LogLevel を判別している。

	/*
	 * Copy the output into log_buf.  If the caller didn't provide
	 * appropriate log level tags, we insert them here
	 */
	for ( ; *p; p++) {
		if (new_text_line) {
			/* Always output the token */
			emit_log_char('<');
			emit_log_char(current_log_level + '0');
			emit_log_char('>');
			printed_len += 3;
			new_text_line = 0;

			if (printk_time) {
				/* Follow the token with the time */
				char tbuf[50], *tp;
				unsigned tlen;
				unsigned long long t;
				unsigned long nanosec_rem;

				t = cpu_clock(printk_cpu);
				nanosec_rem = do_div(t, 1000000000);
				tlen = sprintf(tbuf, "[%5lu.%06lu] ",
						(unsigned long) t,
						nanosec_rem / 1000);

				for (tp = tbuf; tp < tbuf + tlen; tp++)
					emit_log_char(*tp);
				printed_len += tlen;
			}

			if (!*p)
				break;
		}

		emit_log_char(*p);
		if (*p == '\n')
			new_text_line = 1;
	}

print_buf の内容を一文字ずつ解析し、emit_log_char() で log_buf[] に送っている。 log_buf[] は char log_buf[VMM_LOG_LEN] で定義されるリングバッファでこの大きさはカーネルコンフィグレーションで変更可能。

結局 printk() は log_buf[] ( カーネル内のバッファ ) に格納されるだけ。
dmesg でこのバッファ内容は読み取ることができ、また、klogd というデーモンが定期的に内容を吸い上げ syslog に書き出す。
kluged デーモンの吸い上げが遅れるとリングバッファが一周し、一部のデータが出力されない場合もある。( printk() の使用頻度が多いなど )


 © 2023, Dealing with Ambiguity