Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

N4: TCP/IPプロトコルスタック自作入門

1. 概要

この講義では、インターネットをはじめとする現在のコンピュータネットワークを支えている基盤技術「TCP/IP」のプロトコルスタックをフルスクラッチで実装し、実際のOSに搭載してネットワーク機能を持たせる演習を行います。

講師が開発している教育用のプロトコルスタック「microps」を教材に、Ethernetフレームを組み立てて送受信するところから ARP、IP、ICMP、UDP、TCP などのプロトコルを処理するプログラムを、全て自分の手で作り上げてもらいます。

なお、講義時間が限られているためプロトコルスタックの基本的な実装は事前学習の期間(約1ヶ月)で済ませてもらいます。各自で事前学習を進めてもらうにあたり、解説資料を配布するとともに、ミーティングの機会を設けてしっかりフォローアップしますので安心してください。

講義時間では、あらかじめ開発を進めておいてもらったプロトコルスタックを、教育用OSの「xv6」に搭載して実際にTCP/IPでの通信を実現することを目指します。具体的には、次のような作業を行うことになります。

具体的には、次のような作業を行うことになります。

  • デバイスドライバの実装
  • プロトコルスタックの移植(プラットフォーム依存の処理の実装)
  • ソケット関連システムコールとユーザライブラリの実装

この講義を通じてTCP/IPへの理解を深めるとともに、パケットやプロトコル処理の楽しさを知ってもらえたら嬉しいです。

2. 開発環境

各自で開発用のLinux環境を準備してください。

  • Ubuntu 24.04 を推奨
    • 実機 or 仮想環境どちらでもOK(WindowsはWSL2で動作確認済)
    • Dockerで構築する場合には –privileged または –cap-add=NET_ADMIN が必要
  • 必要パッケージ(Ubuntuの場合)
    • build-essential
    • git
    • iproute2
    • iputils-ping
    • netcat-openbsd
    • gdb-multiarch
    • qemu-system-misc
    • gcc-riscv64-linux-gnu
    • binutils-riscv64-linux-gnu
  • 開発環境からインターネットへ接続できること

3. 事前学習

本講義の受講生には、事前学習のために以下の書籍を配布します。

講義の時間が限られているため、当日はプロトコルスタック本体についての細かな説明はしません。上記の書籍に従ってLinuxのユーザ空間で動作するプロトコルスタックを実装し、全体の構成やパケット処理のフローを把握しておいてください。

なお、事前学習を最後まで進められなかった場合には、以下の手順で完成状態のコードを取得してください。

$ git clone https://github.com/pandax381/microps.git
$ cd microps
$ git checkout book

4. 講義内容

  1. 導入
    • xv6について
    • コードの取得
    • ビルド
    • xv6の起動
    • QEMUモニタ
    • xv6の終了
    • 再ビルド
  2. 下準備
    • 型定義の追加
    • コンソール出力の改良
    • 現在時刻の取得
    • カーネルスタックの拡張
    • 移植の土台作り
  3. 自作プロトコルスタックの移植(前編)
    • メインモジュール
    • Ethernetモジュール
    • virtio-netドライバ
    • ハードウェア割り込み
    • ソフトウェア割り込み
  4. 自作プロトコルスタックの移植(後編)
    • IPモジュール
    • ARPモジュール
    • ICMPモジュール
    • UDPモジュール
    • TCPモジュール
  5. ソケット
    • ファイルディスクリプタとの互換性
    • ソケット層の移植
    • システムコールの追加
    • 通信アプリケーション
  6. 応用課題(選択式)
    • A: タイマー機能の有効化
    • B: ソケットのTCP対応
    • C: pingコマンドの実装
    • D: インタフェース制御

1. 導入

この章では、自作プロトコルスタックの移植先となる教育用OS「xv6」を紹介し、以降の作業を進めるための開発環境を整えます。

まずxv6がどのようなOSなのかを知り、そのソースコードを手元に取得します。続いて、作業用のブランチを作成してxv6をビルドし、QEMU上で起動できることを確認します。あわせて、QEMUモニタの使い方や終了・再ビルドの手順といった、この先くり返し使う基本操作にも触れておきます。

ここでの目的は、まっさらなxv6を「ビルドして起動できる」状態にしておくことです。次章以降で、このxv6に少しずつ手を入れながら、自作プロトコルスタックを移植していきます。

1.1. xv6について

xv6は、UNIX Version 6 (V6)をIntel x86アーキテクチャ向けにANSI Cで再実装したもので、MIT(マサチューセッツ工科大学)の「6.828 Operating System Engineering」コースのために開発されたマルチプロセッサ対応の教育用OSです。

なお、x86向けの開発は既に終了しており、現在はRISC-V向けの「xv6-riscv」に置き換えられています。

本講義ではRISC-V向けのxv6-riscv(以下、xv6と呼ぶ)を対象に開発を行い、CPUエミュレータのQEMUを利用して動作確認を実施します。

1.2. コードの取得

xv6のコードはGitHubで公開されています。

まず、各自の開発環境(Linux)にxv6のコードを取得します。$WORKDIRには各自の環境に合わせてコードを保存するディレクトリを指定してください。

$ export WORKDIR=/path/to/dir
$ mkdir -p $WORKDIR
$ git clone https://github.com/mit-pdos/xv6-riscv.git $WORKDIR
$ cd $WORKDIR

Important

この先の作業は全て$WORKDIRの中で実施します。

続いて、作業用のブランチを作成します。xv6は単一のブランチで管理されているとともに日々コミットが追加されているため、講義資料を作成した時点で動作確認が取れているコミットをベースに、新しく作業用のブランチを作成して開発を行います。

$ git checkout -b seccamp2026 75c4638

1.3. ビルド

xv6はビルドツールに「Make」を採用しています。ビルドのルールはMakefileにあらかじめ記述されており、makeコマンドを実行するだけでビルドが始まります。

$ make

ビルドに成功するとkernelディレクトリの配下にカーネルが生成されます。

$ ls -l kernel/kernel
-rwxrwxr-x 1 pandax381 pandax381 273592  8月  7 18:42 kernel/kernel

Note

推奨環境のUbuntuでは、build-essentialのパッケージがインストールされていないとmakeコマンドやコンパイラのgccなどが見つからずエラーとなります。また、クロスコンパイル用のgcc-riscv64-linux-gnubinutils-riscv64-linux-gnuがインストールされていない場合には *** Error: Couldn't find a riscv64 version of GCC/binutils. というエラーとなります。いずれの場合もapt installで該当パッケージをインストール後、あらためてmakeコマンドでビルドを実行してください。

1.4. xv6の起動

QEMUを利用してxv6を起動します。Makefileの中にQEMUのパラメータ等を指定したターゲット(qemu)が用意されているので、これを指定してmakeコマンドを実行します。

$ make qemu

まずユーザランドのプログラムのビルドと、それらを格納したディスクイメージ「fs.img」の作成が行われます。

gcc -Wno-unknown-attributes -I. -o mkfs/mkfs mkfs/mkfs.c
riscv64-linux-gnu-gcc -Wall -Werror -Wno-unknown-attributes -O -fno-omit-frame-pointer -ggdb -gdwarf-2 -march=rv64gc -std=gnu99 -MD -mcmodel=medany -ffreestanding -fno-common -nostdlib -fno-builtin-strncpy -fno-builtin-strncmp -fno-builtin-strlen -fno-builtin-memset -fno-builtin-memmove -fno-builtin-memcmp -fno-builtin-log -fno-builtin-bzero -fno-builtin-strchr -fno-builtin-exit -fno-builtin-malloc -fno-builtin-putc -fno-builtin-free -fno-builtin-memcpy -Wno-main -fno-builtin-printf -fno-builtin-fprintf -fno-builtin-vprintf -I. -fno-stack-protector -fno-pie -no-pie   -c -o user/ulib.o user/ulib.c
perl user/usys.pl > user/usys.S
...
nmeta 47 (boot, super, log blocks 31, inode blocks 13, bitmap blocks 1) blocks 1953 total 2000
balloc: first 940 blocks have been allocated
balloc: write bitmap block at sector 46

続けてCLI版のQEMUが起動し、xv6がブートします。

qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

xv6 kernel is booting

hart 1 starting
hart 2 starting
init: starting sh
$

Note

推奨環境のUbuntuでは、qemu-syste-miscのパッケージがインストールされていないとqemu-system-riscv64が見つからずエラーとなります。apt installで該当パッケージをインストール後、あらためてmakeコマンドでビルドを実行してください。

ログイン認証はなく、そのままコンソールでシェルが起動します。xv6のシェルは最低限の機能しか備えていないため、TABキーによる入力補完はできません。lsコマンドを実行すると実行可能なバイナリ等が確認できます。

$ ls
.              1 1 1024
..             1 1 1024
README         2 2 2441
cat            2 3 35440
echo           2 4 34360
forktest       2 5 17336
grep           2 6 42800
init           2 7 34816
kill           2 8 34288
ln             2 9 34104
ls             2 10 41568
mkdir          2 11 34352
rm             2 12 34328
sh             2 13 56184
stressfs       2 14 35208
usertests      2 15 186584
grind          2 16 50576
wc             2 17 36384
zombie         2 18 33712
logstress      2 19 36256
forphan        2 20 35096
dorphan        2 21 34544
sync           2 22 33784
console        3 23 0

1.5. QEMUモニタ

xv6のコンソールでCtrl+Aに続けてCを入力(以降、Ctrl+A Cと表記)するとQEMUモニタに切り替わります。

$ QEMU 8.2.2 monitor - type 'help' for more information
(qemu) 

QEMUモニタでは、レジスタやメモリの内容を確認できたりデバッグに役立つ機能が盛り沢山です。

再度Ctrl+A Cを入力するとQEMUモニタからxv6のコンソールに切り替わります(トグル操作)。

1.6. xv6の終了

xv6にはシステム終了のためのコマンドが存在しません。コンソールでCtrl+A Xを入力するか、QEMUモニタでquitを実行してQEMUを終了させます。

1.7. 再ビルド

再ビルドを行う際には次の手順で実施してください。

$ make clean
$ make

make cleanは、ビルドで生成したオブジェクトファイル等を削除するために用意されているターゲットです。一部ファイルの変更が反映されない等の無用なトラブルを避けるためにも、必ずmake cleanでクリーンな状態にした後にビルドを実施することを推奨します。

2. 下準備

xv6に自作プロトコルスタックを移植するにあたって、いくつか下準備をしておきます。

自作プロトコルスタックは、通常のC言語プログラムとして標準ライブラリやビット幅指定の整数型、コンソール出力、時刻の取得といった機能を前提に書かれています。しかし、これらはxv6のカーネル内にそのままは存在しません。そこで移植を始める前に、本体のコードが期待する「足回り」をxv6の側に用意しておきます。

具体的には、ビット幅指定の整数型の追加、ログ出力に必要なprintk()の機能拡張、現在時刻を取得する仕組みの実装を順に行います。これらを土台として、最後に標準ライブラリを肩代わりする簡易libcと便利ライブラリを移植します。あわせて、深い呼び出しになりがちなプロトコル処理に耐えられるよう、カーネルスタックの拡張も行います。

この章を終えると、#include <stdio.h>のように書かれた本体のコードを、ほとんど手を加えずにxv6のカーネル内でビルドできる状態になります。

2.1. 型定義の追加

自作プロトコルスタックで多用しているビット幅指定の数値型(uint8_tuint32_t)などの型定義を追加しておきます。xv6では型定義をkernel/types.hに集約しているので、このファイルに追加します。

📝 kernel/types.h

...

typedef uint64 pde_t;
+
+#if defined(_STDIO_H)
+#define MKFS
+#endif
+
+#if !defined(MKFS)
+
+#define NULL ((void*)0)
+
+typedef char int8_t;
+typedef unsigned char uint8_t;
+typedef short int16_t;
+typedef unsigned short uint16_t;
+typedef int int32_t;
+typedef unsigned int uint32_t;
+typedef long int64_t;
+typedef unsigned long uint64_t;
+
+typedef int64_t ssize_t;
+typedef uint64_t size_t;
+
+typedef int64_t intptr_t;
+typedef uint64_t uintptr_t;
+
+#endif

Note

xv6を起動するためにmake qemuを実行すると、ユーザランドの実行ファイルなどを格納したディスクイメージ(fs.img)を作製するプログラム(mkfs)がコンパイルおよび実行されます。

このmkfsは、xv6ではなくホストで実行するプログラムであるため、コンパイル時にはホスト環境のヘッダファイルが読み込まれます。ホスト環境のヘッダファイルが読み込まれると、ここで追加している型定義との衝突が発生してしまうため、mkfsのコンパイル時のみ追加した型定義が読み込まれないようにしています。

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_tkernel/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

コンソール出力関数を改造した後も問題なくテキストが出力されていれば大丈夫です。

2.3. 現在時刻の取得

2.3.1. RTC(Real Time Clock)の利用

x86版のxv6にはRTCから時刻情報を取得するための関数(cmostime())が用意されていますが、RISC-V版のxv6にはRTC関連のコードは用意されていません。

QEMUはRISC-V環境向けにもRTCを提供してくれているのでこれを利用して現在時刻を取得する機能を追加します。

Note

RISC-V版のxv6ではタイマー割り込みを用いて100ミリ秒毎にtickをカウントする機能だけが存在しています。

QEMUがRISC-V環境で提供しているRTCは「Goldfish RTC」です。これはGoogleがAndroidエミュレータ向けに開発したMMIOベースのRTCで、マッピングされたメモリアドレスへアクセスすることでRTCのレジスタを読み出すことができます。

RTCの物理アドレスの定義

まず、QEMUが提供するRTCの物理アドレスをkernel/memlayout.hに定義します。

📝 kernel/memlayout.h

 // Physical memory layout

 // qemu -machine virt is set up like this,
 // based on qemu's hw/riscv/virt.c:
 //
 // 00001000 -- boot ROM, provided by qemu
+// 00101000 -- RTC
 // 02000000 -- CLINT
 // 0C000000 -- PLIC
 // 10000000 -- uart0
 // 10001000 -- virtio disk
 // 80000000 -- qemu's boot ROM loads the kernel here,
 //             then jumps here.
 // unused RAM after 80000000.

 // the kernel uses physical memory thus:
 // 80000000 -- entry.S, then kernel text and data
 // end -- start of kernel page allocation area
 // PHYSTOP -- end RAM used by the kernel

+// Goldfish RTC
+#define RTC 0x00101000L
+
 // qemu puts UART registers here in physical memory.
 #define UART0     0x10000000L
 #define UART0_IRQ 10

...

Note

0x00101000Lという値はRTCを提供するQEMUの仮想ハードウェアの仕様で決められています。

物理アドレスを仮想アドレスにマッピング

kernel/vm.cにあるkvmmake()の中に、RTCの物理アドレスを仮想アドレスにマッピングするためのコードを追加します。

📝 kernel/vm.c

...

 // Make a direct-map page table for the kernel.
 pagetable_t
 kvmmake(void)
 {
   pagetable_t kpgtbl;

   kpgtbl = (pagetable_t)kalloc();
   memset(kpgtbl, 0, PGSIZE);

+  // rtc registers
+  kvmmap(kpgtbl, RTC, RTC, PGSIZE, PTE_R | PTE_W);
+
   // uart registers
   kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

...
 }

...

Note

xv6カーネルではダイレクトマッピングが採用されており、マッピングする物理アドレスと仮想アドレスはどちらも同じ値となります。

RTCから時刻情報を読み出す関数

kernel/rtc.cを作成し、RTCから時刻情報を読み出す関数rtcread()を定義します。Goldfish RTCは8つの32bitレジスタを持ちますが、このうち時刻情報に関連するのはRTC_TIME_LOWRTC_TIME_HIGHの2つです。この2つのレジスタに、64bitの時刻情報を32bitづつ格納しています。リトルエンディアンの環境であればRTC_TIME_LOWからまとめて64bit読み出すことでRTCが保持している時刻情報をそのまま取得できます。

Note

64bitの時刻情報はUNIXエポック(1970年1月1日午前0時0分0秒 UTC)からの経過ナノ秒の値となっています。

📝 kernel/rtc.c

#include "types.h"
#include "riscv.h"
#include "memlayout.h"
#include "defs.h"

#define RTC_TIME_LOW 0x00
#define RTC_TIME_HIGH 0x04

uint64
rtcread(void)
{
  return *(volatile uint64 *)(RTC + RTC_TIME_LOW);
}

プロトタイプ宣言の追加

追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。

📝 kernel/defs.h

...
 int             either_copyout(int user_dst, uint64 dst, void *src, uint64 len);
 int             either_copyin(void *dst, int user_src, uint64 src, uint64 len);
 void            procdump(void);

+// rtc.c
+uint64          rtcread(void);

 // swtch.S
 void            swtch(struct context*, struct context*);

...

Makefileの修正

新しくソースファイルを追加したので、オブジェクトファイルのリスト(OBJS)に定義を追加します。

📝 Makefile

 K=kernel
 U=user

 OBJS = \
...
   $K/pipe.o \
   $K/exec.o \
   $K/sysfile.o \
   $K/kernelvec.o \
   $K/plic.o \
+  $K/rtc.o \
   $K/virtio_disk.o

...

動作確認

kernel/main.cmain()に動作確認用のコードを追加します。

  • rtcread()を呼び出して64bitの時刻情報を取得
  • 64bitの時刻情報を「秒」と「ナノ秒」に分けて出力

📝 kernel/main.c

...
 // start() jumps here in supervisor mode on all CPUs.
 void
 main()
 {
   if (cpuid() == 0) {
     consoleinit();
     printkinit();
     printk("\n");
     printk("xv6 kernel is booting\n");
     printk("\n");
     kinit();            // physical page allocator
     kvminit();          // create kernel page table
     kvminithart();      // turn on paging
     procinit();         // process table
     trapinit();         // trap vectors
     trapinithart();     // install kernel trap vector
     plicinit();         // set up interrupt controller
     plicinithart();     // ask PLIC for device interrupts
     binit();            // buffer cache
     iinit();            // inode table
     fileinit();         // file table
     virtio_disk_init(); // emulated hard disk
+    uint64 rtc = rtcread();
+    printk("%ld.%09ld\n", rtc / 1000000000, rtc % 1000000000);
     userinit();         // first user process
 
     __atomic_store_n(&started, 1, __ATOMIC_RELEASE);
   } else {
     while (__atomic_load_n(&started, __ATOMIC_ACQUIRE) == 0)
       ;
 
     printk("hart %d starting\n", cpuid());
     kvminithart();  // turn on paging
     trapinithart(); // install kernel trap vector
     plicinithart(); // ask PLIC for device interrupts
   }
 
   scheduler();
 }

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

1786522635.308689000
hart 1 starting
hart 2 starting
init: starting sh
$ 

シェルが立ち上がる前の起動ログの中に「秒.ナノ秒」の形式で時刻情報が出力されます。このうち秒の部分はUNIXタイムそのものです。

Tip

dateコマンドを利用するとUNIXタイムを任意の書式の時刻に変換できます。これを利用して出力されているUNIXタイムが正しい値かどうか検証してみましょう。開発環境のシェルで次のコマンドを実行してください。

$ date -d @1786522635 +"%Y/%m/%d %T"

2.3.2. 現在時刻を得る関数の追加

rtcread()でRTCから現在時刻を取得できるようになったので、これをベースにして現在時刻を得るために使われているtime()gettimeofday()を作成します。

型定義の追加

kernel/types.hに、time()が使用するtime_tの定義を追加します。

📝 kernel/types.h

 typedef unsigned int uint;
 typedef unsigned short ushort;
 typedef unsigned char uchar;

 typedef unsigned char uint8;
 typedef unsigned short uint16;
 typedef unsigned int uint32;
 typedef unsigned long uint64;

 typedef uint64 pde_t;

+typedef long time_t;
+
 #if defined(_STDIO_H)
 #define MKFS
 #endif

...

構造体定義の追加

新しくkernel/time.hを作成し、gettimeofday()が使用するstruct timevalを定義します。

📝 kernel/time.h

struct timeval {
  long tv_sec;
  long tv_usec;
};

関数の追加

新しくkernel/time.cを作成し、time()gettimeofday()関数を定義します。

📝 kernel/time.c

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "time.h"

time_t
time(time_t *t)
{
  time_t _t;
  if (!t)
    t = &_t;
  *t = rtcread() / 1000000000;
  return *t;
}

int
gettimeofday(struct timeval *tv, void *tz)
{
  (void)tz;
  uint64 rtc = rtcread();
  tv->tv_sec = rtc / 1000000000;
  tv->tv_usec = (rtc % 1000000000) / 1000;
  return 0;
}

プロトタイプ宣言の追加

追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。

📝 kernel/defs.h

 struct buf;
 struct context;
 struct file;
 struct inode;
 struct pipe;
 struct proc;
 struct spinlock;
 struct sleeplock;
 struct stat;
 struct superblock;
+struct timeval;

...

 // syscall.c
 void            argint(int, int*);
 int             argstr(int, char*, int);
 void            argaddr(int, uint64 *);
 int             fetchstr(uint64, char*, int);
 int             fetchaddr(uint64, uint64*);
 void            syscall();

+// time.c
+time_t          time(time_t*);
+int             gettimeofday(struct timeval*, void*);

 // trap.c
 extern uint     ticks;
 void            trapinit(void);
 void            trapinithart(void);
 extern struct spinlock tickslock;
 void            prepare_return(void);

...

Makefileの修正

新しくソースファイルを追加したので、オブジェクトファイルのリスト(OBJS)に定義を追加します。

📝 Makefile

 K=kernel
 U=user

 OBJS = \
...
   $K/sysfile.o \
   $K/kernelvec.o \
   $K/plic.o \
   $K/rtc.o \
+  $K/time.o \
   $K/virtio_disk.o

...

動作確認

kernel/main.cmain()にある動作確認用のコードを書き換えます。

Warning

gettimeofday()を使用する際はtime.hが必要なのでインクルードを忘れないようにしてください。

📝 kernel/main.c

 #include "types.h"
 #include "param.h"
 #include "memlayout.h"
 #include "riscv.h"
 #include "defs.h"
+#include "time.h"

 volatile static int started = 0;

 // start() jumps here in supervisor mode on all CPUs.
 void
 main()
 {
...
-    uint64 rtc = rtcread();
-    printk("%ld.%09ld\n", rtc / 1000000000, rtc % 1000000000);
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
...
 }

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

tv: {sec: 1786522939, usec: 308376}
hart 2 starting
hart 1 starting
init: starting sh
$

struct timeval型の変数が保持している秒とマイクロ秒が出力されます。

Note

ナノ秒の精度が必要な場合はstruct timespecを返すclock_gettime()を作成するといいでしょう。

2.3.3. カレンダー形式への変換

UNIXタイムのままだと可読性が乏しいため、カレンダー形式の日時情報を保持するstruct tmを追加します。加えて、UNIXタイムとカレンダー形式の値を相互に変換するための関数を作ります。

構造体定義の追加

カレンダー形式の日時情報を扱うための構造体(struct tm)の定義をkernel/time.hに追加します。

📝 kernel/time.h

 struct timeval {
   long tv_sec;
   long tv_usec;
 };
+
+struct tm {
+  int tm_sec;   // 0-60
+  int tm_min;   // 0-59
+  int tm_hour;  // 0-23
+  int tm_mday;  // 1-31
+  int tm_mon;   // 0-11
+  int tm_year;  // since 1900
+  int tm_wday;  // 0-6
+  int tm_yday;  // 0-365
+  int tm_isdst; // zero
+};

Important

struct tmを扱う際には以下の点に注意しましょう。

  • tm_mday(日)だけ1から始まる
  • tm_year(年)は1900年からの経過年数
  • tm_wday(曜日)は日曜日(0)から土曜日(6)までの値
  • tm_isdstはサマータイムに関する値(ここでは常にゼロにしておく)

関数の追加

ここでは、kernel/time.cへ以下の4つの関数を追加します。

  • isleapyear(): うるう年を判定するための関数(内部利用のみ)
  • ndays(): 指定した年月に存在する日数を得るための関数(内部利用のみ)
  • mktime(): カレンダー形式の値(struct tm)からUNIXタイム(time_t)へ変換する関数
  • localtime_r(): UNIXタイム(time_t)からカレンダー形式の値(struct tm)へ変換する関数

📝 kernel/time.c

 #include "types.h"
 #include "riscv.h"
 #include "defs.h"
 #include "time.h"
+
+#define TZ_OFFSET 9 //JST

...

 int
 gettimeofday(struct timeval *tv, void *tz)
 {
   (void)tz;
   uint64 rtc = rtcread();
   tv->tv_sec = rtc / 1000000000;
   tv->tv_usec = (rtc % 1000000000) / 1000;
   return 0;
 }

+static int
+isleapyear(int y)
+{
+  return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
+}
+
+static int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
+
+static int
+ndays(int y, int m)
+{
+  int n = days[m];
+
+  if (m == 1 && isleapyear(y)) {
+    n++;
+  }
+  return n;
+}
+
+time_t
+mktime(struct tm *tm)
+{
+  const int epoch = 1970;
+  time_t result = 0;
+
+  for (int y = epoch; y < tm->tm_year + 1900; y++) {
+    result += (isleapyear(y) ? 366 : 365) * 24 * 3600;
+  }
+  for (int m = 0; m < tm->tm_mon; m++) {
+    result += ndays(tm->tm_year + 1900, m) * 24 * 3600;
+  }
+  result += (tm->tm_mday - 1) * 24 * 3600;
+  result += tm->tm_hour * 3600;
+  result += tm->tm_min * 60;
+  result += tm->tm_sec;
+  result -= TZ_OFFSET * 3600;
+  return result;
+}
+
+struct tm *
+localtime_r(const time_t *timep, struct tm *result)
+{
+  time_t local_time;
+
+  local_time = *timep + (TZ_OFFSET * 3600);
+  result->tm_sec = local_time % 60;
+  local_time /= 60;
+  result->tm_min = local_time % 60;
+  local_time /= 60;
+  result->tm_hour = local_time % 24;
+  local_time /= 24;
+
+  int days = local_time;
+  result->tm_wday = (days + 4) % 7;
+
+  int y = 1970;
+  while (1) {
+    int n = isleapyear(y) ? 366 : 365;
+    if (days < n) {
+      break;
+    }
+    days -= n;
+    y++;
+  }
+  result->tm_year = y - 1900;
+  result->tm_yday = days;
+
+  int m = 0;
+  while (1) {
+    int n = ndays(y, m);
+    if (days < n) {
+      break;
+    }
+    days -= n;
+    m++;
+  }
+  result->tm_mon = m;
+  result->tm_mday = days + 1;
+  result->tm_isdst = 0;
+  return result;
+}

Warning

簡略化のためにカレンダー形式では固定的に日本のタイムゾーン(JST)に合わせた値を保持するようにしてます。

プロトタイプ宣言の追加

追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。

📝 kernel/defs.h

 struct buf;
 struct context;
 struct file;
 struct inode;
 struct pipe;
 struct proc;
 struct spinlock;
 struct sleeplock;
 struct stat;
 struct superblock;
 struct timeval;
+struct tm;

...


 // time.c
 time_t          time(time_t*);
 int             gettimeofday(struct timeval*, void*);
+time_t          mktime(struct tm*);
+struct tm*      localtime_r(const time_t*, struct tm*);

...

動作確認

kernel/main.cmain()に動作確認用のコードを追加します。

📝 kernel/main.c

...
     struct timeval tv;
     gettimeofday(&tv, NULL);
     printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
+    struct tm tm;
+    localtime_r(&tv.tv_sec, &tm);
+    printk("%04d/%02d/%02d %02d:%02d:%02d\n",
+      tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+    printk("%ld\n", mktime(&tm));
...

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

tv: {sec: 1786523184, usec: 308273}
2026/08/12 17:26:24
1786523184
hart 1 starting
hart 2 starting
init: starting sh
$

struct timevalの値に続けて、localtime_r()で取得したstruct tmの値を用いたカレンダー形式の日時情報と、mktime()struct tmから逆変換したUNIXタイムが出力されるはずです。

動作確認用コードの整理

動作確認が済んだら、main()に直接書いていた一時的なコードを整理します。カレンダー形式の日時表示は起動メッセージとして残しておくと便利なので、printdate()という関数にまとめます。それ以外の確認用の出力(struct timevalの生の値とmktime()による逆変換)は削除します。

📝 kernel/main.c

 #include "types.h"
 #include "param.h"
 #include "memlayout.h"
 #include "riscv.h"
 #include "defs.h"
 #include "time.h"

 volatile static int started = 0;

+static void
+printdate()
+{
+  struct timeval tv;
+  struct tm tm;
+  gettimeofday(&tv, NULL);
+  localtime_r(&tv.tv_sec, &tm);
+  printk("%04d/%02d/%02d %02d:%02d:%02d\n",
+    tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
+}
+
 // start() jumps here in supervisor mode on all CPUs.

...

     virtio_disk_init(); // emulated hard disk
-    struct timeval tv;
-    gettimeofday(&tv, NULL);
-    printk("tv: {sec: %ld, usec: %ld}\n", tv.tv_sec, tv.tv_usec);
-    struct tm tm;
-    localtime_r(&tv.tv_sec, &tm);
-    printk("%04d/%02d/%02d %02d:%02d:%02d\n",
-      tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
-    printk("%ld\n", mktime(&tm));
+    printdate();
     userinit();         // first user process

...

再ビルドして起動すると、起動メッセージに日時だけが表示されます。以降の章はこの状態を前提に進めます。

xv6 kernel is booting

2026/08/12 17:26:24
hart 1 starting
hart 2 starting
init: starting sh
$

2.4. カーネルスタックの拡張

自作プロトコルスタックを移植する前に、カーネルスタックのサイズを拡張します。

xv6は1プロセスあたり「1ページ(4KB)」のカーネルスタックを割り当てています。しかし自作プロトコルスタックは、送受信のたびに「ソケット層 → UDP/TCP → IP → デバイスドライバ」と深い関数呼び出しを重ね、各層がパケット組み立て用のバッファをローカル変数(スタック)に確保します。さらにログ出力(debugf等)も整形用のバッファをスタックに積みます。これらを合わせると 4KB を容易に超え、スタックが溢れてカーネルがクラッシュしてしまいます。

そこで、あらかじめカーネルスタックのサイズを「4ページ(16KB)」に拡張しておきます。

Important

xv6には2種類のカーネルスタックがあり、両方を拡張する必要があります。

  • プロセスごとのカーネルスタック: プロセスがシステムコールを実行するときに使われます
  • CPUごとのスタック(stack0: スケジューラや、CPUがアイドル状態のときに発生した割り込みの処理に使われます

パケットの送受信は、システムコール(例: sendto)からの送信と、割り込み駆動の受信・応答の両方で発生します。前者はプロセスのカーネルスタック、後者はCPUがアイドルならCPUごとのスタック上で走るため、これら両方を拡張する必要があります。

プロセスごとのカーネルスタックの拡張

プロセスのカーネルスタックは、kernel/memlayout.hKSTACKマクロで配置が決められています。スタックのページ数をKSTACK_PAGESとして定義し、スロットの間隔を「スタックNページ+ガードページ1枚」に変更します。

📝 kernel/memlayout.h

...
 // map kernel stacks beneath the trampoline,
 // each surrounded by invalid guard pages.
-#define KSTACK(p) (TRAMPOLINE - ((p) + 1) * 2 * PGSIZE)
+#define KSTACK_PAGES 4
+#define KSTACK(p) (TRAMPOLINE - ((p) + 1) * (KSTACK_PAGES + 1) * PGSIZE)
...

Note

ガードページ(未マップの1ページ)はスタックの直下に残るので、スタックがあふれて下限を突き抜けるとページフォルトで検出できる、というxv6の安全機構はそのまま維持されます。

kernel/proc.cproc_mapstacks()で、スタック領域をKSTACK_PAGES枚分マッピングします。kalloc()は4KBページを1枚ずつ返しますが、物理的には非連続でも、連続した仮想アドレスへマップすればスタックとして機能します。

📝 kernel/proc.c

...
 void
 proc_mapstacks(pagetable_t kpgtbl)
 {
   struct proc *p;

   for (p = proc; p < &proc[NPROC]; p++) {
-    char *pa = kalloc();
-    if (pa == 0)
-      panic("kalloc");
-    uint64 va = KSTACK((int)(p - proc));
-    kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
+    for (int j = 0; j < KSTACK_PAGES; j++) {
+      char *pa = kalloc();
+      if (pa == 0)
+        panic("kalloc");
+      uint64 va = KSTACK((int)(p - proc)) + j * PGSIZE;
+      kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
+    }
   }
 }
...

スタックポインタの初期値も、スタック頂上(KSTACK_PAGES枚上)を指すように変更します。ここで注意が必要なのは、プロセスのカーネルスタックポインタが「2ヶ所」で設定されている点です。

1つ目はkernel/proc.callocproc()で、これはカーネルスレッド(forkret)に切り替わるときに使われます。

📝 kernel/proc.c

...
 static struct proc *
 allocproc(void)
 {
...
   memset(&p->context, 0, sizeof(p->context));
   p->context.ra = (uint64)forkret;
-  p->context.sp = p->kstack + PGSIZE;
+  p->context.sp = p->kstack + KSTACK_PAGES * PGSIZE;

   return p;
 }
...

2つ目はkernel/trap.cprepare_return()で、こちらは「ユーザプロセスがシステムコールなどでカーネルに入るとき」に使われます。ここを直し忘れると、sendto()のようなシステムコール経由の送信だけが1ページのスタックで走ってしまい、溢れます。

📝 kernel/trap.c

 void 
 prepare_return(void)
 {
...
   p->trapframe->kernel_satp = r_satp();         // kernel page table
-  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
+  p->trapframe->kernel_sp = p->kstack + KSTACK_PAGES * PGSIZE; // process's kernel stack
   p->trapframe->kernel_trap = (uint64)usertrap;
   p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
...
 }

CPUごとのスタックの拡張

CPUごとのスタックstack0kernel/start.cで宣言されています。1CPUあたりのサイズをKSTACK_PAGES倍(4倍)にします。

📝 kernel/start.c

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"

void main();
void timerinit();

// entry.S needs one stack per CPU.
-__attribute__((aligned(16))) char stack0[4096 * NCPU];
+__attribute__((aligned(16))) char stack0[4096 * KSTACK_PAGES * NCPU];

...

stack0の配列を大きくするだけでは足りません。各CPUのスタックポインタはkernel/entry.Sで「stack0 + (hartid + 1) × 4096」と計算されており、1CPUあたりの間隔が4096バイト固定になっています。ここも合わせます。

📝 kernel/entry.S

         # set up a stack for C.
         # stack0 is declared in start.c,
         # with a 4096-byte stack per CPU.
         # sp = stack0 + ((hartid + 1) * 4096)
         la sp, stack0
-        li a0, 1024*4
+        li a0, 1024*4*4
         csrr a1, mhartid
         addi a1, a1, 1
         mul a0, a0, a1
         add sp, sp, a0

動作確認

再ビルドして、これまでどおりxv6が起動することを確認します。

$ make qemu

見た目の動作は変わりませんが、これでカーネルスタックに 16KB の余裕ができ、この先の自作プロトコルスタックが深い呼び出しやパケットバッファをスタック上に確保しても溢れなくなります。

2.5. 移植の土台作り

型定義・コンソール出力・時刻と足回りが揃ったので、自作プロトコルスタックのコードを受け入れるための土台を作ります。

ここでの大方針は「プロトコルスタック本体のコードはできる限り書き換えない」です。本体を書き換える代わりに、xv6の側に「本体が期待している環境」を用意して差を吸収します。ディレクトリ構成にもその方針がそのまま現れます。

kernel/net/                          ... プロトコルスタック本体(コードをそのまま配置)
kernel/net/platform/xv6-riscv/       ... プラットフォーム依存コード(xv6向けの差分はすべてここへ)
kernel/net/platform/xv6-riscv/libc/  ... 簡易libc(標準ライブラリの肩代わり)

この章では、簡易libcと便利ライブラリ(util.cutil.h)まで移植し、ログ出力が動くところまで確認します。

2.5.1. ディレクトリ構成とMakefile

ディレクトリの作成

上記のディレクトリを一括で作成します(libcの下にはシステムヘッダ用のsysサブディレクトリも必要です)。

$ mkdir -p kernel/net/platform/xv6-riscv/libc/sys

Makefileの修正

新しいディレクトリをMakefileに組み込みます。パスを表す変数(NPL)を定義し、インクルードパス・依存関係ファイルの取り込み・make cleanの対象に追加します。

📝 Makefile

 K=kernel
 U=user
+N=$K/net
+P=$N/platform/xv6-riscv
+L=$P/libc

...

 CFLAGS += -fno-builtin-printf -fno-builtin-fprintf -fno-builtin-vprintf
-CFLAGS += -I.
+CFLAGS += -I. -I $K -I $N -I $P -I $L
 CFLAGS += $(shell $(CC) -fno-stack-protector -E -x c /dev/null >/dev/null 2>&1 && echo -fno-stack-protector)

...

--include kernel/*.d user/*.d
+-include $K/*.d $U/*.d $N/*.d $P/*.d $L/*.d

 clean: 
 	rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \
 	*/*.o */*.d */*.asm */*.sym \
+	$N/*.o $N/*.d $P/*.o $P/*.d $L/*.o $L/*.d \
 	$K/kernel fs.img \
 	mkfs/mkfs .gdbinit \
        $U/usys.S \
 	$(UPROGS)

Important

-Iで指定したディレクトリは、#include "..."だけでなく#include <...>(山括弧)の検索でも、標準のシステムディレクトリより先に探索されます。「-I $Lを指定しておくと、<stdio.h>のような標準ヘッダの参照がlibcディレクトリのファイルで解決される」というこの性質が、次の簡易libcの仕掛けの核心です。あわせて追加した-I $Kにより、<types.h><time.h>がxv6のkernel/types.hkernel/time.hに解決されるようになります。

Note

-includeは、先頭の-が付いていない通常のincludeと違い「指定したファイルが存在しなくてもエラーにせず読み飛ばす」ディレクティブです。ここで取り込んでいる*.dは、コンパイル時にGCCが自動生成するヘッダ依存関係ファイルで、「どの.oがどのヘッダに依存しているか」が書かれています。これを取り込むことで、ヘッダを変更したときに関連する.oだけが再ビルドされるようになります。ただし一度もビルドしていない初回は.dがまだ存在しないため、通常のincludeだとエラーで止まってしまいます。-includeにしておくことで、初回は黙ってスキップし、生成された.dが2回目以降のビルドで効くようになります。今回はディレクトリを追加したので、その配下の.dも取り込み対象に加えています。

2.5.2. 簡易libcの実装

自作プロトコルスタックのコードは、通常のC言語プログラムとして<stdio.h><string.h>といった標準ヘッダをインクルードしています。しかし、xv6のカーネル空間に標準ライブラリはありません。

そこで、標準ヘッダと同じ名前のファイルをlibcディレクトリに配置します。前述のインクルードパスの性質により、本体のコードが#include <stdio.h>と書いたままで簡易libcのヘッダが読み込まれるため、プロトコルスタック側のコードを書き換えずにビルドが通るようになります。

なお、ここで作成するファイルの中身は「移植対象のプロトコルスタックが実際に使っている機能」だけを最小限にまとめます。

ファイル内容
stdio.hFILE型とfprintf()などの宣言(実装は後述のstdio.c
stdint.hビット幅指定の整数型の定数(型そのものは2.1でkernel/types.hに追加済み)
string.hxv6のkernel/string.cにある関数のプロトタイプ宣言
ctype.hisascii() / isprint()
errno.herrnoEINTR
limits.hINT_MAXなどの定数
stddef.h / stdlib.h / unistd.hほぼ空の受け皿(インクルードされてもエラーにしないため)
sys/types.hkernel/types.hへの橋渡し
sys/param.hMAX() / MIN()
sys/time.hkernel/time.hへの橋渡し

ポイントは「橋渡し」のヘッダです。たとえばsys/types.h#include <types.h>と書いてあるだけで、これが-I $Kによってxv6のkernel/types.hに解決されます。つまり、2.1で追加した型定義がそのままプロトコルスタック側から見えるようになります。同様にsys/time.hは2.3で作ったkernel/time.hへつながります。ここまでの作業が、簡易libcを通してプロトコルスタック本体へ供給されるという構図です。

ヘッダファイルの配置

まず橋渡しと定数だけの小さなヘッダファイル群を作成します。

📝 kernel/net/platform/xv6-riscv/libc/sys/types.h

#ifndef SYS_TYPES_H
#define SYS_TYPES_H

#include <types.h>

#endif

📝 kernel/net/platform/xv6-riscv/libc/sys/param.h

#ifndef SYS_PARAM_H
#define SYS_PARAM_H

#ifndef MAX
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#endif
#ifndef MIN
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#endif

#endif

📝 kernel/net/platform/xv6-riscv/libc/sys/time.h

#ifndef SYS_TIME_H
#define SYS_TIME_H

#include <time.h>

#endif

📝 kernel/net/platform/xv6-riscv/libc/stddef.h

#ifndef STDDEF_H
#define STDDEF_H

#include <sys/types.h>

#endif

📝 kernel/net/platform/xv6-riscv/libc/stdint.h

#ifndef STDINT_H
#define STDINT_H

#include <sys/types.h>

#define INT8_MAX   127
#define INT8_MIN   (-128)
#define UINT8_MAX  255
#define INT16_MAX  32767
#define INT16_MIN  (-32768)
#define UINT16_MAX 65535
#define INT32_MAX  2147483647
#define INT32_MIN  (-2147483648)
#define UINT32_MAX 4294967295U

#endif

📝 kernel/net/platform/xv6-riscv/libc/limits.h

#ifndef LIMITS_H
#define LIMITS_H

#define CHAR_BIT 8
#define INT_MAX  2147483647
#define INT_MIN  (-2147483648)
#define UINT_MAX 4294967295U
#define LONG_MAX 9223372036854775807L
#define LONG_MIN (-LONG_MAX - 1L)

#endif

📝 kernel/net/platform/xv6-riscv/libc/ctype.h

#ifndef CTYPE_H
#define CTYPE_H

#define isascii(x) ((x >= 0x00) && (x <= 0x7f))
#define isprint(x) ((x >= 0x20) && (x <= 0x7e))

#endif

📝 kernel/net/platform/xv6-riscv/libc/errno.h

#ifndef ERRNO_H
#define ERRNO_H

#define EINTR 4

extern int errno;

#endif

📝 kernel/net/platform/xv6-riscv/libc/stdlib.h

#ifndef STDLIB_H
#define STDLIB_H

#include <sys/types.h>

#endif

📝 kernel/net/platform/xv6-riscv/libc/unistd.h

#ifndef UNISTD_H
#define UNISTD_H

#include <sys/types.h>

#endif

string.hにはxv6が既に持っている文字列関数(kernel/string.c)のプロトタイプ宣言を書き並べます。実装はxv6のものをそのまま利用するので、宣言だけで済みます。

📝 kernel/net/platform/xv6-riscv/libc/string.h

#ifndef STRING_H
#define STRING_H

#include "types.h"

extern int
memcmp(const void *v1, const void *v2, uint n);
extern void *
memmove(void *dst, const void *src, uint n);
extern void *
memcpy(void *dst, const void *src, uint n);
extern void *
memset(void *dst, int c, uint n);
extern int
strlen(const char *s);
extern int
strncmp(const char *p, const char *q, uint n);
extern char *
strncpy(char *s, const char *t, int n);

#endif

stdio.hではFILE型と標準入出力関数を宣言します。snprintf()vsnprintf()の実体は2.2でprintk.cに実装済みなので、ここでは宣言だけです。

📝 kernel/net/platform/xv6-riscv/libc/stdio.h

#ifndef STDIO_H
#define STDIO_H

#include <stdarg.h>

#include <sys/types.h>

typedef struct _FILE FILE;

extern FILE *stderr;

extern int
fprintf(FILE *fp, const char *fmt, ...);
extern int
vfprintf(FILE *fp, const char *fmt, va_list ap);
extern int
snprintf(char *buf, size_t n, const char *fmt, ...);
extern int
vsnprintf(char *buf, size_t n, const char *fmt, va_list ap);
extern void
flockfile(FILE *fp);
extern void
funlockfile(FILE *fp);

#endif

標準入出力の実装

stdio.hで宣言した残りの関数(fprintf()vfprintf()flockfile()funlockfile())を実装します。

  • FILEの実体はスピンロックと再帰カウントを持つ構造体で、出力先としてはstderrだけを用意します
  • vfprintf()は、2.2で作ったvsnprintf()で文字列バッファに整形してからprintk()で出力します
  • flockfile() / funlockfile()はロックの獲得と解放です。複数行にわたる出力が他のログと混ざらないように、本体のログ出力関数がこれで囲んでいます

Important

flockfile()は再帰ロックとして実装しています。POSIXのflockfile()は同じスレッドからの再ロックを許容する仕様で、自作プロトコルスタックもこれを前提にしています。各プロトコルの*_print()ether_print()ip_print()など)はflockfile()で囲んだ中から、パケットの16進ダンプのためにhexdump()を呼ぶことがあり、hexdump()もまた内部でflockfile()します。ここで単純なスピンロックを使うと同じロックの二重取得によりpanic: acquireのエラーが発生してしまうため、holding()で「すでに自分が保持しているか」を判定し、再帰時はカウントを増やすだけにしています。funlockfile()はカウントが0になったときにだけ解放します。

📝 kernel/net/platform/xv6-riscv/libc/stdio.c

#include <stdio.h>
#include <stdarg.h>

#include "param.h"
#include "riscv.h"
#include "spinlock.h"
#include "defs.h"

struct _FILE {
    struct spinlock lock;
    int depth; /* recursion count (POSIX flockfile allows recursive locking) */
};

static FILE _stderr;

FILE *stderr = &_stderr;

void
flockfile(FILE *fp)
{
    if (holding(&fp->lock)) { /* already locked by this context: just recurse */
        fp->depth++;
        return;
    }
    acquire(&fp->lock);
    fp->depth = 1;
}

void
funlockfile(FILE *fp)
{
    if (--fp->depth == 0)
        release(&fp->lock);
}

int
vfprintf(FILE *fp, const char *fmt, va_list ap)
{
    char buf[256];
    int n;

    n = vsnprintf(buf, sizeof(buf), fmt, ap);
    printk("%s", buf);
    return n;
}

int
fprintf(FILE *fp, const char *fmt, ...)
{
    va_list ap;
    int n;

    va_start(ap, fmt);
    n = vfprintf(fp, fmt, ap);
    va_end(ap);
    return n;
}

Makefileの修正

stdio.cをビルド対象に追加します。

📝 Makefile

 OBJS = \
...
   $K/plic.o \
   $K/rtc.o \
   $K/time.o \
-  $K/virtio_disk.o
+  $K/virtio_disk.o \
+  $L/stdio.o

2.5.3. プラットフォームヘッダの用意

プロトコルスタック本体のコードは「プラットフォーム固有の機能はplatform.hをインクルードすれば使える」という約束で書かれています。xv6向けのplatform.hを用意します。

いまの時点では、xv6の基本ヘッダを取りまとめてインクルードするだけの薄いヘッダです。メモリ操作・割り込み・スケジューラなどのプラットフォーム依存コードは、本体の移植を進める中で必要になったタイミングで追加していきます。

📝 kernel/net/platform/xv6-riscv/platform.h

#ifndef PLATFORM_H
#define PLATFORM_H

#include "types.h"
#include "riscv.h"
#include "defs.h"

#endif

2.5.4. 便利ライブラリの移植

準備が整ったので、自作プロトコルスタックの便利ライブラリ(util.cutil.h)を移植します。ログ出力・16進ダンプ・キュー・バイトオーダー変換・チェックサム計算といった、本体の全モジュールが使う道具箱です。

strftimeの追加

便利ライブラリのログ出力関数lprintf()は、タイムスタンプの整形にstrftime()を使います。2.3で作った時刻関連コードの延長として、kernel/time.cに簡易版(書式は"%T"のみ対応)を追加します。

📝 kernel/time.c

 struct tm *
 localtime_r(const time_t *timep, struct tm *result)
 {
...
   result->tm_isdst = 0;
   return result;
 }
+
+size_t
+strftime(char *s, size_t max, const char *format, const struct tm *tm)
+{
+  (void)format; /* only supports "%T" */
+  return snprintf(s, max, "%02d:%02d:%02d", tm->tm_hour, tm->tm_min, tm->tm_sec);
+}

あわせてkernel/time.hも整備します。簡易libcのsys/time.hから<time.h>としてインクルードされ、プロトコルスタック本体からも参照されるヘッダになるので、インクルードガードを付けて時刻関連関数のプロトタイプ宣言をここに集約します。

📝 kernel/time.h

+#ifndef TIME_H
+#define TIME_H
+
+#include "types.h"
+
 struct timeval {
   long tv_sec;
   long tv_usec;
 };

...

   int tm_yday;  // 0-365
   int tm_isdst; // zero
 };
+
+extern time_t   time(time_t*);
+extern int      gettimeofday(struct timeval*, void*);
+extern time_t   mktime(struct tm*);
+extern struct tm* localtime_r(const time_t*, struct tm*);
+extern size_t   strftime(char*, size_t, const char*, const struct tm*);
+
+#endif

便利ライブラリの配置

ここからは、事前学習で開発したmicropsのコードをコピーして利用していきます。以降、手元のmicropsのディレクトリを$MICROPSと表記します。

$ export MICROPS=/path/to/microps

便利ライブラリのファイルをkernel/netディレクトリの直下にコピーします。

$ cp $MICROPS/util.{h,c} kernel/net/

コピーしたソースコードはそのまま利用できます。util.hの冒頭のインクルードが<stdio.h>のような標準ヘッダのままである点に注目してください。ここまでに用意した簡易libcのおかげで、コピーしたファイルがそのままビルドできます。

Makefileの修正

util.cをビルド対象に追加します。

📝 Makefile

 OBJS = \
...
   $K/rtc.o \
   $K/time.o \
   $K/virtio_disk.o \
+  $N/util.o \
   $L/stdio.o

2.5.5. 動作確認

便利ライブラリの動作確認のために、自作プロトコルスタックのメインモジュールに相当するファイルを追加して、ログ出力と16進ダンプをテストします。

Warning

このファイルは一時的なもので後ほど正式なものに差し替えます。

📝 kernel/net/net.c

#include "platform.h"

#include "util.h"

int
net_init(void)
{
    char msg[] = "Hello, SecCamp2026!";

    debugf("%s", msg);
    debugdump(msg, sizeof(msg));

    return 0;
}

netinit()はxv6のmain()関数から呼び出すことになるので、defs.hにプロトタイプ宣言を追加しておきます。

📝 kernel/defs.h

...

 // virtio_disk.c
 void            virtio_disk_init(void);
 void            virtio_disk_rw(struct buf *, int);
 void            virtio_disk_intr(void);

+// net/net.c
+int             net_init(void);
+
 // number of elements in fixed-size array
 #define NELEM(x) (sizeof(x)/sizeof((x)[0]))

...

MakefileのOBJSにnet.oを追加します。

Note

16進ダンプのためのdebugdump()マクロを有効にするためにCFLAGS-DHEXDUMPも追加しておきます。

📝 Makefile

 OBJS = \
...
   $K/virtio_disk.o \
   $N/util.o \
+  $N/net.o \
   $L/stdio.o

...

-CFLAGS += -I. -I $K -I $N -I $P -I $L
+CFLAGS += -I. -I $K -I $N -I $P -I $L -DHEXDUMP

xv6のmain()関数からnet_init()を呼び出すようにします。

📝 kernel/main.c

...
     virtio_disk_init(); // emulated hard disk
     printdate();
+    net_init();         // network stack
     userinit();         // first user process
...

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.123 [D] net_init: Hello, SecCamp2026! (kernel/net/net.c:10)
+------+-------------------------------------------------+------------------+
| 0000 | 48 65 6c 6c 6f 2c 20 53 65 63 43 61 6d 70 32 30 | Hello, SecCamp20 |
| 0010 | 32 36 21 00                                     | 26!.             |
+------+-------------------------------------------------+------------------+
hart 1 starting
hart 2 starting
init: starting sh
$

Note

見覚えのある書式のログメッセージと16進ダンプが出力されるはずです。プロトコルスタック本体のコードが、簡易libc越しにxv6のカーネル内で動いた瞬間です。

お疲れさまでした、これでプロトコルスタックを移植する準備が整いました!

3. 自作プロトコルスタックの移植(前編)

ここから自作プロトコルスタックのコードをxv6に移植していきます。作業量が多いため前編と後編にわけており、前編ではNICから読み取ったパケットをプロトコルスタックの受信キューに格納してソフトウェア割り込みを発生させるところまで進めます。

移植は「micropsのコードをcpでコピーして、必要な箇所だけ修正する」というスタイルで進めます。2.5で用意した簡易libcのおかげで、#include <stdio.h>のようなシステムヘッダのインクルードもそのままコンパイルが通るため、本体のコードはほとんどそのまま利用できます。修正が必要になるのは次の2種類だけで、該当するモジュールの節で差分を示します。

Important

本体のコードに加える修正は下記の2種類だけです。

  • 未移植モジュールの初期化処理のコメントアウト(net.c

    • 1モジュールずつ移植していく都合上の一時的な措置
    • 各モジュールの移植が完了するたびにコメントを解除していきます
  • 送信バッファのサイズ上限の縮小(ip.hIP_TOTAL_SIZE_MAX

    • 各層は送信パケット構築用のバッファをローカル変数(スタック)に確保します
    • 2.4でカーネルスタックを拡張しましたが、IP_TOTAL_SIZE_MAXの本来の値(UINT16_MAX = 約64KB)はそれでもスタックに載りません
    • Ethernetで一度に送れるのはMTU(1500バイト)までなので、この値に縮めます

プラットフォーム固有の処理(メモリ・ロック・割り込み・ドライバなど)は本体と分離してあるので、xv6向けの実装をplatform/xv6-riscvの下に追加していきます。

3.1. メインモジュール

自作プロトコルスタックのメインモジュールを移植します。まず、プロトコルスタック本体が必要とするプラットフォーム依存コードを実装し、その上にmicropsのメインモジュールを載せます。

プラットフォーム依存コードの実装

プロトコルスタック本体が利用するプラットフォーム依存の機能のうち、メモリ操作・ロック・乱数をplatform/xv6-riscvに実装します。まずplatform.hを、機能ごとのAPI宣言を持つ形に拡張します。

📝 kernel/net/platform/xv6-riscv/platform.h

 #ifndef PLATFORM_H
 #define PLATFORM_H
 
-#include "types.h"
+#include <sys/types.h>
 #include "riscv.h"
+#include "spinlock.h"
 #include "defs.h"
 
+extern int
+platform_init(void);
+extern int
+platform_run(void);
+extern int
+platform_shutdown(void);
+
+/*
+ * Memory
+ */
+
+extern void *
+memory_alloc(size_t size);
+extern void
+memory_free(void *ptr);
+
+/*
+ * Lock
+ */
+
+typedef struct spinlock lock_t;
+
+#define LOCK_INITIALIZER {0}
+
+extern int
+lock_init(lock_t *lock);
+extern int
+lock_acquire(lock_t *lock);
+extern int
+lock_release(lock_t *lock);
+
+/*
+ * Random
+ */
+
+extern uint16_t
+random16(void);
+
 #endif

実装をplatform.cとして追加します。

  • メモリ操作: kalloc()/kfree()で実装します
  • ロック: lock_tはxv6のスピンロックの別名で、各関数も対応するスピンロック操作を呼ぶだけです
  • 乱数: RTCから読んだ時刻をシードにした線形合同法(LCG)の簡易実装です

📝 kernel/net/platform/xv6-riscv/platform.c

#include "platform.h"

static uint32 seed = 1;

int
platform_init(void)
{
    seed = rtcread();
    return 0;
}

int
platform_run(void)
{
    return 0;
}

int
platform_shutdown(void)
{
    return 0;
}

/*
 * Memory
 */

void *
memory_alloc(size_t size)
{
    void *p;

    if (PGSIZE < size) {
        return NULL;
    }
    p = kalloc();
    if (p) {
        memset(p, 0, size);
    }
    return p;
}

void
memory_free(void *ptr)
{
    kfree(ptr);
}

/*
 * Lock
 */

int
lock_init(lock_t *lock)
{
    initlock(lock, "");
    return 0;
}

int
lock_acquire(lock_t *lock)
{
    acquire(lock);
    return 0;
}

int
lock_release(lock_t *lock)
{
    release(lock);
    return 0;
}

/*
 * Random
 */

uint16_t
random16(void)
{
    /* Linear Congruential Generator (LCG) */
    seed = seed * 1103515245 + 12345;
    return (seed >> 16) & 0xffff;
}

Note

xv6のカーネル内ではメモリをkalloc()で確保します。確保するサイズは指定できず、常にページサイズ(4KB)のメモリが返されます。自作プロトコルスタックが一度に確保するメモリは4KBを超えないため、memory_alloc()kalloc()したページをゼロ初期化して返すだけの実装で済ませています。

MakefileのOBJSにplatform.oを追加します。

📝 Makefile

 OBJS = \
...
   $K/virtio_disk.o \
   $N/util.o \
   $N/net.o \
+  $P/platform.o \
   $L/stdio.o

暫定コードの削除

2.5で暫定的に作成したkernel/net/net.cを削除します。

$ rm kernel/net/net.c

メインモジュールのコードのコピー

メインモジュールのコードをmicropsからコピーします。

$ cp $MICROPS/net.{h,c} kernel/net/

1モジュールずつ移植していく都合上、まだ移植していないモジュールの初期化処理をコメントアウトしておきます。この後、各モジュールの移植が完了するたびにコメントを解除していきます。

📝 kernel/net/net.c

...

-#include "arp.h"
-#include "ip.h"
-#include "icmp.h"
-#include "udp.h"
-#include "tcp.h"
+//#include "arp.h"
+//#include "ip.h"
+//#include "icmp.h"
+//#include "udp.h"
+//#include "tcp.h"
 
 int
 net_init(void)
 {
     infof("initialize...");
     if (platform_init() == -1) {
         errorf("platform_init() failure");
         return -1;
     }
-    if (arp_init() == -1) {
-        errorf("arp_init() failure");
-        return -1;
-    }
-    if (ip_init() == -1) {
-        errorf("ip_init() failure");
-        return -1;
-    }
-    if (icmp_init() == -1) {
-        errorf("icmp_init() failure");
-        return -1;
-    }
-    if (udp_init() == -1) {
-        errorf("udp_init() failure");
-        return -1;
-    }
-    if (tcp_init() == -1) {
-        errorf("tcp_init() failure");
-        return -1;
-    }
+//    if (arp_init() == -1) {
+//        errorf("arp_init() failure");
+//        return -1;
+//    }
+//    if (ip_init() == -1) {
+//        errorf("ip_init() failure");
+//        return -1;
+//    }
+//    if (icmp_init() == -1) {
+//        errorf("icmp_init() failure");
+//        return -1;
+//    }
+//    if (udp_init() == -1) {
+//        errorf("udp_init() failure");
+//        return -1;
+//    }
+//    if (tcp_init() == -1) {
+//        errorf("tcp_init() failure");
+//        return -1;
+//    }
     infof("success");
     return 0;
 }

...

割り込み関連コードのスタブ

メインモジュールはソフトウェア割り込みのハンドラ登録などで割り込み関連のAPI(intr_xxx())を必要とします。実体は3.4(ハードウェア割り込み)と3.5(ソフトウェア割り込み)で実装するので、ここではビルドを通すためのスタブを用意しておきます。

ソフトウェア割り込みのIRQ番号INTR_IRQ_SOFTは、PLICの割り込みソース番号(1〜53)と衝突しないよう、あらかじめ範囲外の64と定義しておきます(この値を選ぶ理由は3.5で説明します)。

📝 kernel/net/platform/xv6-riscv/intr.h

#ifndef INTR_H
#define INTR_H

#define INTR_IRQ_SOFT 64 /* out of PLIC source range (1-53) */

#define INTR_IRQ_SHARED 0x0001

typedef void (*intr_isr_t)(unsigned int irq, void *arg);

extern int
intr_register(unsigned int irq, intr_isr_t isr, int flags, void *arg);
extern int
intr_raise(unsigned int irq);

extern int
intr_init(void);
extern int
intr_run(void);
extern int
intr_shutdown(void);

#endif

📝 kernel/net/platform/xv6-riscv/intr.c

/* stub: to be implemented in the software interrupt step */

#include "platform.h"

int
intr_register(unsigned int irq, intr_isr_t isr, int flags, void *arg)
{
    return 0;
}

int
intr_raise(unsigned int irq)
{
    return 0;
}

int
intr_init(void)
{
    return 0;
}

int
intr_run(void)
{
    return 0;
}

int
intr_shutdown(void)
{
    return 0;
}

platform.hの末尾でintr.hを読み込むようにします。

📝 kernel/net/platform/xv6-riscv/platform.h

...

 extern uint16_t
 random16(void);
 
+#include "intr.h"
+
 #endif

platform_init()platform_run()に割り込みの初期化・起動処理を組み込みます。ソフトウェア割り込みが発生した際にメインモジュールのnet_softirq_handler()が呼び出されるようにハンドラを登録します(いまはスタブなのでまだ何も起きません)。

📝 kernel/net/platform/xv6-riscv/platform.c

 #include "platform.h"
 
+#include "util.h"
+#include "net.h"
+
 static uint32 seed = 1;
 
 int
 platform_init(void)
 {
     seed = rtcread();
+    if (intr_init() == -1) {
+        return -1;
+    }
+    intr_register(INTR_IRQ_SOFT, net_softirq_handler, 0, NULL);
     return 0;
 }
 
 int
 platform_run(void)
 {
+    if (intr_run() == -1) {
+        return -1;
+    }
     return 0;
 }

...

MakefileのOBJSにintr.oを追加します(net.oは暫定コードで追加したものがそのまま使われます)。

📝 Makefile

 OBJS = \
...
   $N/util.o \
   $N/net.o \
   $P/platform.o \
+  $P/intr.o \
   $L/stdio.o

プロトタイプ宣言の追加

kernel/defs.hにあるnet_init()のプロトタイプ宣言の下にnet_run()の宣言を追加します。

📝 kernel/defs.h

...

 // net/net.c
 int             net_init(void);
+int             net_run(void);
 
...

プロトコルスタックの起動

kernel/main.cmain()で、net_init()に続けてnet_run()を呼び出すようにします。

📝 kernel/main.c

...
     virtio_disk_init(); // emulated hard disk
     printdate();
     net_init();         // network stack
+    net_run();          // start networking
     userinit();         // first user process
...

動作確認

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_init: success (kernel/net/net.c:302)
10:00:00.103 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.104 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

Note

プロトコルスタックの初期化(net_init)に続けて起動処理(net_run)が実行されていることが確認できます。まだネットワークデバイスが1つも登録されていないため、これ以上の出力はありません。

3.2. Ethernetモジュール

自作プロトコルスタックのEthernetモジュールを移植します。

Ethernetモジュールのコードのコピー

Ethernetモジュールのコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/ether.{h,c} kernel/net/

不足しているlibc関数の追加

ether.cether_addr_pton()は、MACアドレスの文字列(xx:xx:xx:xx:xx:xx)の解析にstrtol()を使用しています。簡易libcにはまだこの関数がないため、stdlib.cを作成してstdlib.hにプロトタイプ宣言を追加します。

📝 kernel/net/platform/xv6-riscv/libc/stdlib.c

#include <stdlib.h>

long
strtol(const char *s, char **endptr, int base)
{
    int neg = 0;
    long val = 0;

    // gobble initial whitespace
    while (*s == ' ' || *s == '\t')
        s++;

    // plus/minus sign
    if (*s == '+')
        s++;
    else if (*s == '-')
        s++, neg = 1;

    // hex or octal base prefix
    if ((base == 0 || base == 16) && (s[0] == '0' && s[1] == 'x'))
        s += 2, base = 16;
    else if (base == 0 && s[0] == '0')
        s++, base = 8;
    else if (base == 0)
        base = 10;

    // digits
    while (1) {
        int dig;

        if (*s >= '0' && *s <= '9')
            dig = *s - '0';
        else if (*s >= 'a' && *s <= 'z')
            dig = *s - 'a' + 10;
        else if (*s >= 'A' && *s <= 'Z')
            dig = *s - 'A' + 10;
        else
            break;
        if (dig >= base)
            break;
        s++, val = (val * base) + dig;
        // we don't properly detect overflow!
    }

    if (endptr)
        *endptr = (char *) s;
    return (neg ? -val : val);
}

📝 kernel/net/platform/xv6-riscv/libc/stdlib.h

 #ifndef STDLIB_H
 #define STDLIB_H
 
 #include <sys/types.h>
 
+extern long
+strtol(const char *s, char **endptr, int base);
+
 #endif

Makefileの修正

ether.ostdlib.oをビルド対象に追加します。

📝 Makefile

 OBJS = \
...
   $N/util.o \
   $N/net.o \
+  $N/ether.o \
   $P/platform.o \
   $P/intr.o \
-  $L/stdio.o
+  $L/stdio.o \
+  $L/stdlib.o

一旦、この状態でビルドが通ることを確認しておきましょう。

$ make

3.3. virtio-netドライバ

QEMUが提供する「virtio-net」デバイスのドライバを作成します。

Note

virtio-netをはじめとするvirtioデバイスは、物理的なハードウェアをソフトウェアで完全にエミュレートするのとは異なり、ゲストOS(のドライバ)が相手を仮想デバイスだと認識した上でハイパーバイザーと協調して効率的に動作するように設計されています。

Note

RISC-V版のxv6では、ディスク装置にvirtio-diskを利用しており、既にvirtioデバイスを扱うためのコードが含まれています。これを参考にして、新たにvirtio-netに対応させていきます。

virtio-netデバイスの接続

まず、QEMUを起動した際にゲストであるxv6に対してvirtio-netデバイスが提供されるようにMakefileを修正します。

📝 Makefile

...

 ifndef CPUS
 CPUS := 3
 endif
 
+TAPDEV=tap0
+TAPADDR=192.0.2.1/24
+
 QEMUOPTS = -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic
 QEMUOPTS += -global virtio-mmio.force-legacy=false
 QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0
 QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

+QEMUOPTS += -netdev tap,ifname=$(TAPDEV),id=en0,script=no,downscript=no
+QEMUOPTS += -device virtio-net-device,netdev=en0,csum=off,gso=off,guest_csum=off,bus=virtio-mmio-bus.1
+
+tap:
+	@ip addr show $(TAPDEV) 2>/dev/null || (echo "Create '$(TAPDEV)'"; \
+		sudo ip tuntap add mode tap user $(USER) name $(TAPDEV); \
+		sudo sysctl -w net.ipv6.conf.$(TAPDEV).disable_ipv6=1; \
+		sudo ip addr add $(TAPADDR) dev $(TAPDEV); \
+		sudo ip link set $(TAPDEV) up; \
+		ip addr show $(TAPDEV); \
+	)
+
-qemu: check-qemu-version $K/kernel fs.img
+qemu: check-qemu-version $K/kernel fs.img tap
 	$(QEMU) $(QEMUOPTS)

...

Note

QEMUの起動オプションを記述するQEMUOPTSに対してvirtio-netを用いたネットワークデバイスを追加する設定を加えます。このネットワークデバイスはホスト側のTapデバイスに接続するようにします。

Note

virtioのトランスポート仕様にはPCIとMMIOがありますが、ここではPCIよりも実装しやすいMMIOを採用しています。

Note

QEMUは起動時にTapデバイスを作成しますが、IPアドレスの設定まではやってくれません。make qemuを実行した際にTapデバイスが存在しなければ作成してIPアドレスを設定する処理も合わせて追加しています。

再ビルドした後、make qemuを実行してxv6を起動させます。

Note

QEMUを起動する前にTapデバイスを作成してIPアドレスが設定されるはずです。

Create 'tap0'
net.ipv6.conf.tap0.disable_ipv6 = 1
79: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
    link/ether 16:2a:af:19:b5:91 brd ff:ff:ff:ff:ff:ff
    inet 192.0.2.1/24 scope global tap0
       valid_lft forever preferred_lft forever
...

Note

続けてQEMUが起動してくれば大丈夫です。起動に失敗した場合はQEMUOPTへの追加内容にミスがないか確認してください。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_init: success (kernel/net/net.c:302)
10:00:00.103 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.104 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

MMIOアドレスのマッピング

QEMUが提供するvirtio-netデバイスをxv6で扱えるようにするための設定を行います。これは同じMMIOデバイスであるGoldfish RTCを扱えるようにした時と同じ作業です。

Note

Goldfish RTCと同様にvirtioデバイスの物理アドレスもQEMU側で決められており、1つめのvirtioデバイスが0x10001000、2つめのvirtioデバイスが0x10002000に配置されます。

📝 kernel/memlayout.h

 // 0C000000 -- PLIC
 // 10000000 -- uart0
 // 10001000 -- virtio disk
+// 10002000 -- virtio network
 // 80000000 -- qemu's boot ROM loads the kernel here,
 //             then jumps here.
 // unused RAM after 80000000.

...

 // virtio mmio interface
 #define VIRTIO0     0x10001000
 #define VIRTIO0_IRQ 1
+#define VIRTIO1     0x10002000
+#define VIRTIO1_IRQ 2

...

📝 kernel/vm.c

...
 pagetable_t
 kvmmake(void)
 {
...
   // virtio mmio disk interface
   kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
 
+  // virtio mmio network interface
+  kvmmap(kpgtbl, VIRTIO1, VIRTIO1, PGSIZE, PTE_R | PTE_W);
+
   // PLIC
   kvmmap(kpgtbl, PLIC, PLIC, 0x4000000, PTE_R | PTE_W);
...
 }
...

virtio-netデバイスのドライバを追加

ドライバのコードはplatform/xv6-riscvの下にdriverディレクトリを作成して配置します。

$ mkdir -p kernel/net/platform/xv6-riscv/driver

まず、ドライバが外部へ公開する関数の宣言を持つヘッダファイルを追加します。

📝 kernel/net/platform/xv6-riscv/driver/virtio_net.h

#ifndef VIRTIO_NET_H
#define VIRTIO_NET_H

#include "net.h"

extern struct net_device *
virtio_net_init(void);

#endif

続いてドライバの本体です。

📝 kernel/net/platform/xv6-riscv/driver/virtio_net.c

#include <stdio.h>
#include <stdint.h>
#include <string.h>

#include "platform.h"

#include "memlayout.h"
#include "virtio.h"

#include "util.h"
#include "net.h"
#include "ether.h"

#include "driver/virtio_net.h"

#define R(r) ((volatile uint32_t *)(VIRTIO1 + (r)))

/*
 * virtq
 */

struct virtq {
    struct virtq_desc *desc;
    struct virtq_avail *avail;
    struct virtq_used *used;
    int num;
    int last_used_idx;
    char *free;
};

static void
virtq_init(struct virtq *vq, int sel, int num)
{
    uint32_t max;

    // select queue
    *R(VIRTIO_MMIO_QUEUE_SEL) = sel;

    // ensure selected queue is not in use.
    if (*R(VIRTIO_MMIO_QUEUE_READY)) {
        panic("queue already in use");
    }

    // check maximum queue size.
    max = *R(VIRTIO_MMIO_QUEUE_NUM_MAX);
    if (max == 0) {
        panic("queue not available");
    }
    if (max < num) {
        panic("queue too short");
    }

    // allocate and zero queue memory.
    vq->desc = kalloc();
    vq->avail = kalloc();
    vq->used = kalloc();
    vq->free = kalloc();
    if (!vq->desc || !vq->avail || !vq->used || !vq->free) {
        panic("kalloc failed");
    }
    memset(vq->desc, 0, PGSIZE);
    memset(vq->avail, 0, PGSIZE);
    memset(vq->used, 0, PGSIZE);
    memset(vq->free, 0,  PGSIZE);

    // set queue size.
    vq->num = num;
    *R(VIRTIO_MMIO_QUEUE_NUM) = vq->num;

    // write physical addresses.
    *R(VIRTIO_MMIO_QUEUE_DESC_LOW) = (uint64_t)vq->desc;
    *R(VIRTIO_MMIO_QUEUE_DESC_HIGH) = (uint64_t)vq->desc >> 32;
    *R(VIRTIO_MMIO_DRIVER_DESC_LOW) = (uint64_t)vq->avail;
    *R(VIRTIO_MMIO_DRIVER_DESC_HIGH) = (uint64_t)vq->avail >> 32;
    *R(VIRTIO_MMIO_DEVICE_DESC_LOW) = (uint64_t)vq->used;
    *R(VIRTIO_MMIO_DEVICE_DESC_HIGH) = (uint64_t)vq->used >> 32;

    // queue is ready.
    *R(VIRTIO_MMIO_QUEUE_READY) = 1;

    // all descriptors start out unused.
    for (int i = 0; i < vq->num; i++) {
        vq->free[i] = 1;
    }
    vq->last_used_idx = 0;
}

// find a free descriptor, mark it non-free, return its index.
static int
virtq_alloc_desc(struct virtq *vq)
{
    for (int i = 0; i < vq->num; i++) {
        if (vq->free[i]) {
            vq->free[i] = 0;
            return i;
        }
    }
    return -1;
}

// mark a descriptor as free.
static void
virtq_free_desc(struct virtq *vq, int i)
{
    if (i >= vq->num) {
        panic("virtq_free_desc: invalid index");
    }
    if (vq->free[i]) {
        panic("virtq_free_desc: freeing free descriptor");
    }
    vq->desc[i].addr = 0;
    vq->desc[i].len = 0;
    vq->desc[i].flags = 0;
    vq->desc[i].next = 0;
    vq->free[i] = 1;
}

/*
 * virtio-net
 */

#define VIRTIO_NET_IRQ VIRTIO1_IRQ

#define RXQ 0
#define TXQ 1

#define QSIZE NUM
#define BUF_SIZE 2048

#define VIRTIO_MMIO_CONFIG 0x100
#define VIRTIO_NET_F_CSUM 0
#define VIRTIO_NET_F_GUEST_CSUM 1
#define VIRTIO_NET_F_MAC 5

struct virtio_net {
    struct net_device *dev;
    uint32_t status;
    uint64_t features;
    lock_t lock;
    struct virtq rx_q;
    struct virtq tx_q;
    char rx_bufs[QSIZE][BUF_SIZE];
    char tx_bufs[QSIZE][BUF_SIZE];
} _nic0;

struct virtio_net_hdr {
#define VIRTIO_NET_HDR_F_NEEDS_CSUM 1
#define VIRTIO_NET_HDR_F_DATA_VALID 2
#define VIRTIO_NET_HDR_F_RSC_INFO 4
    uint8_t flags;
#define VIRTIO_NET_HDR_GSO_NONE 0
#define VIRTIO_NET_HDR_GSO_TCPV4 1
#define VIRTIO_NET_HDR_GSO_UDP 3
#define VIRTIO_NET_HDR_GSO_TCPV6 4
#define VIRTIO_NET_HDR_GSO_ECN 0x80
    uint8_t gso_type;
    uint16_t hdr_len;
    uint16_t gso_size;
    uint16_t csum_start;
    uint16_t csum_offset;
    uint16_t num_buffers;
};

#define PRIV(x) ((struct virtio_net *)(x)->priv)

static int
virtio_net_open(struct net_device *dev)
{
    struct virtio_net *nic = PRIV(dev);
    char addr[ETHER_ADDR_STR_LEN];

    lock_acquire(&nic->lock);

    // set receive buffers
    for (int i = 0; i < QSIZE; i++) {
        nic->rx_q.desc[i].addr = (uint64_t)nic->rx_bufs[i];
        nic->rx_q.desc[i].len = BUF_SIZE;
        nic->rx_q.desc[i].flags = VRING_DESC_F_WRITE;
        nic->rx_q.avail->ring[i] = i;
        nic->rx_q.free[i] = 0;
    }
    __atomic_thread_fence(__ATOMIC_SEQ_CST);
    nic->rx_q.avail->idx = QSIZE;
    nic->rx_q.last_used_idx = 0;

    // tell device we're completely ready.
    nic->status |= VIRTIO_CONFIG_S_DRIVER_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // Notify the device of new RX buffers.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = RXQ;

    lock_release(&nic->lock);

    infof("dev=%s, addr=%s", dev->name, ether_addr_ntop(dev->addr, addr, sizeof(addr)));
    return 0;
}

static int
virtio_net_close(struct net_device *dev)
{
    struct virtio_net *nic = PRIV(dev);

    lock_acquire(&nic->lock);

    nic->status = *R(VIRTIO_MMIO_STATUS);

    // clear DRIVER_OK bit
    nic->status &= ~VIRTIO_CONFIG_S_DRIVER_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    lock_release(&nic->lock);

    infof("dev=%s", dev->name);
    return 0;
}

static ssize_t
virtio_net_write(struct net_device *dev, const uint8_t *frame, size_t flen)
{
    struct virtio_net *nic = PRIV(dev);
    int idx;
    struct virtio_net_hdr *hdr;

    if (sizeof(*hdr) + flen > BUF_SIZE) {
        errorf("frame too long, dev=%s, flen=%zu", dev->name, flen);
        return -1;
    }

    lock_acquire(&nic->lock);

    // Allocate descriptor
    idx = virtq_alloc_desc(&nic->tx_q);
    if (idx == -1) {
        lock_release(&nic->lock);
        errorf("no free descriptor, dev=%s", dev->name);
        return -1;
    }

    // Setup virtio-net header.
    hdr = (struct virtio_net_hdr *)nic->tx_bufs[idx];
    memset(hdr, 0, sizeof(*hdr));
    hdr->gso_type = VIRTIO_NET_HDR_GSO_NONE;
    memcpy(hdr+1, frame, flen);

    // Configure descriptor
    nic->tx_q.desc[idx].addr = (uint64_t)nic->tx_bufs[idx];
    nic->tx_q.desc[idx].len = sizeof(*hdr) + flen;
    nic->tx_q.desc[idx].flags = 0; // read by device

    // Deploy descriptor in the available ring.
    nic->tx_q.avail->ring[nic->tx_q.avail->idx % nic->tx_q.num] = idx;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);
    nic->tx_q.avail->idx++;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    lock_release(&nic->lock);

    // Notify the device of a new TX packet.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = TXQ;

    return flen;
}

static int
virtio_net_output(struct net_device *dev, uint16_t type, const uint8_t *buf, size_t len, const void *dst)
{
    uint8_t frame[ETHER_FRAME_SIZE_MAX] = {};
    struct ether_hdr *hdr;
    size_t flen, pad = 0;

    hdr = (struct ether_hdr *)frame;
    memcpy(hdr->dst, dst, ETHER_ADDR_LEN);
    memcpy(hdr->src, dev->addr, ETHER_ADDR_LEN);
    hdr->type = hton16(type);
    memcpy(hdr+1, buf, len);
    if (len < ETHER_PAYLOAD_SIZE_MIN) {
        pad = ETHER_PAYLOAD_SIZE_MIN - len;
    }
    flen = sizeof(*hdr) + len + pad;
    debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, flen);
    ether_print(frame, flen);
    if (virtio_net_write(dev, frame, flen) == -1) {
        return -1;
    }
    return 0;
}

static int
virtio_net_input(struct net_device *dev, uint8_t *frame, size_t flen)
{
    struct ether_hdr *hdr;
    uint16_t type;

    if (flen < (ssize_t)sizeof(*hdr)) {
        errorf("too short");
        return -1;
    }
    hdr = (struct ether_hdr *)frame;
    if (memcmp(dev->addr, hdr->dst, ETHER_ADDR_LEN) != 0) {
        if (memcmp(ETHER_ADDR_BROADCAST, hdr->dst, ETHER_ADDR_LEN) != 0) {
            /* for other host */
            return -1;
        }
    }
    type = ntoh16(hdr->type);
    debugf("dev=%s, type=0x%04x, len=%zd", dev->name, type, flen);
    ether_print(frame, flen);
    return net_input(type, (uint8_t *)(hdr+1), flen - sizeof(*hdr), dev);
}

static void
virtio_net_isr(unsigned int irq, void *arg)
{
    struct net_device *dev;
    struct virtio_net *nic;

    (void)irq;
    dev = (struct net_device *)arg;
    nic = PRIV(dev);

    lock_acquire(&nic->lock);

    // Acknowledge the interrupt and clear the status by writing it back.
    *R(VIRTIO_MMIO_INTERRUPT_ACK) = *R(VIRTIO_MMIO_INTERRUPT_STATUS) & 0x3;
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    // Process completed descriptors from the tx used ring.
    while (nic->tx_q.last_used_idx != nic->tx_q.used->idx) {
        int ring_idx = nic->tx_q.last_used_idx % nic->tx_q.num;
        int idx = nic->tx_q.used->ring[ring_idx].id;
        virtq_free_desc(&nic->tx_q, idx);
        nic->tx_q.last_used_idx++;
    }

    // Process incoming packets from the rx used ring.
    while (nic->rx_q.last_used_idx != nic->rx_q.used->idx) {
        int ring_idx = nic->rx_q.last_used_idx % nic->rx_q.num;
        int idx = nic->rx_q.used->ring[ring_idx].id;
        int len = nic->rx_q.used->ring[ring_idx].len;
        int hdrlen = sizeof(struct virtio_net_hdr);
        if (len < hdrlen || len - hdrlen > ETHER_FRAME_SIZE_MAX) {
            errorf("invalid length, dev=%s, len=%d", dev->name, len);
        } else {
            virtio_net_input(dev, (uint8_t *)nic->rx_bufs[idx] + hdrlen, len - hdrlen);
        }
        // Recycle the receive buffer descriptor.
        nic->rx_q.desc[idx].addr = (uint64_t)nic->rx_bufs[idx];
        nic->rx_q.desc[idx].len = BUF_SIZE;
        nic->rx_q.avail->ring[nic->rx_q.avail->idx % nic->rx_q.num] = idx;
        __atomic_thread_fence(__ATOMIC_SEQ_CST);
        nic->rx_q.avail->idx++;
        nic->rx_q.last_used_idx++;
    }
    __atomic_thread_fence(__ATOMIC_SEQ_CST);

    lock_release(&nic->lock);

    // Notify the device of new RX buffers.
    *R(VIRTIO_MMIO_QUEUE_NOTIFY) = RXQ;
}

static struct net_device_ops virtio_net_ops = {
    .open = virtio_net_open,
    .close = virtio_net_close,
    .output = virtio_net_output,
};

struct net_device *
virtio_net_init(void)
{
    struct virtio_net *nic = &_nic0;
    struct net_device *dev;

    lock_init(&nic->lock);

    // find virtio-net device
    if (*R(VIRTIO_MMIO_MAGIC_VALUE) != 0x74726976 ||
        *R(VIRTIO_MMIO_VERSION) != 2 ||
        *R(VIRTIO_MMIO_DEVICE_ID) != 1 || // network device
        *R(VIRTIO_MMIO_VENDOR_ID) != 0x554d4551) {
        errorf("device not found");
        return NULL;
    }

    debugf("device found");

    // reset device
    nic->status = 0;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // set ACKNOWLEDGE status bit
    nic->status |= VIRTIO_CONFIG_S_ACKNOWLEDGE;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // set DRIVER status bit
    nic->status |= VIRTIO_CONFIG_S_DRIVER;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // negotiate features
    nic->features = *R(VIRTIO_MMIO_DEVICE_FEATURES);
    nic->features &= ~(1ULL << VIRTIO_RING_F_EVENT_IDX);
    nic->features &= ~(1ULL << VIRTIO_NET_F_CSUM);
    nic->features &= ~(1ULL << VIRTIO_NET_F_GUEST_CSUM);
    *R(VIRTIO_MMIO_DRIVER_FEATURES) = nic->features;

    // tell device that feature negotiation is complete.
    nic->status |= VIRTIO_CONFIG_S_FEATURES_OK;
    *R(VIRTIO_MMIO_STATUS) = nic->status;

    // re-read status to ensure FEATURES_OK is set.
    if (!(*R(VIRTIO_MMIO_STATUS) & VIRTIO_CONFIG_S_FEATURES_OK)) {
        errorf("FEATURES_OK failed");
        return NULL;
    }

    // initialize TXQ/RXQ
    virtq_init(&nic->tx_q, TXQ, QSIZE);
    virtq_init(&nic->rx_q, RXQ, QSIZE);

    // setup device driver structure
    dev = net_device_alloc();
    if (!dev) {
        errorf("net_device_alloc() failure");
        return NULL;
    }
    dev->type = NET_DEVICE_TYPE_ETHERNET;
    dev->mtu = ETHER_PAYLOAD_SIZE_MAX;
    dev->flags = (NET_DEVICE_FLAG_BROADCAST | NET_DEVICE_FLAG_NEED_ARP);
    dev->hlen = ETHER_HDR_SIZE;
    dev->alen = ETHER_ADDR_LEN;
    memcpy(dev->broadcast, ETHER_ADDR_BROADCAST, ETHER_ADDR_LEN);
    if (nic->features & (1 << VIRTIO_NET_F_MAC)) {
        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
            dev->addr[i] = *(volatile uint8_t *)((uint64_t)(VIRTIO1 + VIRTIO_MMIO_CONFIG + i));
        }
    } else {
        errorf("device does not provide a MAC address");
        return NULL;
    }
    dev->ops = &virtio_net_ops;
    dev->priv = nic;
    if (net_device_register(dev) == -1) {
        errorf("net_device_register() failure");
        return NULL;
    }
    nic->dev = dev;
    if (intr_register(VIRTIO_NET_IRQ, virtio_net_isr, INTR_IRQ_SHARED, dev) == -1) {
        errorf("intr_register() failure");
        return NULL;
    }
    return dev;
}

Note

virtioデバイスの初期化の流れ。

  • STATUSレジスタに0を書き込んでデバイスをリセットする。
  • STATUSレジスタにACKNOWLEDGEビットをセットする。
  • STATUSレジスタにDRIVERビットをセットする。
  • デバイスとドライバの間で機能をネゴシエーションする。
    • DEVICE_FEATURESレジスタを読んでデバイスが提供する機能を取得する。
    • DEVICE_FEATURESで提供された機能のうち、使用する機能だけを選び(使わない機能を落として)DRIVER_FEATURESにセットする。
  • STATUSレジスタにFEATURES_OKをセットする。
  • STATUSレジスタを読み取ってFEATURES_OKがセットされていることを確認する。
    • FEATURES_OKがセットされていなかったらネゴシエーション失敗。
  • 送受信キューの初期化
  • 受信キューにパケットを格納するためのバッファを設定
  • STATUSレジスタにDRIVER_OKをセットする。

Note

ネゴシエーション時の注意点。

  • VIRTIO_RING_F_EVENT_IDXフラグを取り除いています。このフラグが設定されているとイベント通知が最適化され、ドライバ側で適切に対処しないと次のイベント通知が受け取れなくなってしまいます。
  • チェックサムオフロード関連のフラグを取り除いています。VIRTIO_NET_F_CSUM(送信オフロード)とVIRTIO_NET_F_GUEST_CSUM(受信オフロード)をいずれも無効化しています。これらのオフロードが有効だと、デバイスがチェックサムの計算・検証を肩代わりし、パケットがチェックサム未計算のままドライバへ渡されることがあります。その状態だと自作プロトコルスタックのチェックサム検証(IP/ICMP/UDP/TCP)が成立しません。あわせて、QEMUの起動オプションでもcsum=off,guest_csum=offを指定し、デバイス側でオフロードを提供しない設定にしています。

Note

送受信キュー初期化の流れ。

  • QUEUE_SELレジスタに初期化したいキューのインデックス(0から始まる番号)を書き込む。
  • QUEUE_READYレジスタを読み取る
    • 読み取った値が0でなかったらそのキューは既に初期化済みで使用されているので失敗。
  • QUEUE_NUM_MAXレジスタを読み取ってデバイスがサポートするキューの最大サイズを取得する
    • 値が0の場合、そのインデックスのキューは存在しない。
  • キューを構成する3つの領域(Descriptor Table, Available Ring, Used Ring)それぞれにメモリを確保する。モダンなvirtio(MMIOバージョン2)では領域ごとに独立した物理アドレスをレジスタで通知できるため、3つの領域が物理的に連続している必要はない(本ドライバも各領域を個別にkalloc()している)。
  • QUEUE_NUMレジスタにドライバが使用するキューのサイズ(ディスクリプタ数)を書き込む。
  • 確保した各領域の物理アドレスを、対応するレジスタに書き込んでデバイスに通知する。
  • QUEUE_READYレジスタに1を書き込み、キューを有効化する。

Note

virtioのキューを構成する3つの領域について。

  • Descriptor Table
    • 送受信したいデータの実体(パケットなど)がメモリのどこにあるのか、そのアドレスとサイズを保持する。
  • Available Ring
    • ドライバは処理してほしいディスクリプタの番号をこのリングに書き込み、デバイスに新しく処理を要求する。
  • Used Ring
    • デバイスは処理が終わったディスクリプタの番号をこのリングに書き込み、ドライバに処理が完了したことを通知する。

プロトタイプ宣言の追加

追加した関数のプロトタイプ宣言をkernel/defs.hに追加します。

📝 kernel/defs.h

...

 struct spinlock;
 struct sleeplock;
 struct stat;
 struct superblock;
+struct net_device;
 struct timeval;
 struct tm;

...

 // net/net.c
 int             net_init(void);
 int             net_run(void);
 
+// net/platform/xv6-riscv/driver/virtio_net.c
+struct net_device* virtio_net_init(void);
+
 // number of elements in fixed-size array
 #define NELEM(x) (sizeof(x) / sizeof((x)[0]))

ドライバの初期化関数の呼び出し

kernel/main.cmain()virtio_net_init()の呼び出しを追加します。追加する場所はnet_init()net_run()のあいだです。

📝 kernel/main.c

...

     virtio_disk_init(); // emulated hard disk
     printdate();
     net_init();         // network stack
+    virtio_net_init();  // emulated network card
     net_run();          // start networking
     userinit();         // first user process

...

Makefileの修正

ソースファイルを追加したのでMakefileを修正します。

📝 Makefile

 OBJS = \
...
   $P/platform.o \
   $P/intr.o \
+  $P/driver/virtio_net.o \
   $L/stdio.o \
   $L/stdlib.o

...

--include $K/*.d $U/*.d $N/*.d $P/*.d $L/*.d
+-include $K/*.d $U/*.d $N/*.d $P/*.d $P/driver/*.d $L/*.d

 clean: 
 	rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg \
 	*/*.o */*.d */*.asm */*.sym \
-	$N/*.o $N/*.d $P/*.o $P/*.d $L/*.o $L/*.d \
+	$N/*.o $N/*.d $P/*.o $P/*.d $P/driver/*.o $P/driver/*.d $L/*.o $L/*.d \
 	$K/kernel fs.img \

...

動作確認

再ビルドした後、make qemuを実行してxv6を起動させます。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_init: success (kernel/net/net.c:302)
10:00:00.103 [D] virtio_net_init: device found (kernel/net/platform/xv6-riscv/driver/virtio_net.c:394)
10:00:00.104 [I] net_device_register: success, dev=net0, type=0x0002 (kernel/net/net.c:58)
10:00:00.105 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.106 [I] virtio_net_open: dev=net0, addr=52:54:00:12:34:56 (kernel/net/platform/xv6-riscv/driver/virtio_net.c:195)
10:00:00.107 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

Note

出力されているログからvirtio-netデバイスの初期化とプロトコルスタックへの登録に成功していることが分かります。

Important

この時点ではまだパケットを受信できません。受信の割り込みハンドラ(virtio_net_isr())はintr_register()で登録済みですが、それを呼び出すハードウェア割り込みの仕組み(PLICの設定とトラップからのディスパッチ)は次の3.4で実装します。それまでは、デバイスが受信キューにパケットを積んで割り込みを上げても、virtio_net_isr()が起動されないためパケットは処理されません。実際にパケットを受信できるようになるのは3.4を終えてからです。

3.4. ハードウェア割り込み

virtio-netデバイスの初期化に成功しましたが、まだ割り込みを捕捉するための設定が出来ていないため、デバイスにパケットが届いても何も起きません。ここではデバイスからの通知であるハードウェア割り込みを扱えるようにします。

Note

RISC-Vでは、デバイスからの外部割り込みを「PLIC(Platform-Level Interrupt Controller)」で扱います。PLICは複数の割り込み元をまとめてCPU(hart)へ配送するコントローラで、x86でいうI/O APICに相当します。タイマ割り込みやソフトウェア割り込みはPLICを経由せず、CPUコアごとに直接扱われます。

PLICの設定

PLIC関連のコードはkernel/plic.cにあります。既にuartとvirtio-diskの割り込みを捕捉できるようにするためのコードがあるので、これを参考にしてvirtio-netの割り込みも受け付けるようにします。

📝 kernel/plic.c

 void
 plicinit(void)
 {
   // set desired IRQ priorities non-zero (otherwise disabled).
   *(uint32 *)(PLIC + UART0_IRQ * 4) = 1;
   *(uint32 *)(PLIC + VIRTIO0_IRQ * 4) = 1;
+  *(uint32 *)(PLIC + VIRTIO1_IRQ * 4) = 1;
 }
 
 void
 plicinithart(void)
 {
   int hart = cpuid();
 
   // set enable bits for this hart's S-mode
   // for the uart and virtio disk.
-  *(uint32 *)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);
+  *(uint32 *)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ) | (1 << VIRTIO1_IRQ);
 
   // set this hart's S-mode priority threshold to 0.
   *(uint32 *)PLIC_SPRIORITY(hart) = 0;
 }

Note

plicinit()では割り込みの優先度を設定し、plicinithart()では有効にする割り込みを設定します。

割り込みハンドラの登録と呼び出し

virtio-netドライバは、初期化時にintr_register()で自身の割り込みハンドラ(virtio_net_isr())を登録しています。3.1で用意したintr.cはスタブのままだったので、ハンドラの登録(intr_register())と、割り込み番号に応じた登録済みハンドラの呼び出し(intr_dispatch())を実装します。

📝 kernel/net/platform/xv6-riscv/intr.c

-/* stub: to be implemented in the software interrupt step */
+#include <stdio.h>
 
 #include "platform.h"
 
+#include "util.h"
+
+struct irq_entry {
+    struct irq_entry *next;
+    unsigned int irq;
+    intr_isr_t isr;
+    int flags;
+    void *arg;
+};
+
+/*
+ * NOTE: if you want to add/delete the entries after intr_run(),
+ *       you need to protect these lists with a mutex.
+ */
+static struct irq_entry *irqs;
+
 int
 intr_register(unsigned int irq, intr_isr_t isr, int flags, void *arg)
 {
+    struct irq_entry *entry;
+
+    for (entry = irqs; entry; entry = entry->next) {
+        if (entry->irq == irq) {
+            if (entry->flags ^ INTR_IRQ_SHARED || flags ^ INTR_IRQ_SHARED) {
+                errorf("conflicts with already registered IRQs, irq=%u", irq);
+                return -1;
+            }
+        }
+    }
+    entry = memory_alloc(sizeof(*entry));
+    if (!entry) {
+        errorf("memory_alloc() failure");
+        return -1;
+    }
+    entry->irq = irq;
+    entry->isr = isr;
+    entry->flags = flags;
+    entry->arg = arg;
+    entry->next = irqs;
+    irqs = entry;
+    infof("success, irq=%u", irq);
     return 0;
 }
 
 int
 intr_raise(unsigned int irq)
 {
     return 0;
 }
 
+/*
+ * NOTE: called from devintr() in kernel/trap.c
+ */
+void
+intr_dispatch(unsigned int irq)
+{
+    struct irq_entry *entry;
+
+    for (entry = irqs; entry; entry = entry->next) {
+        if (entry->irq == irq) {
+            entry->isr(entry->irq, entry->arg);
+        }
+    }
+}
+
 int
 intr_init(void)
 {
     return 0;
 }

...

intr_dispatch()はxv6側の割り込み処理から呼び出すため、kernel/defs.hにプロトタイプ宣言を追加します。

📝 kernel/defs.h

...

 // net/net.c
 int             net_init(void);
 int             net_run(void);
 
+// net/platform/xv6-riscv/intr.c
+void            intr_dispatch(unsigned int);
+
 // net/platform/xv6-riscv/driver/virtio_net.c
 struct net_device* virtio_net_init(void);

...

割り込みの捕捉

割り込み関連のコードはkernel/trap.cにあります。devintr()の中にuartとvirtio-diskの割り込みを捕捉するコードがあるので、これを参考にしてvirtio-netの割り込みを捕捉したらintr_dispatch()を呼び出すようにします。

📝 kernel/trap.c

...
 int
 devintr()
 {
...
     if (irq == UART0_IRQ) {
       uartintr();
     } else if (irq == VIRTIO0_IRQ) {
       virtio_disk_intr();
+    } else if (irq == VIRTIO1_IRQ) {
+      intr_dispatch(irq);
     } else if (irq) {
       printk("unexpected interrupt irq=%d\n", irq);
     }
...
 }

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。起動したら、開発環境で別のシェルを開き192.0.2.2に対してpingを実行します。

$ ping -c 3 192.0.2.2

割り込みの設定が正しく行えていれば、virtio-netデバイスからの割り込みを捕捉してパケットを受信できるようになっているはずです。xv6のコンソールに受信したフレームの内容が出力されます。

10:05:00.101 [D] virtio_net_input: dev=net0, type=0x0806, len=42 (kernel/net/platform/xv6-riscv/driver/virtio_net.c:315)
        src: 16:2a:af:19:b5:91
        dst: ff:ff:ff:ff:ff:ff
       type: 0x0806 (ARP)
10:05:00.102 [D] net_input: dev=net0, type=0x0806, len=28 (kernel/net/net.c:232)
+------+-------------------------------------------------+------------------+
| 0000 | 00 01 08 00 06 04 00 01 16 2a af 19 b5 91 c0 00 | .........*...... |
| 0010 | 02 01 00 00 00 00 00 00 c0 00 02 02             | ............     |
+------+-------------------------------------------------+------------------+

Note

pingはホスト側がARPで192.0.2.2のMACアドレスを解決できず失敗したままで大丈夫です。ARPを処理するモジュールはまだ移植していないため、受信したフレーム(ARP要求)はnet_input()まで届いた後、対応するプロトコルが見つからず破棄されます。ここでは「割り込み経由でパケットを受信できたこと」が確認できれば成功です。

3.5. ソフトウェア割り込み

Supervisor Software Interrupt を利用して、自作プロトコルスタックが必要とするソフトウェア割り込み機能を作ります。ハードウェア割り込みの処理(ISR)はパケットを受信キューへ積むだけの最小限に留め、時間のかかるプロトコル処理はソフトウェア割り込みへ遅延させる、という役割分担です。

Note

RISC-Vの割り込みは「ソフトウェア割り込み」「タイマ割り込み」「外部割り込み」の3種類に分かれており、それぞれが特権レベルごとに用意されています。このうち Supervisor Software Interrupt は、スーパバイザ(S)モードのソフトウェア割り込みです。外部割り込みがデバイス起因なのに対し、ソフトウェア割り込みは「ソフトウェアが自分自身に対して発生させる割り込み」で、sipレジスタのSSIPビットを立てることで発火し、sieレジスタのSSIEビットで有効/無効を制御します。本来はマルチコア間の通知(IPI)などに使われる仕組みですが、ここでは「重い処理を後回しにするために、あとで割り込みハンドラを起動させる」ための手段として利用します。

Supervisor Software Interrupt を扱えるようにする

スーパバイザレベルで処理待ちの割り込みを示すsipレジスタと、割り込みの有効/無効を制御するsieレジスタについて、Supervisor Software Interrupt に対応するビットの定義をkernel/riscv.hに追加します。

📝 kernel/riscv.h

...

 // Supervisor Interrupt Pending
+#define SIP_SSIP (1L << 1) // software
 static inline uint64
 r_sip()
 {

...

 // Supervisor Interrupt Enable
 #define SIE_SEIE (1L << 9) // external
 #define SIE_STIE (1L << 5) // timer
+#define SIE_SSIE (1L << 1) // software
 static inline uint64
 r_sie()
 {

...

Note

どちらもビット1が Software Interrupt に対応することはRISC-Vの仕様で定められています。

CPUの起動時にソフトウェア割り込みが有効になるように、kernel/start.csieレジスタのSIE_SSIEビットをセットします。

📝 kernel/start.c

...
 void
 start()
 {
...

   // delegate all interrupts and exceptions to supervisor mode.
   w_medeleg(0xffff);
   w_mideleg(0xffff);
-  w_sie(r_sie() | SIE_SEIE | SIE_STIE);
+  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

...
 }
...

ソフトウェア割り込みの発生と種別の管理

Supervisor Software Interrupt そのものは「発生したかどうか」しか分かりません。自作プロトコルスタックでは、ソフトウェア割り込みの発生理由(どのIRQに対するものか)を自前のビットマップ(pending変数)で管理します。

3.1でスタブにしていたintr_raise()を実装します。あわせて、保留中のソフトウェア割り込みを処理するintr_soft_dispatch()を追加します。

📝 kernel/net/platform/xv6-riscv/intr.c

...

 /*
  * NOTE: if you want to add/delete the entries after intr_run(),
  *       you need to protect these lists with a mutex.
  */
 static struct irq_entry *irqs;
 
+static lock_t pendinglock = LOCK_INITIALIZER;
+static uint64_t pending;
+
 int
 intr_register(unsigned int irq, intr_isr_t isr, int flags, void *arg)
 {

...

+/*
+ * NOTE: only accepts soft IRQs (single bit value out of PLIC source range)
+ */
 int
 intr_raise(unsigned int irq)
 {
+    lock_acquire(&pendinglock);
+    pending |= irq;
+    lock_release(&pendinglock);
+    w_sip(r_sip() | SIP_SSIP);
     return 0;
 }

...

+/*
+ * NOTE: called from devintr() in kernel/trap.c
+ */
+void
+intr_soft_dispatch(void)
+{
+    uint64_t irqs, irq;
+
+    // clear SSIP before taking the pending snapshot, so that an irq
+    // raised while the ISRs run re-triggers the software interrupt.
+    w_sip(r_sip() & ~SIP_SSIP);
+
+    lock_acquire(&pendinglock);
+    irqs = pending;
+    pending = 0;
+    lock_release(&pendinglock);
+
+    for (irq = 1; irqs; irq <<= 1) {
+        if (irqs & irq) {
+            intr_dispatch(irq);
+            irqs &= ~irq;
+        }
+    }
+}
+
 int
 intr_init(void)
 {

...

Important

intr_raise()が受け付けるのはソフトウェア割り込みのIRQ番号だけです。ソフトウェア割り込みのIRQ番号はビットマップで管理する都合上、PLICの割り込みソース番号(1〜53)の範囲外で、かつ1ビットだけ立った値にする必要があります(3.1でINTR_IRQ_SOFT64と定義しておいたのはこのためです)。

Note

intr_raise()pendingにビットを立てたあと、w_sip(r_sip() | SIP_SSIP)で現在のCPUコアに対して Supervisor Software Interrupt を保留状態に設定します。これにより、割り込みが有効になったタイミングでソフトウェア割り込みが発生します。

Note

intr_soft_dispatch()は冒頭でw_sip(r_sip() & ~SIP_SSIP)を実行し、保留中のSSIPをクリアします。これは「今回のソフトウェア割り込みを受け付けた」というアックで、クリアしないと同じ割り込みが繰り返し発火してしまいます。重要なのは、このクリアをpendingのスナップショットを取る前に行っている点です。こうしておくと、スナップショット後(ハンドラ実行中)に新たなintr_raise()が呼ばれた場合、SSIPが改めてセットされ、今回のディスパッチが終わってトラップから戻ったあとに再びソフトウェア割り込みが発生します。もし順序が逆(スナップショットの後にクリア)だと、その間に立ったSSIPを消してしまい、新しいIRQを取りこぼす恐れがあります。

intr_soft_dispatch()はxv6側の割り込み処理から呼び出すため、kernel/defs.hにプロトタイプ宣言を追加します。

📝 kernel/defs.h

...

 // net/platform/xv6-riscv/intr.c
 void            intr_dispatch(unsigned int);
+void            intr_soft_dispatch(void);
 
 // net/platform/xv6-riscv/driver/virtio_net.c
 struct net_device* virtio_net_init(void);

...

Supervisor Software Interruptの捕捉

kernel/trap.cdevintr()で Supervisor Software Interrupt を捕捉してintr_soft_dispatch()を呼び出すようにします。

📝 kernel/trap.c

...
 int
 devintr()
 {
...
     if (irq)
       plic_complete(irq);

     return 1;
+  } else if (scause == 0x8000000000000001L) {
+    // software interrupt.
+    intr_soft_dispatch();
+    return 1;
   } else if (scause == 0x8000000000000005L) {
     // timer interrupt.
...
 }

Note

scause0x8000000000000001Lなら Supervisor Software Interrupt が発生しています。

  • 最上位ビットが1 … 割り込みが発生していることを示しています。0だった場合は例外が発生していることを示しています。
  • 下位の1L … 割り込みの種別が Software Interrupt であることを示しています。

ビルドの確認

ここで実装したソフトウェア割り込みが実際に使われるのは、受信キューを持つプロトコルモジュール(IPやARP)を移植してからです。ここではビルドが通ることを確認して次へ進みます。

$ make

4. 自作プロトコルスタックの移植(後編)

自作プロトコルスタックの残りのモジュール(IP・ARP・ICMP・UDP・TCP)を順次xv6へ移植していきます。

前編と同じく、micropsのコードをcpでコピーして必要な箇所だけ修正するスタイルで進めます。各モジュールは相互に依存していますが、net.cの初期化呼び出しをコメントアウトで制御することで、1モジュールずつ動作を確認しながら移植できます。

Note

本体への修正は、ip.hIP_TOTAL_SIZE_MAXをMTU(1500)に縮める点だけです(4.1で実施)。これによりip.cicmp.cudp.ctcp.cはいずれもそのまま移植できます。ip.cだけは、まだ移植していないARP・ICMPへの依存を一時的にコメントアウトし、後続の節で解除していきます。プラットフォーム依存のコード(タイマー・スケジューラなど)は必要になったタイミングでplatform/xv6-riscvに追加します。

4.1. IPモジュール

IPモジュールを移植します。IPモジュールは他のプロトコル(ARP・ICMP・UDP・TCP)から参照される土台になるので、後半で最初に移植します。

IPモジュールのコードのコピー

IPモジュールのコードをmicropsからコピーします。

$ cp $MICROPS/ip.{h,c} kernel/net/

コピーしたら、2種類の修正を加えます。

1つ目は、ip.hIP_TOTAL_SIZE_MAXの縮小です。ip_output()は送信パケットをuint8_t buf[IP_TOTAL_SIZE_MAX]というスタック上のバッファに組み立てます。この定数の本来の値(UINT16_MAX = 64KB)は、拡張したカーネルスタックにも載りません。Ethernetで一度に送れるのはMTU(1500バイト)までであり、IPの断片化もサポートしないため、この値をMTUまで縮めて対処します。

📝 kernel/net/ip.h

 #define IP_HDR_SIZE_MIN 20
 #define IP_HDR_SIZE_MAX 60
 
-#define IP_TOTAL_SIZE_MAX UINT16_MAX /* maximum value of uint16 */
+#define IP_TOTAL_SIZE_MAX 1500 /* MTU */
 #define IP_PAYLOAD_SIZE_MAX (IP_TOTAL_SIZE_MAX - IP_HDR_SIZE_MIN)

Note

IP_PAYLOAD_SIZE_MAXIP_TOTAL_SIZE_MAXから算出されるので、ICMP・UDP・TCPが送信バッファに使うサイズも連動して小さくなります。これらのモジュールはこの定数のおかげで、いずれもそのまま移植できます。

2つ目は、まだ移植していないARPモジュールとICMPモジュールへの依存を一時的に取り除くことです。ip_output_device()が呼ぶarp_resolve()と、ip_input()が未知プロトコルに対して呼ぶicmp_output()をコメントアウトします。これらはそれぞれ4.2・4.3でコメントを解除します。

📝 kernel/net/ip.c

...
 #include "util.h"
 #include "net.h"
-#include "arp.h"
+//#include "arp.h"
 #include "ip.h"
-#include "icmp.h"
+//#include "icmp.h"

...

 static void
 ip_input(const uint8_t *data, size_t len, struct net_device *dev)
 {
...
     /* unsupported protocol */
     if (hlen + 8 <= total) {
         /*
          * It should not be sent in response to ICMP error messages,
          * but ICMP is always registered and will not reach this point.
          */
-        icmp_output(ICMP_TYPE_DEST_UNREACH, ICMP_CODE_PROTO_UNREACH, 0, data, hlen + 8,
-                    iface->unicast, hdr->src);
+//        icmp_output(ICMP_TYPE_DEST_UNREACH, ICMP_CODE_PROTO_UNREACH, 0, data, hlen + 8,
+//                    iface->unicast, hdr->src);
     }
 }

...

 static int
 ip_output_device(struct ip_iface *iface, const uint8_t *data, size_t len, ip_addr_t target)
 {
     char addr[IP_ADDR_STR_LEN];
     uint8_t hwaddr[NET_DEVICE_ADDR_LEN] = {};
-    int ret;
+//    int ret;

...
         } else {
-            ret = arp_resolve(NET_IFACE(iface), target, hwaddr);
-            if (ret != ARP_RESOLVE_FOUND) {
-                return ret;
-            }
+//            ret = arp_resolve(NET_IFACE(iface), target, hwaddr);
+//            if (ret != ARP_RESOLVE_FOUND) {
+//                return ret;
+//            }
+            return -1;
         }
...
 }
...

不足しているlibc関数の追加

ip.cip_endp_pton()はエンドポイント文字列の解析にstrrchr()を使用します。簡易libcに実装を追加します。

📝 kernel/net/platform/xv6-riscv/libc/string.c

#include <string.h>

char *
strrchr(const char *cp, int ch)
{
    char *save;
    char c;

    for (save = (char *) 0; (c = *cp); cp++) {
        if (c == ch) {
            save = (char *) cp;
        }
    }
    return save;
}

📝 kernel/net/platform/xv6-riscv/libc/string.h

...

 extern char *
 strncpy(char *s, const char *t, int n);
+extern char *
+strrchr(const char *cp, int ch);
 
 #endif

初期化関数の呼び出し

net.cでIPモジュールのヘッダをインクルードし、net_init()からip_init()を呼び出すようにします(3.1でコメントアウトしていた分の解除です)。

📝 kernel/net/net.c

...
-//#include "ip.h"
+#include "ip.h"
...

 int
 net_init(void)
 {
...
-//    if (ip_init() == -1) {
-//        errorf("ip_init() failure");
-//        return -1;
-//    }
+    if (ip_init() == -1) {
+        errorf("ip_init() failure");
+        return -1;
+    }
...
 }
...

Makefileの修正

ip.ostring.oをビルド対象に追加します。

📝 Makefile

...
   $N/net.o \
   $N/ether.o \
+  $N/ip.o \
   $P/platform.o \
   $P/intr.o \
   $P/driver/virtio_net.o \
   $L/stdio.o \
-  $L/stdlib.o
+  $L/stdlib.o \
+  $L/string.o

...

IPインタフェースの登録(暫定対応)

IPモジュールを追加したことでIPインタフェースを登録できるようになりました。ここでは暫定的にvirtio-netドライバの初期化の中でIPインタフェース(192.0.2.2/24)の登録を済ませます。

📝 kernel/net/platform/xv6-riscv/driver/virtio_net.c

...
+#include "ip.h"
+
 struct net_device *
 virtio_net_init(void)
 {
...
+    /* TODO: Temporary */
+    {
+        struct ip_iface *iface = ip_iface_alloc("192.0.2.2", "255.255.255.0");
+        if (iface) {
+            ip_iface_register(dev, iface);
+        }
+    }
     return dev;
 }

Note

このハードコードは暫定的なものです。応用課題の「インタフェース制御」を実装してユーザ空間からインタフェースにアドレスを設定できるようになったら削除します。

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。起動ログから、タイプ0x0800(IP)のプロトコル登録、192.0.2.0/24宛の経路、net0への192.0.2.2インタフェースの登録が確認できます。

xv6 kernel is booting

2026/08/11 10:00:00
10:00:00.101 [I] net_init: initialize... (kernel/net/net.c:277)
10:00:00.102 [I] net_protocol_register: success, type=0x0800 (kernel/net/net.c:184)
10:00:00.103 [I] net_init: success (kernel/net/net.c:302)
10:00:00.104 [D] virtio_net_init: device found (kernel/net/platform/xv6-riscv/driver/virtio_net.c:396)
10:00:00.105 [I] net_device_register: success, dev=net0, type=0x0002 (kernel/net/net.c:58)
10:00:00.106 [I] ip_iface_register: dev=net0, 192.0.2.2, 255.255.255.0, 192.0.2.255 (kernel/net/ip.c:237)
10:00:00.107 [I] net_device_add_iface: success, dev=net0 (kernel/net/net.c:142)
10:00:00.108 [I] ip_route_add: 192.0.2.0/255.255.255.0 dev net0 src 192.0.2.2 (kernel/net/ip.c:136)
10:00:00.109 [I] net_run: startup... (kernel/net/net.c:311)
10:00:00.110 [I] net_device_open: dev=net0 (kernel/net/net.c:65)
10:00:00.111 [I] virtio_net_open: dev=net0, addr=52:54:00:12:34:56 (kernel/net/platform/xv6-riscv/driver/virtio_net.c:197)
10:00:00.112 [I] net_run: success (kernel/net/net.c:319)
hart 1 starting
hart 2 starting
init: starting sh
$

開発環境で別のシェルを開き、ブロードキャストアドレス192.0.2.255に対してpingを実行します。

$ ping -b -c 3 192.0.2.255

Note

宛先をブロードキャストアドレスにしているのは、ARPによるアドレス解決をスキップして即座にIPパケットを送信できるからです(ユニキャスト宛だとARPが必要ですが、まだ移植していません)。

pingへの応答は返りませんが、xv6側のログから受信したパケットがIPモジュールまで届いていることが確認できます。

10:00:05.101 [D] ip_input: permit, dev=net0, iface=192.0.2.2 (kernel/net/ip.c:377)

ICMPをまだ移植していないため、IPモジュールは「対応するプロトコルなし」として黙って破棄します(icmp_output()はコメントアウト済みなので応答は生成されません)。

4.2. ARPモジュール

ARPモジュールを移植します。ARPはIPアドレスからMACアドレスを解決するプロトコルで、これを追加するとユニキャスト宛のIPパケットを送信できるようになります。ARPはタイマー機能を利用するため、あわせてプラットフォーム依存のタイマー関連コードも用意します。

ARPモジュールのコードのコピー

ARPモジュールのコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/arp.{h,c} kernel/net/

タイマー機能の追加

ARPモジュールはキャッシュの有効期限管理のために、初期化時にタイマー(timer_register())を登録します。この呼び出しでビルドと実行が通るように、プラットフォーム依存のタイマー関連コードを追加します。ただし中身は空のスタブで、timer_register()は渡された登録内容を保持せず成功(0)を返すだけです。タイマーを駆動する仕組みも用意しないため、この段階では周期的な処理は一切行われません(ARPキャッシュの有効期限管理は働きませんが、以降の動作確認には支障ありません)。実際にタイマーを機能させるのは応用課題で扱います。

📝 kernel/net/platform/xv6-riscv/timer.h

#ifndef TIMER_H
#define TIMER_H

#include <sys/time.h>

extern int
timer_register(struct timeval interval, void (*handler)(void));

extern int
timer_init(void);
extern int
timer_run(void);
extern int
timer_shutdown(void);

#endif

📝 kernel/net/platform/xv6-riscv/timer.c

#include <stdio.h>
#include <sys/time.h>

#include "platform.h"

#include "util.h"

int
timer_register(struct timeval interval, void (*handler)(void))
{
    return 0;
}

int
timer_init(void)
{
    return 0;
}

int
timer_run(void)
{
    return 0;
}

int
timer_shutdown(void)
{
    return 0;
}

arp.cはキャッシュのタイムスタンプ比較にtimersub()timercmp()のマクロを使うので、簡易libcのsys/time.hに追加します。

📝 kernel/net/platform/xv6-riscv/libc/sys/time.h

...

 #include <time.h>
 
+#define timerclear(tv) ((tv)->tv_sec = (tv)->tv_usec = 0)
+
+#define timersub(a, b, res)                          \
+    do {                                             \
+        (res)->tv_sec = (a)->tv_sec - (b)->tv_sec;   \
+        (res)->tv_usec = (a)->tv_usec - (b)->tv_usec; \
+        if ((res)->tv_usec < 0) {                    \
+            (res)->tv_sec--;                         \
+            (res)->tv_usec += 1000000;               \
+        }                                            \
+    } while (0)
+
+#define timercmp(a, b, CMP) \
+    (((a)->tv_sec == (b)->tv_sec) ? ((a)->tv_usec CMP (b)->tv_usec) : ((a)->tv_sec CMP (b)->tv_sec))
+
 #endif

platform.hplatform.cにタイマーの初期化・起動を組み込みます。

📝 kernel/net/platform/xv6-riscv/platform.h

...

 #include "intr.h"
+#include "timer.h"
 
 #endif

📝 kernel/net/platform/xv6-riscv/platform.c

...

     intr_register(INTR_IRQ_SOFT, net_softirq_handler, 0, NULL);
+    if (timer_init() == -1) {
+        return -1;
+    }
     return 0;
 }

...

     if (intr_run() == -1) {
         return -1;
     }
+    if (timer_run() == -1) {
+        return -1;
+    }
     return 0;
 }

...

IPモジュールからのアドレス解決の有効化

4.1で暫定的にコメントアウトしていたip.carp_resolve()呼び出しを有効に戻します。

📝 kernel/net/ip.c

...
 #include "util.h"
 #include "net.h"
-//#include "arp.h"
+#include "arp.h"
 #include "ip.h"

...

 static int
 ip_output_device(struct ip_iface *iface, const uint8_t *data, size_t len, ip_addr_t target)
 {
     char addr[IP_ADDR_STR_LEN];
     uint8_t hwaddr[NET_DEVICE_ADDR_LEN] = {};
-//    int ret;
+    int ret;

...
         } else {
-//            ret = arp_resolve(NET_IFACE(iface), target, hwaddr);
-//            if (ret != ARP_RESOLVE_FOUND) {
-//                return ret;
-//            }
-            return -1;
+            ret = arp_resolve(NET_IFACE(iface), target, hwaddr);
+            if (ret != ARP_RESOLVE_FOUND) {
+                return ret;
+            }
         }
...
 }
...

初期化関数の呼び出し

net.cでARPモジュールのヘッダをインクルードし、net_init()からarp_init()を呼び出すようにします。

📝 kernel/net/net.c

...
-//#include "arp.h"
+#include "arp.h"
...

 int
 net_init(void)
 {
...
-//    if (arp_init() == -1) {
-//        errorf("arp_init() failure");
-//        return -1;
-//    }
+    if (arp_init() == -1) {
+        errorf("arp_init() failure");
+        return -1;
+    }
...
 }
...

Makefileの修正

arp.otimer.oをビルド対象に追加します。

📝 Makefile

...
   $N/ether.o \
   $N/ip.o \
+  $N/arp.o \
   $P/platform.o \
   $P/intr.o \
+  $P/timer.o \
   $P/driver/virtio_net.o \
...

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。今度はユニキャストアドレス192.0.2.2に対してpingを実行します。

$ ping -c 3 192.0.2.2

xv6側のログに、ホストからのARP要求を受信して応答を返し(アドレス解決)、続いてICMP Echo要求を受信している様子が出力されます。

10:05:00.101 [D] net_input: dev=net0, type=0x0806, len=28 (kernel/net/net.c:232)
10:05:00.102 [D] arp_input: dev=net0, len=28 (kernel/net/arp.c:250)
10:05:00.103 [D] arp_cache_insert: INSERT: pa=192.0.2.1, ha=xx:xx:xx:xx:xx:xx (kernel/net/arp.c:185)
10:05:00.104 [D] arp_reply: dev=net0, len=28 (kernel/net/arp.c:224)
10:05:00.105 [D] net_input: dev=net0, type=0x0800, len=84 (kernel/net/net.c:232)
10:05:00.106 [D] ip_input: permit, dev=net0, iface=192.0.2.2 (kernel/net/ip.c:377)

Note

pingへの応答(ICMP Echo Reply)はまだ返りません。ICMPモジュールを移植していないため、Echo要求はIPモジュールまで届いた後、対応するプロトコルが見つからず破棄されます。次の4.3で応答できるようになります。

4.3. ICMPモジュール

ICMPモジュールを移植します。これを追加するとping(ICMP Echo)に応答できるようになります。

ICMPモジュールのコードのコピー

ICMPモジュールのコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/icmp.{h,c} kernel/net/

IPモジュールのICMPエラー通知の有効化

4.1で暫定的にコメントアウトしていたip.cicmp_output()呼び出し(未対応プロトコル受信時のICMPエラー通知)を有効に戻します。

📝 kernel/net/ip.c

...
 #include "util.h"
 #include "net.h"
 #include "arp.h"
 #include "ip.h"
-//#include "icmp.h"
+#include "icmp.h"

...
 static void
 ip_input(const uint8_t *data, size_t len, struct net_device *dev)
 {
...
     /* unsupported protocol */
     if (hlen + 8 <= total) {
         /*
          * It should not be sent in response to ICMP error messages,
          * but ICMP is always registered and will not reach this point.
          */
-//        icmp_output(ICMP_TYPE_DEST_UNREACH, ICMP_CODE_PROTO_UNREACH, 0, data, hlen + 8,
-//                    iface->unicast, hdr->src);
+        icmp_output(ICMP_TYPE_DEST_UNREACH, ICMP_CODE_PROTO_UNREACH, 0, data, hlen + 8,
+                    iface->unicast, hdr->src);
     }
 }
...

初期化関数の呼び出し

net.cでICMPモジュールのヘッダをインクルードし、net_init()からicmp_init()を呼び出すようにします。

📝 kernel/net/net.c

...
-//#include "icmp.h"
+#include "icmp.h"
...

 int
 net_init(void)
 {
...
-//    if (icmp_init() == -1) {
-//        errorf("icmp_init() failure");
-//        return -1;
-//    }
+    if (icmp_init() == -1) {
+        errorf("icmp_init() failure");
+        return -1;
+    }
...
 }
...
...

Makefileの修正

icmp.oをビルド対象に追加します。

📝 Makefile

...
   $N/ether.o \
   $N/arp.o \
   $N/ip.o \
+  $N/icmp.o \
   $P/platform.o \
...

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。ユニキャストアドレス192.0.2.2に対してpingを実行します。

$ ping -c 3 192.0.2.2

今度はICMP Echo Replyが返るので、pingが成功します。

$ ping -c 3 192.0.2.2
PING 192.0.2.2 (192.0.2.2) 56(84) bytes of data.
64 bytes from 192.0.2.2: icmp_seq=1 ttl=255 time=1.23 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=255 time=0.98 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=255 time=1.05 ms

--- 192.0.2.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms

xv6側のログにも、ICMP Echo要求を受信してEcho Replyを送信している様子が出力されます。

10:05:00.101 [D] icmp_input: 192.0.2.1 => 192.0.2.2, len=64 (kernel/net/icmp.c:117)
10:05:00.102 [D] icmp_output: 192.0.2.2 => 192.0.2.1, len=64 (kernel/net/icmp.c:163)

4.4. UDPモジュール

UDPモジュールを移植します。UDPは受信データをキューに溜めてアプリケーションに渡すため、タスクの休止・起床(sched)の仕組みを必要とします。あわせてプラットフォーム依存のタスク制御コードも用意します。

UDPモジュールのコードのコピー

UDPモジュールのコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/udp.{h,c} kernel/net/

環境依存のタスク制御

UDPモジュールは、受信キューが空のときにタスクを休止し、パケットが届いたら起床する、という制御を行います。xv6のsleep()/wakeup()を使ってこれを実装したコードをsched.c/sched.hとして追加します。

Note

xv6ではsleep()wakeup()でタスクを休止・起床できます。両者は休止と起床を対応付けるために任意のポインタ(xv6では「チャネル」と呼びます)を受け取りますが、これはアドレスの一致確認にのみ使われるため、どのような資源を指定しても構いません。また、sleep()を呼び出す際にはあらかじめスピンロックを獲得しておく必要があります。pthread_condを使う場合とほぼ同じように実装できます。

📝 kernel/net/platform/xv6-riscv/sched.h

#ifndef SCHED_H
#define SCHED_H

#include <time.h>

struct sched_task {
    struct sched_task *next;
    int interrupted;
    int wc; /* wait count */
};

#define SCHED_TASK_INITIALIZER {NULL, 0, 0}

extern int
sched_task_init(struct sched_task *task);
extern int
sched_task_destroy(struct sched_task *task);
extern int
sched_task_sleep(struct sched_task *task, lock_t *lock, const struct timespec *abstime);
extern int
sched_task_wakeup(struct sched_task *task);

extern int
sched_init(void);
extern int
sched_run(void);
extern int
sched_shutdown(void);

#endif

📝 kernel/net/platform/xv6-riscv/sched.c

#include <stdio.h>
#include <errno.h>
#include <time.h>

#include "platform.h"

#include "util.h"

int
sched_task_init(struct sched_task *task)
{
    task->next = NULL;
    task->interrupted = 0;
    task->wc = 0;
    return 0;
}

int
sched_task_destroy(struct sched_task *task)
{
    if (task->wc) {
        return -1;
    }
    return 0;
}

/*
 * NOTE: sleep() releases the lock while sleeping and reacquires it on wakeup,
 *       so the caller can use it just like pthread_cond_wait().
 */
int
sched_task_sleep(struct sched_task *task, lock_t *lock, const struct timespec *abstime)
{
    (void)abstime; /* timeout is not supported */

    if (task->interrupted) {
        errno = EINTR;
        return -1;
    }
    task->wc++;
    sleep(task, lock);
    task->wc--;
    if (task->interrupted) {
        if (!task->wc) {
            task->interrupted = 0;
        }
        errno = EINTR;
        return -1;
    }
    return 0;
}

int
sched_task_wakeup(struct sched_task *task)
{
    wakeup(task);
    return 0;
}

int
sched_init(void)
{
    return 0;
}

int
sched_run(void)
{
    return 0;
}

int
sched_shutdown(void)
{
    return 0;
}

sched.hstruct timespec(タイムアウト指定の型)を参照するので、kernel/time.hに定義を追加します。

📝 kernel/time.h

...

 struct timeval {
   long tv_sec;
   long tv_usec;
 };
 
+struct timespec {
+  long tv_sec;
+  long tv_nsec;
+};
+
 struct tm {

...

platform.hplatform.cにスケジューラの初期化・起動を組み込みます。あわせて、プロトコルスタック本体が参照するerrnoの実体をplatform.cに置きます。

📝 kernel/net/platform/xv6-riscv/platform.h

...

 #include "intr.h"
 #include "timer.h"
+#include "sched.h"
 
 #endif

📝 kernel/net/platform/xv6-riscv/platform.c

 #include "platform.h"

 #include "util.h"
 #include "net.h"
 
+int errno;
+
 static uint32 seed = 1;

 int
 platform_init(void)
 {
...
     if (timer_init() == -1) {
         return -1;
     }
+    if (sched_init() == -1) {
+        return -1;
+    }
     return 0;
 }

 int
 platform_run(void)
 {
...
     if (timer_run() == -1) {
         return -1;
     }
+    if (sched_run() == -1) {
+        return -1;
+    }
     return 0;
 }
...

初期化関数の呼び出し

net.cでUDPモジュールのヘッダをインクルードし、net_init()からudp_init()を呼び出すようにします。

📝 kernel/net/net.c

...
-//#include "udp.h"
+#include "udp.h"
...

 int
 net_init(void)
 {
...
-//    if (udp_init() == -1) {
-//        errorf("udp_init() failure");
-//        return -1;
-//    }
+    if (udp_init() == -1) {
+        errorf("udp_init() failure");
+        return -1;
+    }
...
 }
...

Makefileの修正

udp.osched.oをビルド対象に追加します。

📝 Makefile

...
   $N/ip.o \
   $N/icmp.o \
+  $N/udp.o \
   $P/platform.o \
   $P/intr.o \
   $P/timer.o \
+  $P/sched.o \
   $P/driver/virtio_net.o \
...

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。起動ログから、プロトコル番号17(UDP)がIPモジュールに登録されていることが確認できます。

10:00:00.101 [I] ip_protocol_register: success, protocol=17 (kernel/net/ip.c:290)

UDPの受信を試すアプリケーションはまだ無いので、ここではプロトコルの登録とビルドが通ることの確認までとします。ホストから192.0.2.2のUDPポートにパケットを送ると、そのポートを使うアプリケーションが無いためxv6側はICMPのPort Unreachableを返します。この挙動からもUDPモジュールが受信処理まで動いていることが分かります。

$ echo hello | nc -u -w1 192.0.2.2 7
10:00:05.201 [D] udp_input: 192.0.2.1:xxxxx => 192.0.2.2:7, len=14, dev=net0 (kernel/net/udp.c:201)
10:00:05.202 [D] icmp_output: 192.0.2.2 => 192.0.2.1, len=96 (kernel/net/icmp.c:163)

4.5. TCPモジュール

自作プロトコルスタックの最後のモジュール、TCPを移植します。

TCPモジュールのコードのコピー

TCPモジュールのコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/tcp.{h,c} kernel/net/

乱数関数の追加

TCPは初期シーケンス番号(ISS)の生成にrandom()を使用します。簡易libcにrandom()srand()を追加します。

📝 kernel/net/platform/xv6-riscv/libc/stdlib.c

 #include <stdlib.h>

 long
 strtol(const char *s, char **endptr, int base)
 {
...
 }
+
+static unsigned int seed = 1;
+
+void
+srand(unsigned int newseed)
+{
+    seed = newseed;
+}
+
+long
+random(void)
+{
+    /* Linear Congruential Generator (LCG) */
+    seed = (seed * 1103515245 + 12345) % 0x7fffffff;
+    return seed;
+}

📝 kernel/net/platform/xv6-riscv/libc/stdlib.h

 #ifndef STDLIB_H
 #define STDLIB_H

 #include <sys/types.h>

 extern long
 strtol(const char *s, char **endptr, int base);
 
+extern void
+srand(unsigned int newseed);
+extern long
+random(void);
+
 #endif

初期化関数の呼び出し

net.cでTCPモジュールのヘッダをインクルードし、net_init()からtcp_init()を呼び出すようにします。これで、コメントアウトしていた全モジュールの初期化が揃います。

📝 kernel/net/net.c

...
-//#include "tcp.h"
+#include "tcp.h"

 int
 net_init(void)
 {
...
-//    if (tcp_init() == -1) {
-//        errorf("tcp_init() failure");
-//        return -1;
-//    }
+    if (tcp_init() == -1) {
+        errorf("tcp_init() failure");
+        return -1;
+    }
     infof("success");
     return 0;
 }
...

Makefileの修正

tcp.oをビルド対象に追加します。

📝 Makefile

...
   $N/icmp.o \
   $N/udp.o \
+  $N/tcp.o \
   $P/platform.o \
...

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。起動ログから、プロトコル番号6(TCP)がIPモジュールに登録されていることが確認できます。

10:00:00.101 [I] ip_protocol_register: success, protocol=1 (kernel/net/ip.c:290)
10:00:00.102 [I] ip_protocol_register: success, protocol=17 (kernel/net/ip.c:290)
10:00:00.103 [I] ip_protocol_register: success, protocol=6 (kernel/net/ip.c:290)

TCPの接続を試すアプリケーションはまだ無いので、ここではプロトコルの登録とビルドが通ることの確認までとします。ホストから192.0.2.2のTCPポートへ接続を試みると、そのポートでlistenしているアプリケーションが無いためxv6側はRSTを返します。この挙動からTCPモジュールがセグメントを受信・応答していることが分かります。

$ nc -v 192.0.2.2 7
10:00:05.301 [D] tcp_input: 192.0.2.1:xxxxx => 192.0.2.2:7, len=24, dev=net0 (kernel/net/tcp.c:975)
10:00:05.302 [D] tcp_output_segment: 192.0.2.2:7 => 192.0.2.1:xxxxx, len=20 (kernel/net/tcp.c:394)

Note

これで自作プロトコルスタックの全モジュールの移植が完了しました。次の章では、アプリケーションがこれらのプロトコルを利用して通信するための「ソケット」を実装します。

5. ソケット

自作プロトコルスタックの移植が終わったので、次は「ソケット」を実装してアプリケーションがプロトコルスタックの機能を利用して通信できるようにします。

micropsにはソケット層(sock.c/sock.h)が用意されており、独自のディスクリプタ表でUDP/TCPのPCBを管理します。この章では、micropsのソケット層をxv6のファイルディスクリプタと統合し、socket()bind()recvfrom()sendto()といった標準的なソケットAPIをシステムコールとしてユーザ空間に提供します。

ユーザ空間 (user/udpecho.c)
    ↓ socket(), bind(), recvfrom(), sendto()
システムコール (kernel/syssocket.c)   ← 新規作成
    ↓ struct file (FD_SOCKET) 経由で sock ディスクリプタを保持
ソケット層 (kernel/net/sock.c)        ← micropsからコピー
    ↓ udp_cmd_*()
プロトコルスタック

Note

この章ではUDP(データグラム)ソケットを扱えるところまでを実装します。TCP(ストリーム)ソケットへの対応は応用課題として扱います。

5.1. ファイルディスクリプタとの互換性

xv6では、ファイルやパイプ、デバイスへの参照をstruct fileで表し、プロセスごとのファイルディスクリプタ表を通じて操作します。ソケットもこの枠組みに載せることで、read()/write()/close()といった既存の操作と自然に統合できます。

struct fileにソケット用の種別FD_SOCKETと、ソケットディスクリプタを保持するメンバsockを追加します。

📝 kernel/file.h

 struct file {
-  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
+  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE, FD_SOCKET } type;
   int ref; // reference count
   char readable;
   char writable;
...
   struct inode *ip;  // FD_INODE and FD_DEVICE
   uint off;          // FD_INODE
   short major;       // FD_DEVICE
+  int sock;          // FD_SOCKET
 };

...

Note

micropsのソケット層はソケットをintのディスクリプタで管理します。そのためstruct fileにはポインタではなくint sockを持たせます。

ファイルを閉じるfileclose()に、ソケットの場合の処理(sock_close()の呼び出し)を追加します。

📝 kernel/file.c

...

 #include "file.h"
 #include "stat.h"
 #include "proc.h"
+#include "net/sock.h"

...

 // Close file f.  (Decrement ref count, close when reaches 0.)
 void
 fileclose(struct file *f)
 {
...
     begin_op();
     iput(ff.ip);
     end_op();
+  } else if (ff.type == FD_SOCKET) {
+    sock_close(ff.sock);
   }
 }

...

5.2. ソケット層の移植

micropsのソケット層(sock.c/sock.h)をxv6へ移植します。ソケット層は独自のディスクリプタ表(socks[])でソケットを管理し、sock_open()はその添字を返します。この添字を、5.1でstruct fileに追加したsockメンバに保持します。

ソケット層のコードのコピー

ソケット層のコードをmicropsからコピーします。修正は不要で、そのまま利用できます。

$ cp $MICROPS/sock.{h,c} kernel/net/

sock.hには、ソケットAPIのアドレスファミリ(AF_INETなど)やソケット種別(SOCK_DGRAMなど)の定数と、struct sockaddr/struct sockaddr_in、そしてソケット層の関数プロトタイプが定義されています。

プロトタイプ宣言の追加

カーネルから呼び出すソケット層の関数をkernel/defs.hに宣言します。あわせて、struct sockaddrの前方宣言と、次の5.3でシステムコールから使うsysfile.cargfd()/fdalloc()の宣言も追加します(これらはstaticを外して公開します)。

📝 kernel/defs.h

...

 struct stat;
 struct superblock;
 struct net_device;
+struct sockaddr;
 struct timeval;
 struct tm;

...

 int             strncmp(const char*, const char*, uint);
 char*           strncpy(char*, const char*, int);
 
+// sysfile.c
+int             argfd(int, int*, struct file**);
+int             fdalloc(struct file*);
+
 // syscall.c

...

 int             net_init(void);
 int             net_run(void);
 
+// net/sock.c
+int             sock_open(int, int, int);
+int             sock_close(int);
+ssize_t         sock_recvfrom(int, void*, size_t, struct sockaddr*, int*);
+ssize_t         sock_sendto(int, const void*, size_t, const struct sockaddr*, int);
+int             sock_bind(int, const struct sockaddr*, int);
+
 // net/platform/xv6-riscv/intr.c

...

sysfile.cargfd()fdalloc()からstaticを外します。

📝 kernel/sysfile.c

...

 // Fetch the nth word-sized system call argument as a file descriptor
 // and return both the descriptor and the corresponding struct file.
-static int
+int
 argfd(int n, int *pfd, struct file **pf)
 {

...

 // Allocate a file descriptor for the given file.
 // Takes over file reference from caller on success.
-static int
+int
 fdalloc(struct file *f)
 {

...

Makefileの修正

sock.oをビルド対象に追加します。

📝 Makefile

...
   $N/udp.o \
   $N/tcp.o \
+  $N/sock.o \
   $P/platform.o \
...

一旦、この状態でビルドが通ることを確認しておきましょう。

$ make

5.3. システムコールの追加

ソケットAPIをユーザ空間から使えるように、システムコールを追加します。UDP通信に必要なsocket()bind()recvfrom()sendto()の4つを実装します。

システムコール番号の追加

既存の最大番号(SYS_sync = 22)に続けて、ソケット関連のシステムコール番号を追加します。

📝 kernel/syscall.h

...

 #define SYS_mkdir  20
 #define SYS_close  21
 #define SYS_sync   22
+#define SYS_socket 23
+#define SYS_bind   24
+#define SYS_recvfrom 25
+#define SYS_sendto 26

システムコール番号とカーネル関数の対応付け

kernel/syscall.cに、システムコールを処理するカーネル関数の宣言と、番号から関数への対応表(syscalls[])への登録を追加します。

📝 kernel/syscall.c

...
 extern uint64 sys_close(void);
 extern uint64 sys_sync(void);
+extern uint64 sys_socket(void);
+extern uint64 sys_bind(void);
+extern uint64 sys_recvfrom(void);
+extern uint64 sys_sendto(void);
 
 // An array mapping syscall numbers from syscall.h
 // to the function that handles the system call.
 static uint64 (*syscalls[])(void) = {
...
   [SYS_close]   sys_close,
   [SYS_sync]    sys_sync,
+  [SYS_socket]   sys_socket,
+  [SYS_bind]     sys_bind,
+  [SYS_recvfrom] sys_recvfrom,
+  [SYS_sendto]   sys_sendto,
   // clang-format on
 };

...

カーネル関数の実装

システムコールの実体をkernel/syssocket.cとして新規に作成します。既存のkernel/sysfile.cと同じく、ユーザ空間から渡された引数を検証してから、5.2で移植したソケット層(sock_*())を呼び出します。

ここで、2種類のディスクリプタが登場することに注意してください。sock_open()が返すのはソケット層の内部テーブル(socks[])の添字(ソケットディスクリプタ)で、これはstruct filesockメンバに格納します。一方、socket()がユーザ空間に返すのは、そのstruct fileに割り当てたxv6の**ファイルディスクリプタ(FD)**です。以降のbind()/recvfrom()/sendto()は、ユーザから渡されたFDをargsock()でソケットディスクリプタに解決してからソケット層を呼び出します。

ファイル冒頭では、この橋渡しを担うヘルパを用意しています。filealloc()でファイルを確保してFDを割り当てるsockfdalloc()、FDをソケットディスクリプタに変換するargsock()struct sockaddr_inをユーザ空間とやり取りするfetchsockaddr()/storesockaddr()です。

📝 kernel/syssocket.c

//
// Socket system calls.
// Mostly argument checking, since we don't trust
// user code, and calls into net/sock.c.
//

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "proc.h"
#include "fs.h"
#include "sleeplock.h"
#include "file.h"

#include "net/sock.h"

#define KBUFSIZ 2048

// Allocate a struct file and a file descriptor for the given socket
// descriptor. Closes the socket and returns -1 on failure.
static int
sockfdalloc(int sock)
{
  struct file *f;
  int fd;

  if ((f = filealloc()) == 0) {
    sock_close(sock);
    return -1;
  }
  f->type = FD_SOCKET;
  f->readable = 1;
  f->writable = 1;
  f->sock = sock;
  if ((fd = fdalloc(f)) < 0) {
    fileclose(f); // also closes the socket
    return -1;
  }
  return fd;
}

// Fetch the nth word-sized system call argument as a socket descriptor.
static int
argsock(int n, int *psock)
{
  struct file *f;

  if (argfd(n, 0, &f) < 0)
    return -1;
  if (f->type != FD_SOCKET)
    return -1;
  *psock = f->sock;
  return 0;
}

// Fetch a struct sockaddr_in from user space.
static int
fetchsockaddr(uint64 addr, int addrlen, struct sockaddr_in *sin)
{
  struct proc *p = myproc();

  if (addrlen != sizeof(*sin))
    return -1;
  if (copyin(p->pagetable, (char *)sin, addr, sizeof(*sin)) < 0)
    return -1;
  return 0;
}

// Copy out a struct sockaddr_in and its length to user space.
// Does nothing if the user passed a null pointer for either of them.
static int
storesockaddr(uint64 addr, uint64 addrlen, struct sockaddr_in *sin, int len)
{
  struct proc *p = myproc();

  if (addr == 0 || addrlen == 0)
    return 0;
  if (copyout(p->pagetable, addr, (char *)sin, len) < 0)
    return -1;
  if (copyout(p->pagetable, addrlen, (char *)&len, sizeof(len)) < 0)
    return -1;
  return 0;
}

uint64
sys_socket(void)
{
  int domain, type, protocol;
  int sock;

  argint(0, &domain);
  argint(1, &type);
  argint(2, &protocol);
  if ((sock = sock_open(domain, type, protocol)) < 0)
    return -1;
  return sockfdalloc(sock);
}

uint64
sys_bind(void)
{
  int sock, addrlen;
  uint64 addr;
  struct sockaddr_in sin;

  argaddr(1, &addr);
  argint(2, &addrlen);
  if (argsock(0, &sock) < 0)
    return -1;
  if (fetchsockaddr(addr, addrlen, &sin) < 0)
    return -1;
  return sock_bind(sock, (struct sockaddr *)&sin, sizeof(sin));
}

uint64
sys_recvfrom(void)
{
  int sock, n, len;
  uint64 buf, addr, addrlen;
  char kbuf[KBUFSIZ];
  struct sockaddr_in sin;
  struct proc *p = myproc();
  ssize_t ret;

  argaddr(1, &buf);
  argint(2, &n);
  argaddr(3, &addr);
  argaddr(4, &addrlen);
  if (argsock(0, &sock) < 0)
    return -1;
  if (n < 0)
    return -1;
  if (n > KBUFSIZ)
    n = KBUFSIZ;
  len = sizeof(sin);
  if ((ret = sock_recvfrom(sock, kbuf, n, (struct sockaddr *)&sin, &len)) < 0)
    return -1;
  // copy out only the bytes actually received, not the whole buffer
  if (ret > 0 && copyout(p->pagetable, buf, kbuf, ret) < 0)
    return -1;
  if (storesockaddr(addr, addrlen, &sin, len) < 0)
    return -1;
  return ret;
}

uint64
sys_sendto(void)
{
  int sock, n, addrlen;
  uint64 buf, addr;
  char kbuf[KBUFSIZ];
  struct sockaddr_in sin;
  struct proc *p = myproc();

  argaddr(1, &buf);
  argint(2, &n);
  argaddr(3, &addr);
  argint(4, &addrlen);
  if (argsock(0, &sock) < 0)
    return -1;
  if (n < 0 || n > KBUFSIZ)
    return -1;
  if (fetchsockaddr(addr, addrlen, &sin) < 0)
    return -1;
  if (copyin(p->pagetable, kbuf, buf, n) < 0)
    return -1;
  return sock_sendto(sock, kbuf, n, (struct sockaddr *)&sin, sizeof(sin));
}

Important

sys_recvfrom()sys_sendto()は、受信・送信データを一旦カーネル側のバッファkbufchar kbuf[KBUFSIZ])にコピーしてからソケット層に渡します。

Note

sys_recvfrom()では「実際に受信したバイト数」だけをcopyout()します。バッファ全体をコピーすると、初期化されていないカーネルメモリがユーザ空間へ漏れてしまうためです。

Makefileの修正

syssocket.oをビルド対象に追加します。

📝 Makefile

...
   $K/exec.o \
   $K/sysfile.o \
+  $K/syssocket.o \
   $K/kernelvec.o \
...

ユーザ空間へのシステムコールの提供

システムコールの入り口(トランポリン)を生成するuser/usys.plと、プロトタイプ宣言を持つuser/user.hにソケット関連のエントリを追加します。

📝 user/usys.pl

...
 entry("uptime");
 entry("sync");
+entry("socket");
+entry("bind");
+entry("recvfrom");
+entry("sendto");

📝 user/user.h

...
 #define SBRK_ERROR ((char *)-1)
 
 struct stat;
+struct sockaddr;
 
 // system calls
 int fork(void);
...
 int pause(int);
 int uptime(void);
 int sync(void);
+int socket(int, int, int);
+int bind(int, const struct sockaddr *, int);
+int recvfrom(int, void *, int, struct sockaddr *, int *);
+int sendto(int, const void *, int, const struct sockaddr *, int);
 
 // ulib.c

...

5.4. 通信アプリケーション

ソケットAPIを使うUDPエコーサーバ(udpecho)を作成して、実際に通信できることを確認します。

バイトオーダー変換関数の追加

ポート番号などはネットワークバイトオーダー(ビッグエンディアン)で扱う必要があります。ユーザ空間向けにhtons()/ntohs()/htonl()/ntohl()user/ulib.cに実装します。

📝 user/ulib.c

...

 sbrklazy(int n)
 {
   return sys_sbrk(n, SBRK_LAZY);
 }
+
+//
+// byte order
+//
+
+static int
+byteorder(void)
+{
+  uint x = 1;
+
+  return *(uchar *)&x ? 1 : 0; // 1 if little endian
+}
+
+static ushort
+byteswap16(ushort v)
+{
+  return (v & 0x00ff) << 8 | (v & 0xff00) >> 8;
+}
+
+static uint
+byteswap32(uint v)
+{
+  return (v & 0x000000ff) << 24 | (v & 0x0000ff00) << 8 |
+         (v & 0x00ff0000) >> 8 | (v & 0xff000000) >> 24;
+}
+
+ushort
+htons(ushort h)
+{
+  return byteorder() ? byteswap16(h) : h;
+}
+
+ushort
+ntohs(ushort n)
+{
+  return byteorder() ? byteswap16(n) : n;
+}
+
+uint
+htonl(uint h)
+{
+  return byteorder() ? byteswap32(h) : h;
+}
+
+uint
+ntohl(uint n)
+{
+  return byteorder() ? byteswap32(n) : n;
+}

📝 user/user.h

...
 char *sbrk(int);
 char *sbrklazy(int);
+ushort htons(ushort);
+ushort ntohs(ushort);
+uint htonl(uint);
+uint ntohl(uint);
 
 // printf.c

...

ユーザ空間向けソケットヘッダ

ソケットAPIの定数(AF_INETなど)とアドレス構造体(struct sockaddr_inなど)を、ユーザ空間から使えるようにuser/socket.hとして用意します。構造体のレイアウトはカーネル側のkernel/net/sock.hと一致させる必要があります。

📝 user/socket.h

//
// Socket API definitions for user programs.
// Struct layouts must match the kernel's definitions in kernel/net/sock.h.
//

#define PF_UNSPEC  0
#define PF_INET    2
#define PF_INET6  10

#define AF_UNSPEC PF_UNSPEC
#define AF_INET   PF_INET
#define AF_INET6  PF_INET6

#define SOCK_STREAM 1
#define SOCK_DGRAM  2

#define IPPROTO_TCP 6
#define IPPROTO_UDP 17

#define INADDR_ANY 0

struct in_addr {
  uint s_addr;
};

struct sockaddr {
  ushort sa_family;
  char sa_data[14];
};

struct sockaddr_in {
  ushort sin_family;
  ushort sin_port;
  struct in_addr sin_addr;
};

UDPエコーサーバのコード

受信したデータグラムを送信元へそのまま送り返すUDPエコーサーバです。socket()でソケットを作り、bind()でポート7番に割り当て、recvfrom()/sendto()で送受信を繰り返します。

📝 user/udpecho.c

//
// UDP echo server.
//

#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/socket.h"
#include "user/user.h"

#define ECHO_PORT 7
#define BUFSIZ 1024

int
main(int argc, char *argv[])
{
  int soc;
  struct sockaddr_in self, peer;
  int peerlen;
  char buf[BUFSIZ];
  int ret;
  uchar *addr;

  soc = socket(AF_INET, SOCK_DGRAM, 0);
  if (soc == -1) {
    printf("socket: failure\n");
    exit(1);
  }
  memset(&self, 0, sizeof(self));
  self.sin_family = AF_INET;
  self.sin_addr.s_addr = INADDR_ANY;
  self.sin_port = htons(ECHO_PORT);
  if (bind(soc, (struct sockaddr *)&self, sizeof(self)) == -1) {
    printf("bind: failure\n");
    close(soc);
    exit(1);
  }
  printf("waiting for message on port %d...\n", ECHO_PORT);
  while (1) {
    peerlen = sizeof(peer);
    ret = recvfrom(soc, buf, sizeof(buf), (struct sockaddr *)&peer, &peerlen);
    if (ret == -1) {
      printf("recvfrom: failure\n");
      break;
    }
    addr = (uchar *)&peer.sin_addr.s_addr;
    printf("%d bytes received from %d.%d.%d.%d:%d\n", ret,
      addr[0], addr[1], addr[2], addr[3], ntohs(peer.sin_port));
    if (sendto(soc, buf, ret, (struct sockaddr *)&peer, sizeof(peer)) == -1) {
      printf("sendto: failure\n");
      break;
    }
  }
  close(soc);
  exit(0);
}

Makefileの修正

udpechoをユーザプログラムのリスト(UPROGS)に追加します。

📝 Makefile

...
 UPROGS=\
...
 	$U/_dorphan\
 	$U/_sync\
+	$U/_udpecho\
 
 fs.img: mkfs/mkfs README $(UPROGS)

動作確認

再ビルドした後、make qemuを実行してxv6を起動します。シェルからudpechoをバックグラウンドで起動します。

$ udpecho &
waiting for message on port 7...

開発環境で別のシェルを開き、ncコマンドで192.0.2.2のポート7番にUDPでメッセージを送ります。

$ echo "hello xv6" | nc -u -w1 192.0.2.2 7
hello xv6

送ったメッセージがそのまま返ってくれば成功です。xv6側にも受信ログが出力されます。

10 bytes received from 192.0.2.1:xxxxx

Note

これでアプリケーションが自作プロトコルスタックを通じてUDP通信できるようになりました。TCP(ストリーム)ソケットへの対応や、pingifconfigといったツールの実装は応用課題で扱います。

6. 応用課題(選択式)

残りの時間は応用課題に取り組んでください。難易度の低い順にA・B・C・Dと並べています。

  • 選択式ですが、全部やってしまっても構いません。
  • 他の参加者やAIと共同で作業しても構いません。

A: タイマー機能の有効化

ARPとTCPのモジュールは、キャッシュの有効期限管理や再送制御のためにプロトコルスタックへタイマーを登録します(timer_register())。しかし、4.2で追加したtimer.cは中身のない空のスタブで、timer_register()は登録内容を保持すらせず、タイマーを駆動する仕組みもありません。タイマーの登録から発火までを実装して、タイマー機能を動作させてください。

目標

  • 一定間隔で、登録済みのタイマーハンドラが呼び出されるようにする
  • ARPキャッシュが一定時間で消えること(arp_timerの発火)を確認する

ヒント

タイマー機能は「登録の保持」「周期的に発火するきっかけ」「登録されたハンドラの実行」の3つに分けて考えると実装しやすいです。

  • 登録の保持: 空スタブのtimer_register()を実装し、渡された間隔(interval)とハンドラ、および前回実行時刻を管理領域(リストなど)に保持できるようにします。この部分はmicropsのLinux版timer.cplatform/linux/timer.c)の実装がほぼそのまま流用できます。
  • 周期的なきっかけ: xv6では既にタイマー割り込み(clockintr())が周期的に発生しています。ネットワーク処理をタイマー割り込みのコンテキストで直接行うのは避け、3.5で実装したソフトウェア割り込み(intr_raise())を利用して、処理をソフトウェア割り込みのハンドラへ受け渡すのがよいでしょう。
  • ハンドラの実行: 保持したタイマーのリストを走査し、各タイマーの間隔と前回実行時刻からの経過を比較して、期限が来たものを呼び出します。時刻の取得には2.3で実装したgettimeofday()が使えます。

Note

micropsのLinux版timer.cplatform/linux/timer.c)が実装の参考になります。Linux版はPOSIXタイマーとシグナルで周期的なきっかけを作っていますが、その部分をxv6のタイマー割り込み+ソフトウェア割り込みに置き換えるイメージです。

発展

Goldfish RTCにもアラーム機能があります。タイマー割り込みではなくRTCのアラームを使って周期的なきっかけを作ることもできます。余裕があれば挑戦してみてください。

B: ソケットのTCP対応

5章ではUDP(データグラム)ソケットを実装しました。これをTCP(ストリーム)ソケットにも対応させ、TCPエコーサーバを作成してください。

目標

  • connect()listen()accept()recv()send()のシステムコールを追加する
  • TCPエコーサーバ(tcpecho)を作成し、ホストからncで接続してエコーが返ることを確認する

ヒント

5章で移植したソケット層(kernel/net/sock.c)は、UDPだけでなくTCP用のコマンド(tcp_cmd_*())への振り分けも最初から備えています。そのため、TCP対応で新しく必要になるのは主にシステムコールの追加だけで、ソケット層やTCPモジュール本体に手を入れる必要はありません。

  • システムコールの追加: 5.3でsocket()/bind()/recvfrom()/sendto()を追加したのと同じ要領で、connect()listen()accept()recv()send()kernel/syssocket.cに実装します。sock_connect()sock_listen()sock_accept()sock_recv()sock_send()を呼び出すだけの薄いラッパーになります。システムコール番号(SYS_*)やプロトタイプ宣言の追加も忘れずに行ってください。
  • accept()の注意点: sock_accept()は接続済みの新しいソケットディスクリプタを返します。これに対してstruct fileとファイルディスクリプタを割り当てて返す必要があります(5.3のsockfdalloc()が使えます)。
  • recv()の注意点: recvfrom()と同様に、実際に受信したバイト数だけをcopyout()してください。

完成イメージ

TCPエコーサーバを起動し、ホストから接続すると、送った内容がそのまま返ってきます。

(xv6側)
$ tcpecho &
waiting for connection on port 7...

(開発環境側)
$ nc 192.0.2.2 7
hello
hello

C: pingコマンドの実装

5章で実装したソケットはUDPとTCPを対象としていました。ここではRAWソケット(SOCK_RAW)に対応させ、ICMP Echo要求/応答を直接やり取りするpingコマンドを作成してください。

pingは、宛先ホストにICMP Echo要求を送り、返ってくるEcho応答までの往復時間(RTT)を表示するツールです。TCP/UDPのようにトランスポート層を挟まず、IPの上で直接ICMPメッセージを組み立てるため、RAWソケットが必要になります。

目標

  • RAWソケット(SOCK_RAW)をソケット層に対応させる
  • pingコマンドを作成し、ホスト(192.0.2.1)へICMP Echo要求を送って応答(RTT)を表示できることを確認する

ヒント

RAWソケットは「指定したプロトコル番号のIPデータグラムを、そのまま送受信できるソケット」です。ここまでに実装しているソケットに対してRAWソケット用の仕組みを実装します。

  • RAWソケットの実装: IP層(ip_input())で受信したデータグラムを、プロトコル番号が一致するRAWソケットへ渡す仕組みを用意し、sock.cSOCK_RAWの振り分けを追加します。受信キューやブロックして待つ部分はudp.cの受信処理が参考になります。sock.huser/socket.hへのSOCK_RAWIPPROTO_ICMPの定義追加も必要です(socket()システムコールはprotocolを既にそのまま渡します)。
  • pingコマンド: RAWソケットでICMP Echo要求を組み立てて送受信します。ICMPチェックサムの計算や、RTT測定のための送信時刻の埋め込みが必要です。
  • 実装上の注意: RAWソケットの受信データにはIPヘッダが含まれる点と、応答が返らないときに受信でブロックし続けない工夫(fork()で受信を別プロセスにするなど)に気をつけてください。

Note

送信先の192.0.2.1は開発環境側のTapデバイス(ホスト)のアドレスです。ホストのカーネルがICMP Echo要求に応答するので、xv6側で応答(RTT)を確認できます。

完成イメージ

$ ping 192.0.2.1
PING 192.0.2.1: 56 data bytes
64 bytes from 192.0.2.1: icmp_seq=1 time=0.512 ms
64 bytes from 192.0.2.1: icmp_seq=2 time=0.487 ms
64 bytes from 192.0.2.1: icmp_seq=3 time=0.501 ms
64 bytes from 192.0.2.1: icmp_seq=4 time=0.498 ms

--- 192.0.2.1 ping statistics ---
4 packets transmitted, 4 packets received

D: インタフェース制御

これまで、IPインタフェースのアドレスはvirtio-netドライバの初期化の中にハードコードしていました(4.1で追加した暫定コード)。これを、ユーザ空間からインタフェースの情報を取得したりアドレスを設定できるようにしてください。

目標

  • インタフェースの情報(名前・MACアドレス・IPアドレスなど)を一覧表示するコマンドを作る
  • ユーザ空間からインタフェースにIPアドレスを設定できるようにする
  • 設定したアドレスで実際に通信できることを確認する
  • 4.1で追加したvirtio-netドライバ内のアドレスのハードコードを削除する

ヒント

ifconfigコマンドが良い例ですが、必ずしも同じ仕様である必要はありません。

  • システムコールの追加: プロトコルスタックの情報はカーネル内にあるため、ユーザ空間から取得・設定するにはシステムコールが必要です。「情報を取得する」ものと「アドレスを設定する」ものの2つを追加するとよいでしょう。
  • プロトコルスタックへの機能追加: プロトコルスタックはデバイスの一覧を内部(net.cdevicesリスト)で管理していますが、外部から辿る手段がありません。インデックスやデバイス名からデバイスを取得する関数を追加すると、システムコールから情報を取り出せるようになります。アドレスの設定には、既にあるip_iface_alloc()ip_iface_register()が使えます。
  • カーネルとユーザ空間で共有する構造体: インタフェース情報を受け渡すための構造体は、struct statkernel/stat.h)のように、カーネルとユーザ空間の両方からインクルードできるヘッダに定義するとよいでしょう。

Important

必要に応じてシステムコールの追加や、プロトコルスタックへの機能追加を行ってください。プロトコルスタック本体(kernel/net/*.c)にはできるだけ手を入れず、追加する関数は最小限に留めるのが望ましいです。

完成イメージ

$ ifconfig
net0: flags=0x121<UP> mtu 1500
        ether 52:54:00:12:34:56
$ ifconfig net0 192.0.2.2 255.255.255.0
$ ifconfig
net0: flags=0x121<UP> mtu 1500
        ether 52:54:00:12:34:56
        inet 192.0.2.2 netmask 255.255.255.0 broadcast 192.0.2.255