Linux Advent Calendar 2018 16日目の記事です。
今日はLinuxカーネルモジュールにsysctl変数を追加し、sysctlから値を変更できるようにするカーネルモジュールサンプルの作成方法を紹介しようと思います。
カーネルモジュールのビルド環境の準備
今回は CentOS 7.5.1804
の環境と Linux-4.19.9
カーネルで試してみました。 Linux-4.19.9
は現時点(2018/12/16)で最新のstableカーネルとなっています。開発環境の用意も兼ねて、まずはこのカーネルをCentOSでビルド+インストールするところから始めてみましょう。
カーネルソースコードの展開
カーネルソースコードは単にダウンロードして展開するだけなので、別段難しいことはありません。
$ 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ですが、LinuxカーネルのデフォルトコンフィグではXFSはカーネルモジュールとしてビルドされ、起動時には有効になりません(そのため起動時にルートファイルシステムが解釈できずにPANICが発生します)。
$ sudo bash
# cd /usr/src/linux-4.19.9
# make defconfig
# make menuconfig
そのため、 make menuconfig
等でXFSをカーネルに組み込む形でビルドするように設定します。設定箇所は以下になります。
-> File systems
-> XFS filesystem support
Linuxカーネルのビルドとインストール
# 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
# shutdown -r now
再起動時に Linux-4.19.9
のGRUBエントリが追加されているので、それを選択します。
これで無事に Linux-4.19.9
でCentOSを起動できました。
$ 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
の内容を参考に作成してます。
関連するドキュメントとして、カーネルソースの配布物に含まれている Documentation/kbuild/modules.txt
を参照してみてください。
ビルドして動かしてみる
カーネルモジュールのビルド
ソースコードの内容を見る前に、まずはどういう動作になるのかビルドして試してみます。
サンプルソースは samples/hello
ディレクトリに置いてあるという前提で解説します。
# 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
コマンドによる値の設定と取得ですね。
# 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: そのようなファイルやディレクトリはありません
コンソールから試してみると、以下のようにカーネルメッセージも確認できます。
作成したサンプルソースコードの中身を見てみる
先述したように、今回のサンプルソースコードは以下のGistに置いてあります。
100行程度の小さなサンプルですが、カーネルモジュールとsysctl/procfsの最低限の機能が実装されています。
大まかな実装の流れ
まずは大まかな実装の流れです。以下の4つのステップを経る形で今回のカーネルモジュールを実装しています。
- カーネルモジュール内に変数を用意する
- procfsの設定を行う
- sysctl変数を登録する
- カーネルモジュールのロードとアンロード処理の関数を用意する
カーネルモジュール内に変数を用意する
カーネルモジュール内の変数は、Cソース内にstatic変数として用意した変数を module_param()
で設定するだけです。
併せて sysctl
での値のやり取り用の構造体(この例では struct hello_sysctl_settings
)を定義します。
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
で以下のように定義されています。
(詳細はまだ追い切れていませんが)変数に紐づく諸々の処理を生成するマクロになっているようです。
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, ¶m_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
を連ねる形で定義します。今回のサンプルでは以下のようになります。
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
に戻り値を保持しておきます。
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()
を呼び出します。
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変数の登録と解除を行っているだけです。
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で実装する手順も紹介しています。各OS間での実装方法の差異を見てみるのも面白いかもしれません。