Czym jest linux kernel driver binding

Spis treści

Bawiąc się ostatnio QEMU/KVM na swoim laptopie z zainstalowanym Debianem natrafiłem na ciekawe zagadnienie związane z wirtualizacją, tj. z PCI passthrough. Nie chodzi mi tutaj o samą technikę PCI passthrough ale o dobór sterowników do urządzeń działających pod kontrolą linux. Każdy sprzęt, który ma działać w systemie, musi mieć załadowany w pamięci RAM stosowny moduł kernela. Te moduły zwykle są ładowane automatycznie podczas pracy systemu, np. gdy podłączamy nowy sprzęt do komputera (można też te moduły ładować i ręcznie via modprobe ). Gdy nasz linux z jakiegoś powodu dobierze niewłaściwy (z naszego punktu widzenia) moduł dla jakiegoś urządzenia, to możemy to urządzenie odłączyć od komputera, a moduł bez problemu wyładować, po czym dokonać stosownych poprawek w systemie. Problem zaczyna się w sytuacji, gdy mamy do czynienia ze sprzętem, którego nie da się od komputera fizycznie odłączyć, np. wbudowana w płytę główną karta dźwiękowa, czy też wbudowana grafika bezpośrednio w CPU. Podobnie sprawa wygląda w przypadku wkompilowania modułów na stałe w kernel -- jak wyładować moduł, którego się nie da wyładować? By w takich sytuacjach zmienić przypisany urządzeniu sterownik trzeba dodać parę plików w katalogach /etc/modules-load.d/ / /etc/modprobe.d/ oraz zrestartować maszynę, tak by podczas fazy boot kernel dobrał sprzętowi pożądane przez nas moduły i ich konfigurację. Niemniej jednak, istnieje prostszy sposób na zmianę sterownika działającego w systemie sprzętu i to bez potrzeby fizycznego restartowania maszyny. Chodzi o mechanizm ręcznego przypisywania urządzeń do konkretnych sterowników (manual driver binding and unbinding).

Identyfikacja urządzenia PCI w linux

Na potrzeby tego artykułu weźmy sobie wbudowaną w płytę główną mojego ThinkPad'a T430 kartę dźwiękową ( vid:8086 oraz pid:1e20 ). Rzućmy okiem na katalog /sys/ w miejsce, gdzie ulokowana jest struktura tej karty dźwiękowej, choć jej odszukanie może być ździebko problematyczne. Generalnie to interesuje nas katalog /sys/devices/pci0000:00/0000:00:1b.0/ . Szukamy urządzenia, dlatego katalog /sys/devices/ . Szukane urządzenie jest z gatunku PCI, więc /sys/devices/pci/ . Problemy możemy napotkać w przypadku widocznych dalej w ścieżce numerów.

Jeśli chodzi o pci0000:00 oraz 0000:00:1b.0, to te numerki możemy odczytać z wyjścia lspci :

$ lspci -nnk -d 8086:1e20
00:1b.0 ...

Każde urządzenie PCI obecne w systemie jest identyfikowane przez numer szyny ( 00 ), numer urządzenia ( 1b ) i numer funkcji urządzenia ( .0 ). Specyfikacja PCI pozwala maszynie posiadać do 256 szyn, choć w większych systemach ta wartość może być niewystarczająca. Dlatego też linux wspiera domeny PCI:

$ lspci -vt
-[0000:00]-+-...
           +-...
           +-1b.0  Intel Corporation 7 Series/C216 Chipset Family High Definition Audio Controller
           +-...
           +-...

Każda domena (w tym przypadku jest tylko jedna, tj, 0000 ) może pomieścić do 256 szyn ( 0000:00 ), każda szyna z kolei do 32 urządzeń, a każde urządzenie może mieć kilka funkcji (np. karta dźwiękowa w połączeniu z CD-ROM), maksymalnie 8.

Sterownik sprzętu

By sprawdzić jaki moduł kernela jest oddelegowany do obsługi danego sprzętu, wystarczy posłużyć się ponownie poleceniem lspci . Poniżej jest przykład dla tej wbudowanej w laptop karty dźwiękowej:

$ lspci -nnk -d 8086:1e20
00:1b.0 Audio device [0403]: Intel Corporation 7 Series/C216 Chipset Family High Definition Audio Controller [8086:1e20] (rev 04)
        Subsystem: Lenovo 7 Series/C216 Chipset Family High Definition Audio Controller [17aa:21f3]
        Kernel driver in use: snd_hda_intel

Ta karta, jak widać, korzysta z modułu snd_hda_intel .

Załóżmy teraz, że coś nam z tym modułem nie pasuje i chcielibyśmy się go pozbyć. Zwykle do głowy w takiej sytuacji jako pierwsze przychodzi nam to poniższe polecenie:

# modprobe -r snd_hda_intel
modprobe: FATAL: Module snd_hda_intel is builtin.

Jak widać, użytku z modprobe nie da się zbytnio zrobić, gdy moduły są wbudowane bezpośrednio w kernel.

Podobnie sprawa wygląda, gdy mamy co prawda do czynienia z modułami ale jakiś sprzęt akurat z tego modułu korzysta:

# modprobe -r snd_hda_intel
modprobe: FATAL: Module snd_hda_intel is in use.

Nie da się zatem wyładować sterownika, gdy ten jest używany przez urządzenie, przynajmniej nie powinno się tego robić, zwłaszcza w sposób siłowy.

Pliki bind i unbind

To w jaki niby sposób powinniśmy się zabrać do wyładowania takiego modułu? Trzeba poszukać w katalogu /sys/ plików bind i unbind dla sterownika snd_hda_intel . Interesuje nas ścieżka /sys/bus/pci/drivers/snd_hda_intel/ :

# ls -al  /sys/bus/pci/drivers/snd_hda_intel/
total 0
drwxr-xr-x  2 root root    0 2020-07-27 10:28:45 ./
drwxr-xr-x 26 root root    0 2020-07-27 10:28:45 ../
lrwxrwxrwx  1 root root    0 2020-07-27 11:29:18 0000:00:1b.0 -> ../../../../devices/pci0000:00/0000:00:1b.0/
lrwxrwxrwx  1 root root    0 2020-07-27 11:22:36 module -> ../../../../module/snd_hda_intel/
--w-------  1 root root 4096 2020-07-27 11:22:36 bind
--w-------  1 root root 4096 2020-07-27 03:20:45 new_id
--w-------  1 root root 4096 2020-07-27 11:29:18 remove_id
--w-------  1 root root 4096 2020-07-27 11:29:18 uevent
--w-------  1 root root 4096 2020-07-27 11:20:50 unbind

Jak widać są dostępne szukane przez nas pliki bind i unbind . To właśnie przy pomocy tych plików możemy przypisać jakieś urządzenie do konkretnego sterownika. Jest też link z numerkiem urządzenia, które aktualnie korzysta z tego sterownika, tj. 0000:00:1b.0 (wspomniana wcześniej karta dźwiękowa).

Ten mechanizm przypisania urządzenia do sterownika działa na zasadzie przesłania identyfikatora konkretnego urządzenia do pliku bind lub unbind , w zależności czy chcemy przypisać urządzenie do sterownika, czy też usunąć to przypisanie. Przykładowo, by usunąć przypisanie urządzenia 0000:00:1b.0 do sterownika snd_hda_intel , wpisujemy w terminalu poniższe polecenie:

# echo -n "0000:00:1b.0" > /sys/bus/pci/drivers/snd_hda_intel/unbind

Jeśli teraz sprawdzimy ponownie wyjście lspci , to zauważymy, że nasza karta dźwiękowa nie ma już informacji o tym by to urządzenie korzystało z jakiegokolwiek modułu:

# lspci -nnk -d 8086:1e20
00:1b.0 Audio device [0403]: Intel Corporation 7 Series/C216 Chipset Family High Definition Audio Controller [8086:1e20] (rev 04)
        Subsystem: Lenovo 7 Series/C216 Chipset Family High Definition Audio Controller [17aa:21f3]

Jeśli zajrzymy ponownie do katalogu /sys/bus/pci/drivers/snd_hda_intel/ , to link z ID 0000:00:1b.0 nie będzie już tam figurował:

# ls -al /sys/bus/pci/drivers/snd_hda_intel/
total 0
drwxr-xr-x  2 root root    0 2020-07-27 10:28:45 ./
drwxr-xr-x 26 root root    0 2020-07-27 10:28:45 ../
lrwxrwxrwx  1 root root    0 2020-07-27 11:22:36 module -> ../../../../module/snd_hda_intel/
--w-------  1 root root 4096 2020-07-27 11:22:36 bind
--w-------  1 root root 4096 2020-07-27 03:20:45 new_id
--w-------  1 root root 4096 2020-07-27 11:29:18 remove_id
--w-------  1 root root 4096 2020-07-27 11:29:18 uevent
--w-------  1 root root 4096 2020-07-27 11:34:29 unbind

W taki sposób zniknie nam też urządzenie audio z systemu, co możemy sprawdzić w pliku /proc/asound/cards :

# cat /proc/asound/cards

By ta karta dźwiękowa ponownie zaczęła poprawnie funkcjonować, trzeba dodać jej identyfikator do pliku bind sterownika snd_hda_intel :

# echo -n "0000:00:1b.0" > /sys/bus/pci/drivers/snd_hda_intel/bind

Po wydaniu tego powyższego polecenia, w logu systemowym pojawią się poniższe komunikaty:

kernel: snd_hda_intel 0000:00:1b.0: bound 0000:00:02.0 (ops i915_audio_component_bind_ops)
kernel: snd_hda_codec_realtek hdaudioC0D0: autoconfig for ALC3202: line_outs=1 (0x14/0x0/0x0/0x0/0x0) type:speaker
kernel: snd_hda_codec_realtek hdaudioC0D0:    speaker_outs=0 (0x0/0x0/0x0/0x0/0x0)
kernel: snd_hda_codec_realtek hdaudioC0D0:    hp_outs=2 (0x15/0x1b/0x0/0x0/0x0)
kernel: snd_hda_codec_realtek hdaudioC0D0:    mono: mono_out=0x0
kernel: snd_hda_codec_realtek hdaudioC0D0:    inputs:
kernel: snd_hda_codec_realtek hdaudioC0D0:      Mic=0x18
kernel: snd_hda_codec_realtek hdaudioC0D0:      Dock Mic=0x19
kernel: snd_hda_codec_realtek hdaudioC0D0:      Internal Mic=0x12
kernel: input: HDA Intel PCH Mic as /devices/pci0000:00/0000:00:1b.0/sound/card0/input38
kernel: input: HDA Intel PCH Dock Mic as /devices/pci0000:00/0000:00:1b.0/sound/card0/input39
kernel: input: HDA Intel PCH Headphone as /devices/pci0000:00/0000:00:1b.0/sound/card0/input40
kernel: input: HDA Intel PCH Dock Headphone as /devices/pci0000:00/0000:00:1b.0/sound/card0/input41
kernel: input: HDA Intel PCH HDMI/DP,pcm=3 as /devices/pci0000:00/0000:00:1b.0/sound/card0/input42
kernel: input: HDA Intel PCH HDMI/DP,pcm=7 as /devices/pci0000:00/0000:00:1b.0/sound/card0/input43
kernel: input: HDA Intel PCH HDMI/DP,pcm=8 as /devices/pci0000:00/0000:00:1b.0/sound/card0/input44

Jeśli teraz zajrzymy do pliku /proc/asound/cards , to powinniśmy ujrzeć tam kartę dźwiękową, którą system właśnie dodał:

# cat /proc/asound/cards
 0 [PCH            ]: HDA-Intel - HDA Intel PCH
                      HDA Intel PCH at 0xbfa00000 irq 32

Wyładowanie modułu będącego w użyciu

Gdy już usuniemy przypisanie urządzenia do sterownika, to taki moduł kernela przestanie być przez to urządzenie używany, a my bez problemu będziemy w stanie ten moduł wyładować:

# echo -n "0000:00:1b.0" > /sys/bus/pci/drivers/snd_hda_intel/unbind

# modprobe -r -v snd_hda_intel
rmmod snd_hda_intel
rmmod snd_intel_dspcfg

Trzeba jednak pamiętać, że jeden moduł kernela jest w stanie obsługiwać wiele urządzeń ale konkretne urządzenie może korzystać w danej chwili tylko z jednego modułu.

Mikhail Morfikov avatar
Mikhail Morfikov
Po ponad 10 latach spędzonych z różnej maści linux'ami (Debian/Ubuntu, OpenWRT, Android) mogę śmiało powiedzieć, że nie ma rzeczy niemożliwych i problemów, których nie da się rozwiązać. Jedną umiejętność, którą ludzki umysł musi posiąść, by wybrnąć nawet z tej najbardziej nieprzyjemniej sytuacji, to zdolność logicznego rozumowania.