はじめに
本記事の目的は、linuxのカーネルモジュール(以下カーネルモジュール)というものの作成を通じてlinuxカーネル(以下カーネル)の開発に最低限必要な知識をつけることです。開発スキルはC言語のポインタがわかる程度であれば多分大丈夫です。
本記事は、過去に某カーネル開発者養成イベントにおいて使用した資料を加筆、修正したものです。1つの記事に納めるのは無理がある分量なので、(不定期)連載という形式をとることにしました。
第一回では、そもそもカーネルモジュールとは何かというところから初めて、なぜカーネル開発を始めるためにカーネルモジュール作成という手段をとるかを説明した上で、hello worldを出力するだけの簡単なカーネルモジュールを作成します。
カーネルモジュールとは
カーネルモジュールとは、マシンの起動中にカーネルに機能を追加するための部品です。webブラウザに対するプラグインを思い浮かべてもらえればいいかと思います。カーネルの機能のうちの多くの部分は最初からカーネルに組み込んでおくこともできますし、モジュールとして独立したファイルにしておいて1、必要になった時にカーネルに組み込むこともできます。たとえばみなさんのPCに繋がっている各種デバイスを操作するデバイスドライバなどがそうです。モジュールはカーネル本体と同時にビルドできますし、後から別途個別にビルドもできます。本記事は後者のアプローチをとります。
ディストリビューションのカーネルは、カーネルが提供するほとんどのドライバをカーネルモジュールとして提供しています。起動時に読み込むカーネル本体は最小のサイズに抑え、その後でマシンに搭載されているデバイスに関するモジュールだけを必要に応じて読み込みます。これによって、
- 高速な起動
- カーネルによるメモリ使用量の最小化
- なるべく多くのデバイスのサポート
を同時に達成しています。
なぜカーネルモジュールの作成という手段をとるのか
カーネル開発は、カーネル本体の変更よりも、カーネルモジュールの作成から始めるほうが入門しやすいです。理由は次の通りです。
- カーネルモジュールの開発に使用する言語(C言語。一部アセンブラ)もAPIもカーネル本体と同じ
- 巨大なカーネル本体に手を入れるより単機能かつ少ないコード量で作れる
- ビルド時間が短いので手軽に試せる
- システム全体を意のままにできる万能性、バグがあればシステム全体がパニック/ハングするなどのスリルはカーネル本体とほぼ同じ
準備
開発に必要な環境を用意します。まずはシステムに必要パッケージをインストールします。本記事執筆時点でのdebian/testing(stretch) x86_64の場合は次の通りです。
- git
- vagrant
- kernel-package
- vagrant-libvirt
- qemu-kvm
- libvirt-daemon
- libvirt-clients
他のディストリビューションをお使いのかたは、適宜読み替えて下さい2。
次に開発環境を作成します。
$ git clone https://github.com/satoru-takeuchi/elkdat.git
Cloning into 'elkdat'...
...
$ cd elkdat
$ ./init
...
$
ここまでで、カーネル開発に必要な資材(ソフトウェア、ライブラリ、カーネルソース)が全て揃いました。自作カーネル、およびカーネルモジュールをテストするためのVMも作成済です。
本記事ではカーネルv4.9を対象に開発をします。このため、このカーネルをビルドしてVM上でブートさせます。今回のトピックはカーネルモジュールの作成なので、カーネル自体は変更せず、upstreamのものをそのまま利用します。
$ cd linux
$ git checkout v4.9
$ cd ../ktest
$ ./ktest.pl
...
*******************************************
*******************************************
KTEST RESULT: TEST 1 SUCCESS!!!! **
*******************************************
*******************************************
1 of 1 tests were successful
$
VMにログインして、v4.9でブートできているか確認します。
$ cd ../elkdat
$ vagrant ssh
...
vagrant@packer-qemu:~$ uname -r
4.9.0-ktest
vagrant@packer-qemu:~$ exit
...
$ ../
$
uname -r
とは現在のカーネルバージョンを確かめるためのコマンドです。ちゃんと4.9が動作していることがわかります3。
では次の節で実際にカーネルモジュールを作ってみましょう。
hello world カーネルモジュールの作成
まずは開発用のディレクトリを作成して、そこに移動します。
$ mkdir -p dev/module/hello
$ cd dev/module/hello
$
以下のようなファイルを作成します。これがカーネルモジュールのソースコードです。
#include <linux/module.h>
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("Hello world kernel module");
static int hello_init(void) {
printk(KERN_ALERT "Hello world!\n");
return 0;
}
static void hello_exit(void) {
printk(KERN_ALERT "driver unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
20行程度の簡単なソースです。これだけでまがりなりにもカーネルの一機能を作成できます。見た目から、なんとなく何をしているのか想像できるかもしれません。
ここで覚えておいてほしいのは次のことです。
- カーネルモジュールを作成するときは必ずlinux/module.hをincludeする必要がある
- printk()は、おおよそprintfと同等に扱える。文字列先頭についているKERN_ALERTというのは、メッセージの重要度。今はあまり気にしなくてよい
- hello_init()関数を上記のような引数、戻り値で作成した上でmodule_init()マクロに渡すことによって、このカーネルモジュールのロード時(insmod時)にこの関数が呼ばれる
- hello_exit()関数を上記のような引数、戻り値で作成した上でmodule_exit()マクロに渡すことによって、このカーネルモジュールのロード時(rmmod時)にこの関数が呼ばれる
- MODULE_LICENSE()マクロ内にライセンスを記載する。とくに理由がなければ"GPL v2"でよい。
- MODULE_AUTHOR()マクロ内に作者の名前と連絡先となるメールアドレスを記載する。上記の例では筆者のものを使っているが、自分のものと書き換えてよい
- MODULE_DESCRIPTION()マクロ内に、このモジュールが何をするものなのかという説明を記載する
ビルドするためには以下のようなMakefileの作成が必要です。
.PHONY: all clean
obj-m := hello.o
all:
make -C ../../../output M=$(PWD) modules
clean:
make -C ../../../linux M=$(PWD) clean
"obj-m :=" の後にカーネルモジュールのソースコード名の.cを.oに置換したものを記載すれば、当該ファイルをビルドした結果得られるオブジェクトファイル、hello.oをカーネルモジュール化できます。カーネルモジュールは.koという拡張子をもちます。
他の部分については仕組みが複雑な上に知ってもあまり幸せになれないので気にしなくていいです4。
ではビルドしましょう。といってもmakeを実行するだけです。
$ make
...
$
成功したら、作成したモジュールをVM上にコピーします。
$ cp hello.ko ../../../elkdat
$ cd ../../../elkdat
$ vagrant rsync
...
$
カーネルモジュールをロードします。自作カーネルがブートしている状態ではないと失敗しますのでご注意ください。
$ vagrant ssh
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# insmod /vagrant/hello.ko
root@packer-qemu:/home/vagrant# lsmod | grep hello
hello 16384 0
root@packer-qemu:/home/vagrant#
ロードは成功したようです。失敗した場合は一旦VMから抜けた上で5、次のコマンドを実行すればリカバリできます。その後でまたソースを書き換えて、再度ロードに挑戦してみてください。
$ vagrant reload
...
$ vagrant ssh
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# grub-reboot ktest
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
...
$ vagrant reload
...
$
さて、成功した場合の続きに戻ります。プログラミングしたとおりにカーネルモジュールのロード時にメッセージが出力されたか確認します。
root@packer-qemu:/home/vagrant# dmesg | tail -3
[ 314.198886] random: crng init done
[ 516.935519] hello: loading out-of-tree module taints kernel.
[ 516.936950] Hello world!
root@packer-qemu:/home/vagrant#
ちゃんとメッセージが出たようです。成功です。
カーネルモジュールのライセンスなどの情報が正しく設定できているかについても確認しておきましょう。
root@packer-qemu:/home/vagrant# modinfo /vagrant/hello.ko
filename: /vagrant/hello.ko
description: Hello world kernel module
author: Satoru Takeuchi <satoru.takeuchi@gmail.com>
license: GPL v2
srcversion: 9A88917F1C1411370998811
depends:
vermagic: 4.9.0-ktest+ SMP mod_unload modversions
root@packer-qemu:/home/vagrant#
すべて指定したとおりになっています。成功です。
最後にモジュールのアンロードをしてVMから抜けましょう。
root@packer-qemu:/home/vagrant# rmmod hello
root@packer-qemu:/home/vagrant# exit
exit
vagrant@packer-qemu:~$ exit
logout
...
$ cd ../
$
ここまでで最も単純なカーネルモジュールの作成は終わりです。
演習問題
- カーネルモジュールの説明文書を変更する
- 出力するメッセージを変更する
- アンロード時にもメッセージを出力するように変更する
- 関数名を変更する
- モジュール名を変更する
どれも「そんなのやらなくてもわかるよ」というくらい簡単に見えますが、意外とやってみると間違えるものです。
おわりに
本書で作成したソースと同じものをexample/module/hello以下に配置しています。みなさんが作成したモジュールがどうしてもうまく動かなければ、みなさんが作成したソースとこれを比較してバグの箇所を特定してください。
次回は、カーネル内において所定の時間後に所定の処理をさせたいときに使う、カーネルタイマーを紹介する予定です。