Linuxカーネルモジュールにsysctl変数を追加してみる

[Linux Advent Calendar 2018](https://qiita.com/advent-calendar/2018/linux) 16日目の記事です。
今日はLinuxカーネルモジュールにsysctl変数を追加し、[sysctl](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/s1-proc-sysctl)から値を変更できるようにするカーネルモジュールサンプルの作成方法を紹介しようと思います。

## カーネルモジュールのビルド環境の準備

今回は `CentOS 7.5.1804` の環境と `Linux-4.19.9` カーネルで試してみました。 `Linux-4.19.9` は現時点(2018/12/16)で最新のstableカーネルとなっています。開発環境の用意も兼ねて、まずはこのカーネルをCentOSでビルド＋インストールするところから始めてみましょう。

### カーネルソースコードの展開

カーネルソースコードは単にダウンロードして展開するだけなので、別段難しいことはありません。

```sh 
$ curl -O https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.9.tar.xz
$ sudo tar Jxvf linux-4.19.9.tar.xz -C /usr/src
```

### カーネルコンフィグの設定

カーネルコンフィグもデフォルトコンフィグ( `make defconfig` )の内容でほぼ問題ありませんが、CentOSの場合は一点だけ注意が必要です。
CentOSのデフォルトファイルシステムは[XFS](https://ja.wikipedia.org/wiki/XFS)ですが、LinuxカーネルのデフォルトコンフィグではXFSはカーネルモジュールとしてビルドされ、起動時には有効になりません(そのため起動時にルートファイルシステムが解釈できずにPANICが発生します)。

```sh 
$ sudo bash
# cd /usr/src/linux-4.19.9
# make defconfig
# make menuconfig
```
そのため、 `make menuconfig` 等でXFSをカーネルに組み込む形でビルドするように設定します。設定箇所は以下になります。

```
-> File systems
  -> XFS filesystem support
```

![sample3.gif](https://qiita-image-store.s3.amazonaws.com/0/61137/75f35318-7397-7623-4fe8-5a11f7bfae88.gif)

## Linuxカーネルのビルドとインストール

```sh 
# make -j 2
...時間がかかるのでのんびり待つ...
# make modules_install
# cp arch/x86_64/boot/bzImage /boot/vmlinuz-4.19.9.x86_64
# mkinitrd /boot/initramfs-4.19.9.x86_64.img 4.19.9
#
# grub2-mkconfig -o /boot/grub2/grub.cfg
```

```sh 
# shutdown -r now
```

再起動時に `Linux-4.19.9` のGRUBエントリが追加されているので、それを選択します。

![img02.png](https://qiita-image-store.s3.amazonaws.com/0/61137/4cfbe974-543f-4f11-d2cf-cbc298619e5f.png)

これで無事に `Linux-4.19.9` でCentOSを起動できました。

```sh 
$ uname -a
Linux linuxadvcal 4.19.9 #1 SMP Sun Dec 16 14:20:26 JST 2018 x86_64 x86_64 x86_64 GNU/Linux
```

## カーネルモジュールのひな型を作成する

さっそくカーネルモジュールのサンプルを作成します。Gistに今回のサンプルを上げていますので、それを元に実装内容を見てゆきます。また、今回のサンプルは `Linux-4.9.19` の `drivers/cdrom/cdrom.c` の内容を参考に作成してます。

 * [furandon-pig/hello.c](https://gist.github.com/furandon-pig/571a4afaefe7a8386a4392bdc0a3f84c)

関連するドキュメントとして、カーネルソースの配布物に含まれている `Documentation/kbuild/modules.txt` を参照してみてください。

### ビルドして動かしてみる

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

ソースコードの内容を見る前に、まずはどういう動作になるのかビルドして試してみます。
サンプルソースは `samples/hello` ディレクトリに置いてあるという前提で解説します。

```sh 
# cd samples/hello
#
# # Kbuildファイルを用意する。
# cat <<_EOF > Kbuild
obj-m := hello.o
_EOF
#
# ls
Kbuild  hello.c
#
# make -C /usr/src/linux-4.19.9 M=$PWD
# ls
Kbuild          hello.c   hello.mod.c  hello.o
Module.symvers  hello.ko  hello.mod.o  modules.order
```

ビルドが成功すると、 `hello.ko` というカーネルモジュールが生成されます。

#### カーネルモジュールを動かしてみる

以下の手順でビルドしたカーネルモジュールを動かします。やっていることは単にカーネルモジュールのロート・アンロードと `sysctl` コマンドによる値の設定と取得ですね。

```sh 
# insmod ./hello.ko
#
# # モジュールがロードされていることを確認する。
# lsmod | grep hello
hello                  16384  0
#
# # sysctlと/procからカーネルモジュールの変数の値を見てみる。
# sysctl hello.value
hello.value = 0
# cat /proc/sys/hello/value
0
# # "sysctl -w"で値を設定する。
# sysctl -w hello.value=777
hello.value = 777
# cat /proc/sys/hello/value
777
#
# # カーネルモジュールをアンロードする。
# rmmod hello
# lsmod | grep hello  # helloモジュールがアンロードされていることを確認する。
#
# # sysctlや/procからカーネルモジュール内の変数が参照できなくなる。
# sysctl hello.value
sysctl: cannot stat /proc/sys/hello/value: そのようなファイルやディレクトリはありません
# cat /proc/sys/hello/value
cat: /proc/sys/hello/value: そのようなファイルやディレクトリはありません
```

コンソールから試してみると、以下のようにカーネルメッセージも確認できます。

![sample2.gif](https://qiita-image-store.s3.amazonaws.com/0/61137/7b9c1518-08fe-1e84-1e29-07a6afdc5e89.gif)

## 作成したサンプルソースコードの中身を見てみる

先述したように、今回のサンプルソースコードは以下のGistに置いてあります。
100行程度の小さなサンプルですが、カーネルモジュールとsysctl/procfsの最低限の機能が実装されています。

 * [furandon-pig/hello.c](https://gist.github.com/furandon-pig/571a4afaefe7a8386a4392bdc0a3f84c)

### 大まかな実装の流れ

まずは大まかな実装の流れです。以下の4つのステップを経る形で今回のカーネルモジュールを実装しています。

 * カーネルモジュール内に変数を用意する
 * procfsの設定を行う
 * sysctl変数を登録する
 * カーネルモジュールのロードとアンロード処理の関数を用意する

### カーネルモジュール内に変数を用意する

カーネルモジュール内の変数は、Cソース内にstatic変数として用意した変数を `module_param()` で設定するだけです。
併せて `sysctl` での値のやり取り用の構造体(この例では `struct hello_sysctl_settings` )を定義します。

```c
 11 static int value;
 12 module_param(value, int, 0);
 13
 14 static struct hello_sysctl_settings {
 15         int value;
 16 } hello_sysctl_settings;
```

`module_param()` はカーネルソースの `include/linux/moduleparam.h` で以下のように定義されています。
(詳細はまだ追い切れていませんが)変数に紐づく諸々の処理を生成するマクロになっているようです。

```c 
linux-4.19.9/include/linux/moduleparam.h:
148 #define module_param_named(name, value, type, perm)                        \
149         param_check_##type(name, &(value));                                \
150         module_param_cb(name, &param_ops_##type, &value, perm);            \
151         __MODULE_PARM_TYPE(name, #type)
...
128 #define module_param(name, type, perm)                          \
129         module_param_named(name, name, type, perm)
...
169 #define module_param_cb(name, ops, arg, perm)                                 \
170         __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)
...
221 #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \
222         /* Default value instead of permissions? */                     \
223         static const char __param_str_##name[] = prefix #name;          \
224         static struct kernel_param __moduleparam_const __param_##name   \
225         __used                                                          \
226     __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
227         = { __param_str_##name, THIS_MODULE, ops,                       \
228             VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } }
```

### procfsの設定を行う

今回は `sysctl` や `/proc` に対し、 `hello.value` の形でカーネルモジュール内の変数を見せるようにしています。
この変数の階層は `struct ctl_table` を連ねる形で定義します。今回のサンプルでは以下のようになります。

```c
 36 static struct ctl_table hello_table[] = {
 37         {
 38                 .procname       = "value",
 39                 .data           = &hello_sysctl_settings.value,
 40                 .maxlen         = sizeof(int),
 41                 .mode           = 0644,
 42                 .proc_handler   = hello_sysctl_handler,
 43         },
 44         { }
 45 };
 46
 47 static struct ctl_table hello_root_table[] = {
 48         {
 49                 .procname       = "hello",
 50                 .maxlen         = 0,
 51                 .mode           = 0555,
 52                 .child          = hello_table,
 53         },
 54         { }
 55 };
```

### sysctl変数の登録

sysctl変数の登録は、 `register_sysctl_table()` で行います。先述した `static struct ctl_table hello_root_table[]` を引数に渡す形で登録します。また、カーネルのアンロード時にsysctl変数も見せない(＝sysctl/procから除去)ようにするため、 `static struct ctl_table_header *hello_sysctl_header` に戻り値を保持しておきます。

```c
 58 static void hello_sysctl_register(void)
 59 {
 60         static int initialized;
 61
 62         printk("-=> hello_sysctl_register()\n");
 63
 64         if (initialized == 1)
 65                 return;
 66
 67         hello_sysctl_header = register_sysctl_table(hello_root_table);
 68
 69         hello_sysctl_settings.value = 0;        // 初期値の設定
 70
 71         initialized = 1;
 72 }
```

`hello_sysctl_unregister()` 内で `unregister_sysctl_table()` を呼び出します。

```c
 74 static void hello_sysctl_unregister(void)
 75 {
 76         printk("-=> hello_sysctl_unregister()\n");
 77
 78         if (hello_sysctl_header)
 79                 unregister_sysctl_table(hello_sysctl_header);
 80 }
```

### カーネルモジュールのロードとアンロードに対応する処理

最後にカーネルモジュールのロード・アンロード処理を追加します。ここでは単にロード・アンロードの際に先述したsysctl変数の登録と解除を行っているだけです。

```c
 82 static int hello_init(void)
 83 {
 84         printk("-=> hello_init()\n");
 85         hello_sysctl_register();
 86
 87         return 0;
 88 }
 89
 90 static void hello_exit(void)
 91 {
 92         printk("-=> hello_exit()\n");
 93         hello_sysctl_unregister();
 94 }
 95
 96 module_init(hello_init);
 97 module_exit(hello_exit);
 98 MODULE_LICENSE("GPL");
```

## まとめ

駆け足気味ですがsysctlと/procからカーネルモジュール内の変数を参照できるようにするモジュール作成手順を紹介しました。
最小限の機能であれば100行程度でサンプルが作成できるようなので、皆様の環境でも試してみてはいかがでしょうか。

また、手前味噌ながら今回のカーネルモジュールと同等の処理を[NetBSDで実装する手順](https://qiita.com/furandon_pig/items/6a17283a2c28aca4c1a5)も紹介しています。各OS間での実装方法の差異を見てみるのも面白いかもしれません。