デバイスドライバとは?
インテルx86系CPUはI/Oアドレス空間をメモリアドレス空間と別に持っていますが、
I/O アドレス空間を持つ CPU は少数派です。モトローラ68K系CPUは
I/Oアドレス空間という特別なアドレス空間を持っていません。
I/Oのハードウェアがメモリと同じように、アクセスして、動作するように
設計されているので、問題が無いからです。メモリアドレス空間とI/O アドレ
ス空間を別に持つ必要はありません。しかし、x86は長い歴史を持ち古い機種との
互換性を保ってきたため、アセンブラ命令のIN/OUT命令を継承し、
I/Oアドレス空間を必要としました。
仮想記憶を採用するOSか実メモリをそのまま利用するOSかによって
デバイスへのアクセス法は変わってきます。
- OSのカーネルやユーザプロセスが実メモリ空間
(ないしは物理メモリ空間)上で動作するか?
それとも仮想メモリ空間上で動作するか?という点。
- I/Oのハードウエアがそのカーネルやユーザプロセスから
どのように見えるか?という点。
これらの視点からデバイスのI/Oの違いを見て見ましょう。
図1はアドレス空間について、仮想アドレス、物理アドレス、バスアドレスの
関係を示しています。
図1:仮想アドレス、物理アドレス、バスアドレスの関係
物理アドレスをそのままカーネルやプロセスのアドレスに
使用するOSを実アドレスOSと呼びましょう。また直接物理アドレスを使用しないで
その代わりに仮想アドレスを使うOSを仮想アドレスOSと呼びましょう。
一般には物理アドレス空間は実メモリとI/Oデバイスが持つレジスタやメモリから
成る空間から構成されています。後者の空間は、PCIとかVMEbusとかさまざまな
バス空間があります。ですから物理アドレスが決まれば対応する
デバイスのレジスタやメモリが決まります。I/Oバスがが何段にも重なっている
場合はさらに複雑な対応になりますが、ここでは一意的に決まるものとして
話を進めます。
実アドレスOSの場合はプログラムのアドレスがそのまま物理アドレスなので
対応するバスアドレスが決まります。しかも、その物理アドレスとバスアドレスを
同じ値に対応させるとバスアドレスへのアクセスはそのバス空間の中にある
デバイスのアドレスをそのまま与えるだけで、そのデバイスへのアクセスが
可能になります。このようにハードウエアを構成しているOSと計算機の組合せは
良く見かけます。例えば、デバイスのコントロールステータスレジスタ(CSR)への
アクセスはそのCSRのバスアドレスがわかれば、簡単にアクセスができます。
実アドレスOSはI/Oを行なう上では大変便利で簡単なOSと言えます。しかし、
実アドレスOSは大変危険な側面を持っています。それはユーザのちょっとした
間違いがシステムの停止や破壊に繋がってしまうと言う点です。これを避ける
方法の1つが仮想アドレスOSの採用です。ユーザプログラムのミスが直接
システムの停止/破壊に繋がらないように、OS機能を実現するプログラムや
データをカーネル空間としてユーザプログラムの仮想空間から切り離します。
ですからデバイスを操作する機能をカーネル空間に置いて、ユーザには
直接の操作を許しません.そこで,ユーザプログラムは,デバイスドライバに
作業を依頼し,デバイスドライバが実際にデバイスにアクセスを行ないます。
あるデバイスに関する全ての作業はそれ専用のデバイスドライバを通して
行なうことになります。
図2を見よう。
図2:計算機とバス(1)
CPUはメインメモリやI/O機器が接続されるバスのアダプタとシステムバスを
介して繋がっています。I/Oバスとシステムバスを切り離す理由はスピードの
違いから来ています。I/Oが同じシステムバスにあると、
CPUの性能が向上してCPUとメモリ間のスピードが向上しても
遅いI/Oが入ってボトルネックになります。スピードによる階層構造をとることで
計算機のシステム性能を向上させます。しかし、これはデバイスのアクセス方法を
さらに複雑にさせます。メインメモリのように直接ユーザプログラムから見えるのでは
なく、バスアダプタという仲介を通して見えることになります。
従って、I/Oバス上にある
ディスクやVME/CAMACなどデバイスは接続の方法で異なった見え方をします。
大きく分けると、
- VMEバスのようにデータが大きなバス空間上のメモリのように見える。
- SCSIバスのようにコントロールステータスレジスタや
データレジスタなどのレジスタを介してデータが読み書きできる。直接
ユーザプログラムのアドレスから見えない。
これらのデバイスへのインターフェースの実装法は利用の目的により
決まって来ます。
図3は図2と違って最近の計算機のバス構成法を示しています。
図3:計算機とバス(2)
CPUとメモリはブリッジによって結ばれ、システムバスのように
バスを使用しません。これはますます向上するCPUとメモリ間のスピードを
さらに向上させる方法です。ボード上にバスを構成するとラインの長さや
周辺回路からのノイズ等でスピードの向上が望めません。そこで、バスを
シリコンチップ上に持って来て、ブリッジとして1つのチップに載せるのです。
バスを接続する方法はいろいろあり、クロスバースイッチなどは
高速なバス構成法の1つです。このクロスバースイッチを1つのチップ上に
載せてしまえばいいのです。図2はCompactPCIを例にしました。
図4は大変複雑に見えます。これはRISC/UNIXシステムからVMEバスを
図4:計算機とバス(3)
介してCAMACを接続した例です。システムバス上にVMEバスアダプタが
あります。そこからVMEクレートに向かって特別なI/Oバスがだされ、
VMEバスのマスタコントローラとなるカードに繋がります。
そこにはVMEバスがあり、1つのVMEモジュールとしてCAMACの
コントローラがあります。このモジュールからCAMACのクレートに向かって
さらに特別なケーブルが出され、CAMACクレートコントローラに
接続されます。これは1つの例ですが、接続の方法はさまざまです。
ユーザプログラムはいちいちこのハードウエア構成に従って
書いていたのでは、汎用性がなくハードウエアが作られる度に
書き直されなくては成りません。自分が操作したい
デバイスの操作法に集中してプログラミングしたい、ソフトウエアを
中間に来るバスの構成法には依存させたくない、など、もっと
合理的にソフトウエアを構成する方法はないか?
ハードウエアやOSなどに依存しないソフトウエアを構成する方法として
すぐに思い浮かべることは基本的なデバイス操作をおこなう関数を
定義し、ハードウエアやOSなどからユーザプログラムを切り離すことである。
また、デバイス操作をOSのカーネルにデバイスドライバとして組み込み、
ユーザプログラムはOSが提供するシステムコールを利用することで
汎用性を確保することである。
図5はデバイス操作を抽象化する方法をいくつか示しています。
C言語を前提にしています。
図5:デバイス操作法
ユーザ関数でI/OのOSによる違いを隠蔽する方法(1)についてはじめに
説明します。これはKEK標準のCAMAC関数の1つです。CAM_Openは
CAMACの初期化を行なうユーザ用の関数です。一旦これを呼べば、
CAMAC関数でさまざまのCAMAC操作が可能になります。CAMAC関数は
N, A, FのCAMAC情報を特定CAMACモジュールに送ることでデータを
読み書きできます。どのようなOS、どのようなハードウエアを
持って来てもこれらの関数によってそれらの違いを隠蔽できます。
これは大変な利点であると同時に、違いを吸収するための余計な
プログラムコードを追加することでオーバーヘッドを増やし実行
性能という点では不利になります。
次はユーザ関数でI/OのOSによる違いを隠蔽する方法(2)を見てください。
この関数もKEK標準のVME関数の1つです。この関数を一度呼ぶと
VMEバス上の特定モジュールに直接、I/Oが可能になります。
手順を示します。次のカーネルを介さない方法を見てください。
vme_mapopen関数を呼ぶとあるアドレスが返って来ます。
VMEアドレスに対応する計算機上のアドレスです。仮想アドレスOSでは
ユーザプログラムの仮想アドレスが返って来ます。また、すでに
VMEアドレスがユーザプログラムのアドレス空間から直接見える
実アドレスOSではそのアドレスをそのまま使うことができます。
読み込みや書き込みは図のように簡単にできます。仮想アドレスOSでの
このようなデバイスアクセス法は簡単で容易にでき、しかも
OSへの不正アクセスを防止できているので、よく用いられます。
しかし、割り込みやDMA(ダイレクトメモリアクセス)のような場合は
カーネル関数に依存する部分が生じてきますので、上記のようには
いきません。そこでカーネルを介してデバイスにアクセスする方法を
検討します。図の最後にopen関数があります。これはシステムコールの
1つです。デバイスファイルをオープンするという手順で、
デバイスアクセスに必要なファイルデスクリプタを獲得し、
それを使って、デバイスに読み書きします。read/writeシステムコールは
そのためのものです。データを格納するバッファと読み書きしたいデータの
サイズをバイト単位で指定することで簡単にできます。
UNIXカーネルはユーザプロセスの生成やその実行スケジューリング、
システムコール(システム関数)のサービス、ハードウエア割り込みなどの処理を一
手に引受て、ユーザからこれらの厄介な処理を解放してくれます。ユーザプログラムは
入出力操作用のシステムコールでVMEやCAMACの入出力を行なうのが一般的な入出力
方法です。但し、入出力の方法はこれ以外にもあり、特に直接入出力装置にある
メモリやレジスタをユーザレベルで操作したいとの要求には
一般的な入出力方法ではなくマップ方式の入出力方法を用います。
VMEやCAMACに対して何らかの操作を行なうには、まずその抽象化されたデバイスを
openシステム関数を用いてオープンする。抽象化されたデバイスは、
例えば、/dev/ccというようなデバイスファイルとして表現されます。
これはあたかもUNIXの通常のファイルと同じように扱われています。
このファイルにはMajor番号が必ず付きます。この番号がカーネルの中で
デバイスドライバを対応させる重要な番号になります。
実際の入出力操作にはデータを読み込むためのread関数やデータを
書き込むためのwrite関数や大量なデータの入出力をともなわな
い場合に使用するioctl関数を用います。処理を終えたらclose関数を呼びます。
これらの関数はカーネルの一部にデバイスドライバのコードの一部として
実装されています。それらの関数が呼び出されると処理はカーネルに入りその中で
デバイスドライバが呼び出されます。
デバイスドライバはそれらの関数に対応するエントリーを
持っています。例えばCAMACデバイスドライバの場合、openシステム関数が呼ばれる
と、そのエントリーであるccopenが呼ばれます。
ユーザプログラムがドライバに渡したい
パラメータやデータ、CAMACモジュールから読みだしたデータやステータス情報を
ユーザプログラムに返す方法はいくつか用意されています。
最も簡単な方法はioctl関数を
使う方法です。ioctl関数のパラメータを指し示すポインタがそのままドライバエント
リーccioctlに渡されるので、そのポインタに向かってパラメータやデータの入出力を行
なえばよいのです。
read関数やwrite関数は大量のデータを入出力するのに向いています。またDMA操
作を行なうのに必要ないくつかのデータ構造のポインタを用意してくれるので、
DMA操作を行なうためには便利で、必要な方法です。
デバイスドライバの内部だけにしか使用することができない関
数の中にユーザプログラム上にあるデータをカーネルからコピーする関数があるので
カーネル領域とユーザプログラムのデータ領域間のデータ移動に使われます。
DMA操作を伴うデータ移動の方法はこれらとは全く異なった方法で行なわます。
詳しくは別な章で扱います。
マップ方式の入出力方法は前述の入出力法と異なってread関数やwrite関数やioctl関数
を用いません。代わって、mmap関数をユーザプログラムから呼び出します。
この関数はデバイスをオープンしてそのデバイスに対してmmap関数を発行すると、
そのデバイスを操
作に必要なアドレスが返ってきます。ユーザプログラムレベルでそのアドレスをC言語の
ポインタとして用い、直接デバイスのコントロールステータスレジスタなど
をアクセスすることが可能となります。ただし、この方法ではDMA操作を行なうことはで
きません。
C言語を用いたマップ方式の入出力操作法について簡単に触れます。
計算機からPCI, VMEbusなどのバスを扱う場合、データの読みだし書き込みに
おいてデータが正しく順序よく配列されない場合があります。これは
CPUのバイトオーダとバスのバイトオーダの違いによるもので、
これがエンディアン問題です。インテルx86CPUはリトルエンディアン系で
モトローラ68kCPUはビッグエンディアン系です。
例えば、16bit 以上の整数値が 8bit 単位でどのように
メモリに格納されるかということを考えます。x86 系の CPU では、最下位から、
格納され、68kCPUでは、最上位から格納されます。
データ、0x123456ABはx86系では、"AB 56 34 12" の順に、 68k系
では"12 34 56 AB" の順に格納されます。
それぞれに、利点があって、x86 系では、ビット数の拡張に
向いていて、68k では、可読性に富んでいます。
ビット数の拡張とは、現在 16bit のデータを格納するとき、将来を見越して、64bit の
領域を用意し、xx yy 00 00 ... のように、未使用領域を 0 にしておくと、
32bit でも、64bit でも、同じ先頭アドレスでアクセスできます。
また、同種の問題として、最上位ビットの位置の問題があります。x86 系では、
32bit の場合、 bit31 が最上位ビットですが、ある CPU では bit0 が
最上位ビットになっています。