====================================== Zajęcia 8: Interfejsy wewnętrzne jądra ====================================== Data: 18.04.2019 .. contents:: .. toctree:: :hidden: zadanie p1_scull/index Materiały dodatkowe =================== - :ref:`07-p1-scull` - :ref:`07-zadanie` Wprowadzenie ============ Na poprzednich zajęciach przedstawione zostały sterowniki urządzeń znakowych ich reprezentacja plikowa i koncepcja tablic rozdzielczych sterowników. Opisano sposób rejestracji i wyrejestrowania sterowników oraz metody przydzielania numerów głównych, a także operacje na pliku reprezentującym urządzenie. Na niniejszych zajęciach omówimy funkcjonalności jądra przydatne przy pisaniu bardziej złożonych sterowników oraz przedstawimy przykład takiego sterownika. Wzajemne wykluczanie ==================== Synchronizacja w jądrze Linuksa wraz z jego rozwojem ciągle zyskuje na ważności. Warto uważniej przeanalizować związane z tym zagadnienia opisane tutaj oraz np. w rozdziale 5 [1]. Wszystkie z poniższych funkcji działają poprawnie zarówno na systemach jedno- jak i wieloprocesorowych. Wiele z nich jest zaimplementowane wewnętrznie w dwóch wersjach, zapewniających wyższą wydajność na systemach jednoprocesorowych - np. implementacja spinlocków sprowadza się do wyłączenia wywłaszczania jądra (lub zablokowania przerwań). Zwykłe blokady -------------- Przedstawione na poprzednich zajęciach. Semafory systemowe ------------------ Semafory w Linuksie służą do synchronizacji procesów i reprezentuje je struktura ``struct semaphore`` (zdefiniowana w ``asm/semaphore.h``). Semafory można deklarować używając makr np:: static DECLARE_SEMAPHORE_GENERIC(sem_ogolny, 11); Dostępne są między innymi następujące operacje:: void down(struct semaphore *sem) int down_interruptible(struct semaphore *sem) int down_trylock(struct semaphore *sem) Funkcje opuszczające semafor systemowy. Warianty analogiczne do zwykłych blokad. :: void up(struct semaphore *sem) Funkcja podnosi semafor systemowy. Linux udostępnia także dostępne semafory typu czytelnicy-pisarze typu ``struct rw_semaphore`` (``linux/rwsem.h``). Ćwiczenie: Proszę zapoznać się dostępnymi dla semaforów typu czytelnicy-pisarze funkcjami: (``linux/rwsem.h``). Blokady wirujące (spin locks) ----------------------------- Blokady wirujące mają podobne działanie do zwykłych blokad, lecz używają aktywnego oczekiwania zamiast blokowania procesu. Można ich przez to używać w miejscach, gdzie blokowanie procesu byłoby niedopuszczalne (przede wszystkim funkcje obsługi przerwań), lub gdy zwykłe blokady znacznie zmniejszałyby wydajność (tj. przy małej ilości operacji chronionych). Należy pamiętać, że NIE można wykonywać żadnych(!) operacji blokujących po zajęciu blokady wirującej (najważniejsze z funkcji, które nie współpracują z blokadami wirującymi to: ``copy_to/from_user``, ``kmalloc``, ``down``, ``sleep_on``, ``mutex_lock``). Przed ich wywołaniem należy zwolnić blokadę. Podstawowe blokady wirujące ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Blokady wirujące (zdefiniowane w ``asm/spinlock.h``) w ogólnym przpadku wykorzystuje się w nastepujący sposób:: spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED; unsigned long flags; spin_lock_irqsave(&xxx_lock, flags); /* ... sekcja krytyczna ... */ spin_unlock_irqrestore(&xxx_lock, flags); Powyższe wywołanie jest zawsze bezpieczne (bo blokuje przerwania na lokalnym procesorze, a następnie przywraca pierwotną obsługę przerwań). Istnieje również uproszczona wersja, której możemy użyć, gdy dana blokada NIGDY nie jest używana w funkcji obsługi przerwań:: spinlock_t xxx_lock = SPIN_LOCK_UNLOCKED; spin_lock(&xxx_lock); /* ... sekcja krytyczna ... */ spin_unlock(&xxx_lock); Blokady wirujące typu czytelnicy-pisarze (reader-writer spinlocks) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ten typ blokady wirującej (zdefiniowane w ``asm/spinlock.h``) umożliwia odczyt bez wyłączności i wyłączność na pełny dostęp do danych (np. zapis):: rwlock_t xxx_lock = RW_LOCK_UNLOCKED; unsigned long flags; read_lock_irqsave(&xxx_lock, flags); /* .. sekcja krytyczna czytająca ... */ read_unlock_irqrestore(&xxx_lock, flags); write_lock_irqsave(&xxx_lock, flags); /* .. sekcja krytyczna z wyłącznym dostępem ... */ write_unlock_irqrestore(&xxx_lock, flags); Sprawdza się przy skomplikowanych strukturach danych, gdy większość operacji polega na przechodzeniu (odczycie) struktury. Tylko faktyczna zmiana (zapis) wymaga wyłączności. Istnieje również uproszczona wersja (bez blokowaniem przerwań) blokad wirujących typu czytelnicy-pisarze do wykorzystania w sytacjach, gdy użycie tej samej blokady nie może pojawić się w programie obsługi przerwania:: rwlock_t xxx_lock = RW_LOCK_UNLOCKED; read_lock(&xxx_lock); /* .. sekcja krytyczna czytająca ... */ read_unlock(&xxx_lock); write_lock(&xxx_lock); /* .. sekcja krytyczna z wyłącznym dostępem ... */ write_unlock(&xxx_lock); Operacje atomowe ---------------- Istnieją typy ``atomic_t`` i ``atomic64_t``. Dla każdego z tych typów zdefiniowane są niepodzielne operacje pozwalające na: - zainicjowanie, np: ``atomic_set(&zmienna,wartosc);`` - zmianę, np: ``atomic_add(&zmienna, 12);`` - jednoczesną zmianę i testowanie czy nowa wartość zmiennej jest zerem, np: ``atomic64_sub_and_test(1L, &zmienna64);`` Po szczegóły odsyłam do pliku ``asm/atomic.h``. Istnieją także bitowe operacje atomowe ``asm/bitops.h``. Debugowanie blokad ------------------ Jądro posiada dość zaawansowany wbudowany kod sprawdzający poprawność blokowania, nazywany lockdep. Pozwala on wykryć potencjalne zakleszczenia przez analizę sekwencji blokad zajmowanych przez pojedyncze wątki jądra. Zakleszczenie nie musi wystąpić, aby zostać wykryte - wystarczy, że każda wymagana do zakleszczenia sekwencja została kiedykolwiek użyta przez jakiś wątek. Wykrywane klasy błędów to proste błędy w użyciu blokad (np. niezainicjowane blokady, rekurencyjna blokada), zakleszczenia powodowane przez cykliczne zależności blokad, oraz zakleszczenia powodowane przez nieprawidłowe użycie blokad wewnątrz obsługi przerwań. Debugowanie blokad należy jawnie włączyć w konfiguracji jądra, gdyż spowalnia ono działanie systemu. Operacje blokujące ================== Kolejki oczekiwania ------------------- Proces, który musi oczekiwać na zajście pewnego zdarzenia (np. zakończenie operacji we/wy, czy pojawienie się danych w kolejce FIFO), usuwany jest z kolejki procesów gotowych i umieszczany jest w kolejce oczekiwania (waitqueues) - stan procesu zmienia się z ``TASK_RUNNING`` na ``TASK_INTERRUPTIBLE`` bądź ``TASK_UNINTERRUPTIBLE``. Strukturą reprezentującą głowę listy, w której umieszcza się takie procesy, jest ``wait_queue_head_t``, zdefiniowana w pliku ``linux/wait.h``. Wybrane operacje: ``DECLARE_WAIT_QUEUE_HEAD(name)`` przed użyciem ``wait_queue`` trzeba ją zainicjować ``wait_event(name, cond)`` umieszczenie procesu w kolejce oczekiwania w stanie odpornym na sygnały (``TASK_UNINTERRUPTIBLE``) i spanie, aż cond będzie zachodzić ``wait_event_interruptible(name, cond)`` j.w., ale przejście w stan ``TASK_INTERRUPTIBLE`` (nadejście sygnału spowoduje przejście w stan ``TASK_RUNNING``), zwraca 0 w przypadku zajścia ``cond``, ``-ERESTARTSYS`` w przypadku przerwania sygnałem ``wait_event_timeout(name,cond,timeout)`` jak ``wait_event``, ale z budzeniem po upływie określonego czasu nawet jeśli ``cond`` nie zaszło ``wait_event_interruptible_timeout(name,cond,timeout)`` j.w., ale przejście w stan ``TASK_INTERRUPTIBLE`` ``wake_up(name)`` budzi wszystkie czekające procesy bez ustawionej flagi ``WQ_FLAG_EXCLUSIVE`` oraz jeden, który ma ustawioną. Uwaga: funkcja nie powoduje usunięcia procesu z kolejki oczekiwania - proces "usunie" się sam, po wznowieniu działania ``wake_up_interruptible(name)`` j.w., ale tylko dla procesów w stanie ``TASK_INTERRUPTIBLE`` ``wake_up_all(name)`` wstawienie wszystkich procesów (będących w stanie ``TASK_INTERRUPTIBLE`` bądź ``TASK_UNINTERRUPTIBLE``) znajdujących się w kolejce oczekiwania do kolejki procesów gotowych Powyższe operacje zdefiniowane są w ``linux/sched.h`` (oprócz pierwszej, zdefiniowanej w ``linux/wait.h``). Używając kolejek oczekiwania należy zwrócić uwagę na uniknięcie wyścigów. Jeżeli np. jesteśmy czytelnikiem i chcemy poczekać aż pisarz zapisze jakiekolwiek dane do bufora zabezpieczonego blokadą, kod oczekujący może wyglądać tak:: /* definicje */ DEFINE_MUTEX(blokada); DECLARE_WAIT_QUEUE_HEAD(kolejka); int poz_odczyt, poz_zapis; char *bufor; /* wczytanie bajtu */ char znak; /* blokujemy */ mutex_lock(&blokada); /* sprawdzamy, czy już mamy dane do przeczytania */ while (poz_odczyt == poz_zapis) { /* nie mamy - musimy zdjąć blokadę, aby pisarz mógł cokolwiek * zapisać */ mutex_unlock(&blokada); /* czekamy na zapis - warunek zapewnia, że nie będziemy czekać, jeśli pisarz uaktualnił poz_zapis między zdjęciem przez nas blokady, a dodaniem nas do kolejki - w przeciwnym przypadku wake_up pisarza nie objęło by naszego procesu i czekalibyśmy znacznie dłużej (być może na zawsze). Warunek jest sprawdzany po dodaniu do kolejki oczekiwania, ale przed samym oczekiwaniem. */ wait_event(kolejka, poz_odczyt != poz_zapis); /* wait_event zapewnia tylko, że w pewnym momencie od wywołania zaszło poz_odczyt != poz_zapis, lecz jakiś inny wątek mógł w międzyczasie opróżnić bufor - nie możemy niczego zakładać póki nie sprawdzimy tego warunku trzymając blokadę - zakładamy więc blokadę i sprawdzamy jeszcze raz warunek while, do skutku */ mutex_lock(&blokada); } /* udało się - możemy odczytać znak */ znak = bufor[poz_odczyt++]; mutex_unlock(&blokada); /* zapis bajtu */ mutex_lock(&blokada); /* musimy zapewnić spełnienie warunku, na który czekają czytelnicy * PRZED obudzeniem czytelników */ bufor[poz_zapis++] = znak; wake_up(kolejka); mutex_unlock(&blokada); Czekanie na zakończenie - wait for completion --------------------------------------------- Jest to atomowa operacja czekania na zakończenie wykonywania pewnej operacji. Powstała (zupełnie niedawno) w związku z tym, że użycie do tego semaforów powodowało race conditions. Chodziło o sytuacje typu: proces czekający dysponuje semaforem zainicjalizowanym na 0 i w pewnym momencie wykonuje na nim operację ``down()``, która blokuje go, aż inny proces, gdy nadejdzie odpowiednia pora, wykona ``up()`` i go odblokuje. Oto typowe wykorzystanie mechanizmu "czekania na zakończenie":: struct completion event; init_completion(&event); /* .. przekaż wskaźnik do event temu, kto ma budzić .. */ wait_for_completion(&event); Ten budzący wywołuje:: complete(&event) gdy nadejdzie odpowiednia pora. Implementacja jest w pliku ``kernel/sched.c``, zaś sama struktura jest zadeklarowana w ``linux/completion.h``. sysfs ===== W jądrze często pojawia się konieczność udzielenia dostępu do pewnych danych o urządzeniu do przestrzeni użytkownika. Użycie do tego urządzeń znakowych jest dość nieporęczne - urządzenie znakowe to dość "ciężki" obiekt, a dostęp do niego odbywa się przez ograniczony interfejs ``read/write`` bądź niewygodny interfejs ``ioctl``. Pierwszym rozwiązaniem tych problemów w systemie Linux był system plików ``proc``, pozwalający łatwo stworzyć dużą liczbę plików specjalnych do komunikacji z użytkownikiem. Miał on jednak sporo wad: przede wszystkim brak w nim struktury (każdy wrzuca pliki gdzie mu się spodoba), a przesył danyh wymaga kosztownego i delikatnego formatowania i parsowania strumienia bajtów. Aby rozwiązać problemy z systemem plików ``proc``, powstał system plików ``sysfs``. Ma on następujące cechy: - każde urządzenie, sterownik, moduł itp. w systemie zrobione jest na bazie struktury ``kobject`` i automatycznie otrzymuje katalog w ``sysfs`` - katalogi w ``sysfs`` zorganizowane są hierarchicznie - w przypadku urządzeń, każde urządzenie jest podkatalogiem urządzenia, do którego jest podłączone - relacje między obiektami reprezentowane są przez symlinki - atrybuty obiektów reprezentowane są przez pliki - jest standardowo zamontowany w ``/sys`` Aby udostępnić użytkownikowi dostęp do jakiejś funkcjonalności, należy dostać się do swojej struktury ``kobject`` i podpiąć do niej atrybuty. W przypadku urządzeń, struktura ``kobject`` jest polem ``kobj`` struktury ``device``. Dodawanie atrybutów do urządzeń ------------------------------- Aby dodać atrybut (czyli plik reprezentujący jeden parametr) do obiektu, należy stworzyć strukturę opakowującą strukturę ``attribute`` odpowiednią dla danego typu obiektu. W przypadku urządzeń jest to ``device_attribute``:: struct attribute { char *name; struct module *owner; mode_t mode; }; struct device_attribute { struct attribute attr; ssize_t (∗show) (struct device ∗dev, char ∗buf); ssize_t (∗store) (struct device ∗dev, const char ∗buf, size_t count); }; I dodać ją do naszego urządzenia przez:: int device_create_file(struct device ∗device, struct device_attribute ∗entry); void device_remove_file(struct device ∗dev, struct device_attribute ∗attr); Funkcja ``show`` jest wywoływana przy odczycie atrybutu przez użytkownika i ma do dyspozycji bufor o rozmiarze ``PAGE_SIZE``, do którego może zapisać wartość atrybutu (i zwrócić jego rozmiar). Funkcja ``store`` jest wywoływana przy zapisie atrybutu i otrzymuje jego kompletną wartość. W przeciwieństwie do funkcji ``read``/``write``, funkcje te zawsze operują na całej wartości atrybutu (nie trzeba obsługiwać częściowych odczytów/zapisów). Atrybuty binarne ---------------- Czasami proste atrybuty nie wystarczają i konieczny jest eksport danych binarnych, definiując własną implementację ``read``/``write`` jak przy zwykłym pliku. Do tego służą atrybuty binarne:: struct bin_attribute { struct attribute attr; size_t size; void ∗private; ssize_t (∗read) (struct kobject ∗, char ∗buf, loff_t off, size_t size); ssize_t (∗write) (struct kobject ∗, char ∗buf, loff_t off, size_t size); int (∗mmap) (struct kobject ∗, struct bin_attribute ∗attr, struct vm_area_struct ∗vma); }; int sysfs_create_bin_file(struct kobject ∗kobj, struct bin_attribute ∗attr); int sysfs_remove_bin_file(struct kobject ∗kobj, struct bin_attribute ∗attr); Katalogi -------- W przypadku bardziej skomplikowanych urządzeń, może przydać się możliwość stworzenia drzewiastej struktury obiektów. W takim wypadku konieczne jest utworzenie własnego typu obiektów:: struct kobj_type { void (*release)(struct kobject *kobj); const struct sysfs_ops *sysfs_ops; struct attribute **default_attrs; }; struct sysfs_ops { ssize_t (*show)(struct kobject *, struct attribute *, char *); ssize_t (*store)(struct kobject *, struct attribute *, const char *, size_t); }; Funkcja ``release`` zostanie wywołana przez jądro, gdy znikną wszystkie referencje do danego obiektu. ``sysfs_ops`` zawiera funkcje obsługujące atrybuty - np. w przypadku ``struct device`` są to po prostu funkcje rzutujące ``kobject`` na ``device``, ``attribute`` na ``device_attribute`` i wywołujące ``show``/``store`` z danego atrybutu. ``default_attrs`` jest wskaźnikiem na tablicę wskaźników do atrybytów (zakończoną ``NULL``-em), które zostaną dodane do obiektów danego typu przy tworzeniu. Aby stworzyć nowy obiekt, używa się funkcji:: int kobject_init_and_add( struct kobject *kobj, struct kobj_type *ktype, struct kobject *parent, const char *fmt, ...); Przed wywołaniem jej, należy zainicjować kobj zerami. Aby go usunąć (a tak naprawdę pozbyć się swojego odwołania - obiekt będzie usnięty, gdy wszystkie odwołania znikną), należy użyć:: void kobject_put(struct kobject *kobj); Możemy również zduplikować swoje odwołanie przez:: struct kobject *kobject_get(struct kobject *kobj); By dodać atrybuty do takiego pliku po jego utworzeniu:: int sysfs_create_file(struct kobject *kobj, const struct attribute *attr); void sysfs_remove_file(struct kobject *kobj, const struct attribute *attr); Relacje między obiektami ------------------------ Tworzenie symlinków w sysfs możliwe jest przez funkcje:: int sysfs_create_link(struct kobject ∗kobj, struct kobject ∗target, char ∗name); void sysfs_remove_link(struct kobject ∗kobj, char ∗name); Przydatne makra --------------- Do tworzenia atrybutów można, zamiast ręcznie tworzyć struktury ``*_attribute``, użyć makr które uproszczą zapis:: static DEVICE_ATTR(foo, S_IWUSR | S_IRUGO, show_foo, store_foo); Będzie to to samo, co ręczna deklaracja:: static struct device_attribute dev_attr_foo = { .attr = { .name = "foo", .mode = S_IWUSR | S_IRUGO, }, .show = show_foo, .store = store_foo, }; Idąc tą drogą można pójść jeszcze dalej:: static DEVICE_ATTR_RW(foo_rw); static DEVICE_ATTR_RO(foo_ro); static DEVICE_ATTR_WO(foo_wo); Powyższe makra zakładają że funkcje nazywają się odpowiednio ``foo_rw_show``, ``foo_rw_store``, ``foo_ro_show``, ``foo_wo_store``. Do prostych typów mamy też wersje już z zaimplementowanymi funkcjami ``_show`` i ``_store`` które operują na wskazanej zmiennej:: static ulong var_ulong; static int var_int; static bool var_bool; static DEVICE_ULONG_ATTR(foo_ulong, mode, var_ulong); static DEVICE_INT_ATTR(foo_int, mode, var_int); static DEVICE_BOOL_ATTR(foo_bool, mode, var_bool); Do przekazania adresu zmiennej używają one dodatkowej struktury opakowującej ``device_attribute``:: struct dev_ext_attribute { struct device_attribute attr; void *var; }; Inne przydatne funkcje kernela ============================== Biblioteka jądra zawiera wiele innych gotowych funkcji, które mogą się okazać przydatne przy pisaniu najróżniejszych sterowników. Z przydatniejszych można wymienić: ``linux/idr.h`` mapa ``int`` w ``void *`` z dynamiczną alokacją identyfikatorów ``linux/kref.h`` łatwe zliczanie referencji ``linux/bitmap.h`` efektywne tablice bitów ``linux/btree.h`` B-drzewa ``linux/bug.h``, ``asm-generic/bug.h`` raportowanie krytycznych błędów w kodzie sterownika wynikających z wad kodu oraz ostrzeżeń (coś w rodzaju ``assert``) ``linux/circ_buf.h`` bufory cykliczne ``linux/hash.h`` Proste funkcje hashujące ``linux/kernel.h`` Różne proste funkcje: ``ALIGN(x,a)`` wyrównuje ``x`` w dół do wielokrotności ``a`` (``a`` musi być potęgą dwójki) ``PTR_ALIGN(p, a)`` jak wyżej, ale na wskaźnikach ``IS_ALIGNED(x, a)`` sprawdza, czy ``x`` już jest wyrównane ``ARRAY_SIZE(arr)`` rozmiar tablicy ``arr`` ``DIV_ROUND_UP(n,d)`` ``n/d``, zaokrąglając w górę ``roundup(x, y)`` zakrągla ``x`` w górę do wielokrotności ``y`` ``upper_32_bits(x), lower_32_bits(x)`` jak w nazwie ``might_sleep()`` oznacza miejsce, w którym kod może spać, pomaga w debugowaniu (rzuca błąd jeśli debugownie spinlocków jest włączone, a trzymany jest spinlock) ``min(x,y), max(x,y)`` jak w nazwie ``clamp(val, min, max)`` ``val`` przycięte do zakresu [``min``, ``max``] ``linux/kobject.h`` ogólny typ obiektowy ze zliczaniem referencji i widocznością w sysfs (na ich podstawie jest zrobiony m.in. ``cdev`` oraz ``device``) ``linux/parser.h`` prosty parser do opcji ``linux/rbtree.h`` drzewa czerwono-czarne Literatura ---------- 1. A. Rubini, J. Corbet, G. Kroah-Hartman, Linux Device Drivers, 3rd edition, O'Reilly, 2005. (http://lwn.net/Kernel/LDD3/) 2. http://webpages.charter.net/decibelshelp/LinuxHelp_UDEVPrimer.html 3. Książki podane na stronie przedmiotu: http://students.mimuw.edu.pl/ZSO/ .. ============================================================================== Autor: Grzegorz Marczyński (g.marczynski@mimuw.edu.pl) Aktualizacja: 2003-03-13 Aktualizacja: 2004-10-20 Stanisław Paśko (sp@mimuw.edu.pl) Aktualizacja: 2005-10-22 Piotr Malinowski (malinex@mimuw.edu.pl) Aktualizacja: 2006-11-16 Radek Bartosiak (kedar@mimuw.edu.pl) linux 2.6 Aktualizacja: 2012-03-18 Marcin Kościelnicki (m.koscielnicki@mimuw.edu.pl) Aktualizacja: 2013-04-02 Marcin Kościelnicki (m.koscielnicki@mimuw.edu.pl) ==============================================================================