Linuxお気楽・極楽(笑)デバイスドライバ Version 0.1 東京大学理学部情報科学科 住井 英二郎 (sumii@is.s.u-tokyo.ac.jp) 1. はじめに いまさら説明するまでもないと思いますが、LinuxというのはLinus Torvalds さんが中心になって作られたフリーなPC UNIXです。今日はそのLinux用のデバ イスドライバを作る方法の概略を紹介したいと思います。 DOSならともかくUNIX系OSのデバイスドライバを書くとなると、何だか難しい ように思えるかもしれませんが、少なくともLinuxに関してはいたって簡単で すので、心配する必要はありません。LinuxとC言語に関する基本的な知識があ れば十分理解できる内容です。 なお、この文書が対象とするカーネルのバージョンは2.0.0以上です。それ未 満のバージョンでも基本的なところは同じですが、細かい点が微妙に異なるの で、そのままではうまくいかない可能性があります。 2. とりあえずやってみる いろいろ説明する前に、まずは実際に例を試してみることにしましょう。次の ソースを入力して「counter.c」というファイル名でセーブしてください。短 いプログラムですから、たとえ手で入力してもすぐに終わりますよね。 #define __KERNEL__ #define MODULE #include #include #include #include static unsigned int counter = 0, busy = 0, eof = 0; static int counter_open (struct inode * inode, struct file * file) { if (busy) return - EBUSY; busy = 1; MOD_INC_USE_COUNT; eof = 0; return 0; } static void counter_release (struct inode * inode, struct file * file) { busy = 0; MOD_DEC_USE_COUNT; } static int counter_read (struct inode * inode, struct file * file, char * buf, int count) { char str [16]; int len, i; if (eof) return 0; sprintf (str, "%d\n", counter ++); len = strlen (str) + 1; if (count <= len) return - EINVAL; i = verify_area (VERIFY_WRITE, buf, len); if (i) return i; memcpy_tofs (buf, str, len); eof = 1; return len; } static int counter_lseek (struct inode * inode, struct file * file, off_t offset, int origin) { return - ESPIPE; } static struct file_operations counter_fops = { counter_lseek, counter_read, NULL, /* counter_write */ NULL, /* counter_readdir */ NULL, /* counter_select */ NULL, /* counter_ioctl */ NULL, /* counter_mmap */ counter_open, counter_release, NULL, /* counter_fsync */ NULL, /* counter_fasync */ NULL, /* counter_check_media_change */ NULL, /* counter_revalidate */ }; int init_module (void) { int i; i = register_chrdev (63, "counter", & counter_fops); if (i) return - EIO; return 0; } void cleanup_module (void) { unregister_chrdev (63, "counter"); } 入力できたら、以下の操作を行ってコンパイルと組み込みをします。rootでな いと実行できないコマンドも含まれていますが、この文章をお読みのみなさん はもちろんrootでしょう。:-) % gcc -Wall -O2 -c counter.c % su Password: # insmod counter.o # mknod -m 444 /dev/counter u 63 0 # exit exit うまくいったら「cat /dev/counter」を繰り返し実行してみてください。どう ですか、おもしろいでしょう?(つまらなかったらごめんなさい。) 「どこか『デバイス』ドライバなんだ」という話もありますが、基本的な枠組 みは本当のデバイスドライバでも同じですので、以下はこれを例として説明し ていきたいと思います。このように必ずしも実際のハードウエアを伴わないデ バイスを仮想デバイスといいます(やや嘘)。 3. 今やったのは何? さて、ほとんど何の説明もせずに、いきなり実際にドライバをコンパイルして 組み込んでみました。でも、これではわけがわかりません。先ほどの操作は、 いったいどういう意味だったのでしょうか。順を追って説明していきます。 % gcc -Wall -O2 -c counter.c counter.cをコンパイルして、counter.oを作ります。 ポイントは、オブジェクトファイルのみを作って、実行ファイルは作らないと いう点です。実はこのドライバは「loadable module」という形式をとってい ます。「loadable module」というのは、カーネルを再構築したり、システム を再起動したりせずに、カーネルに新しい機能を追加するものです。ダイナミッ クリンクライブラリと似ていますが、ユーザプログラムが使用するライブラリ ではなく、カーネルに組み込まれるモジュールであるという点が異なります。 あくまでモジュールなので、実行ファイルではなくオブジェクトファイルなの です。 # insmod counter.o たったいま作成したモジュールを組み込みます。現在どういうモジュールが組 み込まれているかは「lsmod」というコマンドでわかります。モジュールを削 除するには「rmmod」というコマンドを使います。 # mknod -m 444 /dev/counter u 63 0 counterのためのデバイスファイルを作成します。メジャー番号63番、マイナー 番号0番のキャラクタデバイスです。 メジャー番号は割り当てが決まっているので、本来勝手に使ってはいけないも のです。割り当ては、カーネルソースに付属している Documentation/devices.txtに書かれています。ただし、それをよく読むと 60-63 LOCAL/EXPERIMENTAL USE Allocated for local/experimental use. For devices not assigned official numbers, these ranges should be used, in order to avoid conflicting with future assignments. とあるので、60〜63番はこのような実験に使っても良いのです。 4. ソースの説明 #define __KERNEL__ #define MODULE #include #include #include #include 必要なヘッダファイルをインクルードします。カーネルに組み込むモジュール であることを表すために、__KERNEL__およびMODULEを#defineします。この #defineはヘッダファイルの中で参照されているので、#includeより前でしな ければなりません。 static unsigned int counter = 0, busy = 0, eof = 0; モジュール内部で使用する変数を宣言します。 先頭の「static」は重要です。組み込み時の名前衝突を回避するために、モジュー ルの外部から参照されない名前は、すべてstaticにしなければいけません。 各変数の意味は後になればわかります。 static int counter_open (struct inode * inode, struct file * file) { if (busy) return - EBUSY; busy = 1; MOD_INC_USE_COUNT; eof = 0; return 0; } デバイスファイル(/dev/counter)がopenされると呼ばれる関数を用意してお きます。何やら二つほど引数がありますが、今回は関係ないので気にしません。 (^^; 一度に多数openされるとややこしいので、busyという変数を使って排他制御を 行ないます。「これじゃあ排他制御になってないじゃん」と思うかもしれませ んが、Linuxではカーネルモードのプロセスはプリエンプトされないので問題 ありません(と思う)。 MOD_INC_USE_COUNTは、モジュールが使用中であることを示す値を一つ増やし ます。この値が0でなければモジュールを削除できないので、誤って使用中の モジュールを削除してしまうおそれがなくなります。 static void counter_release (struct inode * inode, struct file * file) { busy = 0; MOD_DEC_USE_COUNT; } デバイスファイルがcloseされると呼ばれる関数を用意しています。 MOD_DEC_USE_COUNTは、MOD_INC_USE_COUNTの逆です。これを忘れると、モジュー ルがずっと使用中になって、削除できなくなってしまいます。 static int counter_read (struct inode * inode, struct file * file, char * buf, int count) { char str [16]; int len, i; if (eof) return 0; sprintf (str, "%d\n", counter ++); len = strlen (str) + 1; if (count <= len) return - EINVAL; i = verify_area (VERIFY_WRITE, buf, len); if (i) return i; memcpy_tofs (buf, str, len); eof = 1; return len; } デバイスファイルがreadされたとき呼ばれる関数を用意します。このドライバ の中核部です(笑)。現在のcounterの値を10進数の文字列に直し、それをユー ザプロセスが用意したバッファにコピーします。 ここで注意しなければならないのは、ユーザプロセスが用意したバッファはユー ザメモリ空間にあるので、普通のポインタを使ったメモリ操作ではなく、専用 のマクロ(memcpy_tofs)を使って書き込みを行うということです。また、不 当な領域に書き込みを行わないよう、あらかじめverify_areaという関数で、 渡されたアドレスの正当性をチェックしなければいけません。 なお、ここで使っているsprintfとstrlenは(名前と意味は同じでも)決して 普通のライブラリ関数ではなく、モジュールのためにカーネルがエクスポート している関数、ないしはヘッダに含まれているマクロです。 static int counter_lseek (struct inode * inode, struct file * file, off_t offset, int origin) { return - ESPIPE; } デイバスファイルがシークされたときに呼ばれる関数です。このデバイスはシー クできないものとして、常にエラーを返すことにします。 static struct file_operations counter_fops = { counter_lseek, counter_read, NULL, /* counter_write */ NULL, /* counter_readdir */ NULL, /* counter_select */ NULL, /* counter_ioctl */ NULL, /* counter_mmap */ counter_open, counter_release, NULL, /* counter_fsync */ NULL, /* counter_fasync */ NULL, /* counter_check_media_change */ NULL, /* counter_revalidate */ }; デバイスファイルを操作したとき呼び出される関数を登録するための構造体で す。次で説明するデバイスの登録のとき使用します。ioctlやらmmapやらいろ いろなインターフェースが用意されていますが、使わないところはNULLで構い ません。 int init_module (void) { int i; i = register_chrdev (63, "counter", & counter_fops); if (i) return - EIO; return 0; } モジュールを組み込んだときに呼び出される関数です。したがって、この関数 はstaticにしてはいけません。また、名前もこの通りにしなければなりません。 register_chrdevで、メジャー番号63番のcounterという名前のデバイスを登録 します。ちなみに、現在登録されているデバイスの一覧は「cat /proc/devices」で見られます。 void cleanup_module (void) { unregister_chrdev (63, "counter"); } モジュールを削除したときに呼び出される関数です。init_moduleと同様に、 staticにしてはならず、名前も変えてはいけません。 init_moduleで登録したデバイスを、unregister_chrdevで忘れずに削除してお きます。 5. モジュールのための関数とマクロ counterの説明は以上です。簡単ですね。でも、実際に何かのボードのデバイ スドライバを書こうとしたら、さすがにこれだけではすみません。I/Oアクセ スを行なったり、物理メモリやIRQを確保したりするにはどうすればいいので しょうか? 実は先ほどのsprintfやstrlenのように、カーネルがモジュールのために提供 している関数や、ヘッダに含まれているマクロがたくさんあります。カーネル が提供する関数については「ksyms -a」で一覧が見られます。I/Oアクセスや リソース確保を行うときには、これらの関数・マクロを使います。 残念ながら、これらの関数・マクロの説明はあまりありません。一部の重要な 関数・マクロについては、manpagesの9章(!)に書かれていますが、載っている 数は決して多くない上に、情報も古いのであまりあてになりません。ヘッダや カーネル、ドライバのソースを見て理解するしかないでしょう。 もちろんネットワーク上にも情報があります。あまりちゃんと探したわけでは ありませんが、筆者が知っている範囲では http://www.gee.kyoto-u.ac.jp/LDP/LDP/khg/khg.html が参考になるでしょう。 6. おわりに 何だかいろいろと偉そうなことを書きましたが、筆者自身つい最近知ったこと が多いので、ひょっとしたら間違っているところもあるかもしれません。もし 誤りがあったらメールやitalkで教えていただけると幸いです。:-)