システムコール
ソフトウェアはハードウェアを操作することで、その役割を果たします。 例えば、キーボードから入力を受け付けたり、ディスプレイにデータを出力したり、ソフトウェアはハードウェアがあってはじめて成り立つわけです。 しかしアプリケーションはハードウェアを直接操作することはできません。 ハードウェアと直接やりとりできるのはカーネルだけです。 つまり、ソフトウェアはハードウェアを操作したいとき、カーネルに処理を依頼して、間接的に操作することになります。 では、カーネルに処理を依頼するにはどうしたら良いのでしょうか。 その方法が「システムコール」です。 システムコールの「システム」はここでは「カーネル」と捉え、「カーネルをコール」すると考えるとわかりやすいです。 システムコールには下記のような種類が用意されています。 ・open ・close ・read ・write ・ioctl ・fork ・exec ・stat ・unlink etc 上記したのは一部であり、他にもたくさんあります。 ソフトウェアはこのようなシステムコールを介して、ハードウェアを操作することになります。
ライブラリ
ソフトウェアを開発する際、システムコール以外にライブラリ関数を活用することになります。 ライブラリ関数とは、ライブラリ(library)に収められている関数であり、「/lib」配下などにファイルとして格納されています。 C言語の入門書に必ず出てくる、printf関数やscanf関数などがライブラリ関数です。 メインプログラムはライブラリとリンクすることで、、その関数を呼び出すことができるようになります。 ライブラリ関数は、その内部でシステムコールを行っているのもあれば、ライブラリ関数だけで完結しているものもあります。 例えば、文字列もしくは数値を指定の書式に変換して画面(標準出力)に出力するprintf関数は、その内部でwriteというシステムコールを使っています。 逆に、strlen関数は文字列の長さを返すライブラリ関数ですが、その内部ではシステムコールを使っていません。 特定の機能を持ったプログラムを、他のプログラムから利用できるようにライブラリにまとめることで、プログラミングの効率が向上します。
ユーザー空間とカーネル空間
デバイスドライバを開発する場合、ユーザー空間とカーネル空間について理解しておく必要があります。 一般的なアプリケーション開発の場合、カーネル空間を意識したプログラミングは必要ありません。 しかし、デバイスドライバはカーネルの一部であるため、カーネル空間を意識したプログラミングを行わなくてはなりません。 では、ユーザー空間、カーネル空間とはそれぞれ何なのでしょう。 ユーザー空間とは、アプリケーション(OS上で動作するソフトウェア)が使用するメモリ領域のことであり、 カーネル空間とは、カーネルが使用するメモリ領域のことを指します。 つまり両者の違いは使用するメモリ領域の違いと言えます。 例えば、パソコンに4Gバイトのメモリが搭載されていたとします。 このときLinuxは、3Gバイトをユーザー空間としてアプリケーションに割り当て、残りの1Gバイトをカーネル空間としてカーネルに割り当てます。 ユーザー空間とカーネル空間はそれぞれ独立しており、互いが互いのメモリ領域に直接アクセスすることはできません。 では、何のためにメモリ領域を二つに分けるのでしょう。 カーネルはシステムの中枢であるため、カーネルが動いているメモリ領域が侵されてしまったらOSごと停止してしまいます。 例えば、アプリケーションにバグがあった場合、カーネル空間にまでその影響が及んでしまう可能性も十分あります。 しかし、メモリ領域をユーザー空間とカーネル空間に分け、互いが互いの領域にアクセスできないようにしておけば、少なくともOSごと停止してしまう事態は防ぐことができます。
デバイスファイル
デバイスファイルはデバイス(ハードウェア)をファイルとして表現したものです。 アプリケーション(ユーザー空間)とデバイスドライバ(カーネル空間)間でデータを送受信するためには、両者を結びつける「何か」が必要です。 その「何か」がデバイスファイルです。 デバイスファイルは通常、「/dev」配下に保存されており、原則ひとつのデバイスに対して、ひとつのデバイスファイルが存在します。 例えば、「/dev/hda」ファイルは1台目のIDE(ATA)ハードディスクを表しています。 「表している」とはつまり、ソフトウェア(ユーザー空間)から見れば、「/dev/hda」がIDEハードディスクあると見えるわけです。 そしてこのファイルを操作することは、IDEハードディスクを操作するということに対応します。 デバイスファイルには下記の種別が存在します。 ・ブロック型デバイス(block) ・キャラクタ型デバイス(character, unbuffered) ・名前付きパイプ(pipe) ハードディスクやUSBメモリはブロック型デバイス、プリンタやモデムはキャラクタ型デバイスに該当します。 名前付きパイプはプロセス間通信を行う際に使用するデバイスファイルです。
システムコール
システムコールについては以前の記事でも少し解説していますが、今回はもう少し掘り下げてみます。 アプリケーション(ユーザー空間)とデバイスドライバ(カーネル空間)間でデータを送受信するために、デバイスファイルを使用するということは前回の記事で解説しました。 このデバイスファイルを操作することが、デバイス(ハードウェア)を操作することにつながります。 それはつまり、デバイスファイルがデバイス(ハードウェア)を表しているということです。 では、デバイスファイルの操作はどのように行うのでしょう。 その方法が「システムコール」というわけです。 システムコールにはいくつか種類がありますが、まずは基本的な下記の4つについて考えてみます。 ・open ・close ・read ・write デバイスファイルは特殊なファイルではありますが、ファイルであることには変わりありません。 ですので、まずファイルを開いてやる必要があります。 そのためのシステムコールが「open」です。 あとはデバイスに対して何らかのデータを送ったり、受け取ったりする方法です。 「read」がデータを読み込む、つまり、デバイスから送られるデータを受け取るためのシステムコールです。 逆に「write」はデータを書き込む、つまり、デバイスにデータを送るためのシステムコールとなります。 そして最後に、用が済んだら開いていたファイルを閉じてやる必要があります。 そのためのシステムコールが「close」です。 このようにデバイス(ハードウェア)を操作してくわけです。 これらの手順は、普通のテキストファイルをプログラム上から扱うときとそっくりだということがわかります。
開発環境構築
デバイスドライバをビルドするためには、カーネルバージョンと同一バージョンの、kernel-develとkernel-headersパッケージが必要です。 そのため、まずは自身の環境のカーネルバージョンと上記2つのパッケージがインストールされているかどうかを調べます。 # uname -r 2.6.18-238.el5 # rpm -aq | grep kernel kernel-headers-2.6.18-238.el5 kernel-devel-2.6.18-238.el5 上記例では、カーネルのバージョンが「2.6.18-238.el5」であり、同バージョンのkernel-develとkernel-headersパッケージがインストールされています。 もし、インストールされていないのであれば、yumなどを使いインストールします。 # yum install kernel-devel # yum install kernel-headers yumでバージョンが揃わなければ、rpmなど別の方法でインストールすることになります。
ドライバのバージョン確認
ドライバをビルドした環境のカーネルバージョンと、そのドライバをロードする環境のカーネルバージョンが異なると、insmod(ドライバをロードするコマンド)でエラーが発生します。 必ず両バージョンは一致させる必要があります。 開発マシンと動作マシンが同じであるなら問題ありませんが、異なるとそのようなエラーに遭遇することが多いので、注意が必要です。 ドライバをビルドした環境のカーネルバージョンはmodinfoコマンドにより確認できます。 # modinfo hello.ko filename: hello.ko license: Dual BSD/GPL srcversion: 128F11CE82080B148485924 depends: vermagic: 2.6.18-238.el5 SMP mod_unload 686 REGPARM 4KSTACKS gcc-4.1 いくつか項目が出力されますが、今回着目するのは、「vermagic」項目になります。 「vermagic」項目の内容を分割すると下表のようになります。 内容意味 2.6.18-238.el5ビルドした環境のカーネルバージョン SMPマルチプロセッサ対応 mod_unloadドライバアンロード可能 686対応するアーキテクチャ 4KSTACKSカーネルスタックを8KBから4KBにする 「SMP」はマルチプロセッサ対応を意味します。 最近のCPUはマルチコアとなっているため、「SMP」がつくことがほとんどです。
HelloWorld作成
新しいプログラミング言語を覚えるとき、一番初めに書くプログラムは大抵「HelloWorld」だと思います。それはデバイスドライバ開発でも例外ではありません。 まずは新しいディレクトリを作成し、その中に、下記コードを記述した「hello.c」という名前のファイルを作成してください。 #include <linux/module.h> #include <linux/init.h> MODULE_LICENSE(“Dual BSD/GPL”); static int hello_init(void) { printk(KERN_ALERT “driver loaded\n”); 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); 次にMakefileを作成します。 obj-m := hello.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean これで、カレントディレクトリには「hello.c」と「Makefile」が存在していることになります。 # ls hello.c Makefile あとは上記プログラムをビルドしてみましょう。 # make make -C /lib/modules/2.6.18-238.el5/build M=/root/Desktop/kernel modules make[1]: Entering directory `/usr/src/kernels/2.6.18-238.el5-i686′ CC [M] /root/Desktop/kernel/hello.o Building modules, stage 2. MODPOST CC /root/Desktop/kernel/hello.mod.o LD [M] /root/Desktop/kernel/hello.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-238.el5-i686′ ビルド後、カレントディレクトリには、下記のようなファイルが作成されているはずです。 ・Module.symvers ・hello.ko ・hello.mod.c ・hello.mod.o ・hello.o # ls hello.c hello.mod.c hello.o Module.symvers hello.ko hello.mod.o Makefile ビルド後、上記のように5つのファイルが生成されていたらビルド成功です。 ソースコードの解説は後から行います。 まずは、ひととおりのドライバ作成手順を見ていきます。
ドライバのロード・アンロード
ドライバをビルドしただけでは、まだそのドライバは使えません。 使えるようにするためには、ドライバをロードしてやる必要があります。 ドライバのロードはinsmodコマンドにより行います。 koファイルがあるディレクトリに移動し、下記コマンドを実行します。 # insmod hello.ko ドライバのロードはランレベル3以降でも行えます。 それはつまり、OSの再起動なしにドライバのロード・アンロードができるということです。 ただし、insmodコマンドはroot権限で実行する必要があります。 ドライバが正常にロードされたかどうかの確認はlsmodコマンドで行えます。 # lsmod Module Size Used by hello 5504 0 ~以下省略~ lsmodコマンドは現在ロードされているカーネルモジュールを出力します。 ドライバはカーネルモジュールに含まれるので、正常にロードできていれば出力されます。 lsmodコマンドで確認できる内容は下記表になります。 列名意味 Moduleドライバ名 Sizeメモリ上のドライバサイズ Used参照カウンタ by依存ドライバ ドライバのアンロードはrmmodコマンドにより行います。 # rmmod hello rmmodコマンドで指定するパラメータはドライバファイル名ではなく、ドライバ名となります。 ドライバ名はlsmodコマンドのModule列で確認できます。 ドライバのロードはinsmodコマンドだけでなく、modprobeコマンドでも行えます。 insmodコマンドはカレントディレクトリにあるドライバをロードするためのものです。 それに対し、modprobeコマンドは「/lib/modules/`uname -r`」からドライバを探します。 modprobeコマンドはLinux起動時に自動的にドライバをロードさせたいときなど、rcスクリプトでよく使用されます。
ソースファイルの分割
ドライバを複数人で開発する場合など、ソースファイルを分割して開発したい場合もあります。 その場合は、Makefileで複数のソースファイルを指定します。 以下、ソースファイル分割のサンプルです。 main.c #include <linux/module.h> #include <linux/init.h> MODULE_LICENSE(“Dual BSD/GPL”); extern void sub(void); static int hello_init(void) { printk(KERN_ALERT “driver loaded\n”); sub(); return 0; } static void hello_exit(void) { printk(KERN_ALERT “driver unloaded\n”); } module_init(hello_init); module_exit(hello_exit); sub.c #include <linux/module.h> #include <linux/init.h> void sub(void) { printk(“%s: sub() called\n”, __func__); } Makefile CFILES = main.c sub.c obj-m += hello.o hello-objs := $(CFILES:.c=.o) all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 上記サンプルをビルドすると、main.cとsub.cのソースファイルから「hello.ko」が作成されます。 # ls main.c Makefile sub.c # make make -C /lib/modules/2.6.18-238.el5/build M=/root/Desktop/ksample/01 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-238.el5-i686′ CC [M] /root/Desktop/ksample/01/main.o CC [M] /root/Desktop/ksample/01/sub.o LD [M] /root/Desktop/ksample/01/hello.o Building modules, stage 2. MODPOST CC /root/Desktop/ksample/01/hello.mod.o LD [M] /root/Desktop/ksample/01/hello.ko […]