BYB

低レイヤ好き学生エンジニアによる備忘録

自作TCP/IPスタックをMikanOSへ移植

Klab Expert Camp

Klab Expert Camp は Klab株式会社さんが開催してくださっているインターンで、Linuxカーネル上で動作する自作TCP/IPスタックmicropsのコードを写経・穴埋めしながらTCP/IPについての理解を深めることができるイベントである。
3月に開催された第5回に参加してmicropsの再実装に取り組んだ。

micropsの実装について

上が、micropsの構成を図にしたものである。
本来TCP/IPスタックはOSのカーネル内で動いており、もちろんLinuxでも同じである。
そのLinux上のユーザ空間でプロトコルスタックを動かすために、少し工夫が必要がある。
具体的には、

  • パケットを受け取った際に割り込み処理を行う必要があるが、ユーザ空間のプログラムから割り込みを起こすことはできない。そこでmicropsではSIGNALを送ることで擬似割り込みを起こしている。

のような点が、カーネル内に実装されるTCP/IPスタックとmicropsの実装上の違いとして挙げられる。

またユーザ空間で動くmicropsにはシステムコールという概念は存在しないが、カーネル内のTCP/IPスタックのようにTCPUDPの通信をPCB(Protocol Control Block)で管理しており、ソケット風のAPI関数を用いてmicropsで通信を行うプログラムを書くことができるようになっている。


micropsはすごく簡潔な実装がしてあるプロトコルスタックで、他にも紹介したい点が山ほどあるが、それについては自分が話すより、公開資料 を見ていただくほうがいいだろう。



TCP/IPスタックをMikanOSへ移植


micropsのTCP/IPスタックをMikanOSへ移植する際にすべきことはいくつかあるが、ここでは4つに分けて説明する。
なお、このTCP/IPスタックのMikanOS移植はmicrops作者の方が mikanos-net という名前のリポジトリで行っておられるので、今回はその実装を大いに参考にさせていただいた。

NICの準備とMikanOS用のデバイスドライバの実装

自分はMikanOSをQEMUで動かしていたので、QEMUNICを準備してそのデバイスドライバを実装する必要があった。
今回はE1000という種類のNICを使用する。(E1000はIntelNIC の一種のエミュレート バージョン)
NICの準備とデバイスドライバに関しては、mikanos-netの他に以下の一連のサイトの解説を参考にした。
qiita.com

実装の流れを端的に述べると

  • NICバイスとそのI/Oを扱うための特殊なレジスタを見つけ出す。
  • 送受信の際のバッファ用のメモリ領域を用意する。
  • 特殊レジスタを操作することによる、バッファ用領域を通したNICとの送受信を実現するための関数を用意する。


実装においては、NICの仕様に合うように様々な工夫をする必要があるが、ここではその説明は省略する。
是非上で示したサイトを参照していただきたい。

擬似割り込み処理をMikanOSの割り込みによって行うように変更

MikanOS(や他多くのOS)において、割り込みベクタ(番号)とそれに対応する割り込み記述子をIDT(割り込み記述子テーブル)という形で登録することによって割り込みが実現されている。
割り込み記述子には割り込みハンドラなどを登録する。

よってここで追加する処理は以下のようなものになる

  • NICからの割り込みの番号と割り込みハンドラをIDTに登録

kernel/interrupt.cpp

set_idt_entry(InterruptVector::kE1000, IntHandlerE1000);
  • ハンドラの処理内容は、タスクに通知を送ること

kernel/interrupt.cpp

  __attribute__((interrupt))
  void IntHandlerE1000(InterruptFrame* frame) {
    task_manager->SendMessage(1, Message{Message::kInterruptE1000});
    NotifyEndOfInterrupt();
  }
  • mainタスクはその通知を受け取ったら、NICからデータを読み込む関数を呼び出す

kernel/main.cpp

    case Message::kInterruptE1000:
      e1000_intr();
      break;
ソケット風API関数をシステムコール関数として登録


MikanOSのシステムコールは、アセンブリ言語のsyscall命令でSyscallEntry関数にシステムコール番号を渡すような設定をレジスタに入れておき、SyscallEntry関数で番号に応じた関数を呼び出すことによって行われている。
関数をシステムコールとして登録する際は、syscall_tableという関数ポインタ表にシステムコール番号とその関数へのポインタを登録すればよい。
具体的には、

apps/syscall.asm

define_syscall SocketOpen,       0x80000010
define_syscall SocketClose,      0x80000011
define_syscall SocketIOCTL,      0x80000012

apps/syscall.h

struct SyscallResult SyscallSocketOpen(int domain, int type, int protocol);
struct SyscallResult SyscallSocketClose(int soc);
struct SyscallResult SyscallSocketIOCTL(int soc, int req, void *arg);

kernel/syscall.cpp

SYSCALL(SocketOpen) {
  uint64_t ret = socketopen((int)arg1, (int)arg2, (int)arg3);
  return {ret, 0};
}

SYSCALL(SocketClose) {
  struct socket *s = socketget((int)arg1);
  uint64_t ret = socketclose(s);
  return {ret, 0};
}


SYSCALL(SocketIOCTL) {
  struct socket soc = {0, (int)arg1};
  uint64_t ret = socketioctl(&soc, (int)arg2, (void *)arg3);
  return {ret, 0};
}
  /* 0x10 */ syscall::SocketOpen,
  /* 0x11 */ syscall::SocketClose,
  /* 0x12 */ syscall::SocketIOCTL,

このようなソースコードを付け加えればよい。

その他のLinux特有システムコールを用いた関数(Timer関連,Thread関連等)を置き換え


micropsでは、pthread/mutexやtimerなどLinux系特有のシステムコールを用いた関数(POSIX標準関数など)を利用している。
当然自作OSのプログラムではそれらの関数を使用することはできないので、代わりとなる関数を自分で用意する必要がある。

下の例のように、POSIXの関数を同名でMikanOS内のタイマーを用いる関数として定義し直している。

net/port/mikanos.cpp

int
gettimeofday(struct timeval *tv, void *tz)
{
    unsigned long tick = timer_manager->CurrentTick();

    tv->tv_sec = tick / kTimerFreq;
    tv->tv_usec = tick % kTimerFreq * (1000000 / kTimerFreq);
    return 0;
}


mikados-netではmutex_lock/unlockなどのMikanOSに機能のない関数は空の関数として定義している。
続編のブログでは、mutex_lock関数の中身を自作spin_lockに置き換えるまでの作業内容を書いている。

ソースコードは以下の中にある。
https://github.com/yushoyamaguchi/mikanos_sub1github.com