Tym postem zaczynamy na blogu niemałą rewolucję 🙂 Dotychczas skupialiśmy się głównie na mikrokontrolerach, teraz przechodzimy na większe platformy i zaczynamy przygodę z Linuxem. Na pierwszy ogień weźmiemy zagadnienie programowania modułów i sterowników. W artykule przybliżę czym są sterowniki i moduły w Linux, jak można zacząć z ich programowaniem, a na końcu skompilujemy i uruchomimy pierwszy kernelowy „Hello World”!
Czym są sterowniki urządzeń?
Sterownik urządzenia jest oprogramowaniem odpowiedzialnym za kontrolę i zarządzanie określonym urządzeniem. Stanowi on pomost pomiędzy sprzętem, a resztą systemu.
Sterownik może być na stałe wbudowany w jądro, ale może także istnieć w formie modułu, który jest dołączany dynamicznie, w czasie działania systemu – np. gdy podłączymy jakieś urządzenie do komputera. Sterowniki występują często właśnie w formie modułów i są ładowane tylko wtedy, gdy są potrzebne. Takie rozwiązanie ma wiele zalet, jednym z głównych jest oszczędność zasobów systemowych.
Jak zacząć?
Naukę najłatwiej zacząć od napisania prostego modułu, który nie steruje żadnym sprzętem – na to przyjdzie jeszcze czas. Jądro linuxa jest w większości napisane w języku C i z tego języka będziemy korzystać podczas programowania. Na bardzo niskim poziomie, niektóre fragmenty kernela, specyficzne dla danej architektury, są napisane w Assemblerze, jednak jego znajomość nie jest niezbędna. Jest szansa, że w przyszłości moduły będzie można pisać także w innych językach, jak np. Rust (więcej informacji tutaj), ale aktualnie nie jest to możliwe. Programowanie sterowników to programowanie niskopoziomowe, więc jeśli miałeś wcześniej styczność z mikrokontrolerami lub zajmowałeś się embedded to takie doświadczenie będzie tutaj bardzo cenne.
W związku z tym, że chcemy programować moduły/sterowniki pod Linuxa, musimy to robić na Linuxie. Dystrybucja jest dowolna, osobiście pracuje na Ubuntu i jestem zadowolony. Jeśli z jakiś powodów potrzebujesz Windowsa to warto rozważyć dual boot. Inną opcją, chociaż na dłuższą metę jej nie polecam, jest uruchomienie Linuxa w wirtualnej maszynie, np. korzystając z VirtualBoxa. Kolejną sprawą jest platforma docelowa, czyli ta na której chcesz uruchamiać napisany moduł. Tutaj możliwości mamy kilka.
Platforma docelowa
Najprostszą i najłatwiej dostępną opcją jest Twój komputer. Piszesz moduł i uruchamiasz go na tej samej maszynie – na hoście lub w wirtualnej maszynie. Nie jest to jednak idealne rozwiązanie. W przypadku jakiegoś buga w kodzie, podczas pracy modułu może dojść do kernel panic’a – komputer się zawiesi, konieczny będzie reboot. W przypadku wirtualnej maszyny nie jest to aż tak straszne, ale też może być uciążliwe – ponowne uruchomienie VMki trochę trwa. Przyznasz chyba, że nie jest to najprzyjemniejsza wizja developmentu. Jednak na początek, do pisania pierwszych, prostych modułów takie rozwiązanie jest jak najbardziej akceptowalne. W dalszej części artykułu zrobimy to właśnie w ten sposób – uruchomimy przykładowy moduł na tej samej maszynie, z której będziemy korzystać podczas programowania.
Fajną alternatywą jest uruchamianie modułów na jakimś SBC (ang. single board computer), jak np. popularne Raspberry Pi. W tym wypadku potencjalny reboot nie jest już tak problematyczny. Dodatkowym plusem takiego rozwiązania jest to, że do takiej płytki można łatwo podłączyć różnego rodzaju czujniki, urządzenia i do nich pisać sterowniki. O tym jak przygotować i uruchomić prosty moduł na Raspberry Pi możesz przeczytać tutaj – Własny moduł Linux na Raspberry Pi.
Należy jednak pamiętać, że programowania modułów czy sterowników można się nauczyć na zwykłym komputerze – wygląda to tak samo na wszystkich platformach. Dlatego jeśli nie masz takiej płytki to nic straconego. Co więcej, nawet jeśli masz to i tak warto zapoznać się z następną opcją, czyli wykorzystaniem w tym celu QEMU, który umożliwia uruchomienie Linuxa, do tego na różnych emulowanych architekturach. Jest bardzo szybki i daje wiele możliwości. W razie potrzeby, ponowne uruchomienie zajmuje kilka sekund. Jest to świetne rozwiązanie zarówno na początku nauki jak i podczas dalszej, bardziej zaawansowanej pracy. Więcej na temat tego sposobu napisałem w artykule Programowanie sterowników Linux z wykorzystaniem QEMU.
Uruchomienie własnego modułu
W tej części przedstawię przykład bardzo prostego modułu, pokażę jak go skompilować i uruchomić. Całość będzie wykonana na maszynie z Linuxem na pokładzie, beż żadnych wirtualnych maszyn czy dodatkowego sprzętu. Każda dystrybucja będzie ok, w moim wypadku jest to Ubuntu 20.04. W przypadku innych, niektóre przedstawione komendy mogą się odrobinę różnić, ale dostosowanie ich nie powinno stanowić trudności.
Poniżej zamieściłem kod modułu, który będziemy wykorzystywać. Jest to prosty kernelowy „Hello World”. Jego jedynym zadaniem będzie wypisanie tekstu podczas ładowania i odładowywania modułu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <linux/module.h> #include <linux/kernel.h> static int __init hello_world_init(void) { printk(KERN_INFO "Hello World!\n"); return 0; } static void __exit hello_world_exit(void) { printk(KERN_INFO "Goodbye World!\n"); } module_init(hello_world_init); module_exit(hello_world_exit); MODULE_AUTHOR("EmbeddedDev"); MODULE_DESCRIPTION("Hello World module"); MODULE_LICENSE("GPL"); |
Tutaj na razie pominę opis kodu i otoczki związanej z programowaniem modułów. Na ten temat przygotowałem oddzielny artykuł Piszemy pierwszy moduł linux – Hello World.
Korzystając z dowolnego edytora tekstowego zapisujemy wyżej przedstawiony kod do pliku z rozszerzeniem *.c, np. hello_world.c. Oprócz tego będziemy jeszcze potrzebowali prostego Makefile’a:
1 2 3 4 5 6 |
obj-m += hello_world.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean |
Pamiętaj, aby w przypadku Makefile przy wcięciach wykorzystać tab, zamiast spacji.
Kompilacja i uruchomienie
Do skompilowania modułu nie potrzebujemy całego kodu kernela (chociaż możemy go wykorzystać), wystarczą nam same headery. Należy jednak pamiętać, że muszą być one w tej samej wersji co jądro systemu. Do ich instalacji wykorzystamy poniższą komendę:
1 |
$ sudo apt install linux-headers-$(uname -r) |
Została nam ostatnia rzecz, potrzebujemy narzędzi do kompilacji. W tym celu instalujemy pakiet build-essential, który posiada wszystko co potrzebne (make, gcc, etc):
1 |
$ sudo apt install build-essential |
Mamy już wszystko, możemy teraz przejść do skompilowania modułu. W folderze zawierającym kod modułu i Makefile wystarczy wykonać make:
1 |
$ make |
W wyniku kompilacji powinno pojawić się kilka plików. Nas interesuje najbardziej
hello_world.ko, który wykorzystamy do uruchomienia. Do załadowania modułu korzystamy z narzędzia
insmod:
1 |
sudo insmod hello_world.ko |
Jeśli wszystko się powiedzie, powyższa komenda nic nie zwróci, a nasz „Hello World” pojawi się w logach kernelowych. Powinniśmy go zauważyć w dmesg lub /var/log/kern.log:
1 2 3 |
$ dmesg | tail [...] [38587.886957] Hello World! |
Możemy jeszcze wylistować załadowane moduły i sprawdzić czy pojawił się ten przygotowany przez nas:
1 2 |
$ lsmod | grep hello_world hello_world 16384 0 |
Na koniec pokażę jeszcze jak możemy odładować moduł. W tym celu korzystamy z narzędzia
rmmod:
1 |
sudo rmmod hello_world |
Jeśli jeszcze raz sprawdzimy dmesg to powinniśmy tam zauważyć kolejny komunikat:
1 2 3 |
$ dmesg | tail [...] [39001.126647] Goodbye World! |
Natomiast teraz po wykonaniu komendy lsmod naszego modułu nie powinno już być na liście.
Podsumowanie
Jak widać nie jest to chyba tak straszne jak mogłoby się wydawać 🙂 Oczywiście to dopiero początek i temat jeszcze będziemy rozwijać – spoilery odnośnie kolejnych najbliższych artykułów pojawiły się już wyżej 😉 Swoją drogą, jestem bardzo ciekawy czy taka tematyka Was interesuje i czy artykuł się spodobał. Może macie coś konkretnego o czym chcielibyście przeczytać w takiej lub podobnej tematyce? Dajcie znać w komentarzach!