デバイスドライバに頼らないハードウェア操作

[| ]  最終更新: 2023/02/14 18:32:04

デバイスドライバは要らない?

ただ、ハードウェアを操作するだけなら、デバイスドライバは必須ではありません。 なぜなら、Linux は root 権限のあるプログラムであれば、ハードウェア(I/Oポート、メモリ)にアクセスできるからです。

PCIの情報はというと /proc/pci /proc/bus/pci/devices をよむと一通りのPCIデバイスの一覧を得ることができます。 ここでは各デバイスにアクセスするための I/Oポートアドレス、メモリアドレスが得られます。

デバイスドライバを作る利点もいろいろとありますが、ここでは、作らずに済む方法を示します。

デバイスドライバを作らなくともできること

デバイスドライバをつくらなくとも、ハードの操作はできます。

ただし、これらを行うためには root の権限が必要です。 つまり、実行時に root になるか、実行ファイルのオーナを root にした上で chmod +s によって setuid ビットを立てる必要があります。 ですが、実験用のプログラムなど、一品料理で全責任を自分で負えるような場合にはこれで十分です。

デバイスドライバを作らなければならないのは

などの場合です。 また、ポーリングする場合にも、いちいちユーザ空間のプロセスを切り替えるより、カーネル内のタイマ割り込み時に実行したほうが負荷も少なく、確実です。 このような場合にはドライバを必要とします。 しかし、ドライバのハードウェアとのやり取りをする部分の大部分はドライバにすることなく作成できます。 つまり、やっかいな部分は予め普通のプログラムでつくってデバッグしておき、最後にデバイスドライバの皮をかぶせる、という開発が可能で、楽になります。

I/Oポートの読み書き

デバイスドライバからは簡単にできるI/Oポートの読み書きも、通常のプロセスから行うには、幾つか制限・手順があります。 ここではそれらについて解説します。

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バイト 入力 */
といった関数があります(out?はMS-DOS, Windows系と引数順が異なりますのでご注意を)。
これらの関数を使用するためには、 <asm/io.h> をincludeする必要があります。 また、これらはインラインアセンブルを使用しますので、コンパイルする際には "gcc -O" と最適化オプションをつける必要があります。
これらの関数はドライバであるなしに関わらず使用します。

I/O許可関数

LinuxではI/Oアクセスが容易ですが、1プロセスが他のプロセスに迷惑をかける可能性を避けるため、root でなければ、使えません。 さらに、root でも普段はアクセス禁止されていて、使用前に禁止解除しなければなりません。これには以下の2つの関数を使用します。

#include <unistd.h> /* for libc5 */         (man ioperm, man iopl)
#include <sys/io.h> /* for glibc */

int ioperm(unsigned  long  from,  unsigned  long num, int turn_on);
int iopl(int level);
前者はより安全な方法で、アドレス from から num の空間を turn_on=1 でアクセス可能にします。 この方法の弱点は 0x0000-0x03ff の空間しか対応していないことです。 そのため、PCIで自動割り当てされたボードや、最近拡張されている I/O などの高位アドレスにはアクセスできません。

後者はI/O空間全体に対して、一括して禁止解除します。 そのため、ポートを間違ったときにはそのままアクセスされ、注意が必要です(中には読むだけでハングアップするポートもあります)。 この命令は具体的にはi386のI/Oアクセスの許可判断となるフラグレジスタのIOPLビットの操作ですが ( Linux2.2:arch/i386/kernel/ioport.c )、単純には level=3 にすれば I/Oアクセスが可能、 level=0 にすれば不可能、となります。
なお、付随する特権として、ハードウェア割り込みの許可不許可(sti,cli) が使えるようになってしまいますが、うかつに使うとOSごと止りかねないので、使わない方がいいでしょう。


メモリの読み書き

MS-DOSのC言語では、何も考えずポインタ変数にアドレスを代入すれば物理的なメモリ番地は参照できていましたが、Linux(Windowsも)はそうはいきません。 普段プロセスからアクセスしているメモリが物理的なメモリのどこにあるかを知るのはカーネルのみです。 また、マルチタスクOSとして、あるプロセスは他のプロセスのメモリ領域の読み書きは出来ません。
メモリの読み書きには /dev/mem を使います。

/dev/memの使用

man mem に書いてあることによると、open して read/write/lseek して読み書きすると、物理的なアドレスの読み書きできます (fopen, fclose でも出来ないことはないと思いますが)。 他にカーネルが使用しているメモリ空間を読むために kmem というのもあります。 これらは

% ls -l /dev/mem /dev/kmem 
crw-r-----   1 root     kmem       1,   2 May  6  1998 /dev/kmem
crw-r-----   1 root     kmem       1,   1 May  6  1998 /dev/mem
のように、root でないと普通は読めません。間違って書いたらI/Oポート以上に危いので、当然と言えば当然でしょう。

実験

メモリを読んでみるのに実験用のプログラムを書いてみます。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(int argc,char **argv)
{
  char buff[1024];
  char file[256];
  int fd;
  unsigned int st,len;
  int r;
  char *dev=(strrchr(argv[0],'/'))?strrchr(argv[0],'/')+1:argv[0];

  if(argc!=3)
    {
      fprintf(stderr,"%s <start addr> <len>\n",dev);
      return 1;
    }
  st=strtol(argv[1],NULL,16);
  len=strtol(argv[2],NULL,16);

  sprintf(file,"/dev/%s",dev);
  fd=open(file,O_RDONLY);
  if(fd<0)
    {
      fprintf(stderr,"cannot open %s\n",file);
      return 1;
    }
  lseek(fd,st,SEEK_SET);
  while(len)
    {
      r=read(fd,buff,len>1024?1024:len);
      if(r<1) break;
      write(1,buff,r);
      len-=r;
    }
  close(fd);
  return 0;
}
プログラム名が mem, kmem かに応じて(それ以外 でも動きますが...)、/dev/mem, /dev/kmem を読み出して標準 出力にだします。 od -t x1 などと組み合わせて、
# ./mem 0 10 | od -t x1
0000000 01 00 00 00 ff e7 00 f0 c3 e2 00 f0 ff e7 00 f0
0000020
# 
などと利用します。応用例として、
# ksyms -a | grep jiffies
c011b4fc  proc_dointvec_jiffies           
c01f2918  jiffies       ←      タイマ割り込み1回(普通 i386で100回/1秒)
                                で1増える変数
80279280  jiffies_R2gig0da02d67   ←最近はこういうゴミ(ではない)つき
# ./mem 1f2918 4 | od -t x1
0000000 b4 d6 8e 96
0000004
# ./mem 1f2918 4 | od -t x1
0000000 cf 8a 8f 96
0000004
# (./mem 1f2918 4 | od -t x1); sleep 10 ; (./mem 1f2918 4 | od -t x1)
0000000 f5 9f 90 96
0000004
0000000 18 c7 90 96
0000004
のように、カーネル内の変数を調べたりすることも可能です。
なお、Linux 2.0 では ksyms で見えるアドレスは、そのまま物理的なアドレスですが、Linux 2.2 以降ではオフセットしています。 オフセット量はinclude/asm/page.hPAGE_OFFSETで規定されています。
さらに3つ目の mem の実行で、間に sleep 10 しかはさんでないのに、数字が 10019(=0xc718-0x9ff5)も進んでいるのは 実験したカーネルはちょっと事情があってタイマ割り込みが 1000回/1秒になっているからです。

mmap

/dev/mem などをアクセスするのに便利なものに mmap というものがあります。

void *mmap(void  *start, size_t length, int prot,
           int flags, int fd, off_t offset);         (man mmap)
これは、こういったメモリのデバイスドライバなどをわざわざ read 経由で使わなくとも良いようにするためのものです。 また、lseekのオフセットがsigned long なので、PCIなメモリに一発で移動できない、という問題を回避するにも、つかえます。
使うには fdopen()で得たファイル記述子を、 offset にオフセットを、 length に範囲の長さを指定します。 start は重要でなく、0を指定すればよく、 prot には読む場合には PROT_READ、 flags にはMAP_SHARED(他と共有可能)/MAP_PRIVATE(共有不可)を指定します。

先ほどのプログラムを少しいじってみました。

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <asm/page.h>

int main(int argc,char **argv)
{
  char buff[1024];
  char file[256];
  char *mmaped;
  int fd;
  unsigned int st,len,poff;
  int r;
  char *dev=(strrchr(argv[0],'/'))?strrchr(argv[0],'/')+1:argv[0];

  if(argc!=3)
    {
      fprintf(stderr,"%s <start addr> <len>\n",dev);
      return 1;
    }
  st=strtoul(argv[1],NULL,16);
  len=strtoul(argv[2],NULL,16);
  poff=st%PAGE_SIZE;

  sprintf(file,"/dev/%s",dev);
  fd=open(file,O_RDONLY);
  if(fd<0)
    {
      fprintf(stderr,"cannot open %s\n",file);
      return 1;
    }
  fprintf(stderr,"mmap: start %08X  len:%08X\n",st-poff,len+poff);
  mmaped=mmap(0,len+poff,PROT_READ,MAP_SHARED,fd,st-poff);
  if(mmaped==MAP_FAILED)
    {
      fprintf(stderr,"cannot mmap\n");
      return 1;
    }
  write(1,mmaped+poff,len);
  munmap(mmaped,len);
  close(fd);
  return 0;
}
ただし、mmap には微妙に癖があるようで、<asm/page.h>に定義されている PAGE_SIZE 単位の位置しかマップできないようです(手元のmanには明示されてなく、エラーのところにあります)。 その分が小細工してあります。

/proc/pciの利用

ISAバス等に固定したデバイスに関しては以上の方法で基本的にはアクセス可能でしょう。ただし、PCIバスのハードウェアで、アドレスが一意に定まらない場合、それを調べる必要があります。

ひとつの方法は、Linuxのインストールなどでもよくやる、Windowsのデバイスマネージャなどで調べる、というものです。 ですが、Linux がすでに動作しているなら、/proc/pciを使用するのがスマートです。ふつうにカーネルを構築していれば、 /proc はあるでしょう。 /proc は各種情報の宝庫です。

embiped1:kumagai% cat  /proc/pci 
PCI devices found:
  Bus  0, device  16, function  0:
    Multimedia video controller: Intel SAA7116 (rev 0).
      Medium devsel.  IRQ 9.  Master Capable.  Latency=64.  
      Non-prefetchable 32 bit memory at 0xec001000.
  Bus  0, device  15, function  0:
    Bridge: Unknown vendor Unknown device (rev 1).
      Vendor id=136c. Device id=9054.
      Medium devsel.  Fast back-to-back capable.  IRQ 10.  
      Non-prefetchable 32 bit memory at 0xec002000.
      I/O at 0x1400.
      I/O at 0x14f0.
  Bus  0, device  14, function  0:
     Ethernet controller: SMC 9432 TX (rev 8). 
      Fast devsel.  Fast back-to-back capable.  IRQ 11.  Master Capable.  
                      Latency=64.  Min Gnt=8.Max Lat=28.
      I/O at 0x1000.
      Non-prefetchable 32 bit memory at 0xec000000.

                    :
  Bus  0, device   7, function  2:
     USB Controller: Intel 82371AB PIIX4 USB (rev 1). 
      Medium devsel.  Fast back-to-back capable.  IRQ 9.  Master Capable.  
                      Latency=64.  
      I/O at 0x14c0.
  Bus  0, device   7, function  1:
     IDE interface: Intel 82371AB PIIX4 IDE (rev 1). 
      Medium devsel.  Fast back-to-back capable.  Master Capable.  Latency=64.
      I/O at 0x14e0.
                    :
増設したイーサネットカードや マザーボード上の USB, IDE などのデバイスがみられます。 ここで "I/O at 0x????" は各デバイスが要求し、割り当てられた I/Oポートを、"... memory at 0x????????" は同じくメモリを示しています。 イーサネットコントローラはI/Oのほか、メモリを要求しています。

さて、ここに Unknown なデバイスがあります。

  Bus  0, device  15, function  0:
    Bridge: Unknown vendor Unknown device (rev 1).
      Vendor id=136c. Device id=9054.
      Medium devsel.  Fast back-to-back capable.  IRQ 10.  
      Non-prefetchable 32 bit memory at 0xec002000.
      I/O at 0x1400.
      I/O at 0x14f0.
"Unknown" というのは「カーネルが知らない」という意味で、 linux/pci.h に定義されていないだけです。 特殊なI/O拡張ボードなどはUnknownのことが多いでしょう。 これは ID=136c であるメーカがつくった 9054 というIDを持ったハードであることが分かります。 ボードメーカがマニュアルに記載しているとおもいますが、無い場合はPCI SIG のサーチで調べることができます(狙ったのだと思いますが、INTELが8086)。
このボードは私がデバイスドライバを書く練習台にもしたアドテックシステムサイエンス社の48ビットパラレルI/Oボード aPCI-P54です。 マニュアルによると、16バイト分のI/Oアドレスを通して使用するのですが、なぜか、メモリ領域が1つにI/O領域が2つあります。 試しにプログラムをつくって、動作を確認したところ、0x14f0が使用すべきアドレスでした(のこりはPCIバスとのインターフェイスのLSIが使う領域とのことです)。 このように、/proc/pciを使用することで PCIデバイスのアドレスが分かります。 あとは iopl()なり/dev/memをつかってアクセスすれば良いことになります。

ちなみに、"Bus", "device", "function" はデバイスドライバでPCIデバイスの検出を行うときに使用する、物理的にどこにボードが挿さっているかを表す数字です。 ただ、この /proc/pci はプログラムから読むには難儀です。そのためか、Linux2.2以降では /proc/bus/pci/devices というファイルが増えていて、これはただの16進数数字の並びなので、sscanf()などで容易に読めます。 1行1デバイスでバス番号、ベンダ+デバイスID、割り込み番号IRQ、そのあと合計6領域およびROM領域のベースアドレスが並びます。 Linux2.4以降ではさらに続けて、それぞれのサイズも並びます。 ベースアドレスは一番下のビットが1のときはI/O、0のときはメモリで、I/Oの時は下2ビットを無視、メモリの時は下4ビットを無視します (メモリの時はそこにメモリの種類を表すフラグが含まれる)。 プログラムから読むときはこちらが便利でしょう。


まとめ

ここでは、デバイスドライバなしで出来るハードウェア操作について述べました。 上に示しましたように I/Oポートのアクセスおよびメモリの読み書きは可能です。 また、PCIで自動的に決定されるアドレスも容易に知ることが可能です。

これらの方法をつかうと、ちょっと制御をしてみたりするときに、ドライバがいるか、というと要らない可能性が高いと言えそうです。 そのほか、今後のことも考えてドライバをつくる、割り込みを使いたいなどの場合にも、これらの方法で事前にあらからテストした上で、ドライバに取り組んだりする場合にも、これらの方法が役立つことでしょう。 カーネルにドライバを組み込んで恐い思いをするのは、なるべく最低限にとどめたいので。



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