20行できる高精度ハードウェア自動認識

さえないTips系のようなタイトルになってしまいましたが、これは驚きです。しかし一方で悲しい(今までの苦労は…)。


これまでLinuxのハードウェア自動認識と言えば、/sys/bus/pci/devices以下と、/lib/modules/`uname -r`/modules.pcimapを照らし合わせて解析していくのが定石でした。USBにも対応しようとすると、もう一つ大変です。
しかしこれからの常識は、/sys/bus/*/devices/*/modalias/lib/modules/`uname -r`/modules.aliasです。


/lib/modules/`uname -r`/modules.aliasを見てみると、↓このようになっています。

# Aliases extracted from modules themselves.
alias usb:v1604p8005d*dc*dsc*dp*ic*isc*ip* snd_usb_usx2y
alias usb:v1604p8007d*dc*dsc*dp*ic*isc*ip* snd_usb_usx2y
…中略…
alias usb:v0D96p5200d000[1-9]dc*dsc*dp*ic*isc*ip* usb_storage
alias usb:v0D96p410Ad[1-9]*dc*dsc*dp*ic*isc*ip* usb_storage
…中略…
alias pci:v00001725d00007174sv*sd*bc01sc06i00* sata_vsc
alias pci:v00001106d00003249sv*sd*bc*sc*i* sata_via
…

usbやpciの他に、ieee1394やpcmciaなどがあります。



/sys/bus/*/devices/*/modaliasは、↓こうなります。

$ cat /sys/bus/*/devices/*/modalias
pci:v00001002d00005952sv00001002sd00005952bc06sc00i00
pci:v00001002d00005A34sv00000000sd00000000bc06sc04i00
…中略…
usb:v0000p0000d0206dc09dsc00dp00ic09isc00ip00
usb:v056Ap0014d0314dc00dsc00dp00ic03isc01ip02
…


/sys/bus/*/devices/*/modaliasと、/lib/modules/`uanme -r`/modules.aliasの各行をワイルドカードでマッチしていけば、カーネルモジュール名が得られます。
これをRubyシェルスクリプトで実装してみました。


まずはRuby。※同日追記: 問題あり。下に修正版

#!/usr/bin/env ruby
def detect_kmod(modules_alias_path)
  devices = Dir.glob("/sys/bus/*/devices/*/modalias").sort.map {|path|
    File.read(path)
  }
  kmods = []
  File.foreach(modules_alias_path) {|line|
    line.rstrip!
    trash, pattern, kmod = line.split(" ",3)
    devices.each {|dev|
      if File.fnmatch?(pattern, dev)
        kmods.push(kmod)
      end
    }
  }
  kmods.uniq
end
p detect_kmod("/lib/modules/#{`uname -r`.strip}/modules.alias")

続いてシェルスクリプト。※同日追記: 下に改良版

#!/bin/sh
detect_kmod() {
  modules_alias_path="$1"
  kmods=""
  devices=`echo /sys/bus/*/devices/*/modalias | sort | xargs cat`
  while read line ; do
    pattern=`echo $line | cut -d ' ' -f 2`
    for dev in $devices; do
      case $dev in
        $pattern)
          kmods="$kmods
`echo $line | cut -d ' ' -f 3`"
          ;;
        *)
          ;;
      esac
    done
  done < $modules_alias_path
  echo "$kmods" | tail -n +2 | sort | uniq
}
detect_kmod /lib/modules/`uname -r`/modules.alias

シェルスクリプト版はRuby版と比べて40倍くらい遅いので注意。



実行してみると、

$ time ./detect_kmod.rb
["snd_hda_intel", "usb_storage", "wacom", "usbmouse", "usbkbd", "usbhid", "ohci_hcd", "ehci_hcd", "usbcore", "aic7xxx", "shpchp", "skge", "sk98lin", "e1000", "serio_raw", "psmouse", "ohci1394", "generic", "atiixp", "pata_atiixp", "ata_generic", "ahci"]
0.41user 0.10system 0:00.51elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+1166minor)pagefaults 0swaps
$ time ./detect_kmod.sh
ahci
aic7xxx
ata_generic
atiixp
e1000
ehci_hcd
generic
ohci1394
ohci_hcd
pata_atiixp
psmouse
serio_raw
shpchp
sk98lin
skge
snd_hda_intel
usb_storage
usbcore
usbhid
usbkbd
usbmouse
wacom
12.61user 10.33system 0:22.76elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+3548933minor)pagefaults 0swaps

確かに検出できています。


ちなみに、PCIバスに繋がっているコントローラを経由して繋がっているデバイス(USB, IEEE1394, CardBusなど)を検出するには、このスクリプトを2回使う必要があります。
まず1回目で、PCIバスにUSBコントローラ/IEEE1394コントローラ…用のカーネルモジュールをロードします。これでUSBデバイスやIEEE1394デバイスが見えるようになります。2回目で各デバイス用のカーネルモジュールをロードできます。





※同日追記:
シェルスクリプト版が大幅に改良されました!>10行でできる高精度ハードウェア自動認識 - 仙石浩明の日記

※2007/12/18追記:さらに良い方法がありました(後述)


10行でできる高精度ハードウェア自動認識 - 仙石浩明の日記より

ちなみに古橋さんのスクリプトは、 modules.alias の各行それぞれに対し、 マッチするデバイスが /sys/bus/*/devices/*/modalias に存在すれば、 そのモジュールを読み込む処理になっている。
しかしながら、これだと一つのデバイスに対し、 複数のモジュールが読み込まれてしまうことになるのではないだろうか?

…中略…

modules.alias を検索する際は、 マッチする行が見つかった時点で以降の行はスキップしないと、 この例のような余計なモジュール読み込みが起きる恐れがある。 マッチした以降の行を読み飛ばすには、 私が書いた上記 sh スクリプトのように、 /sys/bus/*/devices/*/modalias の各行それぞれに対し、 マッチするモジュールを一つだけ modules.alias から見つけて読み込む処理のほうが、 簡単に書けるのではないかと思うがどうだろうか。

なるほど。私のマシンでもMarvell Yukon用のドライバで、skgeだけでなくsk98linも読み込まれてしまっています。


この問題だけを修正するなら、↓これで良いのですが、

#!/usr/bin/env ruby
def detect_kmod(modules_alias_path)
  devices = Dir.glob("/sys/bus/*/devices/*/modalias").sort.map {|path|
    File.read(path)
  }
  kmods = []
  File.foreach(modules_alias_path) {|line|
    line.rstrip!
    trash, pattern, kmod = line.split(" ",3)
    devices.each {|dev|
      if File.fnmatch?(pattern, dev)
        kmods.push(kmod)
        devices.delete(dev)    # この行を追加
      end
    }
  }
  kmods.uniq
end
p detect_kmod("/lib/modules/#{`uname -r`.strip}/modules.alias")

他にも問題が発覚しました。このスクリプトだと、モジュールがバスIDの順に読み込まれるのではなく、modules.aliasに載っている順に読み込まれます。モジュールのロード順が違うと、ネットワークインターフェスの番号(eth0, eth1, …)やデバイスノード名(/dev/sda, /dev/sdb, …)が変わってしまいます。(固定するように設定された環境なら変わらない)


バスID順とmodules.alias順のどちらか一方に統一すれば良いと思うのですが、バスID順の方が適切だと思うので、バスID順に読み込むように修正しました。


・改良版detet_kmod.rb

#!/usr/bin/env ruby
def detect_kmod(modules_alias_path)
  kmods = []
  modules_alias = []
  File.foreach(modules_alias_path) {|line|
    line.rstrip!
    trash, pattern, kmod = line.split(" ",3)
    modules_alias.push([pattern, kmod])
  }
  Dir.glob("/sys/bus/*/devices/*/modalias").sort.each {|path|
    dev = File.read(path).rstrip
    modules_alias.each {|pattern, kmod|
      if File.fnmatch?(pattern, dev)
        kmods.push(kmod)
        break
      end
    }
  }
  kmods.uniq
end
p detect_kmod("/lib/modules/#{`uname -r`.strip}/modules.alias")

実行してみます。(pの代わりにpp)

$ time ./detect_kmod.rb 
["shpchp",
 "generic",
 "ohci_hcd",
 "ehci_hcd",
 "snd_hda_intel",
 "e1000",
 "aic7xxx",
 "ohci1394",
 "skge",
 "serio_raw",
 "usbcore",
 "wacom",
 "usbkbd",
 "usb_storage"]
0.43user 0.06system 0:00.49elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+2015minor)pagefaults 0swaps


仙石さんの改良版シェルスクリプトと比べてみます。(modprobeの代わりにecho)

$ time ./dev2mod.sh 
shpchp
shpchp
generic
ohci_hcd
ohci_hcd
ohci_hcd
ohci_hcd
ohci_hcd
ehci_hcd
generic
snd_hda_intel
e1000
aic7xxx
ohci1394
skge
serio_raw
usbcore
wacom
usbcore
usbkbd
usbcore
usbcore
usbcore
usbcore
usbcore
usb_storage
usb_storage
2.74user 0.02system 0:02.77elapsed 100%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+4160minor)pagefaults 0swaps

同じ順番で検出されるようになりました。




※2007/12/18追記:
コメント欄でKさんに教えていただきました。

現在では、modprobeは(busyboxのmodprobeでも)、modaliasを使って非常に簡単にドライバを自動読み込みすることができるようになっています。具体的には、以下のようなコマンドを実行するだけです。
  # modprobe `cat /sys/bus/pci/0000¥:00¥:1f.0/modalias`
これで、modprobeが自動的に/lib/modules/`uname -r`/modules.aliasを検索して、依存関係込みで適当なドライバを読んでくれます。これを使うと、もっと簡単で高速にドライバの自動読み込みができるようになります。参考下さい。

なんと!↓これで終わりです。

for mod in /sys/bus/pci/devices/*/modalias; do modprobe `cat $mod`; done

動きました!

おそらくこの方法が一番簡単で高速です。
情報ありがとうございましたm(_ _)m