NetBSDのカーネルモジュールサンプルを試してみる

[NetBSD Advent Calendar 2018](https://qiita.com/advent-calendar/2018/netbsd) 3日目の記事です。
今日はNetBSDのカーネルモジュールの話をしようと思います。

## カーネルモジュール

カーネルモジュールについては `module(7)` の説明を見るのが手っ取り早いです。カーネルモジュールはシステム管理者が稼働中のシステムに対し、動的に追加・削除することができる機能で、開発者が再起動を必要としない形でカーネル機能の追加が可能になります。

```
DESCRIPTION
     Kernel modules allow the system administrator to dynamically add and
     remove functionality from a running system.  This also helps software
     developers add new parts of the kernel without constantly rebooting to
     test their changes.
```

## カーネルモジュールのサンプル

NetBSDソースツリーの `sys/modules/examples` 以下にカーネルモジュールのサンプルが用意されています。
今回はこのサンプルを元にカーネルモジュールを試してみましょう。

```sh 
# cd /usr/src/sys/modules/examples
# ls -F
CVS/           Makefile.inc   hello/         luareadhappy/  properties/
Makefile       README         luahello/      ping/          readhappy/
```

## カーネルモジュールをビルドする環境を作る

カーネルモジュールのサンプルコードは、ビルドツールチェインが生成したパスにgccがあることを想定したMakefileになっています。
そのため、カーネルモジュールをビルドするには `build.sh` でセルフホスト用のツールチェインを構築してしまうのが早道です。
(前提として、 `/usr/src` にカーネルソースが展開されているものとします)

以下の手順で、x86_64向けのツールチェインを構築します。

```sh 
$ sudo bash
# cd /usr/src
# ./build.sh -m amd64 -a x86_64 -T /usr/src/tooldir.NetBSD-8.0-amd64 -r -o -U tools
```

## モジュールのサンプルを触ってみる

さっそくカーネルモジュールのサンプルを触ってみます。
サンプルには `hello` と `readhappy` という小さな2つのモジュールが用意されているので、それぞれを見てみましょう。

### helloモジュール

まずは `hello` モジュールです。コメントを除くと30行程度のソースコードです。

```sh 
# cd hello
# ls -F
CVS/      Makefile  hello.c
```

```c 
#include <sys/cdefs.h>
__KERNEL_RCSID(0, "$NetBSD: hello.c,v 1.1 2015/05/13 07:07:36 pgoyette Exp $");

#include <sys/param.h>
#include <sys/module.h>

/*
 * Last parameter of MODULE macro is a list of names (as string; names are
 * separated by commas) of dependencies.  If module has no dependencies,
 * then NULL should be passed.
 */

MODULE(MODULE_CLASS_MISC, hello, NULL);

static int
hello_modcmd(modcmd_t cmd, void *arg __unused)
{
        switch (cmd) {
        case MODULE_CMD_INIT:
                printf("Example module loaded.\n");
                break;

        case MODULE_CMD_FINI:
                printf("Example module unloaded.\n");
                break;

        case MODULE_CMD_STAT:
                printf("Example module status queried.\n");
                break;

        default:
                return ENOTTY;
        }

        return 0;
}
```

`make` コマンドでそのままビルドできます。

```sh 
# make
# ls -F
CVS/             amd64@           hello.kmod       hello.o          machine@
Makefile         hello.c          hello.kmod.map   i386@            x86@
```

### カーネルモジュールのロード

カーネルモジュールのロードは `modload` コマンドで行います。

```sh 
# cd /usr/src/sys/modules/examples/hello/
#
# # まだhelloモジュールはロードされていない
# modstat | grep hello | wc -l
       0
#
# # modloadコマンドでロード。
# # 「./」で相対パス指定にしないとロードエラーになるので注意。
# modload ./hello.kmod
#
# # helloモジュールがロードされていることを確認する。
# modstat | egrep 'NAME|hello'
NAME                    CLASS    SOURCE   FLAG  REFS    SIZE REQUIRES
hello                   misc     filesys  -        0      77 -
```

ソースコードと照らし合わせる形で、カーネルモジュールのロード・アンロードがどう振る舞うのかを見てみます。

```c 
static int
hello_modcmd(modcmd_t cmd, void *arg __unused)
{
        switch (cmd) {
        case MODULE_CMD_INIT:
                printf("Example module loaded.\n");
                break;
```

モジュールをロードすると...。

```sh 
# modunload hello
```

対応する処理として、 `hello_modcmd()` 関数の `MODULE_CMD_INIT` が処理されます。

```c 
static int
hello_modcmd(modcmd_t cmd, void *arg __unused)
{
        switch (cmd) {
...中略...
        case MODULE_CMD_INIT:
                printf("Example module loaded.\n");
                break;
```

`modunload` の場合は、同関数の `MODULE_CMD_FINI` が処理されます。

```c 
        case MODULE_CMD_FINI:
                printf("Example module unloaded.\n");
                break;
```

### readhappyモジュール

次に `readhappy` モジュールを見てみます。こちらもシンプルなコードのようですね。

```sh 
# cd /usr/src/sys/modules/examples/readhappy
# ls -F
CVS/         Makefile     readhappy.c
```

こちらも `make` コマンドでビルドし、 `modload` コマンドでロードします。

```sh 
# modload ./readhappy.kmod
# modstat | egrep '^NAME|happy'
NAME                    CLASS    SOURCE   FLAG  REFS    SIZE REQUIRES
happy                   misc     filesys  -        0     377 -
```

`readhappy.c` のソースコードコメントを見ると以下の記述があるので、それに従ってmknodコマンドを実行します。

```c 
 * Create a device /dev/happy from which you can read sequential
 * happy numbers.
 *
 * To use this device you need to do:
 *     mknod /dev/happy c 210 0
```

```sh 
# mknod /dev/happy c 210 0
# ls -l /dev/happy
crw-r--r--  1 root  wheel  210, 0 Dec  3 18:58 /dev/happy
# file /dev/happy
/dev/happy: character special (210/0)
```

ソースコードを見ると、 `readhappy.c` の `struct cdevsw.d_read` に `read(2)` に対応する関数が紐付けられています。そこで、紐づけられている `happy_read()` 関数に適当な `printf()` を挿入して動作を見てみましょう。

```c 
static struct cdevsw happy_cdevsw = {
        ...中略...
        .d_read = happy_read,
...中略...

happy_read(dev_t self __unused, struct uio *uio, int flags __unused)
{
        ...中略...
        /* Send it to User-Space */
        int e;
        printf("-=> (^_^)/ %s", line);  // 確認用に追加した行。
        if ((e = uiomove(line, len, uio)))
                return e;
```

ユーザランド側で簡単なサンプルコードを用意し、 `/dev/happy` からデータを読みだしてみます。

```c 
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
        char buf[BUFSIZ];
        int fd, i;

        fd = open("/dev/happy", O_RDONLY);
        if (fd == -1) {
                fprintf(stderr, "open() failed.\n");
                exit(-1);
        }

        for (i = 0; i < argc; i++) {
                read(fd, buf, BUFSIZ);
                printf("%s", buf);
        }
        close(fd);

        return 0;
}
```

データを `read()` する度に `printf()` で追加した文言がカーネルメッセージとして出力されていることと、カーネル内の値(データ)が `read()` でユーザランド側でも取得できることが分かります。
<img width="263" alt="img1.png" src="https://qiita-image-store.s3.amazonaws.com/0/61137/f42ab61c-e498-0392-5c6f-972138c462c2.png">

## まとめ

NetBSDのカーネルモジュールについて、提供されているサンプルを元にビルドやロード方法を紹介しました。
特に `readhappy.c` はシンプルでありながら、ユーザランドへのデータの渡し方等、ツボを押さえたサンプルになっています。

カーネルモジュールのサンプルには、他にもLuaで記述されたカーネルモジュールも含まれているので、こちらも後日紹介できればと思います。