C MAGAZINE Linux programming Tips
第2回 PCIプローブ
吉井一友 kazutomo@turbolinux.co.jp
■はじめに
前回の"ifconfigもどき"はいかがでしたか?今回は、PCIバスに接続されているデバイスのプローブ方法について簡単なプログラムをまじえて説明します。 それと PCIユーティリティーの一つであるlspciをハックして、PCIの情報だけでなく関連のLinuxモジュールを表示するクールなパッチも収録しましたので、 それについても説明します。おそらく今回のお話しは前回の"ifconfigもどき"よりは実用性が高いでしょう(前回が実用性無さ過ぎ!?)。
■PCIバスとは
PCI(Peripheral Component Interconnect)バスは、長い間標準だった(古くなりすぎた?腐りすぎた?)ISAバスの置き換えとして設計されたバスです。 ISAバスと比較して優れている点といえば、まず転送スピードがあげられるでしょう。最初の規格では、バス幅は32ビットで動作周波数は33MHz、 最大データ転送速度は132MB/sでしたが、最新の規格では、バス幅64ビット、66MHz動作で最大533MB/sのデータ転送可能なものもあります。 またOS側から見た場合、IRQなどのリソース管理が簡単になったことも大きな利点と言えます。
上記のような利点により現在では、ほぼPCIバスは標準的なものとなっており、ISAカード用のスロットの無いマザーボードすら出現しています (あたりまえ?)。おなじみのネットワークカードやビデオカードなどの外部機器のほとんどは、PCIバスに移行しています。 ただISAバスはなくなったわけでなく内部的にはまだ残っています(図1参照)。
Linuxのカーネルバージョンが1.2ごろ(3年ぐらい前)のLinux関連の書籍に、"PCIバスのデザインのカードは高価であるためハイエンドシステム用途です" のようなことを書いてあったのを思い出しますが、現在では市販されているカードもLinuxのカーネルドライバーもほとんどがPCI用になっています。
■とりあえずプローブ - /proc/pci
近頃のカーネル(2.2.16あたり)でてっとり早く自分のマシンに接続されている PCIデバイスを確認するには、
/proc/pci
をcatすることで可能です。まずは実行してみましょう。以下は、あるマシンの出力結果の一部です。 読者の皆様が普段お使いのマシンは、おそらく10個以上のPCIデバイスを確認できるでしょう。
PCI devices found:
Bus 0, device 0, function 0:
Host bridge: Intel 440BX - 82443BX Host (rev 3).
Medium devsel. Master Capable. Latency=32.
Prefetchable 32 bit memory at 0xf0000000 [0xf0000008].
Bus 0, device 14, function 0:
Ethernet controller: Intel 82557 (rev 8).
Medium devsel. Fast back-to-back capable. IRQ 18.
Master Capable. Latency=32. Min Gnt=8.Max Lat=56.
Non-prefetchable 32 bit memory at 0xfe100000 [0xfe100000].
I/O at 0xcc80 [0xcc81].
Non-prefetchable 32 bit memory at 0xfe000000 [0xfe000000].
Bus 2, device 11, function 0:
SCSI storage controller: Adaptec AIC-7890/1 (rev 1).
Medium devsel. Fast back-to-back capable. BIST capable.
IRQ 16. Master Capable. Latency=32. Min Gnt=39.Max Lat=25.
I/O at 0xdc00 [0xdc01].
Non-prefetchable 64 bit memory at 0xf9fff000 [0xf9fff004].
|
上記の出力結果では、3つのPCIデバイス(ホストブリッジ,ネットワークカード ,SCSIカード)を見ることができます。まず Busで始まる行に注目してください。
Bus 0, device 0, function 0:
Busのあとの数字は、バス番号を表しています。PCIの仕様では、一つのシステムに256までバスを持てるようになっています。 次のdeviceの続くの数値は、バスに接続されているデバイスの番号になります。一つのバスに32個まで接続可能です。最後のfunctionに続く数値は、 ファンクション番号になります。PCIデバイスは、一つのデバイス中に8つまでの機能を持たせることが可能です。複数のファンクション持つものとして、 モデムとネットワークカードが一つのなったデバイスや、ISA,IDE,USBなどをまとめて処理するブリッジなどが挙げられます。
次に、Busで始まる行の次の行に、PCIデバイスの種類と名称が書かれている行を見ることができます。
Host bridge: Intel 440BX - 82443BX Host (rev 3).
PCIデバイスは、revに続くリビジョン番号は持っていますが、種別や名称に関する文字列はカードの中には持っていません。実際は、 PCIデバイスのコンフィグレーションレジスターに書かれている数値を元に対応表によってPCIデバイスの種別や名称を割り当てています。
残りの行には、見ての通りPCIデバイスのIRQやI/Oポートなどが記述されています。 細かく/proc/pciの説明をすると長くなってしまうのでこのあたりにしておきます。 カーネルソースのdrivers/pci/oldproc.cを眺めると大体の処理を理解できるかと思います。
■ちょっとまともなプローブ - /proc/bus/pci
実は、前述の/proc/pciは、それを処理するソースがoldproc.cという名前であるように古いものでそのうちカーネルツリーから無くなる可能性があります。 "そんな古いもの説明して無責任だよ"とは言わないでください。/proc/pciは古いものですが、 テキストで出力されるので説明しやすいので使わさせて頂きました。
これから説明する(由緒正しい) /proc/bus/pci は、/proc/pciと比べるとちょっとだけ手ごわいですが実用性は高いでしょう。 後で登場するpciutilsパッケージに含まれるlspciも/proc/bus/pciからPCIの情報を取るようになっています。 /proc/pciと違い、/proc/bus/pciは、一つのファイルでなく決まったディレクトリー構造にもって情報を提供します。 参考のために、あるマシンでtreeコマンド を実行した結果をのせておきます。
$ tree /proc/bus/pci
/proc/bus/pci/
|-- 00
| |-- 00.0
| |-- 01.0
| |-- 02.0
| |-- 07.0
| |-- 07.1
| |-- 07.2
| |-- 07.3
| |-- 0e.0
| |-- 10.0
| `-- 11.0
|-- 01
| `-- 00.0
|-- 02
| `-- 0b.0
`-- devices
|
数個のディレクトリーとdevicesという名前のファイルを見ることができます。 ファイルdevicesは、必ず一つ存在するもので、システムに接続されている全てのPCIデバイスの主要な情報が16進表記でリストされています。 ファイルdevicesは、テキストファイルなのでcatコマンドなどで中身を見ることはできます。実際に確認してみてください。 以下のような行を複数みることができるはずです。
0258 9005001f 10 0000dc01 f9fff004 00000000
00000000 00000000 00000000 fa000000
|
/proc/pciと照らし合わせてみると、最初のフィールドを除き何を意味しているのが大体判断できると思います。最初のフィールドは、 バス番号(最大256)、デバイス番号(最大32)、ファンクション番号(最大8)を16ビットで表したものです(図2参照)。2番目のフィールドからは、 ベンダーID、デバイスID、IRQ, IOポート、メモリーアドレスなどと続きます。
また、ディレクトリー名の00, 01などはバス番号を意味し、バスごとにPCIデバイスの情報が(256バイトのバイナリー)ファイルとして格納されるような構造になっています。 各ファイルは、小数点付の16進数のようなファイル名が付いていますが、 /proc/pciのところで説明したデバイス番号とファンクション番号を意味しています。
各PCIデバイスは、図3で示すような配置で256バイトのコンフィグレーションレジスターを持っており、 この内容がバス番号の付いたディレクトリー以下にある(バイナリー)ファイルに格納されています。 図3は先頭から0x2f番地までしか記述されていませんが、規格的には、 最初の64バイトが標準化されているようです。0x30以降の番地に何が記述されているかは、 カーネルヘッダーのinclude/linux/pci.hあたりを見るのが手っ取り早いでしょう。
/proc/bus/pciの中身は、16進数の世界で生きている人(?)なら理解できるかもしれませんが、 普通の人にはちょっとしんどいと思うのでプログラムにたよったほうがよさそうです。 前置きが長くなってしまいましたが、ではそろそろプログラミングを始めましょう。
表示を行うプログラム
ベンダーID, デバイスID, サブベンダーID, サブデバイスID, クラスID, リビジョンの一覧をテキストで表示するプログラムを作成しましょう。 もう一度、図3を見てください。斜線の引かれているところが今回注目している所になります。 ちょっと違いますが、カーネルドライバーの初期化ルーチンでも、 PCIデバイスのベンダーIDなどの情報を利用してドライバーに対応するデバイスがシステムに付いているかの判断を行っています。 また、いくつかのドライバーでは複数のバージョンのデバイスを一つのソースコードで処理するようになっておりバージョンの判断が必要になります。
まずベンダーID, デバイスIDは、ファイルdevicesから取得できます。 それ以外の情報は各デバイスから得ます。そういうことでまず以下のような関数を書くことができます。 それ以外のデータは、関数getconfigdata()を呼び出すことで表示するようになっています。
static int list_proc_bus_pci(char* proc_bus_pci_path)
{
FILE* fp;
char buf[BUFSIZ];
char devfn[256];
short bus,dev,func;
int vend,dfn;
#define PCI_SLOT(devfn) (((devfn) >> 3) & 0x1f)
#define PCI_FUNC(devfn) ((devfn) & 0x07)
sprintf(devfn,"%s/devices",proc_bus_pci_path);
fp = fopen(devfn,"r");
if(!fp) {
printf("can't open %s\n",devfn );
return 1;
}
while( fgets(buf, sizeof(buf), fp) ) {
sscanf( buf, "%x %x", &dfn, &vend);
bus = dfn >> 8U;
dev = PCI_SLOT(dfn & 0xff);
func = PCI_FUNC(dfn & 0xff);
printf("Bus %2x, device %2x, function %x:\n", bus, dev, func);
printf("\tVendorID %04x / DeviceID %04x\n",(vend>>16)&0xffff,vend&0xffff);
if( getconfigdata( proc_bus_pci_path, bus, dev, func) ) return 1;
puts("");
}
fclose(fp);
return 0;
}
|
上記の続きで、PCIデバイスごとのコンフィグレーションレジスターを表示するための関数は以下のような実装になるでしょう。
static int getconfigdata( char* proc_bus_pci_path,
short bus, short dev, short func)
{
FILE* fp;
char fn[128];
char buf[128];
short class, subven, subdev;
short rev;
#define PCI_REVISION_ID 0x08
#define PCI_CLASS_DEVICE 0x0a
#define PCI_SUBSYSTEM_VENDOR_ID 0x2c
#define PCI_SUBSYSTEM_ID 0x2e
sprintf(fn,"%s/%02x/%02x.%x", proc_bus_pci_path, bus, dev, func);
fp = fopen( fn, "r" );
if( !fp ) {
printf("can't open %s\n",fn);
return 1;
}
fread( buf, sizeof(buf), 1, fp);
rev = (short)buf[PCI_REVISION_ID];
class = (short)((buf[PCI_CLASS_DEVICE+1]&0xff)<<8)|(buf[PCI_CLASS_DEVICE]&0xff);
subven = (short)((buf[PCI_SUBSYSTEM_VENDOR_ID+1]&0xff)<<8)
|(buf[PCI_SUBSYSTEM_VENDOR_ID]&0xff);
subdev = (short)((buf[PCI_SUBSYSTEM_ID+1]&0xff)<<8)| (buf[PCI_SUBSYSTEM_ID]&0xff);
printf("\tClassID %04x / RevisionID %x\n",
class, rev);
if( !(subven==0&&subdev==0) )
printf("\tSubVendorID %04x / SubDeviceID %04x\n",
subven&0xffff, subdev&0xffff);
fclose(fp);
return 0;
}
|
PCIバスから得られるデータはリトル・エンディアンになっていることに注意が必要なぐらいで、 図3のような情報があれば比較的簡単にこのようなプログラムは記述できるはずです。
■もっと便利なプローブ - lspci
前述の例題プログラムを使えば自分のマシンに接続されているPCIデバイスの情報を取ることができますが、 デバイスIDとかが16進数表記なので実際になんて名前のデバイスが付いているかまたはどのカーネルドライバーを使えばよいのかわかりません。 そこでナイスなプログラムとクールなパッチを紹介します。
知っている人も多いと思いますが、Linuxではpciutils(ftp://atrey.karlin.mff.cuni.cz/pub/linux/pci/) という名前の便利なフリーソフトウエアが利用できます(実はpciutilsはFreeBSDでも動作する)。 pciutilsには、PCIデバイスの情報を表示するlspciコマンドと逆にPCIデバイスに値をセットするsetpciコマンド、 とても重宝するPCIデバイスのデバイス名やベンダー名などのデータベースが収録されています。
それでこの便利なlspciコマンドをさらに、Linuxのカーネルモジュールまでも表示するように改造してみます。 このパッチをあてるとどのようになるかというと以下のようになります。 Linux KernelModuleに必要なカーネルモジュールが表示されるようになります。
$ lspci -v
00:0e.0 Ethernet controller: Intel Corporation 82557 [Ethernet Pro 100] (rev 08)
Linux KernelModule: e100
Flags: bus master, medium devsel, latency 32, IRQ 18
Memory at fe100000 (32-bit, non-prefetchable)
I/O ports at cc80
Memory at fe000000 (32-bit, non-prefetchable)
Capabilities:
00:10.0 Ethernet controller: Winbond Electronics Corp W89C840
Linux KernelModule: winbond-840
Flags: bus master, medium devsel, latency 32, IRQ 19
I/O ports at cc00
Memory at fe101000 (32-bit, non-prefetchable)
00:11.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8029(AS)
Linux KernelModule: ne2k-pci
Flags: medium devsel, IRQ 20
I/O ports at ccc0
Expansion ROM at f8000000 [disabled]
|
このパッチは、ベンダーID、デバイスIDもしくはサブベンダーID、 サブデバイスIDに対応するカーネルモジュール名のデータベースと、 ルックアップする機構をlspciに追加します。 もともとTurboLinuxの6.1以降のインストーラで使用していた機構をlspciに埋め込めるようにしたものです。
pciutilsと今回のパッチはCDROMに収録されているはずです。コンパイルは以下ような手順で行えます。
$ tar xvfz pciutils-2.1.8.tar.gz
$ tar xvfz patch -p0 < pciutils-2.1.8.linux-module.patch
$ make
# make install
|
もしRPMシステムがインストールされているなら以下のような方法でバイナリーRPMを作れます。
$ tar xvfz pciutils-2.1.8.tar.gz
$ tar xvfz patch -p0 < pciutils-2.1.8.linux-module.patch
$ tar cvfz /tmp/pciutils-2.1.8.tar.gz pciutils-2.1.8/
$ rpm -ta /tmp/pciutils-2.1.8.tar.gz
# rpm -Uvh /foo/bar/pciutils-2.1.8-2.i386.rpm
|
まだまだPCIバスに関する話はありますが、今回はこのあたりで話を終らせて頂きます。 特にカーネルがブート時にPCI関連の処理をどのように行っているか?とか、 もっとハードウエアよりになってBIOSが何を行っているのか?などは興味深いところなので勉強してみてください。 カーネルに関しては、カーネルソースのinit/main.cあたりからたどってdrivers/pci/pci.cや drivers/pci/proc.cを眺めると大体内容が理解できると思うのでトライしてみてください。
■ダウンロード