2.2. コンソール出力の改良
2.2.1. コンソール出力
xv6のカーネル内からコンソールへ文字列を出力するにはprintk()を使用します。
printk("Hello, world!\n");
Note
以前のxv6ではカーネル内のコンソール出力関数もユーザ空間と同じ
printf()という名前でしたが、現在はLinuxカーネルと同じ流儀のprintk()に改名されています。
標準ライブラリのprintf()と似ていますが、フォーマット文字列のサポートが限定的です。
- サポートしている変換指定子は
%d,%u,%x,%p,%c,%sのみ %d,%u,%xに関しては、それぞれlおよびll修飾子をサポート- フラグやフィールド幅、精度はサポートしていない
カーネル内で使用するprintk()のコードはkernel/printk.cに含まれています。
📝 kernel/printk.c
// Print to the console.
int
printk(char *fmt, ...)
{
va_list ap;
int i, cx, c0, c1, c2;
char *s;
if (panicking == 0)
acquire(&pr.lock);
va_start(ap, fmt);
for (i = 0; (cx = fmt[i] & 0xff) != 0; i++) {
if (cx != '%') {
consputc(cx);
continue;
}
i++;
c0 = fmt[i + 0] & 0xff;
c1 = c2 = 0;
if (c0)
c1 = fmt[i + 1] & 0xff;
if (c1)
c2 = fmt[i + 2] & 0xff;
if (c0 == 'd') {
printint(va_arg(ap, int), 10, 1);
} else if (c0 == 'l' && c1 == 'd') {
printint(va_arg(ap, uint64), 10, 1);
i += 1;
} else if (c0 == 'l' && c1 == 'l' && c2 == 'd') {
printint(va_arg(ap, uint64), 10, 1);
i += 2;
} else if (c0 == 'u') {
printint(va_arg(ap, uint32), 10, 0);
} else if (c0 == 'l' && c1 == 'u') {
printint(va_arg(ap, uint64), 10, 0);
i += 1;
} else if (c0 == 'l' && c1 == 'l' && c2 == 'u') {
printint(va_arg(ap, uint64), 10, 0);
i += 2;
} else if (c0 == 'x') {
printint(va_arg(ap, uint32), 16, 0);
} else if (c0 == 'l' && c1 == 'x') {
printint(va_arg(ap, uint64), 16, 0);
i += 1;
} else if (c0 == 'l' && c1 == 'l' && c2 == 'x') {
printint(va_arg(ap, uint64), 16, 0);
i += 2;
} else if (c0 == 'p') {
printptr(va_arg(ap, uint64));
} else if (c0 == 'c') {
consputc(va_arg(ap, uint));
} else if (c0 == 's') {
if ((s = va_arg(ap, char *)) == 0)
s = "(null)";
for (; *s; s++)
consputc(*s);
} else if (c0 == '%') {
consputc('%');
} else if (c0 == 0) {
break;
} else {
// Print unknown % sequence to draw attention.
consputc('%');
consputc(c0);
}
}
va_end(ap);
if (panicking == 0)
release(&pr.lock);
return 0;
}
2.2.2. printkの拡張
自作プロトコルスタックのコードは、コンソール出力に関してprintk()が備えていない機能を必要とします。
- ゼロ埋めのフィールド幅(
%02xなど): MACアドレスの表示などに使用 size_t/ssize_t型に対応するz修飾子(%zuなど): パケット長などのログ出力に使用
また、IPアドレスやMACアドレスの文字列化、ログの組み立てにsnprintf()を使用しているのでこちらも必要です。
Important
printk()は未知の変換指定子を「そのまま出力して、対応する引数を消費しない」という動作をします。そのため、非対応の書式が1つでも混ざると、それ以降の引数がすべてズレて出力が壊れてしまいます。見た目が悪くなるだけでは済まない、という点に注意してください。
他のOSからより高機能なprintfの実装を丸ごと移植する方法もありますが、今回はprintk.cに手を入れて必要な機能だけを拡張します。
変更の中心は「整形」と「出力」の分離です。元のprintk()は整形しながらconsputc()でコンソールへ直接出力していました。これを、整形だけを行うkvprintf()と、出力先を差し替え可能にするコールバック(putch)に分けます。
- 整形ループを
kvprintf()として独立させ、1文字出力するごとに引数で受け取ったputchを呼ぶ形にします。printint()・printptr()も同様にconsputc()ではなくputchを呼ぶようにします - 出力先の実装を2つ用意します。コンソールへ出す
cons_putch()と、文字列バッファへ書き込むsprint_putch()です printk()は「ロックを取り、cons_putchを渡してkvprintf()を呼ぶだけ」の薄い関数になります。vsnprintf()/snprintf()は、同じkvprintf()にsprint_putchを渡すことで実現します- あわせて
kvprintf()とprintint()の書式解釈を拡張し、ゼロ埋めのフィールド幅(%04xなど)とsize_t用のz修飾子に対応させます
これらをまとめて反映したprintk.cが次の差分です。変更のある関数は丸ごと置き換えるので、削除する関数の内容は...で省略しています。
📝 kernel/printk.c
static char digits[] = "0123456789abcdef";
-static void
-printint(long long xx, int base, int sign)
-{
- ...
-}
-
-static void
-printptr(uint64 x)
-{
-}
-
-// Print to the console.
-int
-printk(char *fmt, ...)
-{
- ...
-}
+static void
+printint(void (*putch)(int, void *), void *arg, long long xx, int base,
+ int sign, int width, int padc)
+{
+ char buf[20];
+ int i;
+ unsigned long long x;
+
+ if (sign && (sign = (xx < 0)))
+ x = -xx;
+ else
+ x = xx;
+
+ i = 0;
+ do {
+ buf[i++] = digits[x % base];
+ } while ((x /= base) != 0);
+
+ if (sign && padc == '0')
+ putch('-', arg);
+ for (int w = i + sign; w < width; w++)
+ putch(padc, arg);
+ if (sign && padc != '0')
+ putch('-', arg);
+
+ while (--i >= 0)
+ putch(buf[i], arg);
+}
+
+static void
+printptr(void (*putch)(int, void *), void *arg, uint64 x)
+{
+ int i;
+ putch('0', arg);
+ putch('x', arg);
+ for (i = 0; i < (sizeof(uint64) * 2); i++, x <<= 4)
+ putch(digits[x >> (sizeof(uint64) * 8 - 4)], arg);
+}
+
+static void
+kvprintf(void (*putch)(int, void *), void *arg, const char *fmt, va_list ap)
+{
+ int i, cx, c0, width, padc, lflag;
+ char *s;
+
+ for (i = 0; (cx = fmt[i] & 0xff) != 0; i++) {
+ if (cx != '%') {
+ putch(cx, arg);
+ continue;
+ }
+ i++;
+ padc = ' ';
+ if ((fmt[i] & 0xff) == '0')
+ padc = '0';
+ width = 0;
+ for (; (c0 = fmt[i] & 0xff) >= '0' && c0 <= '9'; i++)
+ width = width * 10 + c0 - '0';
+ lflag = 0;
+ for (; (c0 = fmt[i] & 0xff) == 'l'; i++)
+ lflag++;
+ if (c0 == 'z') { // size_t is 64-bit
+ lflag = 1;
+ i++;
+ c0 = fmt[i] & 0xff;
+ }
+ if (c0 == 'd') {
+ if (lflag)
+ printint(putch, arg, va_arg(ap, int64_t), 10, 1, width, padc);
+ else
+ printint(putch, arg, va_arg(ap, int), 10, 1, width, padc);
+ } else if (c0 == 'u') {
+ if (lflag)
+ printint(putch, arg, va_arg(ap, uint64), 10, 0, width, padc);
+ else
+ printint(putch, arg, va_arg(ap, uint32), 10, 0, width, padc);
+ } else if (c0 == 'x') {
+ if (lflag)
+ printint(putch, arg, va_arg(ap, uint64), 16, 0, width, padc);
+ else
+ printint(putch, arg, va_arg(ap, uint32), 16, 0, width, padc);
+ } else if (c0 == 'p') {
+ printptr(putch, arg, va_arg(ap, uint64));
+ } else if (c0 == 'c') {
+ putch(va_arg(ap, uint), arg);
+ } else if (c0 == 's') {
+ if ((s = va_arg(ap, char *)) == 0)
+ s = "(null)";
+ for (; *s; s++)
+ putch(*s, arg);
+ } else if (c0 == '%') {
+ putch('%', arg);
+ } else if (c0 == 0) {
+ break;
+ } else {
+ // Print unknown % sequence to draw attention.
+ putch('%', arg);
+ putch(c0, arg);
+ }
+ }
+}
+
+static void
+cons_putch(int c, void *arg)
+{
+ consputc(c);
+}
+
+// Print to the console.
+int
+printk(char *fmt, ...)
+{
+ va_list ap;
+
+ if (panicking == 0)
+ acquire(&pr.lock);
+
+ va_start(ap, fmt);
+ kvprintf(cons_putch, 0, fmt, ap);
+ va_end(ap);
+
+ if (panicking == 0)
+ release(&pr.lock);
+
+ return 0;
+}
+
+struct sprintbuf {
+ char *buf;
+ char *ebuf;
+ int cnt;
+};
+
+static void
+sprint_putch(int c, void *arg)
+{
+ struct sprintbuf *b = arg;
+
+ if (b->buf < b->ebuf)
+ *b->buf++ = c;
+ b->cnt++;
+}
+
+int
+vsnprintf(char *buf, size_t n, const char *fmt, va_list ap)
+{
+ struct sprintbuf b;
+
+ if (n == 0)
+ return 0;
+
+ b = (struct sprintbuf){buf, buf + n - 1, 0};
+ kvprintf(sprint_putch, &b, fmt, ap);
+ *b.buf = '\0';
+
+ return b.cnt;
+}
+
+int
+snprintf(char *buf, size_t n, const char *fmt, ...)
+{
+ va_list ap;
+ int cnt;
+
+ va_start(ap, fmt);
+ cnt = vsnprintf(buf, n, fmt, ap);
+ va_end(ap);
+
+ return cnt;
+}
void
panic(char *s)
補足として、いくつか設計上のポイントを挙げておきます。
- ロックの扱い:
pr.lockの取得・解放はprintk()側に残し、整形エンジンのkvprintf()はロックを取りません。そのためsnprintf()はコンソールのロックと無関係に、割り込みコンテキストを含むどこからでも安全に呼び出せます snprintf()の切り詰め:sprint_putch()は書き込み位置がバッファ終端(ebuf)に達したら書き込みをスキップし、文字数のカウントだけ続けます。出力は必ずナル文字(\0)で終端され、戻り値は「切り詰めが無ければ出力されたであろう文字数」です(標準Cのsnprintf()と同じ挙動)- 書式解釈の書き換え: 旧実装は
c0/c1/c2と最大3文字を先読みして%lldのような書式を判定していました。フィールド幅の数字列が加わると固定長の先読みでは対応できないため、「0フラグ → フィールド幅 →l修飾子(繰り返し可)→z修飾子 → 変換指定子」の順に読み進める方式へ改めています。zが対応するsize_tはkernel/types.hで64bit(unsigned long)と定義しているので、lと同じ扱い(lflag = 1)にしています
つづけて、追加した関数をカーネル内の他のコードから呼び出せるようにkernel/defs.hへプロトタイプ宣言を追加します。
📝 kernel/defs.h
// printk.c
int printk(char*, ...) __attribute__ ((format (printf, 1, 2)));
+int snprintf(char*, size_t, const char*, ...) __attribute__ ((format (printf, 3, 4)));
+int vsnprintf(char*, size_t, const char*, __builtin_va_list);
void panic(char*) __attribute__((noreturn));
void printkinit(void);
Note
__attribute__((format(printf, 3, 4)))は「第3引数が書式文字列、第4引数以降が対応する可変長引数である」とコンパイラへ伝え、書式と引数の型の整合性をチェックさせるためのものです。xv6は-Werror付きでビルドされるため、書式の誤りはコンパイルエラーとして検出されます。
動作確認
再ビルドした後、make qemuを実行してxv6を起動させます。
xv6 kernel is booting
hart 1 starting
hart 2 starting
init: starting sh
Note
コンソール出力関数を改造した後も問題なくテキストが出力されていれば大丈夫です。