1 of 86

TCP/IPプロトコルスタック自作開発

Day1

KLab株式会社

第6回 KLab Expert Camp

2 of 86

自己紹介

twitter.com/pandax381

山本 雅也

YAMAMOTO

Masaya

KLab株式会社 Kラボラトリー

github.com/pandax381

pandax381@gmail.com

主な業務

  • OSS開発
    • 自分のプロジェクト
      • mruby-esp32 - mruby for the ESP32
      • PACKDROP - Raspberry Pi based Handmade Network Emulator
      • tcpeek - TCP 3way-handshake monitor
      • rlogd - Reliable LOG Daemon
      • AccelTCP - ACCELerate TCP proxy
    • 既存プロジェクトへのコントリビューション
      • Keepalived - Load balancing & High-Availability
      • mruby - Lightweight Ruby
      • Jool - SIIT and NAT64 for Linux
  • 自作プロトコルスタックの布教
    • インターンやイベントの実施
    • 書籍の執筆

デジタルハリウッド大学 客員准教授

3 of 86

KLabの技術インターン(通年実施インターンの紹介)

マニュアルのない完全オーダーメイド型

  • 一人ひとりの可能性を最大限に切り開く
    • 参加する学生がテーマや取り組む内容を自由に決める
    • 対象の技術領域で活躍するエンジニアがメンターとしてサポート
    • 目標に向かって一緒にチャレンジ
  • ゲーム以外の分野にも対応
    • ネットワーク同期型のビープ音MIDI演奏システム
    • Type-1 Hypervisorへのmruby移植(SoftFloat対応)
    • 自作プロトコルスタック関連
      • DPDK対応
      • DHCPクライアントの実装
      • IP転送の実装

4 of 86

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

5 of 86

TCP/IPプロトコルスタック自作開発

プロトコルスタックの実装を通じてTCP/IPを完全に理解する

  • TCP/IPの知識を実際にコードとして具現化する機会は少ない
    • トランスポート層より下はOS内部のプロトコルスタックの仕事
    • TCPで3wayハンドシェイクする部分のコードは(普通は)自分では書かない
    • HTTPなどアプリケーションプロトコルのやりとりもライブラリに任せることが多い
    • 自分でコードを書くことによりプロトコルの細部に触れ理解が深まる
  • ブラックボックスを解き明かす楽しさ
    • 点在していた知識がひとつに繋がる瞬間
    • 通信の大部分を支配下に治めることによる全能感
    • もっと下へ(低レイヤ沼へのいざない)

6 of 86

コース説明

参加者の目的に応じて2つのコースに分かれて開発

  • 基本コース
    • リファレンス実装のコード解説を中心に進行
    • 各プロトコルの処理やレイヤ間の連携の方法を学ぶ
    • TCP/IPの主要プロトコルのコードを自分の手で書き上げる
  • アドバンスドコース
    • 各自がチャレンジしたい課題に取り組む
    • 講師陣のフォローを受けながらゴールに向かって黙々と開発

7 of 86

開発スケジュール

イベント全体のスケジュール

  • 8/24(木)~ 8/30(水)の平日5日間
  • 各日 11:00 ~ 20:00 で実施
  • 最終日に成果発表&懇親会

8/24

(木)

8/25

(金)

8/26

(土)

8/27

(日)

8/28

(月)

8/29

(火)

8/30

(水)

1日目

2日目

3日目

4日目

5日目

8 of 86

開発スケジュール

基本コース

アドバンスドコース

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(後半)

9 of 86

成果発表

最終日の講義後に成果発表を実施

  • 発表時間は各自5分以内(人数が多いため長くなりすぎないように)
  • 内容は各自にお任せ(ゆるい感じでOK)

成果発表&懇親会

スライド作成

15:00

17:00

17:30

20:00

10 of 86

コミュニケーション

Meet と Slack を併用

  • Meet
    • メイン会場(全員集合&基本コース講義用)
    • もくもく部屋(アドバンスドコース作業用)
  • Slack
      • #0-全体アナウンス
      • #1-雑談
      • #2-基本コース
      • #3-アドバンスドコース

URL削除

URL削除

URL削除

11 of 86

KLab Expert Camp

Start🎉

12 of 86

本日の目標とタスク

自作プロトコルスタックの骨格を組み上げる

  • ネットワーク基礎知識のおさらい
  • 開発環境とリファレンス実装の説明
  • デバイスの管理
  • 割り込み処理
  • ループバックデバイスの実装
  • プロトコルの管理
  • ソフトウェア割り込み

13 of 86

ネットワーク基礎知識のおさらい

14 of 86

TCP/IPのアーキテクチャ

シンプルな4階層のネットワークモデル

物理層

データリンク層

ネットワーク層

トランスポート層

セッション層

プレゼンテーション層

アプリケーション層

インターネット層

トランスポート層

アプリケーション層

ネットワークインタフェース層

OSI

TCP/IP

媒体上で直接接続されているノード間のデータ転送

異なるネットワークに存在するノード間でのデータ転送

ノード上のアプリケーションプロセス間のデータ転送

アプリケーションプロセス固有のやりとり

15 of 86

プロトコルスタック

複数のプロトコルが協調して動作する

  • 各階層には責務を果たすための様々なプロトコルが存在する
  • 同じ階層や上下の階層のプロトコルと連携しながら通信を成立させる

インターネット層

(ネットワーク層)

トランスポート層

アプリケーション層

ネットワークインタフェース層

(リンク層)

Ethernet

SLIP

PPP

IP

IPv6

ARP

ICMP

UDP

TCP

User

Kernel

Device Driver

HTTP

DNS

SMTP

DHCP

16 of 86

データの流れ

各階層がデータ転送に必要な情報をヘッダとして付与(カプセル化)

インターネット層

トランスポート層

アプリケーション層

ネットワークインタフェース層

データ

データ

ヘッダ

データ

ヘッダ

データ

ヘッダ

ヘッダ

ヘッダ

ヘッダ

インターネット層

トランスポート層

アプリケーション層

ネットワークインタフェース層

17 of 86

アプリケーションとプロトコルスタックの関係

アプリケーションは ソケット を通じてプロトコルスタックの機能を利用する

Application

Protocol Stack

Device Driver

Network Device

User

Kernel

Socket API

Software

Hardware

socket

bind

listen

accept

connect

recv

send

close

ソケット関連の主要なシステムコール

18 of 86

開発環境とリファレンス実装の説明

19 of 86

開発環境

各自のLinux開発環境で作業

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

20 of 86

リファレンス実装

https://github.com/pandax381/microps

  • 学習用のTCP/IPプロトコルスタック
    • Ethernet / ARP / IP / ICMP / UDP / TCP を全て自前で実装
    • 10年くらい細々と作り続けている(5世代目くらい)
    • C言語で4,500行程度(一番大きなtcp.cが1,500行)
    • 教育用OSへの移植実績あり(xv6-net, mikanos-net
  • GitHubでリポジトリをforkしたものを手元にcloneする
    • masterブランチが最新のコード
    • 講義で解説するコードはこれを少しだけ簡略化している(kec5ブランチ)
    • GitHubでリポジトリをforkする際には「Copy the master branch only」のチェックを外す
    • ついでにスターも付けてくれると作者が喜びます
  • まずはチュートリアルの通りに動かしてみよう

21 of 86

チュートリアル(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したリポジトリをクローンする

22 of 86

チュートリアル(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番ポート(サンプルアプリケーション)への接続に成功

入力したテキストと全く同じものがサンプルアプリケーションから送り返されてきている

23 of 86

microps

ユーザアプリケーション用のライブラリとして実装

  • OSのプロトコルスタックを通さずにパケットを処理する(詳細は後日解説)

NIC

デバイスドライバ

TCP/IP

プロトコルスタック

ソケットAPI

ユーザ

アプリケーション

Kernel

ユーザ

アプリケーション

microps

(ライブラリ)

ユーザ

アプリケーション

ソケット風 API

自作 TCP/IP

プロトコルスタック

疑似デバイスドライバ

microps

24 of 86

初期コード

以下のコミットをチェックアウトして初期コードとして使用する

> make clean

> git checkout bdbf73b -b work

初期コードに含まれているファイル

  • LICENSE … ライセンスファイル(MITライセンス、著作権表示)※ 消したらダメ
  • Makefile … makeコマンド用のルールを記述するファイル
  • platform/linux/platform.h … プラットフォームごとの仕様の違いを吸収するコード
  • test/step0.c … 初期コードのビルドを通すためのテストプログラム
  • test/test.h … テストプログラム用ヘッダファイル(テストデータ等)
  • util.c
  • util.h

便利機能の詰め合わせライブラリ

チュートリアルで生成したファイルが残っているので掃除

> git remote add upstream git@github.com:pandax381/microps.git

> git fetch upstream

該当するコミットが見つからない場合の対処(リポジトリをforkする際にmasterブランチしかコピーしなかった等)

25 of 86

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コンパイラのオプション

  • あらかじめビルドルールを記述してある
  • 新しくファイルを追加したら Makefile に追記する

26 of 86

テストプログラム

テストプログラムのビルドと実行

#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 実行前に毎回やることを推奨)

27 of 86

マルチプラットフォーム対応

Linux以外のプラットフォームへ容易に移植できるような構成にしている

  • platform ディレクトリ配下に各プラットフォーム依存のコードを配置
    • ここではLinuxを対象とするため platform/linux ディレクトリが該当
  • プラットフォームによって使い分けが必要な機能
    • メモリの確保、解放
      • LinuxやmacOSではmallocfreeを使う
      • xv6のカーネル内ではkmallockfreeを使う�(MikanOSではC++のnewdeleteを使う)
    • 排他制御
      • LinuxやmacOSではpthreadのmutexを使う
      • xv6カーネル内ではspinlockを使う

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

28 of 86

便利機能

よく使いそうな関数やマクロを 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 している)

実際に使うタイミングで解説する

29 of 86

開発の進め方

下位層から上位層へ向かってインクリメンタルに実装

  • 機能単位で「実装」「テスト」を繰り返して少しずつ組み上げていく
  • 最終的に自作プロトコルスタックを利用する通信アプリケーションを作成する

初期コードに各自でコードを追記

  • 各ステップでコードの雛形を掲示
  • スライドで実装に必要な手順等を解説
  • 写経だけでなく考えながら実装

Network Interface Layer

(Link Layer)

Internet Layer

Transport Layer

Application Layer

30 of 86

デバイスの管理

STEP 1

31 of 86

ネットワークデバイス

ネットワークへの接続を提供する装置

  • TCP/IPのネットワークインタフェース層に位置する
    • NIC(Network Interface Card)とも呼ばれる
    • Ethernetが支配的だが他のリンクプロトコル用のネットワークデバイスも存在する
  • IPパケットの出入口
    • IPパケットはネットワークデバイスを通じてリンクプロトコルによって運ばれる

接続されているネットワークデバイスの一覧

> ip link show

ifconfig コマンドでも確認できるが本講義では ip コマンドを使用して解説する

32 of 86

デバイス管理の方針

様々なデバイスを扱えるようにする

  • TCP/IPは様々なネットワークデバイスの上で動作する
    • ネットワークデバイスはIPパケットを運べさえすればいい
    • 媒体の種別や信頼性の有無, 運べるデータの許容量も問わない
    • Ethernetだけではなく他のデバイスにも対応できるような設計にする

複数のネットワークデバイスを同時に扱えるようにする

  • ノードが複数のネットワークデバイスを持つことは珍しくない
  • ルータとして振る舞う際には複数のデバイスを同時に扱える必要がある
  • 後から根幹の設計を変えるのは大変なので当初から対応させておく
    • micropsは後から対応させようとして丸ごと作り直しになった

33 of 86

このステップの作業

デバイスを管理する仕組みを作りダミーのデバイスを登録して動作を確認する

  • デバイス構造体
  • デバイスの生成と登録
  • デバイスのオープンとクローズ
  • デバイスへの出力
  • デバイスからの入力
  • プロトコルスタックの起動と停止
  • ダミーデバイスの実装

34 of 86

このステップのコードの雛形

  • driver/dummy.c
  • driver/dummy.h
  • net.c
  • net.h
  • test/step1.c

コードの雛形なので関数の中身など記述されていない部分がある

  • まず、雛形の内容を写経して追記する
  • スライドと解説を聞いて未実装の部分を埋める
    • スライドの 緑色のコード はそのまま写経すればOK、Exercise は自分で考えてコードを書く

新規

新規

新規

新規

新規

> git show db2fe1f

初期コードからの差分が表示される

35 of 86

デバイス構造体

デバイスの情報を格納するための構造体

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

36 of 86

デバイスの生成と登録

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

37 of 86

デバイスのオープンとクローズ

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

38 of 86

デバイスへの出力

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)

  • そのデータリンクで一度に送信可能なデータの最大サイズ
  • データリンクよってMTUの値は異なる(Ethernetの標準的なMTUの値は1500)
  • MTUを超えるサイズのデータは送信できない(上位のレイヤでサイズを調整する必要がある)

39 of 86

デバイスからの入力

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

デバイスが受信したパケットをプロトコルスタックに渡す関数

  • プロトコルスタックへのデータの入口であり�デバイスドライバから呼び出されることを想定している
  • この先のステップで利用する(このステップでは利用しない)

いまの段階では呼び出されたことが分かればいいのでデバッグ出力のみ

40 of 86

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

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

登録済みの全デバイスをオープン

登録済みの全デバイスをクローズ

今は何もしない(後のステップで処理を追記)

41 of 86

ダミーデバイスの実装

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データグラムの最大サイズ)

42 of 86

テストプログラム

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 型の変数への書込み

43 of 86

このステップで追加したコードの動作確認

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 を実行した際にソースファイルが消えてしまうので間違えないように!

44 of 86

割り込み処理

STEP 2

45 of 86

割り込み

  • ハードウェアからの要求に応じて実行中の処理を一時的に中断し別の処理を実行する仕組み(割り込み処理を終えた後に中断していた処理を再開する)
  • 割り込みによって実行される処理は 割り込みハンドラ(Interrupt Handler)割り込みサービスルーチン(Interrupt Service Routine:ISR)と呼ばれる
  • 割り込みの要求が発生源を識別するために 割り込み番号(IRQ番号)を割り当て、IRQ番号ごとにドライバの入力関数などを割り込みハンドラとして設定する

割り込み処理

実行中の処理

中断

再開

割り込み

発生

NIC

CPU

割り込み信号

IDT

ドライバ

IRQ番号とISRの対応表

ISRを呼び出し

ISRのアドレスを取得

パケット到着

プロトコルスタックへ

46 of 86

micropsでの実装

シグナルを使って割り込みの動作を模倣する

  • ユーザ空間で動作するためハードウェアの割り込みを直接扱うことはできない
  • OSが提供しているシグナルを利用して似たような動きを模倣する
    • シグナルはアプリケーションが利用できる割り込みに相当する機能
    • アプリケーションプロセスに対して外部からイベントの発生を通知するための仕組み
      • Ctrl+C でアプリケーションを強制終了できるのもシグナルで実現されている
    • シグナルを受け取った際に本来の処理を中断して任意の処理を実行できる
      • テストプログラムでは Ctrl+C で発生するシグナル用のシグナルハンドラを設定
  • プラットフォームごとに実装を変えられるようにする
    • Linuxの場合にはシグナルを利用して割り込みを模倣する
    • xv6などOSのカーネルに組み込む場合には本物の割り込みを利用する

47 of 86

このステップの作業

  • 割り込みハンドラの登録
  • 割り込みの発生
  • 割り込みの捕捉と振り分け
  • 割り込み機構の初期化と起動・停止
  • ダミーデバイスを割り込みに対応させる
  • プロトコルスタックの動きと連動させる

48 of 86

このステップのコードの雛形

  • driver/dummy.c
  • platform/linux/intr.c
  • platform/linux/platform.h

> git show 09d6772

直近のステップからの差分が表示される

新規

49 of 86

割り込みハンドラの登録

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番号を共有可能)

50 of 86

割り込み機構の初期化と起動・停止

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()

スレッドが起動して完全に動き出すのをバリアで同期をとる

ブロック指定されている

シグナルは処理待ちとなる

返されたシグナルの

種類に応じた処理を実行

スレッド生成

51 of 86

割り込みの捕捉と振り分け

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ページを参照)ため、割り込み処理のために専用のスレッドを起動してシグナルの発生を待ち受けて処理する

52 of 86

割り込みの発生

static pthread_t tid;

...

int

intr_raise_irq(unsigned int irq)

{

return pthread_kill(tid, (int)irq);

}

platform/linux/intr.c

割り込み処理スレッドへシグナルを送信

割り込み処理スレッドのスレッドID

53 of 86

ダミーデバイスを割り込みに対応させる

...

#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 した番号から利用するようにしている。

54 of 86

プロトコルスタックの動きと連動させる

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

割り込み機構の起動

割り込み機構の終了

割り込み機構の初期化

55 of 86

テストプログラム

> cp test/step1.c test/step2.c

step1のテストプログラムをコピーして使用する

56 of 86

このステップで追加したコードの動作確認

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の割り込みハンドラ登録

割り込み捕捉&割り込みハンドラ呼び出し

57 of 86

ループバックデバイス

STEP 3

58 of 86

ループバックデバイス

書き込まれたデータがそのまま入力として折り返し戻ってくる特殊なデバイス

  • 自分自身と通信するために使われる
    • ローカルホスト上で稼働しているサービスとの通信

ソフトウェアとして実装された仮想的なデバイス

  • 物理的にネットワークと接続されていない状態であっても機能する
  • lolo0 といったデバイス名でシステム起動時に自動的に設定されている

output

input

通常のデバイス

ループバックデバイス

output

input

59 of 86

このステップの作業

ループバックデバイスを実装して入力データをプロトコルスタックへ渡す

  • 送信関数(loopback_transmit)
    • 渡されたデータをキューに格納する
    • 割り込みを発生させる
  • 割り込みハンドラ(loopback_isr)
    • キューからデータを取り出す
    • プロトコルスタックの入力ハンドラ(net_input_handler)を呼び出す
  • 初期化(loopback_init)
    • デバイスの生成と登録
    • 割り込みハンドラの登録

60 of 86

このステップのコードの雛形

  • driver/loopback.c
  • driver/loopback.h

新規

新規

> git show e537f78

直近のステップからの差分が表示される

61 of 86

ループバックデバイスの初期化関数

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: デバイスの登録と割り込みハンドラの設定

ドライバの中で使用するプライベートなデータの準備

プライベートなデータをデバイス構造体に格納する(ドライバの関数が呼び出される際にはデバイス構造体が渡されるのでここから取り出す)

ループバックデバイスのドライバ内で使用するプライベートなデータを格納するための構造体

62 of 86

ループバックデバイスの送信関数

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)

63 of 86

ループバックデバイスの割り込みハンドラ

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を忘れずに

64 of 86

テストプログラム

#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 に置き換える

65 of 86

このステップで追加したコードの動作確認

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() まで渡されている

66 of 86

プロトコルの管理

STEP 4

67 of 86

ネットワークデバイスによって運ばれるデータ

インターネット層のプロトコルのデータが運ばれてくる

  • IP / IPv6 / ARP など(ICMPは例外でIPによって運ばれる)
  • どのプロトコルのデータを運んでいるのかはリンクプロトコルの情報で把握できる

プロトコル番号

  • プロトコルの種別を判別するために番号で管理する
    • どのプロトコルのデータを運んでいるのかを示す
  • リンクプロトコルによって番号体系が異なる
    • IPしか運ばないリンクプロトコルではプロトコル番号という概念がないこともある
  • プロトコルスタック内部では Ethernet Type の値を採用する
    • もし番号体系が異なる場合にはデバイスドライバでプロトコル番号を変換する
      • ex) PPP は運んでいるプロトコルを 別の番号体系 で表している

68 of 86

プロトコル管理の方針

デバイスと同じようにリストで管理

  • 利用するプロトコルをプロトコルスタックの初期化時に選択できる
    • ex) IPv4 は使うけど IPv6 は使わない
  • 新しいプロトコルへ容易に対応できる
    • プロトコル番号と入力関数を登録するだけで新しいプロトコルに対応できる

プロトコル毎に受信キューを持つ

  • 入力データの処理に時間が掛かるとパケットを取りこぼしてしまう可能性がある
    • 本物のデバイスドライバで割り込みで処理する場合は特に時間をかけてはいけない
    • プロトコルスタックの他の処理も出来なくなってしまう
  • 受信キューを挟むことによって入力処理とその後の処理を分離できる

69 of 86

到着データのその後

デバイスから渡された入力データを適切なプロトコルの入力関数へ引き渡す

  • プロトコル番号をもとに対象プロトコルを特定
    • 登録されているプロトコルのリストを検索
  • プロトコルの受信キューにデータをプッシュ
    • net_input_handler() の仕事はここまで
  • プロトコルの入力関数を呼び出す(次のステップ)
    • 受信キューにデータが存在する場合�そのプロトコルの入力関数を呼び出す
    • ソフトウェア割り込みをトリガーとして実行する

Device Driver

net_input_handler()

Device Driver

IP

ARP

70 of 86

このステップの作業の説明

プロトコルを管理する仕組みを作る

  • プロトコル構造体
  • プロトコルの登録
  • 到着データの振り分けと受信キューへの挿入

テスト用にプロトコルを登録して動作を確認

  • IPの登録

71 of 86

このステップのコードの雛形

  • ip.c
  • ip.h
  • net.c
  • net.h

新規

新規

> git show b99d67c

直近のステップからの差分が表示される

72 of 86

プロトコル構造体

デバイス構造体と同様に連結リストで管理

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

73 of 86

プロトコルの登録

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

74 of 86

到着データの振り分けと受信キューへの挿入

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)

プロトコルが見つからなかったら黙って捨てる

75 of 86

テスト用に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の初期化関数を呼び出す

76 of 86

テストプログラム

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のテストプログラムをコピーして編集する

マジックナンバーを定数に置き換える

77 of 86

このステップで追加したコードの動作確認

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の受信キューに挿入

78 of 86

ソフトウェア割り込み

STEP 5

79 of 86

受信キューに格納されたデータの処理

前のステップではプロトコルの受信キューに格納するところまで

  • 受信キューに格納されたデータをプロトコルの入力関数に渡す処理が存在しない
  • 受信キューに格納したらソフトウェア割り込みを発生させる
  • その後の処理はソフトウェア割り込みをトリガーとして非同期に実行する

Device Driver

net_input_handler()

ip_input()

ハードウェア割り込み

ソフトウェア割り込み

受信キュー

bottom-half

top-half

CPU

割り込み信号

ハードウェア割り込み

割り込み命令

ソフトウェア割り込み

80 of 86

このステップの作業の説明

ステップ2で実装した割り込みの仕組みでソフトウェア割り込みを模倣する

  • ソフトウェア割り込みの発生
  • 割り込みハンドラの定義
  • 割り込みの捕捉とハンドラの呼び出し

81 of 86

このステップのコードの雛形

  • net.c
  • net.h
  • platform/linux/intr.c
  • platform/linux/platform.h

> git show 8fa69f8

直近のステップからの差分が表示される

82 of 86

ソフトウェア割り込みの発生とハンドラの定義

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

83 of 86

ソフトウェア割り込みの捕捉とハンドラの呼び出し

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() を呼び出す

84 of 86

テストプログラム

> cp test/step4.c test/step5.c

step4のテストプログラムをコピーして使用する

85 of 86

このステップで追加したコードの動作確認

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の入力関数の呼び出し

86 of 86

お疲れさまでした!

ゆっくり休んで明日も頑張りましょう💪