ハードがあって、ドライバの様式さえわかれば、デバイスドライバはつくれます。
が、そのために 知ってると便利な技法がいくつかあります。
ここでは、それらについて触れます。
デバイスドライバたるもの、ハードウェアにアクセスできなければなりません(一部偏見)。 そのためには I/Oポートアクセス、物理メモリへのアクセスが必要です。 I/Oポートへのアクセスはすでに述べたように
#include <asm/io.h> outb(val,port); /* ポート port に 1バイト val を出力 */ outw(val,port); /* ポート port に 2バイト val を出力 */ outl(val,port); /* ポート port に 4バイト val を出力 */ inb(port); /* ポート port から 1バイト 入力 */ inw(port); /* ポート port から 2バイト 入力 */ inl(port); /* ポート port から 4バイト 入力 */によって可能です。
メモリについては、以前はポインタに直接メモリアドレスを代入すればよかったのですが(物理メモリのアドレスとカーネルのメモリ空間が1:1)、最近はメモリの扱いが変ったため、
#include <asm/io.h> /* ここで #define されてます */
inline unsigned long virt_to_phys(volatile void * address)
/* カーネル空間から物理アドレス */
inline void * phys_to_virt(unsigned long address)
/* 物理アドレスからカーネル空間 */
unsigned char readb(addr) /* 1バイト読み */
unsigned short readw(addr) /* 2バイト読み */
unsigned int readd(addr) /* 4バイト読み */
void? writeb(unsigned char ,addr); /* 1バイト書き */
void? writeb(unsigned short ,addr); /* 2バイト書き */
void? writeb(unsigned int ,addr); /* 4バイト書き */
void *memset_io(void *addr,int c,size_t n);
void *memcpy_fromio(void *dest, const void *io_src, size_t n);
void *memcpy_toio(void *io_dest, const void *src, size_t n);
/* それぞれ物理メモリへのmemset, からのmemcpy, へのmemcpy */
と定義されています。実体は完全にマクロです(ので返り値はあえて書いてみました)。一度ご覧になってみてください。
なお、addr はキャストされるのでポインタでも数値でも動作はかわりません。それらを扱うために
void *vremap(unsigned long offset,unsigned long size); /* Linux 2.0 */ void *ioremap (unsigned long offset, unsigned long size); /* Linux 2.1- */ void vfree(void *addr);なる関数があります。
カーネル内の時間というと、jiffies が有名です。 ただ、これはタイマ割り込みの時間分解能しかありません。 もっと細かい時間を計りたい、というときの手法です。
Pentium 以降のインテル系CPUには RDTSC という便利な命令があります。 これらのCPUにはリセット時に0にクリアされ、1クロック毎にカウントアップする64ビットカウンタ(TSC)が内蔵されています。 これを読みための命令です。 とはいえ、これはアセンブリ言語レベルの命令でしかも、古めのアセンブラでは解釈してくれません。 そこでバイナリで埋め込んでしまいます。
unsigned int h,l;
/* read Pentium cycle counter */
__asm__(".byte 0x0f,0x31" : "=a" (l),"=d" (h));
unsigned long long tsc=((unsigned long long int)h<<32)|l;
じつはカーネルソースを見てて初めてこの命令の存在をしりました。
それからIntel のマニュアル(PDFで数百ページ)で確認しています。
この命令は実行するとカウンタの値を32ビットずつに分け、レジスタ EAX EDX にそれぞれ下位と上位の値をいれます。
__asm__ がインラインアセンブルで命令をバイナリで直接たたき込んで(コード 0x0f,0x31)、結果を変数 h,l にいれています。
これを long long 型の64ビット変数にしたてています。注意点は当然CPUのクロックの影響をうけるので、その変換をべつにしなければならない、ということでしょうか。 また、486でもLinuxが動くことは確かなので、切り捨てたくない場合は
struct timeval tv; do_gettimeofday(&tv); tsc=(unsigned long long)(tv.tv_sec)*1000000+tv.tv_usec;と、gettimeofdayシステムコールのカーネル内の本体である
デバイスの立ち上がりが必要だったりする場合に、ある程度待機させたい、という場面にはよく遭遇します。
その時間がだいたいでいいならば、カーネル内の変数である jiffies を使うことができます。
これは1秒あたり、
int timeout=jiffies+10; 使用禁止
while(jiffies<timeout)
;
などと考えてしまいますが、これはやってはいけません。
なんのためにLinuxでマルチタスクなんだか分からなくなります。
条件次第ではOSごと止ってしまうこともあるようです。ではどうするか、というと
int timeout=jiffies+10;
while(jiffies<timeout)
schedule();
と この対策には「寝て待つ」ことにします。
// gcc -c waittest.c -Wall -Wstrict-prototypes -O -pipe -m486
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
// drivers/char/n_hdlc.cより
#if LINUX_VERSION_CODE < 0x020100
#define schedule_timeout(a){current->timeout = jiffies + (a); schedule();}
#endif
int init_module(void)
{
int i;
for(i=0;i<5;i++)
{
current->state = TASK_INTERRUPTIBLE; // 寝た状態
schedule_timeout(HZ); // HZ をいれれば1秒のはず
printk("sleeping %d\n",i);
}
return 1;
}
void cleanup_module(void)
{
}
このようにすると、いま、ドライバを呼び出しているプロセスが寝た状態で一定時間が経過するのを待ちます。
一番穏やかな待ち方です。なお、この待機は途中でシグナルがきたりすると解除されます。
どこかのI/Oポートの条件がそろうまで待ちたい、という場合には仕方ないので、2例目をつかうことにしましょう。
その場合も絶対に1例目はつかってはなりません。
ただ、ほんの少し待ちたいという場合には
なんらかのハードウェアの状態が変化するまで待つ、という場合には本当は割り込みをつかえれば、それが一番です。 ですが、割り込みがつかえるとは限らないですし、てっとり早くはポーリングでしょう。 具体的には、一定時間毎に状態のチェックを行います。
そのためには一定時間毎に特定の関数を呼んでもらえると便利です。 実際、Linuxにはそういう便利な機能が備わっています。
次の例は1秒に1回
// gcc -c cycletest.c -Wall -Wstrict-prototypes -O -pipe -m486
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
static void cyclefunc(unsigned long /*dummy*/);
static struct timer_list cyclefunc_list =
{NULL, NULL, 0, 0, cyclefunc};
/* { NULL, NULL, 終了時間, 関数呼出引数, 呼出関数 } */
static int cycle=HZ;
static void cyclefunc(unsigned long c)
{
printk("cycle %lu\n",c);
/* タイマ再登録 */
cyclefunc_list.expires = jiffies + cycle;
cyclefunc_list.data = c+1;
add_timer (&cyclefunc_list);
}
int init_module(void)
{
cyclefunc_list.expires = jiffies + cycle;
cyclefunc_list.data = 0;
add_timer (&cyclefunc_list);
return 0;
}
void cleanup_module(void)
{
del_timer(&cyclefunc_list); タイマ除去をわすれずに!
}
Linux のタイマ機能は残念ながら、1度登録したら1度呼ばれておしまいです。
そのため、定期的に呼び出してもらうときは、再登録が必要です。
また、rmmod するときは、当然ながらタイマが残っているとまずいので、
なお、このタイマは、一定時間毎にカーネルで発生している割り込みを利用して動作しているものです。
ので、やたらと重い処理をさせたりしてはなりません。
そういう意味では、この例題は
malloc というと、いわずとしれたメモリ確保の malloc です。 ただし、名前と機能が微妙に異なります。
Linux2.0:include/linux/malloc.h from 2.0.36
void * kmalloc(unsigned int size, int priority);
void kfree(void * obj);
Linux2.2:include/linux/slab.h (malloc.h) from 2.2.10
void *kmalloc(size_t, int);
void kfree(const void *);
普通のmalloc と違うのは2つ目の引数です。「ほしさ加減」を
示すフラグでいろいろと種類はあるのですが
GFP_ATOMIC 物理メモリがのこってればすぐよこす
GFP_KERNEL 残り少ないとスワップアウト待ち
| GFP_DMA DMAに使いやすいメモリ(|で結合)
の2(+1)種類がメジャーどころです。
前者は残量に関わらず、あれば割り当ててくれるので、割り込み中など速効性の必要なところでつかえます。
なお、まちがっても、メモリリークはしないようにしましょう :-)
いちいちデバイスのメジャー番号の空きを探して、メジャー番号をきめて insmod するというのも面倒です。 さらに、そのあと mknod でアクセス用の特別ファイルをつくらなければなりません。
そんなあなたのために(というより自分のため:-))に"おきらくインストーラ"をつくりました。
それぞれ perl のスクリプトです。ダウンロードしたら chmod +x して下さい。 insmod, mknod を呼ぶので、root でなければ動きません(ので不安な方はチェックの上どうぞ)。 多くのLinuxではそのまま動くとおもいますが、エラーが出たら perl のパス(先頭行)を確認してください。具体的な使用例です。
# ls -l /dev/oc*test* ls: /dev/oc*test*: No such file or directory # ./insdev ocrtest /dev/ocrtest /dev/ocrtest2=10 auto-selected devcie major(c)=60 device 60:0: /dev/ocrtest device 60:10: /dev/ocrtest2 # ./insdev octest /dev/octest /dev/octest2=10 auto-selected devcie major(c)=61 device 61:0: /dev/octest device 61:10: /dev/octest2 # ls -l /dev/oc*test* crw-rw-rw- 1 root root 60, 0 Mar 24 21:45 /dev/ocrtest crw-rw-rw- 1 root root 60, 10 Mar 24 21:45 /dev/ocrtest2 crw-rw-rw- 1 root root 61, 0 Mar 24 21:45 /dev/octest crw-rw-rw- 1 root root 61, 10 Mar 24 21:45 /dev/octest2 # cat /dev/ocrtest linux drivers! linux drivers! linux drivers! linux drivers! linux drivers! # ./rmdev ocrtest unlink '/dev/ocrtest' unlink '/dev/ocrtest2' # ./rmdev octest unlink '/dev/octest' unlink '/dev/octest2' # ls -l /dev/oc*test* ls: /dev/oc*test*: No such file or directory #動作的には insmod+mknod, rmmod+rm という形で、insmod するときに同時に特別ファイルをつくってしまい、rmmod するときに消します(情報は/tmp/insdev.tabに記録)。
それより便利な機能は、メジャー番号の自動設定です。ドライバが増えるとメジャー番号の管理が大変です。 そこで、(1)Linuxで「実験用」として解放されている番号で かつ(2)/proc/devices にみられない番号 をメジャー番号として使用します(番号固定のデバイスは事前に登録するか insdevの予約部分を書き換える)。
こうしてきめたメジャー番号を、モジュールの変数初期化機能をつかって、変数 "devmajor" に渡すと同時に、mknod の引数にします。
こうすることで、デバイスのメジャー番号を変えてしまってもそれに合わせて アクセス用の特別ファイルをつくってくれるわけです。
もし、すでにファイルがあると消すかどうかを聞いてきますので 'y'/'n'で答えてください。
ここで消して、insdevがつくり直した場合は rmdev で自動消去されます。'n'と答えた場合には古いものが残ります。
なお、rm でサクッと消すので、まちがっても hda1 などと書かないように。
(そもそも、insdev/rmdev の組で正しく挿入・削除されている場合は、消すか?と聞かれることはありません。)
insdev 使い方
insdev (option) modulename device_file_list
option: -m ooo, --mode=mode mknod のパーミッション default:666(a+rw)
-b blockdevice ブロックデバイスinsmod default:キャラクタ
--major=major メジャー番号強制 default:自動設定
+var=value モジュールへのオプション
('+'をとって insmod にわたされる)
d_f_list: 作成するデバイスファイルのリスト
ただ /dev/dev1 /dev/dev2 と並べた場合はマイナー番号0から順に
/dev/devx=y とした場合は devx にマイナー番号 y をふって
次は y+1 になる。
rmdev 使い方
rmdev modulename
とくにオプションなし
モジュール作成の注意
static int 変数 devmajor を宣言し、これをもとに register_chrdev() する。
それ以外の初期化方法では自動設定不可。
これで、複数のデバイスドライバをつかったりするのが楽になります。
実はこのために、いままでのドライバサンプルは devmajor がついてたりします(^^;。