LoginSignup
6
5

More than 5 years have passed since last update.

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

Posted at

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

sample3.gif

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エントリが追加されているので、それを選択します。

img02.png

これで無事に 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.19drivers/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: そのようなファイルやディレクトリはありません

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

sample2.gif

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

先述したように、今回のサンプルソースコードは以下の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, &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 を連ねる形で定義します。今回のサンプルでは以下のようになります。

 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間での実装方法の差異を見てみるのも面白いかもしれません。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5