自作TCP/IPスタックをMikanOSへ移植
概要
Klab Expert Camp にて作った自作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上のユーザ空間でプロトコルスタックを動かすために、少し工夫が必要がある。
具体的には、
- ユーザ空間からNICへアクセスすることはできないので、Ethernetデバイスをエミュレートするtapデバイスを利用する。tapデバイスはread/writeシステムコールによってユーザ空間からアクセスすることができる。
- パケットを受け取った際に割り込み処理を行う必要があるが、ユーザ空間のプログラムから割り込みを起こすことはできない。そこでmicropsではSIGNALを送ることで擬似割り込みを起こしている。
のような点が、カーネル内に実装されるTCP/IPスタックとmicropsの実装上の違いとして挙げられる。
またユーザ空間で動くmicropsにはシステムコールという概念は存在しないが、カーネル内のTCP/IPスタックのようにTCPやUDPの通信を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で動かしていたので、QEMUのNICを準備してそのデバイスドライバを実装する必要があった。
今回はE1000という種類のNICを使用する。(E1000はIntelの NIC の一種のエミュレート バージョン)
NICの準備とデバイスドライバに関しては、mikanos-netの他に以下の一連のサイトの解説を参考にした。
qiita.com
実装の流れを端的に述べると
- NICデバイスとそのI/Oを扱うための特殊なレジスタを見つけ出す。
- 送受信の際のバッファ用のメモリ領域を用意する。
- 特殊レジスタを操作することによる、バッファ用領域を通したNICとの送受信を実現するための関数を用意する。
実装においては、NICの仕様に合うように様々な工夫をする必要があるが、ここではその説明は省略する。
是非上で示したサイトを参照していただきたい。
擬似割り込み処理をMikanOSの割り込みによって行うように変更
MikanOS(や他多くのOS)において、割り込みベクタ(番号)とそれに対応する割り込み記述子を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