Wstęp
W tej lekcji zapoznamy się z metodami synchronizacji. Ponieważ zajęliście się linuksowym kernelem to zakładam, że całkiem dobrze programujecie i wiecie czym jest mutex oraz spinlock i ogólnie na czym polega synchronizacja.
Implementacja
W tej lekcji zaimplementujemy aż 5 modułów, ale bardzo prostych modułów. Dwa jako przykład dla muteksa oraz dwa jako przykład dla spinlocka oraz jeden, który eksportuje potrzebne symbole. W obu przypadkach zasada działania będzie taka sama, moduł będzie blokować muteksa lub spinlocka w funkcji init co uniemożliwi załadowanie się drugiemu modułowi. Mutex lub spinlock jest odblokowywany w funkcji exit.
Eksportowanie symboli
Aby umożliwić dowolną kolejność ładowania modułów musimy wyeksportować muteksa oraz spinlocka w innym module, jego kod jest bardzo prosty i wygląda tak:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
#include <linux/spinlock.h>
DEFINE_MUTEX(my_mutex);
DEFINE_SPINLOCK(my_spinlock);
EXPORT_SYMBOL(my_mutex);
EXPORT_SYMBOL(my_spinlock);
static int __init exporter_init(void) {
return 0;
}
static void __exit exporter_exit(void) {
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
Jak widać kod jest bardzo prosty, jeśli zapoznałeś się z poprzednią lekcją to w zasadzie nie wymaga on większego komentarza. Jedyną nowością tutaj są dwa makra- DEFINE_MUTEX oraz DEFINE_SPINLOCK. Są to makra służące do inicjalizacji odpowiednio muteksa i spinlocka.
Mutex
Najpierw się zajmiemy muteksem. Jak zostało powiedziane wcześniej, zostaną utworzone dwa moduły, które będą blokować wzajemnie swoje załadowanie. Kod pierwszego modułu wygląda następująco:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
extern struct mutex my_mutex;
static int __init mutex_1_init(void)
{
mutex_lock(&my_mutex);
pr_info("MUTEX 1 init\n");
return 0;
}
static void __exit mutex_1_exit(void)
{
mutex_unlock(&my_mutex);
pr_info("MUTEX 1 exit\n");
}
module_init(mutex_1_init);
module_exit(mutex_1_exit);
MODULE_LICENSE("GPL");
Drugi kod jest niemalże identyczny jak pierwszy i wygląda tak:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/mutex.h>
extern struct mutex my_mutex;
static int __init mutex_2_init(void)
{
mutex_lock(&my_mutex);
pr_info("MUTEX 2 init\n");
return 0;
}
static void __exit mutex_2_exit(void)
{
mutex_unlock(&my_mutex);
pr_info("MUTEX 2 init\n");
}
module_init(mutex_2_init);
module_exit(mutex_2_exit);
MODULE_LICENSE("GPL");
W kodzie pojawiają się dwie nowe funkcje- mutex_lock oraz mutex_unlock. Służą one odpowiednio do zablokowania muteksa i jego zwolnienia. Obie przyjmują jeden parametr i jest nim wskaźnik do muteksa.
Spinlock
Spinlock- czy jak kto woli rygiel pętlowy(tak, takie jest polskie tłumaczenie). Implementacja będzie bliźniacza do przykładu z muteksem:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>
extern struct spinlock my_spinlock;
static int __init spinlock_1_init(void)
{
spin_lock(&my_spinlock);
pr_info("SPINLOCK 1 init\n");
return 0;
}
static void __exit spinlock_1_exit(void)
{
spin_unlock(&my_spinlock);
pr_info("SPINLOCK 1 exit\n");
}
module_init(spinlock_1_init);
module_exit(spinlock_1_exit);
MODULE_LICENSE("GPL");
Kod drugiego modułu jest analogiczny:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>
extern struct spinlock my_spinlock;
static int __init spinlock_2_init(void)
{
spin_lock(&my_spinlock);
pr_info("SPINLOCK 2 init\n");
return 0;
}
static void __exit spinlock_2_exit(void)
{
spin_unlock(&my_spinlock);
pr_info("SPINLOCK 2 exit\n");
}
module_init(spinlock_2_init);
module_exit(spinlock_2_exit);
MODULE_LICENSE("GPL");
W tym przypadku ponownie pojawiły się dwie nowe funkcje- spin_lock oraz spin_unlock, które to służą odpowiednio do zablokowania i odblokowania spinlocka. Obie przyjmują jeden parametr- wskaźnik do spinlocka.
Mutex a spinlock
Patrząc na kod użycie obu mechanizmów synchronizacji wygląda identycznie i może się pojawić pytanie- po co mnożyć byty ponad potrzebę? Skoro użycie wygląda właściwie identycznie i oba mechanizmy robią to samo to może się wydawać bez sensu.
Rzeczywiście oba mechanizmy robią to w zasadzie to samo- czyli blokują dostęp do jakiś sekcji kodu, ale robią to w nieco inny sposób. Mutex może być zastosowany w sytuacji kiedy dane zadanie może zostać uśpione, spinlock może zostać użyty w zasadzie każdej sytuacji(choć nie zawsze będzie to wskazane) ponieważ jego implementacja uniemożliwia uśpienie danego zadania, jest to tzw. aktywna synchronizacja. Spinlocków będziemy używać np. wewnątrz przerwań ponieważ przerwania nie mogą zostać uśpione.
Kompilacja
No to jeszcze szybki makefile:
obj-m += 12_symbols_exporter.o 12_mutex_1.o 12_mutex_2.o 12_spinlock_1.o 12_spinlock_2.o
all:
make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) modules
clean:
make -C /ścieżka/do/zbudowanego/kernela M=$(PWD) clean
Przebuduj moduł dla swojej płytki:
# BBB
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# RPi4
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- KERNEL=kernel8
Testowanie sterownika
Prześlij zbudowane moduły na swoją płytkę np. za pomocą scp.
Testowanie muteksa i spinlocka zacznimy od załadowania modułu eksportującego symbole:
sudo insmod 12_symbols_exporter.ko
W dalszej części najlepiej będzie jeśli otworzysz dwie sesje SSH, wtedy będzie lepiej widać co się dzieje.
Test zacznijmy od muteksa:
# pierwsza sesja ssh
sudo insmod 12_mutex_1.ko
# druga sesja ssh
sudo insmod 12_mutex_2.ko
Pierwszy moduł powinien się załadować, a drugi powinien wisieć. Możesz teraz odładować pierwszy moduł:
# pierwsza sesja ssh
sudo rmmod 12_mutex_1.ko
W momencie jego odładowania drugi moduł został załadowany. Teraz możesz ponownie załadować moduł 12_mutex_1.ko co objawi się ponownie tym, że komenda insmod będzie wisieć.
Teraz przetestujmy spinlocka w analogiczny sposób:
# pierwsza sesja ssh
sudo insmod 12_spinlock_1.ko
# druga sesja ssh
sudo insmod 12_spinlock_2.ko
Oczywiście ładowanie modułu 12_spinlock_2.ko wisi, ale co więcej, w pierwszej sesji również nie jesteś raczej w stanie nic zrobić. Wynika to właśnie z faktu, że spinlock zajął znaczącą część czasu pracy procesora co uniemożliwia pracę z urządzeniem. Po zresetowaniu twojego BBB lub RPi wszystko wróci do normy.