TCP/IPプロトコルスタック自作開発
Day1
KLab株式会社
第6回 KLab Expert Camp
自己紹介
twitter.com/pandax381
山本 雅也
YAMAMOTO
Masaya
KLab株式会社 Kラボラトリー
github.com/pandax381
pandax381@gmail.com
主な業務
デジタルハリウッド大学 客員准教授
KLabの技術インターン(通年実施インターンの紹介)
マニュアルのない完全オーダーメイド型
KLab Expert Camp
技術的に深いテーマに取り組む学生の発掘・育成を目的としたイベント
第1回 | 2019 夏 | TCP/IPプロトコルスタック自作開発 #1 |
第2回 | 2020 夏 | シェーダー(3DCG) |
第3回 | 2021 春 | TCP/IPプロトコルスタック自作開発 #2 |
第4回 | 2021 春 | 機械学習(オンラインゲーム) |
第5回 | 2022 春 | TCP/IPプロトコルスタック自作開発 #3 |
第6回 | 2023 夏 | TCP/IPプロトコルスタック自作開発 #4 |
TCP/IPプロトコルスタック自作開発
プロトコルスタックの実装を通じてTCP/IPを完全に理解する
コース説明
参加者の目的に応じて2つのコースに分かれて開発
開発スケジュール
イベント全体のスケジュール
8/24 (木) | 8/25 (金) | 8/26 (土) | 8/27 (日) | 8/28 (月) | 8/29 (火) | 8/30 (水) |
1日目 | 2日目 | | | 3日目 | 4日目 | 5日目 |
開発スケジュール
| 基本コース | アドバンスドコース |
11:00 ~ 11:10 (10分) | 全体アナウンス等 | |
11:10 ~ 12:30 (80分) | 講義(1限) | |
60分 | お昼休み | |
13:30 ~ 14:50 (80分) | 講義(2限) | 各自のペースで開発 ※ 適宜休憩を挟むこと |
20分 | 休憩 | |
15:10 ~ 16:30 (80分) | 講義(3限) | |
20分 | 休憩 | |
16:50 ~ 18:10 (80分) | 講義(4限) | |
20分 | 休憩 | |
18:30 ~ 19:50 (80分) | 講義(5限) | |
19:50 ~ 20:00 (10分) | 全体アナウンス等 |
基本コースの講義の合間に様子を伺いに行く�(進捗確認&相談タイム)
基本コースのスケジュール | |
1日目 | デバイスとプロトコルの管理 |
2日目 | IP / ICMP |
3日目 | Ethernet / ARP |
4日目 | UDP / TCP(前半) |
5日目 | TCP(後半) |
成果発表
最終日の講義後に成果発表を実施
成果発表&懇親会
スライド作成
15:00
17:00
17:30
20:00
コミュニケーション
Meet と Slack を併用
URL削除
URL削除
URL削除
KLab Expert Camp
Start🎉
本日の目標とタスク
自作プロトコルスタックの骨格を組み上げる
ネットワーク基礎知識のおさらい
TCP/IPのアーキテクチャ
シンプルな4階層のネットワークモデル
物理層
データリンク層
ネットワーク層
トランスポート層
セッション層
プレゼンテーション層
アプリケーション層
インターネット層
トランスポート層
アプリケーション層
ネットワークインタフェース層
OSI
TCP/IP
媒体上で直接接続されているノード間のデータ転送
異なるネットワークに存在するノード間でのデータ転送
ノード上のアプリケーションプロセス間のデータ転送
アプリケーションプロセス固有のやりとり
プロトコルスタック
複数のプロトコルが協調して動作する
インターネット層
(ネットワーク層)
トランスポート層
アプリケーション層
ネットワークインタフェース層
(リンク層)
Ethernet
SLIP
PPP
IP
IPv6
ARP
ICMP
UDP
TCP
User
Kernel
Device Driver
HTTP
DNS
SMTP
DHCP
データの流れ
各階層がデータ転送に必要な情報をヘッダとして付与(カプセル化)
インターネット層
トランスポート層
アプリケーション層
ネットワークインタフェース層
データ
データ
ヘッダ
データ
ヘッダ
データ
ヘッダ
ヘッダ
ヘッダ
ヘッダ
インターネット層
トランスポート層
アプリケーション層
ネットワークインタフェース層
アプリケーションとプロトコルスタックの関係
アプリケーションは ソケット を通じてプロトコルスタックの機能を利用する
Application
Protocol Stack
Device Driver
Network Device
User
Kernel
Socket API
Software
Hardware
socket
bind
listen
accept
connect
recv
send
close
ソケット関連の主要なシステムコール
開発環境とリファレンス実装の説明
開発環境
各自のLinux開発環境で作業
リファレンス実装
https://github.com/pandax381/microps
チュートリアル(1/2)
1. コードの取得とビルド
2. TAPデバイスの準備
3. サンプルアプリケーションの起動
> git clone git@github.com:your-account/microps.git
> cd microps
> make
> sudo ip tuntap add mode tap user $USER name tap0
> sudo ip addr add 192.0.2.1/24 dev tap0
> sudo ip link set tap0 up
> ./app/tcps.exe 192.0.2.2 7
〜 省略 〜
00:00:00.000 [D] tcp_bind: success: addr=192.0.2.2, port=7 (tcp.c:1156)
192.0.2.2の7番ポートでTCP接続を待っている状態
forkしたリポジトリをクローンする
チュートリアル(2/2)
4. pingコマンドで疎通確認(開発環境上で別のシェルを開いて実行)
5. ncコマンドでTCP接続の確認(開発環境上で別のシェルを開いて実行)
> ping 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=0.660 ms
64 bytes from 192.0.2.2: icmp_seq=2 ttl=255 time=0.688 ms
64 bytes from 192.0.2.2: icmp_seq=3 ttl=255 time=0.574 ms
> nc -v 192.0.2.2 7
Connection to 192.0.2.2 7 port [tcp/echo] succeeded!
hoge
hoge
fuga
fuga
サンプルプログラムを動かしているシェルに通信内容を示すログメッセージが出力されていることを確認
Ctrl+Cで終了
Ctrl+Cで終了
サンプルプログラムを動かしているシェルに通信内容を示すログメッセージが出力されていることを確認
192.0.2.2(サンプルアプリケーション)からの応答
192.0.2.2の7番ポート(サンプルアプリケーション)への接続に成功
入力したテキストと全く同じものがサンプルアプリケーションから送り返されてきている
microps
ユーザアプリケーション用のライブラリとして実装
NIC
デバイスドライバ
TCP/IP
プロトコルスタック
ソケットAPI
ユーザ
アプリケーション
Kernel
ユーザ
アプリケーション
microps
(ライブラリ)
ユーザ
アプリケーション
ソケット風 API
自作 TCP/IP
プロトコルスタック
疑似デバイスドライバ
microps
初期コード
以下のコミットをチェックアウトして初期コードとして使用する
> make clean
> git checkout bdbf73b -b work
初期コードに含まれているファイル
便利機能の詰め合わせライブラリ
チュートリアルで生成したファイルが残っているので掃除
> git remote add upstream git@github.com:pandax381/microps.git
> git fetch upstream
該当するコミットが見つからない場合の対処(リポジトリをforkする際にmasterブランチしかコピーしなかった等)
Makefile
古典的なビルドツール「make」の設定ファイル
APPS =
DRIVERS =
OBJS = util.o \
TESTS = test/step0.exe \
CFLAGS := $(CFLAGS) -g -W -Wall -Wno-unused-parameter -iquote .
ifeq ($(shell uname),Linux)
# Linux specific settings
BASE = platform/linux
CFLAGS := $(CFLAGS) -pthread -iquote $(BASE)
endif
ifeq ($(shell uname),Darwin)
# macOS specific settings
endif
...
Makefile
生成ファイルの定義(ファイルを追加したらここに書き加える)
Linux に依存する設定項目
Darwin(Mac OSX / macOS) に依存する設定項目
Cコンパイラのオプション
テストプログラム
テストプログラムのビルドと実行
#include "util.h"
#include "test.h"
int
main(void)
{
debugf("Hello, World!");
debugdump(test_data, sizeof(test_data));
return 0;
}
test/step0.c
#ifndef TEST_H
#define TEST_H
#include <stdint.h>
#define LOOPBACK_IP_ADDR "127.0.0.1"
#define LOOPBACK_NETMASK "255.0.0.0"
#define ETHER_TAP_NAME "tap0"
#define ETHER_TAP_HW_ADDR "00:00:5e:00:53:01"
#define ETHER_TAP_IP_ADDR "192.0.2.2"
#define ETHER_TAP_NETMASK "255.255.255.0"
#define DEFAULT_GATEWAY "192.0.2.1"
const uint8_t test_data[] = {
0x45, 0x00, 0x00, 0x30,
0x00, 0x80, 0x00, 0x00,
0xff, 0x01, 0xbd, 0x4a,
0x7f, 0x00, 0x00, 0x01,
0x7f, 0x00, 0x00, 0x01,
0x08, 0x00, 0x35, 0x64,
0x00, 0x80, 0x00, 0x01,
0x31, 0x32, 0x33, 0x34,
0x35, 0x36, 0x37, 0x38,
0x39, 0x30, 0x21, 0x40,
0x23, 0x24, 0x25, 0x5e,
0x26, 0x2a, 0x28, 0x29
};
#endif
test/test.h
テスト用パケットのバイナリデータ
ネットワーク設定のパラメータ
デバッグ用の便利機能
> make clean
> CFLAGS=-DHEXDUMP make
> ./test/step0.exe
12:35:53.210 [D] main: Hello, World! (test/step0.c:7)
+------+-------------------------------------------------+------------------+
| 0000 | 45 00 00 30 00 80 00 00 ff 01 bd 4a 7f 00 00 01 | E..0.......J.... |
| 0010 | 7f 00 00 01 08 00 35 64 00 80 00 01 31 32 33 34 | ......5d....1234 |
| 0020 | 35 36 37 38 39 30 21 40 23 24 25 5e 26 2a 28 29 | 567890!@#$%^&*() |
+------+-------------------------------------------------+------------------+
> make
> ./test/step0.exe
12:35:35.405 [D] main: Hello, World! (test/step0.c:7)
CFLAGS=-DHEXDUMP を前に付けて make を実行
debugf() の出力
・Internet host loopback address: https://tools.ietf.org/html/rfc5735
・EUI-48 Documentation Values: https://tools.ietf.org/html/rfc7042
・Documentation Address Blocks: https://tools.ietf.org/html/rfc5731
debugdump() が有効になって16進ダンプが出力される
生成済みのオブジェクトファイルを削除(make 実行前に毎回やることを推奨)
マルチプラットフォーム対応
Linux以外のプラットフォームへ容易に移植できるような構成にしている
static inline void *
memory_alloc(size_t size)
{
return calloc(1, size);
}
static inline void
memory_free(void *ptr)
{
free(ptr);
}
typedef pthread_mutex_t mutex_t;
#define MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER
static inline int
mutex_init(mutex_t *mutex)
{
return pthread_mutex_init(mutex, NULL);
}
...
platform/linux/platform.h
便利機能
よく使いそうな関数やマクロを util.c/util.h にあらかじめ用意してある
/*
* Logging
*/
#define errorf(...) lprintf(stderr, 'E', __FILE__, __LINE__, __func__, __VA_ARGS__)
#define warnf(...) lprintf(stderr, 'W', __FILE__, __LINE__, __func__, __VA_ARGS__)
#define infof(...) lprintf(stderr, 'I', __FILE__, __LINE__, __func__, __VA_ARGS__)
#define debugf(...) lprintf(stderr, 'D', __FILE__, __LINE__, __func__, __VA_ARGS__)
#ifdef HEXDUMP
#define debugdump(...) hexdump(stderr, __VA_ARGS__)
#else
#define debugdump(...)
#endif
extern int
lprintf(FILE *fp, int level, const char *file, int line, const char *func, const char *fmt, ...);
extern void
hexdump(FILE *fp, const void *data, size_t size);
...
util.h
+------+-------------------------------------------------+------------------+
| 0000 | 45 00 00 30 00 80 00 00 ff 01 bd 4a 7f 00 00 01 | E..0.......J.... |
| 0010 | 7f 00 00 01 08 00 35 64 00 80 00 01 31 32 33 34 | ......5d....1234 |
| 0020 | 35 36 37 38 39 30 21 40 23 24 25 5e 26 2a 28 29 | 567890!@#$%^&*() |
+------+-------------------------------------------------+------------------+
12:35:35.405 [D] main: Hello, World! (test/step0.c:7)
debugf() の出力
debugdump() の出力
HEXDUMP を define していないと debugdump() は無効になる
(CFLAGS=-DHEXDUMP … ビルド時に HEXDUMP を define している)
実際に使うタイミングで解説する
開発の進め方
下位層から上位層へ向かってインクリメンタルに実装
初期コードに各自でコードを追記
Network Interface Layer
(Link Layer)
Internet Layer
Transport Layer
Application Layer
デバイスの管理
STEP 1
ネットワークデバイス
ネットワークへの接続を提供する装置
接続されているネットワークデバイスの一覧
> ip link show
ifconfig コマンドでも確認できるが本講義では ip コマンドを使用して解説する
デバイス管理の方針
様々なデバイスを扱えるようにする
複数のネットワークデバイスを同時に扱えるようにする
このステップの作業
デバイスを管理する仕組みを作りダミーのデバイスを登録して動作を確認する
このステップのコードの雛形
コードの雛形なので関数の中身など記述されていない部分がある
新規
新規
新規
新規
新規
> git show db2fe1f
初期コードからの差分が表示される
デバイス構造体
デバイスの情報を格納するための構造体
struct net_device {
struct net_device *next;
unsigned int index;
char name[IFNAMSIZ];
uint16_t type;
uint16_t mtu;
uint16_t flags;
uint16_t hlen; /* header length */
uint16_t alen; /* address length */
uint8_t addr[NET_DEVICE_ADDR_LEN];
union {
uint8_t peer[NET_DEVICE_ADDR_LEN];
uint8_t broadcast[NET_DEVICE_ADDR_LEN];
};
struct net_device_ops *ops;
void *priv;
};
struct net_device_ops {
int (*open)(struct net_device *dev);
int (*close)(struct net_device *dev);
int (*transmit)(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst);
};
net.h
次のデバイスへのポインタ
デバイスの種別(net.h に NET_DEVICE_TYPE_XXX として定義)
デバイスの種別によって変化する値
・mtu … デバイスのMTU(Maximum Transmission Unit)の値
・flags … 各種フラグ(net.h に NET_DEVICE_FLAG_XXX として定義)
デバイスのハードウェアアドレス等
・デバイスによってアドレスサイズが異なるので大きめのバッファを用意
・アドレスを持たないデバイスでは値は設定されない
デバイスドライバが使うプライベートなデータへのポインタ
デバイスドライバに実装されている関数が設定された struct net_device_ops へのポインタ
デバイスドライバに実装されている関数へのポインタを格納
・送信関数(transmit)は必須, それ以外の関数は任意
struct net_device {
struct net_device *next;
...
}
struct net_device {
struct net_device *next;
...
}
NULL
#ifndef IFNAMSIZ
#define IFNAMSIZ 16
#endif
#define NET_DEVICE_TYPE_DUMMY 0x0000
#define NET_DEVICE_TYPE_LOOPBACK 0x0001
#define NET_DEVICE_TYPE_ETHERNET 0x0002
#define NET_DEVICE_FLAG_UP 0x0001
#define NET_DEVICE_FLAG_LOOPBACK 0x0010
#define NET_DEVICE_FLAG_BROADCAST 0x0020
#define NET_DEVICE_FLAG_P2P 0x0040
#define NET_DEVICE_FLAG_NEED_ARP 0x0100
#define NET_DEVICE_ADDR_LEN 16
デバイスの生成と登録
/* NOTE: if you want to add/delete the entries after net_run(), you need to protect these lists with a mutex. */
static struct net_device *devices;
struct net_device *
net_device_alloc(void)
{
struct net_device *dev;
dev = memory_alloc(sizeof(*dev));
if (!dev) {
errorf("memory_alloc() failure");
return NULL;
}
return dev;
}
/* NOTE: must not be call after net_run() */
int
net_device_register(struct net_device *dev)
{
static unsigned int index = 0;
dev->index = index++;
snprintf(dev->name, sizeof(dev->name), "net%d", dev->index);
dev->next = devices;
devices = dev;
infof("registered, dev=%s, type=0x%04x", dev->name, dev->type);
return 0;
}
net.c
デバイス構造体のサイズのメモリを確保
・memory_alloc() で確保したメモリ領域は0で初期化されている
・メモリが確保できなかったらエラーとしてNULLを返す
デバイスのインデックス番号を設定
デバイス名を生成(net0, net1, net2 …)
デバイスリストの先頭に追加
/* 呼び出し側のコード例 */
struct net_device *
driver_init(...)
{
struct net_device *dev;
dev = net_device_alloc();
if (!dev) {
return NULL;
}
dev->type = NET_DEVICE_TYPE_XXX;
dev->mtu = xxx;
...
if (net_device_register(dev) == -1) {
return -1;
}
...
return dev;
}
デバイスリスト(リストの先頭を指すポインタ)
メモリの確保と解放には memory_alloc() と memory_free() を使う
net1
NULL
net0
devices
net2
デバイスのオープンとクローズ
static int
net_device_open(struct net_device *dev)
{
if (NET_DEVICE_IS_UP(dev)) {
errorf("already opened, dev=%s", dev->name);
return -1;
}
if (dev->ops->open) {
if (dev->ops->open(dev) == -1) {
errorf("failure, dev=%s", dev->name);
return -1;
}
}
dev->flags |= NET_DEVICE_FLAG_UP;
infof("dev=%s, state=%s", dev->name, NET_DEVICE_STATE(dev));
return 0;
}
static int
net_device_close(struct net_device *dev)
{
if (!NET_DEVICE_IS_UP(dev)) {
errorf("not opened, dev=%s", dev->name);
return -1;
}
if (dev->ops->close) {
if (dev->ops->close(dev) == -1) {
errorf("failure, dev=%s", dev->name);
return -1;
}
}
dev->flags &= ~NET_DEVICE_FLAG_UP;
infof("dev=%s, state=%s", dev->name, NET_DEVICE_STATE(dev));
return 0;
}
net.c
デバイスの状態を確認(既にUP状態の場合はエラーを返す)
デバイスドライバのオープン関数を呼び出す
・オープン関数が設定されてない場合は呼び出しをスキップ
・エラーが返されたらこの関数もエラーを返す
UPフラグを立てる
デバイスの状態を確認(UP状態でない場合はエラーを返す)
デバイスドライバのクローズ関数を呼び出す
・クローズ関数が設定されてない場合は呼び出しをスキップ
・エラーが返されたらこの関数もエラーを返す
UPフラグを落とす
#define NET_DEVICE_FLAG_UP 0x0001
...
#define NET_DEVICE_IS_UP(x) ((x)->flags & NET_DEVICE_FLAG_UP)
#define NET_DEVICE_STATE(x) (NET_DEVICE_IS_UP(x) ? "up" : "down")
net.h
ビット演算
・論理積(AND)
・論理和(OR)
・論理否定(NOT)
・排他的論理和(XOR)
&
|
~
^
0x1F
0xF1
&
➡
0x11
0x1F
0xF1
|
➡
0xFF
0x1F
➡
0xE0
~
0x1F
0xF1
^
➡
0xEE
0001 1111
1111 0001
0001 0001
0001 1111
1111 0001
1111 1111
0001 1111
1110 0000
0001 1111
1111 0001
1110 1110
同じ位置のビットがどちらも 1 なら 1
同じ位置のビットのどちらかが 1 なら 1
各ビットを反転する(単項演算子)
同じ位置のビットが異なれば 1
デバイスへの出力
int
net_device_output(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst)
{
if (!NET_DEVICE_IS_UP(dev)) {
errorf("not opened, dev=%s", dev->name);
return -1;
}
if (len > dev->mtu) {
errorf("too long, dev=%s, mtu=%u, len=%zu", dev->name, dev->mtu, len);
return -1;
}
debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, len);
debugdump(data, len);
if (dev->ops->transmit(dev, type, data, len, dst) == -1) {
errorf("device transmit failure, dev=%s, len=%zu", dev->name, len);
return -1;
}
return 0;
}
デバイスの状態を確認(UP状態でなければ送信できないのでエラーを返す)
データのサイズを確認(デバイスのMTUを超えるサイズのデータは送信できないのでエラーを返す)
デバイスドライバの出力関数を呼び出す(エラーが返されたらこの関数もエラーを返す)
net.c
MTU(Maximum Transmission Unit)
デバイスからの入力
int
net_input_handler(uint16_t type, const uint8_t *data, size_t len, struct net_device *dev)
{
debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, len);
debugdump(data, len);
return 0;
}
net.c
Device Driver
net_input_handler()
Device Driver
Device
Device
デバイスが受信したパケットをプロトコルスタックに渡す関数
いまの段階では呼び出されたことが分かればいいのでデバッグ出力のみ
プロトコルスタックの起動と停止
int
net_run(void)
{
struct net_device *dev;
debugf("open all devices...");
for (dev = devices; dev; dev = dev->next) {
net_device_open(dev);
}
debugf("running...");
return 0;
}
void
net_shutdown(void)
{
struct net_device *dev;
debugf("close all devices...");
for (dev = devices; dev; dev = dev->next) {
net_device_close(dev);
}
debugf("shutting down");
}
int
net_init(void)
{
infof("initialized");
return 0;
}
/* 呼び出し側のコード例 */
int
main(void)
{
if (net_init() == -1) {
return -1;
}
/* デバイスの登録 */
if (net_run() == -1) {
return -1;
}
/* アプリケーションの処理 */
net_shutdown();
return 0;
}
net.c
登録済みの全デバイスをオープン
登録済みの全デバイスをクローズ
今は何もしない(後のステップで処理を追記)
ダミーデバイスの実装
static int
dummy_transmit(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst)
{
debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, len);
debugdump(data, len);
/* drop data */
return 0;
}
static struct net_device_ops dummy_ops = {
.transmit = dummy_transmit,
};
struct net_device *
dummy_init(void)
{
struct net_device *dev;
dev = net_device_alloc();
if (!dev) {
errorf("net_device_alloc() failure");
return NULL;
}
dev->type = NET_DEVICE_TYPE_DUMMY;
dev->mtu = DUMMY_MTU;
dev->hlen = 0; /* non header */
dev->alen = 0; /* non address */
dev->ops = &dummy_ops;
if (net_device_register(dev) == -1) {
errorf("net_device_register() failure");
return NULL;
}
debugf("initialized, dev=%s", dev->name);
return dev;
}
driver/dummy.c
デバイスドライバが実装している関数のアドレスを保持する構造体へのポインタを設定する
データを破棄
ヘッダもアドレスも存在しない(明示的に0を設定)
ダミーデバイスの仕様
デバイスを生成
デバイスを登録
送信関数(transmit)のみ設定
種別は net.h に定義してある
#define DUMMY_MTU UINT16_MAX
ダミーデバイスの MTU(IPデータグラムの最大サイズ)
テストプログラム
int
main(int argc, char *argv[])
{
struct net_device *dev;
signal(SIGINT, on_signal);
if (net_init() == -1) {
errorf("net_init() failure");
return -1;
}
dev = dummy_init();
if (!dev) {
errorf("dummy_init() failure");
return -1;
}
if (net_run() == -1) {
errorf("net_run() failure");
return -1;
}
while (!terminate) {
if (net_device_output(dev, 0x0800, test_data, sizeof(test_data), NULL) == -1) {
errorf("net_device_output() failure");
break;
}
sleep(1);
}
net_shutdown();
return 0;
}
test/step1.c
シグナルハンドラの設定(Ctrl+C が押された際にお行儀よく終了するように)
1秒おきにデバイスにパケットを書き込む
・まだパケットを自力で生成できないのでテストデータを用いる
プロトコルスタックの初期化
プロトコルスタックの起動
プロトコルスタックの停止
ダミーデバイスの初期化(デバイスドライバがプロトコルスタックへの登録まで済ませる)
Ctrl+C が押されるとシグナルハンドラ on_signal() の中で terminate に 1 が設定される
static volatile sig_atomic_t terminate;
static void
on_signal(int s)
{
(void)s;
terminate = 1;
}
原則、シグナルハンドラの中では下記以外の事をしない
・非同期シグナル安全な関数の呼び出し� https://www.jpcert.or.jp/sc-rules/c-sig30-c.html
・volatile sig_atomic_t 型の変数への書込み
このステップで追加したコードの動作確認
Makefileを修正してビルド&実行
DRIVERS = driver/dummy.o \
OBJS = util.o \
net.o \
TESTS = test/step0.exe \
test/step1.exe \
Makefile
> make
> ./test/step1.exe
14:07:03.237 [I] net_init: initialized (net.c:132)
14:07:03.237 [I] net_device_register: registered, dev=net0, type=0x0000 (net.c:36)
14:07:03.237 [D] dummy_init: initialized, dev=net0 (driver/dummy.c:42)
14:07:03.237 [D] net_run: open all devices... (net.c:109)
14:07:03.237 [I] net_device_open: dev=net0, state=up (net.c:54)
14:07:03.237 [D] net_run: running... (net.c:113)
14:07:03.237 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:87)
14:07:03.237 [D] dummy_transmit: dev=net0, type=0x0800, len=48 (driver/dummy.c:13)
14:07:04.237 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:87)
14:07:04.237 [D] dummy_transmit: dev=net0, type=0x0800, len=48 (driver/dummy.c:13)
...
14:07:04.689 [D] net_shutdown: close all devices... (net.c:122)
14:07:04.689 [I] net_device_close: dev=net0, state=down (net.c:72)
14:07:04.689 [D] net_shutdown: shutting down (net.c:126)
ダミーデバイスが net0 として登録された
net0 への書き込み(デバイスドライバの transmit 関数が呼ばれる)
1秒おきに書き込み
Ctrl+C を押すとお行儀よく終了する
登録されている全てのデバイスをオープン
新しく生成するオブジェクトファイルを追記
※ ソースファイルではなく「生成するオブジェクトファイル」であることに注意!
“~.c” と書くと make clean を実行した際にソースファイルが消えてしまうので間違えないように!
割り込み処理
STEP 2
割り込み
割り込み処理
実行中の処理
中断
再開
割り込み
発生
NIC
CPU
割り込み信号
IDT
ドライバ
IRQ番号とISRの対応表
ISRを呼び出し
ISRのアドレスを取得
パケット到着
プロトコルスタックへ
micropsでの実装
シグナルを使って割り込みの動作を模倣する
このステップの作業
このステップのコードの雛形
> git show 09d6772
直近のステップからの差分が表示される
新規
割り込みハンドラの登録
struct irq_entry {
struct irq_entry *next;
unsigned int irq;
int (*handler)(unsigned int irq, void *dev);
int flags;
char name[16];
void *dev;
};
/* 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 sigset_t sigmask;
...
int
intr_request_irq(unsigned int irq, int (*handler)(unsigned int irq, void *dev), int flags, const char *name, void *dev)
{
struct irq_entry *entry;
debugf("irq=%u, flags=%d, name=%s", irq, flags, name);
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");
return -1;
}
}
}
return 0;
}
platform/linux/intr.c
IRQリストへ新しいエントリを追加
entry = memory_alloc(sizeof(*entry));
if (!entry) {
errorf("memory_alloc() failure");
return -1;
}
entry->irq = irq;
entry->handler = handler;
entry->flags = flags;
strncpy(entry->name, name, sizeof(entry->name)-1);
entry->dev = dev;
entry->next = irqs;
irqs = entry;
sigaddset(&sigmask, irq);
debugf("registered: irq=%u, name=%s", irq, name);
IRQリスト(リストの先頭を指すポインタ)
新しいエントリのメモリを確保
IRQリストの先頭へ挿入
シグナル集合へ新しいシグナルを追加
IRQ構造体に値を設定
IRQ番号が既に登録されている場合、IRQ番号の共有が許可されているかどうかチェック。どちらかが共有を許可してない場合はエラーを返す。
割り込み要求(IRQ)の構造体
・デバイスと同様にリスト構造で管理する
シグナル集合(シグナルマスク用)
割り込み番号(IRQ番号)
次のIRQ構造体へのポインタ
割り込みハンドラ(割り込みが発生した際に呼び出す関数へのポインタ)
デバッグ出力で識別するための名前
割り込みの発生元となるデバイス(struct net_device 以外にも対応できるように void * で保持)
フラグ(INTR_IRQ_SHARED が指定された場合はIRQ番号を共有可能)
割り込み機構の初期化と起動・停止
static sigset_t sigmask;
static pthread_t tid;
static pthread_barrier_t barrier;
...
int
intr_run(void)
{
int err;
err = pthread_sigmask(SIG_BLOCK, &sigmask, NULL);
if (err) {
errorf("pthread_sigmask() %s", strerror(err));
return -1;
}
err = pthread_create(&tid, NULL, intr_thread, NULL);
if (err) {
errorf("pthread_create() %s", strerror(err));
return -1;
}
pthread_barrier_wait(&barrier);
return 0;
}
void
intr_shutdown(void)
{
if (pthread_equal(tid, pthread_self()) != 0) {
/* Thread not created. */
return;
}
pthread_kill(tid, SIGHUP);
pthread_join(tid, NULL);
}
platform/linux/intr.c
int
intr_init(void)
{
tid = pthread_self();
pthread_barrier_init(&barrier, NULL, 2);
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGHUP);
return 0;
}
シグナルマスクの設定
割り込み処理スレッドの起動
スレッドが動き出すまで待つ(他のスレッドが同じように pthread_barrier_wait() を呼び出し、バリアのカウントが指定の数になるまでスレッドを停止する)
シグナルマスク用のシグナル集合
pthread_sigmask()
pthread_create()
sigwait()
BLOCK
BLOCK
SIGHUP
SIGHUP
SIGINT
on_signal()
設定されている
シグナルハンドラを実行
シグナルマスクで任意のシグナルをブロック(保留)する
・ブロックしたシグナルはブロックを解除したタイミングで配送される
・ブロックしたシグナルは sigwait() で待ち受けることも可能
※ 設定したシグナルマスクは新しく生成したスレッドにも引き継がれる
指定したシグナル集合のうちの1つがブロックされて処理待ちになるまでスレッドの処理を中断して待つ。呼び出し前に既にブロックされているシグナルがあればすぐに返る。
SIGHUP
スレッドIDの初期値にメインスレッドのIDを設定する
割り込み処理スレッドが
起動済みかどうか確認
割り込み処理スレッドにシグナル(SIGHUP)を送信
割り込み処理スレッドが完全に終了するのを待つ
pthread_barrier の初期化(カウントを2に設定)
シグナル集合に SIGHUP を追加(割り込みスレッド終了通知用)
シグナル集合を初期化(空にする)
割り込みスレッドのスレッドID
スレッド間の同期のためのバリア
pthread_barrier_wait()
pthread_barrier_wait()
スレッドが起動して完全に動き出すのをバリアで同期をとる
ブロック指定されている
シグナルは処理待ちとなる
返されたシグナルの
種類に応じた処理を実行
スレッド生成
割り込みの捕捉と振り分け
static void *
intr_thread(void *arg)
{
int terminate = 0, sig, err;
struct irq_entry *entry;
debugf("start...");
pthread_barrier_wait(&barrier);
while (!terminate) {
err = sigwait(&sigmask, &sig);
if (err) {
errorf("sigwait() %s", strerror(err));
break;
}
switch (sig) {
case SIGHUP:
terminate = 1;
break;
default:
for (entry = irqs; entry; entry = entry->next) {
if (entry->irq == (unsigned int)sig) {
debugf("irq=%d, name=%s", entry->irq, entry->name);
entry->handler(entry->irq, entry->dev);
}
}
break;
}
}
debugf("terminated");
return NULL;
}
platform/linux/intr.c
割り込みに見立てたシグナルが発生するまで待機(詳しくは前ページで説明)
発生したシグナルの種類に応じた処理を記述
SIGHUP: 割り込みスレッドへ終了を通知するためのシグナル(詳しくは次ページで説明)
terminate を 1 にしてループを抜ける
デバイス割り込み用のシグナル
IRQリストを巡回
IRQ番号が一致するエントリの割り込みハンドラを呼び出す
メインスレッドと同期をとるための処理(詳しくは前ページで説明)
割り込みスレッドのエントリポイント
シグナル受信時に非同期に実行されるシグナルハンドラでは実行できる処理が大きく制限される(42ページを参照)ため、割り込み処理のために専用のスレッドを起動してシグナルの発生を待ち受けて処理する
割り込みの発生
static pthread_t tid;
...
int
intr_raise_irq(unsigned int irq)
{
return pthread_kill(tid, (int)irq);
}
platform/linux/intr.c
割り込み処理スレッドへシグナルを送信
割り込み処理スレッドのスレッドID
ダミーデバイスを割り込みに対応させる
...
#define DUMMY_IRQ INTR_IRQ_BASE
static int
dummy_transmit(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst)
{
debugf("dev=%s, type=0x%04x, len=%zu", dev->name, type, len);
debugdump(data, len);
/* drop data */
intr_raise_irq(DUMMY_IRQ);
return 0;
}
static int
dummy_isr(unsigned int irq, void *id)
{
debugf("irq=%u, dev=%s", irq, ((struct net_device *)id)->name);
return 0;
}
struct net_device *
dummy_init(void)
{
...
if (net_device_register(dev) == -1) {
errorf("net_device_register() failure");
return NULL;
}
intr_request_irq(DUMMY_IRQ, dummy_isr, INTR_IRQ_SHARED, dev->name, dev);
debugf("initialized, dev=%s", dev->name);
return dev;
}
driver/dummy.c
割り込みハンドラとして dummy_isr を登録する
テスト用に割り込みを発生させる
ダミーデバイスが使うIRQ番号
呼び出されたことが分かればいいのでデバッグ出力のみ
/* platform/linux/platform.h より抜粋 */
#define INTR_IRQ_BASE (SIGRTMIN+1)
Linuxでは SIGRTMIN ~ SIGRTMAX(34~64)までのシグナルをアプリケーションが任意の目的で利用できる。
※ SIGRTMIN(34)に関しては glibc が内部的に利用しているため +1 した番号から利用するようにしている。
プロトコルスタックの動きと連動させる
int
net_run(void)
{
struct net_device *dev;
if (intr_run() == -1) {
errorf("intr_run() failure");
return -1;
}
...
return 0;
}
void
net_shutdown(void)
{
...
intr_shutdown();
debugf("shutting down");
}
int
net_init(void)
{
if (intr_init() == -1) {
errorf("intr_init() failure");
return -1;
}
infof("initialized");
return 0;
}
net.c
割り込み機構の起動
割り込み機構の終了
割り込み機構の初期化
テストプログラム
> cp test/step1.c test/step2.c
step1のテストプログラムをコピーして使用する
このステップで追加したコードの動作確認
Makefileを修正してビルド&実行
TESTS = test/step0.exe \
test/step1.exe \
test/step2.exe \
...
ifeq ($(shell uname),Linux)
# Linux specific settings
BASE = platform/linux
CFLAGS := $(CFLAGS) -pthread -iquote $(BASE)
OBJS := $(OBJS) $(BASE)/intr.o
endif
Makefile
> make
> ./test/step2.exe
14:41:15.865 [I] net_init: initialized (net.c:141)
14:41:15.866 [I] net_device_register: registered, dev=net0, type=0x0000 (net.c:36)
14:41:15.866 [D] intr_request_irq: irq=35, flags=1, name=net0 (platform/linux/intr.c:32)
14:41:15.866 [D] intr_request_irq: registered: irq=35, name=net0 (platform/linux/intr.c:54)
14:41:15.866 [D] dummy_init: initialized, dev=net0 (driver/dummy.c:55)
14:41:15.866 [D] intr_thread: start... (platform/linux/intr.c:70)
14:41:15.866 [D] net_run: open all devices... (net.c:113)
14:41:15.866 [I] net_device_open: dev=net0, state=up (net.c:54)
14:41:15.866 [D] net_run: running... (net.c:117)
14:41:15.866 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:87)
14:41:15.866 [D] dummy_transmit: dev=net0, type=0x0800, len=48 (driver/dummy.c:17)
14:41:15.866 [D] intr_thread: irq=35, name=net0 (platform/linux/intr.c:85)
14:41:15.866 [D] dummy_isr: irq=35, dev=net0 (driver/dummy.c:27)
...
14:41:16.676 [D] net_shutdown: close all devices... (net.c:126)
14:41:16.676 [I] net_device_close: dev=net0, state=down (net.c:72)
14:41:16.677 [D] intr_thread: terminated (platform/linux/intr.c:92)
14:41:16.677 [D] net_shutdown: shutting down (net.c:131)
割り込みスレッドが起動
割り込みスレッドが終了
net0の割り込みハンドラ登録
割り込み捕捉&割り込みハンドラ呼び出し
ループバックデバイス
STEP 3
ループバックデバイス
書き込まれたデータがそのまま入力として折り返し戻ってくる特殊なデバイス
ソフトウェアとして実装された仮想的なデバイス
output
input
通常のデバイス
ループバックデバイス
output
input
このステップの作業
ループバックデバイスを実装して入力データをプロトコルスタックへ渡す
このステップのコードの雛形
新規
新規
> git show e537f78
直近のステップからの差分が表示される
ループバックデバイスの初期化関数
struct loopback {
int irq;
mutex_t mutex;
struct queue_head queue;
};
...
struct net_device *
loopback_init(void)
{
struct net_device *dev;
struct loopback *lo;
lo = memory_alloc(sizeof(*lo));
if (!lo) {
errorf("memory_alloc() failure");
return NULL;
}
lo->irq = LOOPBACK_IRQ;
mutex_init(&lo->mutex);
queue_init(&lo->queue);
dev->priv = lo;
debugf("initialized, dev=%s", dev->name);
return dev;
}
driver/loopback.c
【Exercise】
ダミーデバイスの実装を参考にして自分でコードを書く
ループバックデバイスのパラメータ
・type: net.h に定義されている定数を使う
・mtu: ソースファイル冒頭で定義している定数を使う
・hlen/alen: いずれも0を設定する
・flags: NET_DEVICE_FLAG_LOOPBACK を設定する
Exercise 3-1: デバイスの生成とパラメータの設定
Exercise 3-2: デバイスの登録と割り込みハンドラの設定
ドライバの中で使用するプライベートなデータの準備
プライベートなデータをデバイス構造体に格納する(ドライバの関数が呼び出される際にはデバイス構造体が渡されるのでここから取り出す)
ループバックデバイスのドライバ内で使用するプライベートなデータを格納するための構造体
ループバックデバイスの送信関数
struct loopback_queue_entry {
uint16_t type;
size_t len;
uint8_t data[]; /* flexible array member */
};
static int
loopback_transmit(struct net_device *dev, uint16_t type, const uint8_t *data, size_t len, const void *dst)
{
struct loopback_queue_entry *entry;
unsigned int num;
mutex_lock(&PRIV(dev)->mutex);
if (PRIV(dev)->queue.num >= LOOPBACK_QUEUE_LIMIT) {
mutex_unlock(&PRIV(dev)->mutex);
errorf("queue is full");
return -1;
}
entry = memory_alloc(sizeof(*entry) + len);
if (!entry) {
mutex_unlock(&PRIV(dev)->mutex);
errorf("memory_alloc() failure");
return -1;
}
entry->type = type;
entry->len = len;
memcpy(entry->data, data, len);
queue_push(&PRIV(dev)->queue, entry);
num = PRIV(dev)->queue.num;
mutex_unlock(&PRIV(dev)->mutex);
debugf("queue pushed (num:%u), dev=%s, type=0x%04x, len=%zd", num, dev->name, type, len);
debugdump(data, len);
intr_raise_irq(PRIV(dev)->irq);
return 0;
}
driver/loopback.c
キューの上限を超えていたらエラーを返す
キューに格納するエントリのメモリを確保(エントリ構造体+データ長)
※ 便利ツールのキューはポインタを保持するだけ
メタデータの設定とデータ本体のコピー
エントリをキューへ格納
割り込みを発生させる
キューへのアクセスをmutexで保護する(unlockを忘れずに)
struct loopback_queue_entry {
uint16_t type;
size_t len;
uint8_t data[];
}
data
entry
entry->data[0]
len
sizeof(*entry)
entry->data[len]
/* util.h より抜粋 */
struct queue_head {
...
unsigned int num;
};
extern void
queue_init(struct queue_head *queue);
extern void *
queue_push(struct queue_head *queue, void *data);
extern void *
queue_pop(struct queue_head *queue);
キューのエントリの構造体
・データ本体と付随する情報(メタデータ)を格納
・この構造体の最後のメンバは “フレキブル配列メンバ” と呼ばれる特殊なメンバ変数
・構造体の最後にだけ配置できるサイズ不明の配列
・メンバ変数としてアクセスできるが構造体のサイズには含まれない(必ずデータ部分も含めてメモリを確保すること)
#define PRIV(x) ((struct loopback *)x->priv)
ループバックデバイスの割り込みハンドラ
static int
loopback_isr(unsigned int irq, void *id)
{
struct net_device *dev;
struct loopback_queue_entry *entry;
dev = (struct net_device *)id;
mutex_lock(&PRIV(dev)->mutex);
while (1) {
entry = queue_pop(&PRIV(dev)->queue);
if (!entry) {
break;
}
debugf("queue popped (num:%u), dev=%s, type=0x%04x, len=%zd", PRIV(dev)->queue.num, dev->name, entry->type, entry->len);
debugdump(entry->data, entry->len);
net_input_handler(entry->type, entry->data, entry->len, dev);
memory_free(entry);
}
mutex_unlock(&PRIV(dev)->mutex);
return 0;
}
driver/loopback.c
キューからエントリを取り出す
取り出すエントリが無くなったらループを抜ける
net_input_handler() に受信データ本体と付随する情報を渡す
エントリのメモリを解放する
キューへのアクセスをmutexで保護
mutexのunlockを忘れずに
テストプログラム
#include "driver/dummy.h"
#include "driver/loopback.h"�
...
int
main(int argc, char *argv[])
{
...
dev = dummy_init();
dev = loopback_init();
if (!dev) {
errorf("dummy_init() failure");
errorf("loopback_init() failure");
return -1;
}
...
}
test/step3.c
> cp test/step2.c test/step3.c
step2のテストプログラムをコピーして編集する
dummy を loopback に置き換える
このステップで追加したコードの動作確認
Makefileを修正してビルド&実行
DRIVERS = driver/dummy.o \
driver/loopback.o \
TESTS = test/step0.exe \
...
test/step3.exe \
Makefile
> make
> ./test/step3.exe
16:43:16.639 [I] net_init: initialized (net.c:141)
16:43:16.639 [I] net_device_register: registered, dev=net0, type=0x0001 (net.c:36)
16:43:16.639 [D] intr_request_irq: irq=36, flags=1, name=net0 (platform/linux/intr.c:32)
16:43:16.639 [D] intr_request_irq: registered: irq=36, name=net0 (platform/linux/intr.c:54)
16:43:16.639 [D] loopback_init: initialized, dev=net0 (driver/loopback.c:116)
16:43:16.639 [D] intr_thread: start... (platform/linux/intr.c:64)
16:43:16.639 [D] net_run: open all devices... (net.c:113)
16:43:16.639 [I] net_device_open: dev=net0, state=up (net.c:54)
16:43:16.639 [D] net_run: running... (net.c:117)
16:43:16.639 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:87)
16:43:16.639 [D] loopback_transmit: queue pushed (num:1), dev=net0, type=0x0800, len=48 (driver/loopback.c:53)
16:43:16.639 [D] intr_thread: irq=36, name=net0 (platform/linux/intr.c:79)
16:43:16.640 [D] loopback_isr: queue popped (num:0), dev=net0, type=0x0800, len=48 (driver/loopback.c:72)
16:43:16.640 [D] net_input_handler: dev=net0, type=0x0800, len=48 (net.c:99)
...
ループバックデバイスが net0 として登録された
net0 への書き込み(デバイスドライバの loopback_trasmit() が呼ばれる)
割り込みの捕捉(デバイスドライバの loopback_isr() が呼び出される)
net0 へ書き込んだデータが loopback_isr() を経て net_input_handler() まで渡されている
プロトコルの管理
STEP 4
ネットワークデバイスによって運ばれるデータ
インターネット層のプロトコルのデータが運ばれてくる
プロトコル番号
プロトコル管理の方針
デバイスと同じようにリストで管理
プロトコル毎に受信キューを持つ
到着データのその後
デバイスから渡された入力データを適切なプロトコルの入力関数へ引き渡す
Device Driver
net_input_handler()
Device Driver
IP
ARP
このステップの作業の説明
プロトコルを管理する仕組みを作る
テスト用にプロトコルを登録して動作を確認
このステップのコードの雛形
新規
新規
> git show b99d67c
直近のステップからの差分が表示される
プロトコル構造体
デバイス構造体と同様に連結リストで管理
struct net_protocol {
struct net_protocol *next;
uint16_t type;
struct queue_head queue; /* input queue */
void (*handler)(const uint8_t *data, size_t len, struct net_device *dev);
};
...
static struct net_protocol *protocols;
net.c
次のプロトコルへのポインタ
プロトコルの種別(net.h に NET_PROTOCL_TYPE_XXX として定義)
受信キュー
プロトコルの入力関数へのポインタ
登録されているプロトコルのリスト(グローバル変数)
struct net_protocol {
struct net_protocol *next;
...
}
struct net_protocol {
struct net_protocol *next;
...
}
NULL
protocols
プロトコルの登録
/* NOTE: must not be call after net_run() */
int
net_protocol_register(uint16_t type, void (*handler)(const uint8_t *data, size_t len, struct net_device *dev))
{
struct net_protocol *proto;
for (proto = protocols; proto; proto = proto->next) {
if (type == proto->type) {
errorf("already registered, type=0x%04x", type);
return -1;
}
}
proto = memory_alloc(sizeof(*proto));
if (!proto) {
errorf("memory_alloc() failure");
return -1;
}
proto->type = type;
proto->handler = handler;
proto->next = protocols;
protocols = proto;
infof("registered, type=0x%04x", type);
return 0;
}
重複登録の確認(指定された種別のプロトコルが登録済みの場合はエラーを返す)
プロトコル構造体のメモリを確保
net.c
プロトコルリストの先頭に追加
プロトコル種別と入力関数を設定
protocol1
NULL
protocol0
protocols
protocol2
到着データの振り分けと受信キューへの挿入
struct net_protocol_queue_entry {
struct net_device *dev;
size_t len;
uint8_t data[];
};
...
int
net_input_handler(uint16_t type, const uint8_t *data, size_t len, struct net_device *dev)
{
struct net_protocol *proto;
struct net_protocol_queue_entry *entry;
for (proto = protocols; proto; proto = proto->next) {
if (proto->type == type) {
debugf("queue pushed (num:%u), dev=%s, type=0x%04x, len=%zu",
proto->queue.num, dev->name, type, len);
debugdump(data, len);
return 0;
}
}
/* unsupported protocol */
return 0;
}
net.c
Exercise 4-1: プロトコルの受信キューにエントリを挿入
(1) 新しいエントリのメモリを確保(失敗したらエラーを返す)
(2) 新しいエントリへメタデータの設定と受信データのコピー
(3) キューに新しいエントリを挿入(失敗したらエラーを返す)
受信キューのエントリの構造体
・受信データと付随する情報(メタデータ)を格納
・ループバックデバイスの時と同じ ※ 62ページを参照
struct net_protocol_queue_entry {
struct net_device *dev;
size_t len;
uint8_t data[];
}
data
entry
entry->data[0]
len
sizeof(*entry)
プロトコルが見つからなかったら黙って捨てる
テスト用にIPの登録
static void
ip_input(const uint8_t *data, size_t len, struct net_device *dev)
{
debugf("dev=%s, len=%zu", dev->name, len);
debugdump(data, len);
}
int
ip_init(void)
{
if (net_protocol_register(NET_PROTOCOL_TYPE_IP, ip_input) == -1) {
errorf("net_protocol_register() failure");
return -1;
}
return 0;
}
ip.c
#include "ip.h"
int
net_init(void)
{
...
if (ip_init() == -1) {
errorf("ip_init() failure");
return -1;
}
infof("initialized");
return 0;
}
net.c
このステップでは登録した入力関数が呼び出されたことが分かればいいのでデバッグ出力のみ
プロトコルスタックにIPの入力関数を登録する
プロトコルスタック初期化時にIPの初期化関数を呼び出す
テストプログラム
int
main(int argc, char *argv[])
{
...
while (!terminate) {
if (net_device_output(dev, NET_PROTOCOL_TYPE_IP, test_data, sizeof(test_data), NULL) == -1) {
errorf("net_device_output() failure");
break;
}
sleep(1);
}
net_shutdown();
return 0;
}
test/step4.c
> cp test/step3.c test/step4.c
step3のテストプログラムをコピーして編集する
マジックナンバーを定数に置き換える
このステップで追加したコードの動作確認
Makefileを修正してビルド&実行
OBJS = util.o \
net.o \
ip.o \
TESTS = test/step0.exe \
...
test/step4.exe \
Makefile
> make
> ./test/step4.exe
15:57:32.436 [I] net_protocol_register: registered, type=0x0800 (net.c:132)
15:57:32.436 [I] net_init: initialized (net.c:209)
15:57:32.436 [I] net_device_register: registered, dev=net0, type=0x0001 (net.c:51)
15:57:32.436 [D] intr_request_irq: irq=36, flags=1, name=net0 (platform/linux/intr.c:32)
15:57:32.436 [D] intr_request_irq: registered: irq=36, name=net0 (platform/linux/intr.c:54)
15:57:32.436 [D] loopback_init: initialized, dev=net0 (driver/loopback.c:116)
15:57:32.436 [D] intr_thread: start... (platform/linux/intr.c:70)
15:57:32.436 [D] net_run: open all devices... (net.c:175)
15:57:32.436 [I] net_device_open: dev=net0, state=up (net.c:69)
15:57:32.437 [D] net_run: running... (net.c:179)
15:57:32.437 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:102)
15:57:32.437 [D] loopback_transmit: queue pushed (num:1), dev=net0, type=0x0800, len=48 (driver/loopback.c:53)
15:57:32.437 [D] intr_thread: irq=36, name=net0 (platform/linux/intr.c:85)
15:57:32.437 [D] loopback_isr: queue popped (num:0), dev=net0, type=0x0800, len=48 (driver/loopback.c:72)
15:57:32.437 [D] net_input_handler: queue pushed (num:1), dev=net0, type=0x0800, len=48 (net.c:157)
15:57:33.437 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:102)
15:57:33.437 [D] loopback_transmit: queue pushed (num:1), dev=net0, type=0x0800, len=48 (driver/loopback.c:53)
15:57:33.437 [D] intr_thread: irq=36, name=net0 (platform/linux/intr.c:85)
15:57:33.437 [D] loopback_isr: queue popped (num:0), dev=net0, type=0x0800, len=48 (driver/loopback.c:72)
15:57:33.438 [D] net_input_handler: queue pushed (num:2), dev=net0, type=0x0800, len=48 (net.c:157)
...
IP (0x0800) の受信キューへ挿入されたことが確認できればOK
プロトコルスタックへ IP (0x0800) を登録
IPの受信キューに挿入
IPの受信キューに挿入
ソフトウェア割り込み
STEP 5
受信キューに格納されたデータの処理
前のステップではプロトコルの受信キューに格納するところまで
Device Driver
net_input_handler()
ip_input()
ハードウェア割り込み
ソフトウェア割り込み
受信キュー
bottom-half
top-half
CPU
割り込み信号
ハードウェア割り込み
割り込み命令
ソフトウェア割り込み
①
②
③
④
⑤
このステップの作業の説明
ステップ2で実装した割り込みの仕組みでソフトウェア割り込みを模倣する
このステップのコードの雛形
> git show 8fa69f8
直近のステップからの差分が表示される
ソフトウェア割り込みの発生とハンドラの定義
int
net_input_handler(uint16_t type, const uint8_t *data, size_t len, struct net_device *dev)
{
...
debugdump(data, len);
intr_raise_irq(INTR_IRQ_SOFTIRQ);
return 0;
}
}
/* unsupported protocol */
return 0;
}
int
net_softirq_handler(void)
{
struct net_protocol *proto;
struct net_protocol_queue_entry *entry;
for (proto = protocols; proto; proto = proto->next) {
while (1) {
entry = queue_pop(&proto->queue);
if (!entry) {
break;
}
debugf("queue popped (num:%u), dev=%s, type=0x%04x, len=%zu", proto->queue.num, entry->dev->name, proto->type, entry->len);
debugdump(entry->data, entry->len);
proto->handler(entry->data, entry->len, entry->dev);
memory_free(entry);
}
}
return 0;
}
net.c
プロトコルの受信キューへエントリを追加した後、ソフトウェア割り込みを発生させる
プロトコルリストを巡回(全てのプロトコルを確認)
受信キューからエントリを取り出す(エントリが存在するあいだ処理を繰り返す)
・エントリがなけばループを抜ける
プロトコルの入力関数を呼び出す
使い終わったエントリのメモリを開放
ソフトウェア割り込みが発生した際に呼び出してもらう関数
/* platform/linux/platform.h より抜粋 */
#define INTR_IRQ_SOFTIRQ SIGUSR1
ソフトウェア割り込みの捕捉とハンドラの呼び出し
static void *
intr_thread(void *arg)
{
...
switch (sig) {
case SIGHUP:
terminate = 1;
break;
case SIGUSR1:
net_softirq_handler();
break;
default:
...
}
...
int
intr_init(void)
{
...
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGHUP);
sigaddset(&sigmask, SIGUSR1);
return 0;
}
platform/linux/intr.c
ソフトウェア割り込みとして使用する SIGUSR1 を捕捉するためにマスク用のシグナル集合へ追加
ソフトウェア割り込み用のシグナル(SIGUSR1)を捕捉した際の処理を追加
・net_softirq_handler() を呼び出す
テストプログラム
> cp test/step4.c test/step5.c
step4のテストプログラムをコピーして使用する
このステップで追加したコードの動作確認
Makefileを修正してビルド&実行
TESTS = test/step0.exe \
...
test/step5.exe \
Makefile
> make
> ./test/step5.exe
15:59:10.020 [I] net_protocol_register: registered, type=0x0800 (net.c:132)
15:59:10.020 [I] net_init: initialized (net.c:231)
15:59:10.020 [I] net_device_register: registered, dev=net0, type=0x0001 (net.c:51)
15:59:10.020 [D] intr_request_irq: irq=36, flags=1, name=net0 (platform/linux/intr.c:33)
15:59:10.020 [D] intr_request_irq: registered: irq=36, name=net0 (platform/linux/intr.c:55)
15:59:10.020 [D] loopback_init: initialized, dev=net0 (driver/loopback.c:116)
15:59:10.021 [D] intr_thread: start... (platform/linux/intr.c:71)
15:59:10.021 [D] net_run: open all devices... (net.c:197)
15:59:10.021 [I] net_device_open: dev=net0, state=up (net.c:69)
15:59:10.021 [D] net_run: running... (net.c:201)
15:59:10.021 [D] net_device_output: dev=net0, type=0x0800, len=48 (net.c:102)
15:59:10.021 [D] loopback_transmit: queue pushed (num:1), dev=net0, type=0x0800, len=48 (driver/loopback.c:53)
15:59:10.021 [D] intr_thread: irq=36, name=net0 (platform/linux/intr.c:89)
15:59:10.021 [D] loopback_isr: queue popped (num:0), dev=net0, type=0x0800, len=48 (driver/loopback.c:72)
15:59:10.021 [D] net_input_handler: queue pushed (num:1), dev=net0, type=0x0800, len=48 (net.c:157)
15:59:10.021 [D] net_softirq_handler: queue popped (num:0), dev=net0, type=0x0800, len=48 (net.c:179)
15:59:10.021 [D] ip_input: dev=net0, len=48 (ip.c:11)
...
ソフトウェア割り込みの捕捉&ハンドラの呼び出し
IPの入力関数の呼び出し
お疲れさまでした!
ゆっくり休んで明日も頑張りましょう💪