新版:Linux デバイスドライバの作り方

新版完成してます。より一般的に(特定のハードにたよらず)、 親切(ですます口語調になりました(笑))?に、かつ 2.0/2.2 に 対応したドライバ製作術。

Linux 拡張ボードデバイスドライバの作り方

ここでは私の経験によるキャラクタ型デバイスドライバの作成方法を公開する. 実際にアドテックシステムサイエンス製PIOボード aPCIP54 用の ドライバを題材とする. (ドライバの詳細)

なお, 本ページからの一部リンクは直接ディスク上のファイルを参照する ようになっている. カーネルソースを/usr/src/linux に展開してある コンピュータ以外では正常にはリンクされていない.

なお、ここで取り扱っている内容は主として Linux 2.0.x のものであり、 サンプルのドライバも 2.0.x 用のものである。


概要

制御用などの拡張ボードを購入した場合, Linux で使用するには大まかに 3種類の方法がある. 本文書ではドライバを作成する手法について述べる. おそらく, およその拡張ボードでは, ここで取り上げるドライバの若干の 改造のみで目的を達成することが可能と思われる.

デバイスドライバに求められる機能

ここでは /dev/* にあるような, ファイルとして扱うことができる ドライバを念頭におく. その際最低限必要と思われる機能は 以上の他にドライバとして機能するには入出力を行う機能が必要である. しかし, リード・ライトが必須であるわけではない. メモリ的なデバイスでは もちろん, これらが非常に有効であるが, PIO ボードなどでは, 1バイト 単位の入出力が多かったり, 要求される機能の種類が多いなどする. その際に有効なのが ioctl である. 最後に紹介するが select である. これはデバイスの準備ができるのを 待つためのものであるが, 使い方によっては割り込みをユーザプロセスで 受信させることも容易である.

以上が, およそ必要と思われる機能である. 最低限 open/close が 必要であり, 入出力操作には read,write,ioctl のいずれかは必要である.




ドライバの作成

本説ではドライバ作成の詳細について述べる

ファイル操作テーブル

前説で述べたような機能のうち, どれをサポートし, どの関数で受け持つかを 構造体として設定する.
例:
Linux 2.0.x
static struct file_operations aPCIP54_fops = {
  NULL,             /* lseek */
  NULL,             /* read */
  NULL,             /* write */
  NULL,             /* readdir */
  aPCIP54_select,   /* select */
  aPCIP54_ioctl,    /* ioctl */
  NULL,             /* mmap */
  aPCIP54_open,     /* open */
  aPCIP54_close,    /* release(close) */
  NULL,             /* fsync */
  NULL,             /* fasync */
  NULL,             /* check_media_change */
  NULL,		    /* revalidate */
};
Linux 2.2.x
static struct file_operations aPCIP54_fops = {
  NULL,             /* lseek */
  NULL,             /* read */
  NULL,             /* write */
  NULL,             /* readdir */
  aPCIP54_select,   /* poll(select) */
  aPCIP54_ioctl,    /* ioctl */
  NULL,             /* mmap */
  aPCIP54_open,     /* open */
  NULL,             /* flush */
  aPCIP54_close,    /* release(close) */
  NULL,             /* fsync */
  NULL,             /* fasync */
  NULL,             /* check_media_change */
  NULL,		    /* revalidate */
  NULL,		    /* lock */
};

この例では open,close,ioctl,select の機能に対してそれぞれ aPCIP54〜 という関数を割り当てることを意味している. サポートしない場合は NULL を指定する。それぞれについては以下に解説する.

初期化

ドライバをモジュールとして作成する場合には
  int init_module(void);
という関数が必須である. insmod した際にこの関数が呼ばれるため, 初期化はここで行うのが妥当である. 初期化にはボードの検出等の他に, ドライバの機能をLinuxに登録する作業が必要である. デバイスが ないなどの場合にはエラーを返す.

ドライバの登録

前出のファイル操作テーブルを登録する. 登録には
register_chrdev(メジャー番号(整数), 名前(文字列), テーブル);
を使用する. メジャー番号は重なることが許されないため, すでに 使用されている番号を /usr/src/linux/Documentation/devices.txt などで確認の上で設定する. この番号は mknod でアクセス用の ファイルを作成する場合に必要である.

PCIデバイスの検出

デバイスの検出

PCI のプラグアンドプレイのデバイスを使用するには幾つか手順が必要である. まず,
  unsigned char bus=0xff,dev_fn=0xff;
  pcibios_find_device(VENDOR_ID,DEVICE_ID,num,&bus,&dev_fn);
によって, 目的のボードを探す. VENDOR_ID, DEVICE_ID はそのボード 固有の番号であり, 前者はメーカを示す (参考: /usr/src/linux/include/linux/pci.h). 少なくとも, この番号は調べておく必要があるが, その際に /proc/pciが 役立つことがある. 次のnumは複数の同形式のボードがスロットに 存在する場合の番号であり, 0から順につけられる. bus,dev_fn は 見つかったボードの接続されているPCIバスとデバイス番号・機能番号である. これらは個々のボードの情報を読み取るために必要である. 複数の ボードをサポートする場合 num を0〜増やしていき, 本関数が0を 返す限りボードが存在する. 当然num=0で0以外を返したら指定した デバイスは存在しない.

ボード情報の取得

ボードの存在を確認しバス・デバイス・機能番号を取得したら次は ボード固有の情報を読み出す. ボードのアドレスはこの段階で 得られる. PCIのボードにはコンフィグレーション領域という管理領域が あり, ボードの機能などについての情報が得られる. ボードに自動 設定されたアドレスもこの領域に記載されている. 情報の読み出しは
  pcibios_read_config_byte(bus, dev_fn,PCI_CONFIG,&data_uchar);
  pcibios_read_config_word(bus, dev_fn,PCI_CONFIG,&data_ushort);
  pcibios_read_config_dword(bus, dev_fn,PCI_CONFIG,&data_ulong);
で行う(詳細はlinux/bios32.h). これは検出されたボードを bus,dev_fn を用いて指定し, コンフィグレーション領域の指定された場所から 1/2/4バイト読み込む関数である. この領域の詳細はPCIの解説を 参照して頂くとして, どのようなものがあるかは pci.hの 先頭部分に定義されている. 少なくとも必須であると考えられるのは PCI_BASE_ADDRESS_0 〜 PCI_BASE_ADDRESS_5 で定義されている アドレス情報の格納場所である. ボードのアドレスを知るためには これらを指定して pcibios_read_config_dword(); を呼ぶ.

dword を使用することからも分かるように返り値(&data_ulong に ポインタで指定)は32bitである. この数値はI/Oポートアドレスを 示す場合とメモリを示す場合で形式が若干異なる.

6領域あるうち, どれが目的とする領域か分かるなら, それのみを 読み込めばいいであろう. 割り込みを使用する場合には PCI_INTERRUPT_LINE を指定して byte で読み込む.

割り込み関数の登録

割り込み機能を使用する場合, その登録が必要である. MS-DOS などでは 割り込みベクタを設定の上, 割り込みコントローラのI/Oを設定することで 行っていたが, Linux では, その辺りはOSが管理している. 割り込みを 要求するには
request_irq(aPCIP54[i].irq,aPCIP54_interrupt,
            SA_INTERRUPT|SA_SHIRQ,"aPCIP54",
            (void *)(APCIP54_IRQ_MAGIC|i))
のように request_irq を使用する. 引数は順に, 割り込み番号 (手動設定で既知・PCIなどから取得), 割り込み時実行関数, 割り込みの利用法, 割り込みを登録する名前, 登録を識別するポインタである (sched.h参照). Linux では一つの割り込みを複数のボードで共有する機構がサポート されており, SA_SHIRQ を指定することで共有可能となる. 共有で複数の割り込み実行関数を登録する場合, 最後の識別情報で 割り込み解除時にどれを解除するかを決定するため, うかつにNULLなどは 指定できない. ここでは, 適当な他と重複しないような値を設定している.

実際の割り込み関数は
  static void aPCIP54_interrupt(int irq,void *dev_id,struct pt_regs *regs);
と事前に定義されている. 割り込み番号, 識別ポインタ, およびレジスタの組である. 最後の引数の詳細は不明である ( /usr/src/linux/include/asm/ptrace.hで定義).

正常終了・エラー処理

初期化が正常に終了した場合 init_module から0を返すようにする. 0以外の場合, エラーとしてモジュールの登録に失敗する. 基本的には errno.h(実体は /usr/src/linux/include/linux/errno.h, /usr/src/linux/include/asm/errno.h)で定義されているエラー番号に '-'をつけて負の値にして返す. 一般的にはデバイスが見つからない場合 "-ENODEV", デバイスが使用中などの場合 "-EBUSY" を返すようである.

終了処理

モジュールを rmmod で除去する際、
  void cleanup_module(void)
という関数が呼ばれる。これもモジュールには必須である。 初期化した際に確保したものの解放が主たる作業である。 以上のような処理が必要であろう。

ファイルのオープン、ファイルのクローズ

初期化する際に指定したデバイスのメジャー番号をもつ デバイスの ファイルが open で開かれた場合、また、それで得られたファイル記述子を close した場合、ファイル操作テーブルに記載された、open と close の操作関数が呼び出される。これらは
Linux 2.0.x
static int aPCIP54_open(struct inode * inode, struct file * file)
static void aPCIP54_close(struct inode * inode, struct file * file)
Linux 2.2.x
static int aPCIP54_open(struct inode * inode, struct file * file)
static int aPCIP54_close(struct inode * inode, struct file * file)
という形式で定義される。これらを通じて重要な情報がもたらされる。

open 内では必要なデータの初期化などを、close ではそれらの終了処理を 行う。open で領域が確保できないなどの障害がある場合や、そもそも、 同時には1つのアクセスしか認めていない場合などは -EBUSY を 返すことで "Device is busy" の意味を返すことができる。正常時は 0を返す。

最後に、使用中のドライバがはずされてしまうことがないように open で
  MOD_INC_USE_COUNT;
を実行する。それに対応して、close では
  MOD_DEC_USE_COUNT;
を実行する。これはドライバの参照カウントを増やす/減らすという 動作をするものであり、参照カウントが0の時のみ、rmmod でモジュールを 除去できる。なお、開発時にバグで不整合を起こしても除去できないので 注意が必要である。


リード・ライトによる操作

リード・ライト

aPCI-P54 ドライバではリードライトをサポートしていないので、 メモリンクドライバの 一部を元に解説する。基本的にリードもライトも同じ形式の 関数であり、open などで渡される inode, file のほかに read/write システムコールの第2・第3引数が渡される。返り値は 実際に読み書きしたバイト数である。
Linux 2.0.x
static int ml_raw_read(struct inode * inode, struct file * file, 
                       char * buf, int count)
static int ml_raw_write(struct inode * inode, struct file * file, 
                        const char * buf,int count)
Linux 2.2.x
static int ml_raw_read(struct file * file, 
                       char * buf, size_t count, loff_t *)
static int ml_raw_write(struct file * file, 
                        const char * buf,int count, loff_t *)
動作は *buf で指示された領域に対して最大countバイトのデータの 読み込み・書き込みを行うわけだが、直接は書き込めない。buf は ユーザプロセスの動作している空間でのアドレスであり、ドライバの 動作するメモリ空間とは異なるためである。そのため、アクセスには 専用の関数を用いる。
Linux 2.0.x
memcpy_tofs(dest(ユーザ), src(カーネル), size);
memcpy_fromfs(dest(カーネル), src(ユーザ), size);
Linux 2.2.x(asm/uaccess.h)
copy_to_user(dest(ユーザ), src(カーネル), size);
copy_from_user(dest(カーネル), src(ユーザ), size);
これらは memcpy と同様な操作を行うが、 tofs, fromfs という付録が ついている。例として示した、メモリンクドライバの場合、 直接物理アドレス上のメモリ領域とデータを やり取りするため、例として示したソースでは、そのアドレスを 先頭番地と file->f_pos の和で算出している。また、転送前に 領域サイズと現在位置f_posの兼ね合いで転送量を制限している (実際にはこの他にさらに細工がある)。 処理内容によっては一旦、テンポラリのバッファに転送してから、 処理するという手法も有り得、その場合はその先頭ポインタを 渡せば良い。

転送量がごくわずかである場合には
  get_user_byte(addr), get_user_word(addr), get_user_long(addr)
  put_user_byte(val,addr), put_user_word(val,addr),get_user_long(val,addr)
  (Linux 2.2.x: get_user, put_user)
が存在する。これらは 1,2,4バイト単位でやり取りするのに便利であり、 少量であれば、処理速度も速い。
(参考: /usr/src/linux/include/asm/segment.h)

このメモリンクドライバの場合、cat などで読み出せることを条件に 開発したため、 f_pos によって読み書きした位置を記録し、連続的に アクセスできるようにしたが、毎回固定長のデータをやりとりする 場合など、f_pos を使用せずに、常に先頭から読み書きするように するという手法も考えられる。実際、メモリンクの場合には、 別のコンピュータと、共有メモリの特定番地を介した通信を 行うような用途が主であると考えられ、その場合は各読み書きごとに 常にその番地を基準としたほうがよい場合があるためである。 そのような場合、当然 lseek で毎回その場所に移動するのも手では あるが、手間がかかる。

シーク

同じく、メモリンクドライバの 一部を例とする。これは lseek システムコールを実行すると呼び出される 関数で、
Linux 2.0.x
static int ml_raw_lseek(struct inode * inode, struct file * file, 
                        off_t offset, int orig)
Linux 2.2.x
static int ml_raw_lseek(struct file * file, 
                        loff_t offset, int orig)
の形式をとる。第3第4引数は lseek の第2第3引数に相当する。 処理内容は orig に指定された 0〜2の数値による基準点 (先頭・現在値・終点)から offset ずれたところに f_pos など 読み書きポイントを移動させることにある。実際には非常に 単純であるが、f_pos の値の範囲をチェックする必要があるだろう。 不正な場合は -EINVAL を返すことで lseek が -1 を返し、 errono に EINVAL が入る。

IOCTL

メモリなどような、連続的な大きめの領域を扱う場合には リード・ライトの手法が、ドライバを呼び出す際のオーバヘッドを 減らす意味で有効であるが、ごく単純な操作では、ioctl システムコールを 使用するようにしたほうが楽である。実際、aPCI-P54 ドライバでは デバイスとのやり取りをすべて ioctl にまかせるようにした。 ioctl を使用する際の利点は、処理内容を切り替える整数値が存在するため、 多数ある機能を指定しやすいことである。

ioctl をうけるには

Linux 2.0.x
static int aPCIP54_ioctl(struct inode * inode,struct file * file,
                        unsigned int iocmd,unsigned long ioarg)
Linux 2.2.x
static int aPCIP54_ioctl(struct inode * inode,struct file * file,
                        unsigned int iocmd,unsigned long ioarg)
形式の関数を作成し、ファイル操作テーブルに登録することである。 ioctl を呼んだ場合の第2引数、あれば第3引数が iocmd, ioarg に入る。 処理内容は当然、ドライバ作製者の決定による。返り値は0と負値であり 負値の場合にはこれまで同様 errno に そのエラーが入る。 このことを利用すると、ボードの存在の有無程度の真偽を返答すれば 良い内容では ioarg などを使用せず、返り値のみで対応できる。

処理として、複数の情報を要したり、値を返す必要がある場合には ioarg 経由で構造体などのポインタを渡すのが良いと考えられる。 この場合、ドライバ側でその情報を利用するには、リード・ライトで 解説した memcpy_fromfsで 取り込み、何らかの値を返すには memcpy_tofs で書き込めば良い。もちろん、構造体の定義は一致させて おく必要がある。


SELECT

ユーザプロセスに対して割り込み待ちをさせる方法として、select, signal による方法がある。前者は select システムコールで、 ドライバの準備ができるまで待ち、後者は何らかの現象が 起こった場合、signal で通知する。ここでは select による方法を 解説する。なお、この機能に関しては、既存のドライバ類を 解析した上で体験的に開発した方法なので、実際にうまくいっているが、 多くの誤りを含む可能性があることを明記しておく。

休眠とwake up

Linux ではプロセスの状態として、実行(含む実行待ち)状態と休眠 状態がある。休眠状態にあるプロセスには基本的にCPUがわりあて られないため、CPUの負荷とはならない。これは sleep などの 休眠システムコールで時間を決めて起こすこともできるし、 select のように特定のファイルの入出力準備ができるまで休眠 するものがある。これらは、特定のI/Oポートなどをひたすら 読み続けるポーリングに比べて、よりマルチタスクらしい待機法である。

さて、デバイスドライバでこの select を実装するには、最低限、 select を司る関数本体と、休眠状態から起こすための部分と二つ 必要である。select で休眠させる場合には select_wait(); という関数が 休眠中のプロセスを起こすには wake_up_interruptible(); という関数が 存在する。たとえば、aPCI-P54 ドライバの場合は割り込み関数内で wake_up_interruptible(); を使用しているし、他に実験用に開発している ドライバ類には、第1のファイルのアクセスで第2のファイルを 起こすといった機構、また、後述のポーリングによって起こすように しているものもある。とにかく、何らかの方法で起こすようにする。

/usr/src/linux/kernel/sched.cには wake_up_interruptible();のほかにwake_up(); という関数も存在する。 その違いは wake_up させるプロセスの違いのようであるが、 定かではない。ただ、Linux カーネルのドライバ類のソースを 見る限りは前者が使用されているようである。

実装

基本的に select をうけるには
Linux 2.0.x
static int aPCIP54_select(struct inode *inode, struct file *file, 
                         int flag, select_table *wait)
Linux 2.2.x
static int aPCIP54_select(struct file *file, struct_poll_table_struct *)
(注:動作未確認)
なる形式の関数を定義し、ファイル操作テーブルに記載すればよい。 第3引数は select システムコールを使用する際の、読み込み準備、 書き込み準備、例外の3条件のいずれで問い合わされたかをあらわす数値で /usr/src/linux/include/linux/fs.hで定義された SEL_IN, SEL_OUT, SEL_EX のいずれかが渡される。また第4引数は 休眠状態にするための関数で使用する。
selectを処理する関数は基本的に実際にデバイスの準備ができているかに 応じて真偽を返せば良い。ただ、それだけでは、selectシステムコールを 発行した直後・同時待機の他の要件による wake_up 時、および タイムアウト時以外の監視ができないため、以下の 休眠/wake up 処理が必要となる (参考: file:/usr/src/linux-2.0.35/fs/select.c 内 do_select(), check())。

休眠の状態が整ったところで select_wait を実行する。select_wait は 引数として struct wait_queue ** と呼ばれたときに渡された select_table *wait をとる。前者は 「待機状態を 記録する構造体をしめすポインタ」を返すためのようであるが、 詳しいことは不明である。 カーネルソースの解析などにより、各 プロセス用毎に strcut wait_queue * を用意しておき それに & をつけて渡せばいいことが分かっている。これは wake up 時に 必要となる。

割り込みなどの関数では、wake_up_interruptible を呼び出し、 プロセスを実行可能状態にする。引数は select_wait を呼んだ際の 第1引数である。aPCI-P54 ドライバでは見た目若干異なる形式に見えるが、 select_wait の際には単一のファイルをwaitにしているが、割り込み発生時 には、割り込み待機中のすべてのファイルに対して発行しているためである。 注意点としては、FIFO があるような、あとでハードを調べれば確実に データのあるなしが分かるような場合は問題ないが、PIO の割り込みのように 割り込みがあったことがハードから読み取れない場合(読み取れるが、 一般に割り込み受信時にクリアする)、フラグを立て(例では変数waiting)、 select の関数で確実に結果を返せるようにする。そうしなければ、 システムのパフォーマンスに若干の影響を与えるようになる。

割り込みとポーリング

SELECT の項で割り込みなどでデバイスの準備が整った場合、 wake_up_interruptible を呼ぶと述べたが、ハードの状態から状態変化を 捕らえるには割り込みとポーリングが考えられる。割り込みは文字通り ハードウェア割り込みである。これによって、データの読み出し準備が 整ったことが分かることもあるだろうし、aPCI-P54 ドライバでは、 純粋に「割り込みがあったこと」を知ることができる。
割り込みなどによらず、一定時間毎にステータスを読み込んでみて、 準備ができているかを調べる、ポーリングが必要なこともある。実際、 MS-DOS時代はひたすらポーリングで時間を待つようなプログラムを 作成することもよくあった。以下ではOSの機能によるポーリングを 解説する。

割り込みを受信する場合、初期化のところで 前述のように、割り込み関数を登録する。形式は
static void aPCIP54_interrupt(int irq,void *dev_id,struct pt_regs *regs);
となっている。登録時に渡すポインタで dev_id 経由で情報を受け取ることも 可能である。実際の割り込み関数ですべき処理はハードの種類にもよるので ここでは詳細について触れないが、一般には、ボード固有の割り込み要件の クリア、情報の保存、実際のデータ処理などが必要であろう。そして、 select で待つような場合は、準備ができた場合に wake_up_interruptible を 呼び、待機の終了をOSに報せる。

割り込み機構を持たない・利用しない場合、定期的にハードのステータスなどを 読み取り、準備を判断する必要がある場合がある。このとき、OS に 備わっているポーリング機能を利用できる。これはタスクのスケジュールにも 利用されているタイマ割り込み(標準100Hz)の発生時に登録されている 関数を呼び出してくれる、というものである。メモリンクドライバには その機能をとりつけてある。

// polling
static void mld_poll (unsigned long /*dummy*/);
 
static struct timer_list poll_timer =
{NULL, NULL, 0, 0, mld_poll};

static void mld_poll(unsigned long /*dummy*/)
{
  //printk(".");
#ifdef ENABLE_VS
  poll_vs_ml();              // 拡張システムのポーリング関数の呼び出し
#endif
  poll_timer.expires = POLL_CYCLE + jiffies;
  add_timer (&poll_timer);
}

init_module内:
  poll_timer.expires = POLL_CYCLE + jiffies;
  add_timer (&poll_timer);
機構としては周期的なポーリングと言うより、ある時間後に指定の関数を 実行、といったものである。ここでは mld_poll 関数を呼んでいる。 mld_poll の引数はここでは使用していないが、struct timer_list の 第4変数がわたされる。これは1度きりの機構のため、呼び出された 関数内で再設定が必要である。ここで使用されている jiffies は 前述のOSのタイマ割り込み毎に1ずつカウントアップされる数値で、 POLL_CYCLE 後に再度実行されるようにしている。なお、add_timer したままモジュールを切り放すわけには行かないので、終了処理 cleanup_module内で
del_timer(&poll_timer);
を忘れずに実行する。ポーリング処理の内容は割り込み同様ハードに 依存し、やることは同じようなものである。すなわち、準備ができた ことの確認と、 wake_up_interruptible である。
以上がいまのところ、確認しているドライバ作成の知識である。


その他

モジュール

モジュール

モジュールはLinux に動的(=カーネルに最初から組み込むのではなく)に ドライバなどを組み込む機構である。モジュールは基本的に gcc -c で 得られるオブジェクトファイルに他ならない。最低限、init_module, cleanup_module という関数を内部に持つ。挿入は insmod, 除去は rmmod, 一覧を lsmod で得られる。挿入除去はrootでなければできない。

モジュールのコンパイル

gcc -c apcip54.c -DLINUX -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -pipe -m486
コンパイル時には -DLINUX -O2 が必要。また、最大限、安全性を考え、 警告を出力させる。

開発

モジュールを開発する際には幾つかインクルードファイルが必要である。
例:
#define MODULE
#define __KERNEL__

#include <linux/kernel.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/string.h>
#include <linux/malloc.h>
#include <linux/fs.h>
#include <linux/stddef.h>
#include <linux/version.h>
#include <asm/segment.h>
ほかに、PCI関係を操作するには linux/pci.h、linux/bios32.h が必要であり、 I/Oポートにアクセスするには asm/io.h が必要である。

便利な関数


くまがい まさあき