Hello World †ほぼ何もしない "Hello World" プログラムを作って Linux kernel に組み込む単純なモジュールの作り方を見ていきます。まだ、デバイスドライバには遠いです。 ソースコード †
実行部 †
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | - | | | ! - | ! - | | ! - ! |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
C 言語で Linux application の hello world プログラムを書いた場合と違うところを見て行きます。おそらく「知っている」C 言語のプログラムと違う点は「これで動くのだろうか?」と思えてくるでしょう。
main() はありません。モジュールが kernel に組み込まれる時 module_init() で指定した関数を実行します。モジュールが kernel から外されるとき module_exit() で指定した関数を実行します。
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | - | | | | | | ! - | | | | | | | | ! |
|
モジュールが「組み込まれた」、「外された」時に呼ばれる仕掛けを作ることができるのは分りました。しかし、デバイス・ドライバを構成するにはまだ足りません。「ハードウエアにドライバが対応するデバイスが追加されたら」、あるいは、「モジュールが組み込まれた時点で既にドライバが対応するハードウエアが存在したら」、呼び出される様な仕掛けをまだ作っていません。i8253 PIT をアクセスするデバイスドライバで probe, remove と呼ばれるデバイスとドライバを関係づける「メソッドのような関数」を見ていきます。
関数に __init と __exit 修飾子がついています。 include/linux/init.h を読んでみると、広域変数に対して似たようなマクロ定義があることが判ります。__init と __exit 修飾子の目的は 次のようになります。
__init と __exit は kernel のメモリ使用量をなるべく減らすために使われる修飾子です。
42 43 44 |
|
92 |
|
printk は printf に相当する関数です。デバッグをするのに非常に有用な関数です。書式文字列を指定して書式文字列に続く引数の内容を表示します。書式文字列は printf とよく似ています。興味深く注意が必要な拡張は %p とその派生書式です。ここでは printk のヘッダファイル読み込みに linux/kernel.h を使用しました。もちろん、linux/printk.h を直接インクルードするのも良いでしょう。
printk に浮動小数点形式の書式が無い
printkの書式に浮動小数点形式はありません。kernel の中では double, float とその派生型は使えません。自分は実施したことが無いのですが、浮動小数点レジスタの値とこれに関係するコンテキストを保存・復帰すれば使えるはずです。ただし、処理の出入り口に実装する程度の簡単なことではなく、suspend/resume、コンテキスト・スイッチ、プロセッサ active/idle の状況などを細かに意識する必要が有ります。実装に問題があるとアプリケーションで行っている計算結果が不意にとんでもない値になるなど問題追跡が難しい状況に陥ります。
KERN_INFO は kernel message の表示レベルを意味します。include/linux/kern_levels.h には次のレベルが定義されています。おそらく普通に使うのは KERN_ERR より大きい Level 番号のメッセージでしょう。下の表の alternate function pr_xxx は printk に KERN_XXX マクロを組み合わせて定義したマクロです。デバイス・ドライバ向けに dev_xxx 関数も用意されています。dev_xxx はデバイスの場所や関連するコンテキストが格納された struct device を指すポインタを引数に渡します。
Macro | Level | alternate function | alternate function for device driver | Description |
KERN_EMERG | 0 | pr_emerg | dev_emerg | system is unusable |
KERN_ALERT | 1 | pr_alert | dev_alert | action must be taken immediately |
KERN_CRIT | 2 | pr_crit | dev_crit | critical conditions |
KERN_ERR | 3 | pr_err | dev_err | error conditions |
KERN_WARNING | 4 | pr_warn, pr_warning | dev_warn | warning conditions |
KERN_NOTICE | 5 | pr_notice | dev_notice | normal but significant condition |
KERN_INFO | 6 | pr_info | dev_info | informational |
KERN_DEBUG | 7 | pr_debug, pr_devel | dev_dbg | debug-level messages pr_debug, pr_devel, dev_dbg は DEBUG, CONFIG_DYNAMIC_DEBUG マクロにより、実装したコードそのものが削除されたり、/sys/kernel/debug/dynamic_debug/contol を通して動的に制御できる ようになります。 |
KERN_XXX は行の先頭で有効です
KERN_XXX は行の先頭にあるものが有効です。printk を何回か使い、行を分割して出力した場合に、途中に KERN_XXX を入れても効果がありません。途中に誤って入れても、致命的な問題は起こしません。表示やログファイルの内容が乱れるだけです。
/sys/kernel/debug の下に何もない
Linux ディストリビューションあるいは SoC メーカーが提供している Linux 環境によっては /sys/kernel/debug の下にノードが全くない場合が有ります。debugfs (debugfs の kernel 内ヘッダ)というデバッグ専用のファイル・システムをマウントしてください。$ sudo mount -t debugfs none /sys/kernel/debug でマウントします。
/proc/sys/kernel/printk リンク先の printk 節を参照 にログレベルを書き込むことで console と ログファイル(厳密にはログ出力ノード /proc/kmsg (/proc/kmsg を実装している fs/proc/kmsg.c) に出力するレベルを制御できます。説明の中で順位に "Higher" とあるのは上記表 Level においてより小さい値です。
別のページで各種のデバッグ手法について集中的に説明する予定です。
MODULE_LICENSE はモジュールのライセンスを指定します。このライセンスは シンボルの結合規則に関係しています。モジュールが結合可能な kernel symbol は EXPORT_SYMBOL または EXPORT_SYMBOL_GPL によって kernel から export されています。MODULE_LICENSE マクロの引数に指定する license 文字列と結合可能なシンボルは次の通りです。
license 文字列 | モジュールから結合可能な kernel symbol export 方法 |
"GPL", "GPL v2", "GPL and additional rights", "Dual BSD/GPL", "Dual MIT/GPL", "Dual MPL/GPL" | EXPORT_SYMBOL, EXPORT_SYMBOL_GPL |
Proprietary | EXPORT_SYMBOL |
ライセンスの宣言通り、ソースコードを配布できるようにする必要が有ります。
モジュールのソースを公開したくなければ "Proprietary" を指定します。このようにすると、結合可能なシンボルは限られます。開発段階で "GPL"を指定し、リリース直前に "Proprietary" を指定すると結合できないシンボルが出てきて、実装し直しあるいはライセンス変更と言った混乱が発生します。
kernel に静的に結合する場合
kernel に静的に結合する場合は結合先のシンボルが EXPORT_SYMBOL または EXPORT_SYMBOL_GPL で宣言されていなくても結合できます。ただし、結合させようとするコードのライセンスは GPL または GPL ライセンスのコードと(結合可能と見なされるライセンス include/linux/module.h 参照)でリリースする必要が有ります。
MODULE_DESCRIPTION, MODULE_AUTHOR はそれぞれモジュールの説明、モジュールの作者を設定します。 MODULE_AUTHOR は、フルネーム、メールアドレス、会社名などを入れます。ソース・コードに現れる例を参考に設定して下さい。これらの設定はモジュールの動作に影響を与えません。設定内容は次のコマンドで確かめることができます。
$ /sbin/modinfo module_name_or_module_path
module を構築するための Makefile の書き方 modules.txt に詳細が書かれています。ここでは、1 個のソース・ファイルから構築しているのでより簡単な Makefile にしました。Linux kernel のソース・コードからバイナリを構築する Makefile の仕組みの上でビルドします。make の -C オプションでディレクトリを Linux kernel のソース・コード・ツリーに移して M マクロにビルドするディレクトリのパスを設定します。
/lib/modules/$(shell uname -r)/build はどこのディレクトリ?
/lib/modules/$(shell uname -r)/build は今使っている kernel を構築したソース(またはその中から構築に必要なファイルだけを残した)ディレクトリ・ツリーをシンボリック・リンクで指しています。手順通り進めていたならば、構築作業をしたばかりのソース・コードツリーを指しています。
添付ファイルを展開したディレクトリに移って、make します。
$ cd extracted_directory_base/hello_world $ make
特に驚くような警告やエラーは出ないはずです。最終的なターゲットは hello_world.ko です。Linux ドライバを提供しているハードウエアの中には、すべて Makefile で書くのではなく、shell script で make を呼び出すものもあります。shell scripit の中で次の様なコマンドラインを実行する様に書かれています。
make -C /lib/modules/4.1.27-local/build M=/home/furuta/git/lkdq1stmodule modules
make コマンドに続けて次のようにしてモジュールを組み込みます。何か起きましたか?
$ sudo insmod hello_world.ko [sudo] password for user_name:password
コンソールには insmod コマンドに反応した表示は何も出ないはずです(組み込み Linux 環境ならば kernel message がコンソールに流れるように設定されている場合もあります)。プログラムに書かれた "Hello world." が出力されたのかは dmesg コマンドで確かめることができます。
$ dmesg ...省略... [ 910.921955] hello_world: module verification failed: signature and/or required key missing - tainting kernel [ 910.921955] hello_world_init: Hello world.
左側に表れている数値は printk timestamp または printk time と呼ばれているものです。正式な名称は Documentation、ソース・コードの中に書かれていないようです。nano second 単位の local_clock() 関数から得ている時刻です。
小数点付で時刻が表示されている
小数点付で時刻が表示されています。しかし、この表示のために浮動小数点演算はしていません。printk の表示は整数計算で行っています。print_time() がprintk time 表示処理です。整数計算で do_div() を使っています。kernel の中では long を超える語長の除算は / と % が使えない場合があります。アプリケーション・プログラムをコンパイルするときはプロセッサに適した命令が無い場合、演算子に対して自動的に計算するライブラリが結合します。しかし、Linux kernel では演算子に対してライブラリを自動的に結合せず、商と余りを計算するマクロまたは関数を提供します。
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
| - | | | | | | | | | | | | ! |
|
モジュールが kernel に組み込まれていることを lsmod コマンドで確認します。下線を付けて示した行が見つかると思います。
$ lsmod Module Size Used by hello_world 16384 0 rfcomm 69632 0 bnep 24576 2 bluetooth 516096 10 bnep,rfcomm nfsd 299008 13 autofs4 40960 4 auth_rpcgss 61440 1 nfsd nfs_acl 16384 1 nfsd nfs 249856 0 lockd 94208 2 nfs,nfsd grace 16384 2 nfsd,lockd sunrpc 327680 19 nfs,nfsd,auth_rpcgss,lockd,nfs_acl fscache 65536 1 nfs snd_intel8x0 40960 2 snd_ac97_codec 135168 1 snd_intel8x0 ac97_bus 16384 1 snd_ac97_codec snd_pcm 106496 2 snd_ac97_codec,snd_intel8x0 snd_seq_midi 16384 0 snd_seq_midi_event 16384 1 snd_seq_midi snd_rawmidi 32768 1 snd_seq_midi snd_seq 69632 2 snd_seq_midi_event,snd_seq_midi joydev 20480 0 snd_seq_device 16384 3 snd_seq,snd_rawmidi,snd_seq_midi snd_timer 32768 2 snd_pcm,snd_seq snd 86016 11 snd_ac97_codec,snd_intel8x0,snd_timer,snd_pcm,snd_seq,snd_rawmidi,snd_seq_device serio_raw 16384 0 soundcore 16384 1 snd 8250_fintek 16384 0 i2c_piix4 24576 0 mac_hid 16384 0 parport_pc 32768 0 ppdev 20480 0 lp 20480 0 parport 45056 3 lp,ppdev,parport_pc hid_generic 16384 0 usbhid 53248 0 hid 118784 2 hid_generic,usbhid psmouse 122880 0 ahci 36864 5 libahci 32768 1 ahci e1000 139264 0 pata_acpi 16384 0
rmmod コマンドでモジュールをアンロードしない限り、Linux kernel 内にモジュールは留まります。hello_world.c に留まった状態を確認できるような機能を実装したいところですが、まだ先になりそうです。rmmod コマンドを実行します。rmmod コマンドの引数はモジュール名です。.ko を付けていません(付けてもエラーになりません)。
$ sudo rmmod hello_world
続けて、dmesg で確かめてみましょう。module_exit() で指定した関数 hello_world_exit() が実行されているのが分かります。
$ dmesg ...省略... [ 910.921955] hello_world: module verification failed: signature and/or required key missing - tainting kernel [ 910.922372] hello_world_init: Hello world. [ 933.824630] hello_world_exit: Goodbye world.
lsmod コマンドをもう一度し hello_world モジュールが外されているのを確かめてみてください。
メモリアロケートをした場合はどうなるの?
モジュールでメモリアロケートをするのはまだ先の話です。モジュールで確保したメモリは rmmod で自動的に解放されません。デバイスが remove されたとき、module_exit()で指定した関数が実行されます。module_exit() に指定した関数の処理でメモリ開放をする様に書きます。あるいは、メモリを開放できない状況では module_exit() が実行されないようにします。モジュールの参照カウンタを増加・減少させる関数 try_module_get(), module_put() を参照してください。
同じモジュールは 1 つだけ kernel に組み込めます。2 つ組み込めないか試してみましょう。2 回目は "File exists" (EEXIST, 発生源は add_unformed_module() 辺りかな) というエラーになります。
$ sudo insmod hello_world.ko $ sudo insmod hello_world.ko insmod: ERROR: could not insert module hello_world.ko: File exists
この結果から分ることは何でしょうか?
複数の同一デバイスを N 個接続した場合、1 個のモジュールは N 個のデバイスを扱う構造になります。モジュールのインスタンスがデバイスの数に合わせて作られることは有りません。アプリケーション・プログラムがシステム上に同時に複数存在できるのとは大きな違いです。同一デバイスが追加されるたびにモジュールはそれに対応するインスタンス(状態保持情報)を自ら作り「ユーザー・ランドからの要求を処理する」、「デバイスの変化を処理する」などの事象に応じてインスタンスの情報を更新します。
どのデバイス・ドライバも同一のデバイスが複数接続された場合に対応しているのか?
答えは No です。プラットホーム固有のデバイス(あるいは SoC の IP ブロック)を扱うドライバはシステム内に特定個数のデバイスのみ存在すると仮定して作られることが多いです。任意の個数に対応するように書いたところで、ハードウエアで固定された有限個のデバイスしかないので実装した機能が存分に発揮されません。
「kernel を汚した」というメッセージが気になります。このメッセージは 1 回だけ出ます。load_module() 関数で表示しています。「1 回だけ」と言う挙動は pr_notice_once() で実現しています。気になるのであれば、モジュールに署名を付けましょう。自前で構築したカーネルですのでソース・ツリーに秘密キーが有ります。署名を付けることができます。詳細は Documentation/module-signing.txt を参照して下さい。
$ keydir=/lib/modules/`uname -r`/build $ $keydir/scripts/sign-file sha512 $keydir/signing_key.priv $keydir/signing_key.x509 hello_world.ko
hello_world モジュールは何もしないプログラムでした。ここで、こちらから疑問と希望を出しておきましょう。
hello_world モジュールは printk 関数を使用しました。この関数のリンク先はまさに稼働している kernel の中のコードです。アプリケーション・プログラムの様に glibc ライブラリが(論理的には)プロセスそれぞれに独立してリンクされるのとは違います。リンクした関数や変数を通して kernel の中の状態を変更すれば、モジュールが外された後もその変更は残り続けます。
module_init() で指定した関数の引数は void でした。何も引数らしき値は受け取れないのでしょうか? module_param() という特別なマクロと共に変数を使うと引数(そしてより柔軟なパラメータ)を扱うことができます。
module_init() で指定した関数は int 型の値を返す関数です。0 以外の値を返せます。errno-base.h, errno.h にあるシステム・コール・エラー番号を負の値にして返すことができます。これで、ユーザー・ランドにエラーが起きたことを伝えることができます。
module_exit() で指定した関数は void 型でした。モジュールを外すときエラーは許されないのでしょうか?答えは「許されない」です。しかし、どうしても無理がある場合は、後で悪いことが起きない程度に後始末します。外す段になって「エラー」だと状況を伝えたところで、大抵は巻き戻して「無かったこと」にするのは無理な場合が多いです。あらかじめモジュールを外せない状況に在ると判っているならば、モジュールの参照カウンタを加減する try_module_get(), module_put() を使って下さい。
終了処理全般の戻り値はおおよそ void
Linux kernel 内で、Call Back や 関数テーブル経由(メソッド Call)で呼ばれる終了処理の殆どは void 型の関数です。「これ以上処理することが無く、終わると言うのに、失敗だと言っても仕方がない。」という信条が在るようです。
いくつかの疑問と希望を確かめ、叶えるために hello_world モジュールを修正します。長くなったので別ページに移ります。