ドライバ作成のための小ネタ

最終更新: 2023/02/14 18:31:23 [| ]  最終更新: 2023/02/14 18:31:23

ドライバ作成に便利なこと?

ハードがあって、ドライバの様式さえわかれば、デバイスドライバはつくれます。 が、そのために 知ってると便利な技法がいくつかあります。 ここでは、それらについて触れます。


ハードウェアへのアクセス

デバイスドライバたるもの、ハードウェアにアクセスできなければなりません(一部偏見)。 そのためには 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 はキャストされるのでポインタでも数値でも動作はかわりません。
これらの関数はISAバスに挿さるような、メモリデバイスのための関数で、このままでは PCIで設定された、やたらとアドレスの高いメモリにはアクセスできません。
(PAGE_OFFSET が 0xc000 0000 である場合、物理メモリ 0x4000 0000 以上はカーネルの空間には直接は収まりません)

それらを扱うために

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);
なる関数があります。 vremap, ioremap によって、物理アドレス offset を返り値のポインタで扱えるようにするものです。 使い終わったらvfree で解放します。 offset はページ境界限定で、インテル系では4Kbytes 単位になります。
これらの関数(マクロ)を使用すれば、およそのハードウェアはアクセスできることでしょう。

CPUのクロックカウンタ

カーネル内の時間というと、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システムコールのカーネル内の本体であるdo_gettimeofday()を使用するのがいいでしょう。 これは単位はマイクロ秒ですし。ただし、当然、RDTSC をつかうのに比べ、時間はかかります。

ある時間待機させたい

デバイスの立ち上がりが必要だったりする場合に、ある程度待機させたい、という場面にはよく遭遇します。 その時間がだいたいでいいならば、カーネル内の変数である jiffies を使うことができます。 これは1秒あたり、include/asm/param.hに記載されているHZの速度で増加する変数です(起動時0)。 時間がわかれば簡単だ、ということで

    int timeout=jiffies+10;          使用禁止
    while(jiffies<timeout)
	;
などと考えてしまいますが、これはやってはいけません。 なんのためにLinuxでマルチタスクなんだか分からなくなります。 条件次第ではOSごと止ってしまうこともあるようです。

ではどうするか、というと

    int timeout=jiffies+10;
    while(jiffies<timeout)
	schedule();
schedule();を加えます。 この関数は Linux の心臓部ともいうべき、スケジューリングを行う関数で、呼ぶと何事もなかったかのようにいつかは帰ってはくるのですが、その間に 他のプロセスを実行しています。 別の解釈をすると、schedule()を呼ぶと、現在のプロセスが持っているCPUを放棄することができます。
さしあたって、こうすることで、人様には迷惑をかけなくなります。 これでもまだ問題です。 あくまでスケジューラはスケジューラであって、呼ばれた段階でいくつかあるプロセスの中から、あるルールにのっとって選びだし、それに切り替えるだけです。 そのルールと条件があえば、scheduleを呼んだところにそのままもどる可能性も大いにあるのです。 また、Linuxは実行すべきプロセスが無い場合に、「CPUを眠らせて」消費電力を落としたり発熱を抑えたりします。 この機能もつかえません。

この対策には「寝て待つ」ことにします。

// 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/udelay.hに定義されているudelayをつかうと良いでしょう。 一応マイクロ秒単位で指定できます。


ポーリングのための一定周期実行

なんらかのハードウェアの状態が変化するまで待つ、という場合には本当は割り込みをつかえれば、それが一番です。 ですが、割り込みがつかえるとは限らないですし、てっとり早くはポーリングでしょう。 具体的には、一定時間毎に状態のチェックを行います。

そのためには一定時間毎に特定の関数を呼んでもらえると便利です。 実際、Linuxにはそういう便利な機能が備わっています。

次の例は1秒に1回cyclefuncを実行します。 dmesg でも結果はみることができますが、 リアルタイムに見たい場合はコンソールをつかうなどしてください。

// 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 するときは、当然ながらタイマが残っているとまずいので、cleanup_module()del_timer()してキャンセルしておきます。

なお、このタイマは、一定時間毎にカーネルで発生している割り込みを利用して動作しているものです。 ので、やたらと重い処理をさせたりしてはなりません。 そういう意味では、この例題はprintkなど使っている点で失格かもしれません。


Malloc

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)種類がメジャーどころです。 前者は残量に関わらず、あれば割り当ててくれるので、割り込み中など速効性の必要なところでつかえます。
GFP_KERNEL はメモリの残量が少ないときに、つかっていないメモリをスワップアウト(ディスクに書出し)して場所を確保します。 ただ、その間しばらく待たされます。 ので急ぐところは不可ですし、他のプロセスが同じデバイスの同じ関数をつかう危険性もあるので、2度呼ばれてもいいようにする(リエントラント)注意は必要です。 ただし、原則としては、システムの安定性を考慮してGFP_KERNEL を使うようにすべきです。 なお、GFP_DMA をつけるとDMAにつかいやすいメモリが割り当てられます(物理アドレス連続かつアドレス16M以下)。

なお、まちがっても、メモリリークはしないようにしましょう :-)


おきらくデバイスインストーラ

いちいちデバイスのメジャー番号の空きを探して、メジャー番号をきめて 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 がついてたりします(^^;。


熊谷正朗 [→連絡]
東北学院大学 工学部 機械知能工学科 RDE
[| ]