LoginSignup
12
13

More than 5 years have passed since last update.

QEMUに仮想PCIデバイスを追加する(2) : 前提知識 (PCI / デバイスドライバ)

Last updated at Posted at 2015-01-04

PCIデバイスについて

各PCIデバイスはPCIコンフィギュレーション空間を持っています。そこには
デバイスIDやどの程度I/Oポートがいるか、割り込みが必要であるかなどといった
情報が書かれたレジスタがあります。
必要があればBIOSやドライバなどがそのレジスタ値を読み取ったり、値を更新します。
PCIコンフィギュレーション空間については Wikipedia (http://en.wikipedia.org/wiki/PCI_configuration_space) 等を参考にしてください。

PCIデバイスはバス番号、デバイス番号によりOSから一意に認識されます
(デバイスに複数の機能がある場合はファンクション番号があります)。
認識されているデバイスはlspciコマンドで確認できます。PCIデバイスの探索は
BIOSかLinuxが起動時に自動で行ってくれます
(例えばPCにおいてはI/O ポートの 0xCF8/0xCFC がPCI コンフィグレーション空間用に確保されており、
バス/デバイス/ファンクション番号とオフセットを0xCF8に設定することで、
0xCF8にデータを読み込み/書き込みできます。
これを用いてその番号に対応するデバイスが存在するかを探索しています)。

特にドライバがデバイスと通信する上で重要な情報がBAR(Base Address Register) に書かれています。
BAR は0-4 の5つがあり、それらが必要とする I/O ポートや
メモリマップドI/O の領域のサイズを指定しています。BIOS等は起動時に
これらのレジスタにアクセスし、サイズを読み取ることで
必要となるI/OポートやメモリマップドI/Oの領域を確保します。
これらのサイズは2のべき乗である必要があります
(起動時に必要となるサイズ分だけ下位ビットが0、それ以降が1となっており、
BIOS/Linux がアドレス開始位置を設定する際上位ビットのみを変更するため。
ドライバはドライバの初期化時にその値を読み取ります。
I/O ポートの場合はポート番号の開始番号、メモリマップドI/O の場合は
物理アドレスの開始アドレスが指定されます)。

なお、PCIデバイスはプラットフォームによらず必ずリトルエンディアンです。

デバイスドライバについて

Linux におてはデバイスドライバは主にキャラクタデバイス/ブロックデバイス/ネットワークデバイスの3つに分類されます。
前者の2つについてはユーザープログラムがデバイスをファイルとして操作し、
/dev 以下に通常デバイスファイルが存在します。一方で
ネットワークデバイスにはファイルの様に操作はしないため、/dev 以下にデバイスファイルはありません。

デバイスには通常のファイル操作に関するシステムコールに加えて、
ioctlという特別なシステムコールを発行できます。この関数は
コマンド番号及びデータ(へのポインタ)を受け取ることで、
デバイス固有の機能を実現するものです。
いずれの場合もドライバとデバイスの通信にはI/OポートやメモリマップドI/O
を通じて行われます(例えばあるアドレスへ書き込むことで、デバイスの処理を開始する)。

ドライバはカーネルが各デバイスを識別するために使うメジャー番号と
ドライバ自身がデバイスを識別するために使うマイナー番号の2つを持ちます。
今回はマイナー番号はただ1つだけを使っています。

デバイスドライバといってもハードウェアと通信を
しないものもあります(/dev/null など)。

デバイスドライバのロード/アンロード

デバイスドライバはカーネルの一部として予め組み込むことも出来ますが、
insmod/mknod コマンドを利用してモジュールとして
動的に登録/登録解除を行うことが出来ます(今回はこちらです)。

デバイスドライバをモジュールとしてコンパイルすると <ドライバ名>.ko というファイルが出来ます。
このファイルを

    insmod <ドライバ名>.ko

とすることでカーネルにロードできます。
この後

    cat /proc/devices

とすることで、ドライバに割り当てられたメジャー番号が分かります。
ドライバに対応するデバイスファイルを作成するために

    mknod 場所 タイプ メジャー マイナー

コマンドを行います。場所は通常 /dev 以下で、タイプはキャラクタデバイスならば c、
メジャー番号は先ほど確認した番号を使用します。つまり、

    mknod /dev/sample c 254 0

などとなります。
デバイスをアンロードする時は

    rmmod デバイス名 // (.ko はいらない)

を実行します。
現在ロードされているモジュールはlsmodコマンドで確認できます。

insmod/rmmod の代わりにデバイスの依存関係を調べて、
必要な他のモジュールを読み込んでくれる modprobe を使うことも出来ます
(modprobe/modprobe -r <デバイス名>)。
modprobe を使うためには、/lib/modules/< kernel version >/modules.dep に
カーネルの依存関係が記述されている必要があります。
modules.dep は、 /lib/modules/< kernel version > 以下にローダブルモジュール
を置いた後に、 depmod -a コマンドを実行することで更新されます。

なお、insmod をすると自動的にデバイスファイルを作成する仕組みもありますが
(udev)、 今回は使用していません。

デバイスに割り当てられたI/Oポートなどの情報は

    cat /proc/ioports
    cat /proc/iomem
    cat /proc/interrupt

で確認できます。

ドライバ開発の注意点

Linux では通常仮想メモリを使用するため、ユーザー空間とカーネル空間で
メモリ領域が異なります。よって、ユーザープログラムとデバイスドライバの間
でデータをやり取りするとき、単純にポインタを参照することはできません。
このため、データのやり取りには get_user/put_user マクロや copy_to_user/copy_from_user
関数などを用います。

ドライバはカーネル空間で動作しますが、カーネル空間用のスタック (カーネルスタック)
は通常8192バイトと比較的小さいため、関数内でスタック上に大きな配列を確保することが出来ません。
そこで、カーネル空間用のmallocであるkmalloc(開放する時は kfree)を使用します。
kmalloc には確保中にスリープすることが出来るかなど
(割り込みハンドラ中ではスリープできない)の条件を指定できます。

ドライバは標準ライブラリをインクルードしていないので、
printf 関数は使えません。そもそも出力先のコンソールがカーネルにありません。
代わりにカーネルバッファに書き込む printk 関数を使用します。
使用方法は基本的にprintfと同じですが、文字列のまえにメッセージ優先度
を指定できます(KERN_ALERT, KERN_ERR など)。また、数値としては
整数値のみを受け取れます。
カーネルバッファは dmesg コマンドで確認できます
(リングバッファなので、古いメッセージは上書きされます)。

デバイスドライバの作成にはそのドライバを利用するカーネルのソースコードが必要となり、
異なるバーションやコンフィギュレーションのカーネルには通常読み込むことが出来ません。
通常ドライバのMakefile は以下のようになります。

obj-m := デバイス名.o

all:
    make -C ソースコードへのパス M=$(PWD) modules

clean:
    make -C ソースコードへのパス M=$(PWD) clean

また、異なるアーキテクチャに対応する場合にはエンディアンの違いに気をつける必要があります。
Linux Kernel にはエンディアンを変換するマクロが有るためこれを
利用します(cpu_to_le32, le32_to_cpu など)。
また、32bit 環境と 64bit 環境では一部のデータ型のサイズが異なるため(long 型など)、
両方に対応する場合は構造体のアラインメントなどに注意する必要もあります。
今回は簡単のために汎用化のための処理はしていません。


はじめ: (1) 作ったもの

前: (1) 作ったもの
次: (3) 開発/動作環境

12
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13