============================= Zajęcia 7: Urządzenia znakowe ============================= Data: 20.04.2021 .. contents:: .. toctree:: :hidden: zadanie Materiały dodatkowe =================== - :download:`example.tar` - :ref:`07-zadanie` O urządzeniach i sterownikach w Linuksie ======================================== Sterownikiem urządzenia nazywamy zbiór kodu obsługujący jakieś urządzenie (zazwyczaj sprzętowe, ale zdarzają się urządzenia wirtualne) i eksportujący zbiór funkcji pozwalający na wykorzystanie tego urządzenia przez użytkownika. Sterownik prawie zawsze ma wyłączny bezpośredni dostęp do urządzenia. Często jest odpowiedzialny za pogodzenie ze sobą potrzeb różnych użytkowników (choć równie często jest to obsługiwane przez wyższe warstwy -- w przypadku dysku twardego za taką warstwę można uznać system plików). Sterowniki urządzeń zazwyczaj są modułami jądra. Jeśli urządzenie nie jest zbyt "blisko" wpięte w procesor, da się również napisać sterownik w przestrzeni użytkownika (urządzenia USB się do tego świetnie nadają), ale nie będziemy się tym zajmować na zajęciach. Aby udostępnić funkcjonalność sterownika dla programów użytkownika, należy użyć jednego z wielu możliwych mechanizmów komunikacji z jądrem. Najczęściej spotykane to: - plik urządzenie blokowego -- używane do dysków twardych i dostatecznie podobnych tworów (dyskietka, CD-ROM, SSD, ...). Sterownik udostępnia funkcje zapisu i odczytu bloków, a warstwa blokowa zajmuje się obsługą żądań od użytkownika (buforowanie, kolejkowanie, itp). - plik urządzenia znakowego -- używany do większości rodzajów urządzeń. Sterownik po prostu dostarcza funkcje odpowiadające wywołaniom systemowym działającym na plikach -- jądro przekazuje takie wywołania prosto do sterownika, pozwalając na implementację dowolnego interfejsu. - interfejs sieciowy -- sterownik udostępnia funkcje wysłania i odebrania pakietu, do których wpina się podsystem sieci. Użytkownik może korzystać z niego przez wywołania ``socket``. - plik w ``proc`` -- używane w przypadku sterowników z trywialnym interfejsem (np. cała funkcjonalność sterownika to odczyt/zapis jednego parametru). - plik w ``sysfs`` -- jak wyżej, ale nowszy (i prostszy) interfejs. Reprezentacja plikowa --------------------- Użycie urządzeń znakowych i blokowych odbywa się przez stworzenie odpowiadającego im pliku specjalnego gdzieś w systemie plików (prawie zawsze ``/dev``) i otwarcie go. Taki plik specjalny jest tylko "wrotami do jądra" i jedyne informacje o nim przechowywane w systemie plików to: - flagę określająca typ urządzenia (``b`` -- blokowe, ``c`` -- znakowe) - numer główny (major device number) - numer drugorzędny (minor device number) Zazwyczaj numer główny wybiera sterownik urządzenia, a numer drugorzędny wybiera konkretny egzemplarz urządzenia obsługiwany przez ten sterownik. Zdarzają się jednak przypadki, że jeden numer główny jest współdzielony przez wiele sterowników, jeśli eksportują one tylko po jednym urządzeniu. Na poziomie języka C oba numery są pakowane w jedną liczbę typu ``dev_t``. Przydatne są następujące makra (``linux/kdev_t.h``): ``int MAJOR(dev_t numer)`` zwraca numer główny urządzenia. ``int MINOR(dev_t numer)`` zwraca numer drugorzędny. ``dev_t MKDEV(int major, int minor)`` Skleja numery w typ ``dev_t``. Do utworzenia pliku urządzenia ręcznie można użyć następującego polecenia:: mknod /dev/nazwa_pliku typ major minor Wpisując ``ls -l`` zobaczymy numery urządzenia tam, gdzie zazwyczaj znajduje się rozmiar pliku. W dawnych czasach, numery były przydzielane odgórnie i zsyłane przez programistów jądra na ziemię na kamiennych tablicach (``Documentation/devices.txt``). Dystrybucje Linuxa tworzyły na podstawie tych tablic gotowy katalog ``/dev`` z wszystkimi możliwymi urządzeniami (na zapas). Nie było to jednak najlepsze rozwiązanie. We współczesnych czasach, jądro eksportuje informacje o dostępnych sterownikach urządzeń na żywo w systemie plików ``sysfs``, a program ``udevd`` (lub ``systemd-udevd``) na bieżąco monitoruje te informacje i tworzy odpowiednie pliki urządzeń. Ponieważ nie ma potrzeby wcześniejszego ustalenia użytych numerów, są one alokowane dynamicznie. Aby ten mechanizm działał, sterownik musi zarejestrować swoje urządzenie w hierarchii ``sysfs``. Sterowniki urządzeń znakowych w Linuksie ======================================== Rejestracja sterowników urządzeń -------------------------------- Rejestracja sterownika odbywa się w kilku etapach: 1. Alokujemy zakres numerów urządzeń:: int alloc_chrdev_region(dev_t *first, unsigned int count, const char *name); void unregister_chrdev_region(dev_t first, unsigned int count); Tą alokację zazwyczaj wywołuje się tylko raz, w funkcji inicjalizacyjnej, alokując cały zakres numerów na zapas. 2. Przygotowujemy strukturę ``file_operations`` opisującę operacje na naszym urządzeniu. Takie struktury są zazwyczaj globalne (nie ma po co ich alokować dynamicznie). 3. Tworzymy i wypełniamy strukturę ``cdev``:: void cdev_init(struct cdev *cdev, const struct file_operations *fops); Możemy również poprosić o dynamiczne zaalokowanie struktury:: struct cdev *cdev_alloc(void); W tym wypadku należy ręcznie wypełnić pole ``ops`` wskaźnikiem na naszą strukturę (nie należy mieszać tego wywołania z ``cdev_init``). 4. Rejestrujemy naszą strukturę ``cdev``:: int cdev_add(struct cdev *p, dev_t dev, unsigned count); void cdev_del(struct cdev *p); W tym momencie nasze urządzenie staje się dostępne dla przestrzeni użytkownika, gdy tylko otworzy on odpowiedni plik specjalny. Możemy podpinać urządzenia pojedynczo, bądź ciągłymi zakresami (parametr ``count``). Jeśli struktura ``cdev`` była stworzona przez ``cdev_alloc``, struktura zostanie automatycznie zwolniona przez ``cdev_del``. Jeśli natomiast była inicjowana przez ``cdev_init``, zwolnienie jej jest sprawą sterownika. Należy zauważyć, że ``cdev_del`` tylko odpina urządzenie z tablicy urządzeń, ale nie gwarantuje, że nikt już go nie używa - wcześniej otwarte deskryptory plików dalej będą działać (choć jeśli jesteśmy w ``module_exit``, mamy gwarancję że takich deskryptorów nie ma). W przypadku implementacji np. urządzenia, które powinno obsługiwać hot-unplug trzeba samemu zapewnić np. zliczanie referencji. 5. Rejestrujemy klasę urządzeń w ``sysfs`` (bądź używamy istniejącej, jeśli pasuje):: struct class moja_klasa = { .name = "abc", .owner = THIS_MODULE, }; int class_register(struct class *class); void class_unregister(struct class *class); To wykonujemy tylko raz na wszystkie nasze urządzenia (bądź na typ urządzeń, jeśli mamy wiele). 6. Rejestrujemy nasze urządzenie w ``sysfs``:: struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...); void device_destroy(struct class *cls, dev_t devt); ``parent`` wskazuje na urządzenie, do którego nasze urzadzenie jest podłączone -- katalog w sysfs odpowiadający naszemu urządzeniu będzie podkatalogiem katalogu podanego urządzenia. W przypadku sterowników urządzeń znakowych odpowiadających np. urządzeniom PCI, parent będzie ustawiony na pole ``dev`` struktury ``pci_device``. Można ustawić ten parametr na ``NULL``, aby otrzymać urządzenie najwyższego poziomu. ``drvdata`` może służyć do przechowywania dodatkowych informacji prywatnych dla naszego sterownika (przydatne jeśli np. chcemy stworzyć pliki w sysfs do kontroli naszego urządzenia). ``fmt`` i dalsze parametry przekazywane są do ``sprintf`` w celu stworzenia nazwy urządzenia, która pojawi się w ``/dev``. W tym momencie, ``udevd`` dostanie powiadomienie o powstaniu nowego urządzenia i (w skończonym czasie) stworzy odpowiedni plik w ``/dev``. Struktura ``file_operations`` ----------------------------- Struktura ``file_operations`` (zdefiniowana w ``linux/fs.h``) opisuje jak wykonywać operacje na danym pliku. Każdy plik (i w ogólności wszystko, co może być otwartym deskryptorem) w Linuksie ma taką strukturę -- w przypadku zwykłych plików, jest dostarczana przez sterownik systemu plików. W przypadku urządzeń znakowych, dostarcza ją sterownik urządzenia. Ma bardzo wiele pól (odpowiadających operacjom), z których najważniejsze to:: struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); int (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); int (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*release) (struct inode *, struct file *); /* ... */ }; Pole ``owner`` musimy wypełnić wskaźnikiem ``THIS_MODULE`` -- umożliwia to jądru automatyczne zarządzanie licznikiem odwołań do modułu. Pola struktury ``file`` ----------------------- Struktura ``file`` (zdefiniowana w ``linux/fs.h``) reprezentuje w jądrze otwarty plik. Jest tworzona przez jądro w momencie wywołania ``open`` i przekazywana do wszystkich operacji na pliku, aż do ostatniego wywołania ``close`` (czyli momentu, kiedy wywołane zostanie ``release``). Warto zauważyć, że otwarty plik (struktura ``file``) to co innego, niż plik na dysku (reprezentowany przez strukturę ``inode``). Najważniejsze pola tej struktury to:: struct file { mode_t f_mode; loff_t f_pos; unsigned int f_flags; struct file_operations *f_op; void *private_data; /* ... */ }; Pole ``f_mode`` pozwala określić, czy plik jest otwarty do odczytu (``FMODE_READ``), zapisu (``FMODE_WRITE``) lub obu. Pola tego nie trzeba sprawdzać w funkcjach ``read`` i ``write``, bo jądro wykonuje taki test przed wywołaniem odpowiedniej funkcji sterownika. Pole ``f_pos`` określa pozycję do pisania lub odczytu (używana przez ``read``, ``write``, ``lseek`` itp). Flagi ``f_flags`` wykorzystuje się głównie do sprawdzenia, czy operacja ma być blokująca, czy nie (``O_NONBLOCK``), choć zawiera dużo więcej flag. Pole ``f_op`` określa zestaw funkcji implementujących operacje na pliku. Pole to jest ustawiane (na operacje ze struktury ``cdev``) przez jądro przy wywołaniu ``open``, a następnie jest używane do wszystkich następnych operacji (sterownik może podmienić wartość tego pola w ``open``, aby wybrać alternatywny zestaw funkcji). Wskaźnik ``private_data`` jest ustawiany na ``NULL`` przy otwieraniu pliku. Sterownik może wykorzystać ten wskaźnik dla własnych celów (wtedy jest odpowiedzialny za zwolnienie pamięci przydzielonej na rzecz tego pola). Operacja ``open`` -- otwarcie pliku ----------------------------------- Prototyp tej funkcji wyglada następująco:: int open(struct inode *inode, struct file *filp) Operacja ``open`` umożliwia sterownikowi przeprowadzenie czynności przygotowawczych przed innymi operacjami. Zazwyczaj wykonuje się nastepujące kroki: - sprawdzenie błędów związanych z urządzeniem (np. sprawdzenie, czy urządzenie jest gotowe); - inicjalizacja urządzenia jeślijest otwierane po raz pierwszy i stosujemy leniwą inicjalizację; - identyfikacja numeru drugorzędnego (``MINOR(inode->i_rdev)``) i, jeśli jest to konieczne, podmiana zestawu operacji wskazywanej przez ``f_op``; - przydzielenie pamięci na dane związane z urządzeniem, inicjalizacja struktur danych oraz przypisanie wskaźnika ``private_data``; Operacja ``release`` -- zamknięcie pliku ---------------------------------------- Jądro trzyma licznik referencji dla każdej istniejącej struktury ``file``. Może być zwiększony np. przez wywołanie ``dup`` bądź dziedziczenie otwartego pliku przez ``fork``. Gdy ten licznik w końcu spadnie do 0 (zostanie wywołany ``close`` na ostatnim deskryptorze, bądź proces trzymający ten deskryptor wywoła ``exit``), zostanie wywołana funkcja ``release``, służąca za destruktor pliku:: int release(struct inode *inode, struct file *filp) Jej zadaniem jest zwolnienie zasobów alokowanych w operacji ``open`` i wykonanie podobnych czynności sprzątających: - zwolnienie pamięci ``private_data``; - wyłączenie urządzenia, gdy jest to ostatnie wywołanie ``release``; Operacje ``read`` i ``write`` -- transfer danych ------------------------------------------------ Prototyp tej funkcji wyglada następująco:: ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp) ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp) Zadaniem operacji ``read`` jest przepisanie pewnej porcji danych z przestrzeni adresowej jądra pod wskazany adres (``buff``) w przestrzeni adresowej użytkownika. Operacja ``write`` działa w odwrotnym kierunku. Funkcje te są używane do implementacji wielu wywołań systemowych (``read``, ``pread``, ``readv``, ...). ``offp`` jest wskaźnikiem na bieżącą pozycję w pliku. Jeśli w przypadku naszego pliku taka pozycja ma sens, bierzemy ją z tego miejsca i wpisujemy tu zaktualizowaną wartośc pozycji. W przypadku zwykłego ``read`` i ``write`` będzie to wskaźnik na ``filp->f_pos``, a w przypadku ``pread`` i ``pwrite`` będzie to wskaźnik na jakąś zmienną na stosie zawierającą parametr wywołania systemowego. Wartość zwracana przez tę funkcję będzie interpretowana następująco: - wartość większa od zera oznacza liczbę przepisanych bajtów; jeśli jest równa wartości argumentu przekazanego do wywołania systemowego, to oznacza pełen sukces, jeśli zaś mniejsza, to oznacza, że tylko część danych została przekazana - należy się wtedy spodziewać, że program powtórzy wywołanie systemowe (takie jest np. standardowe zachowanie funkcji bibliotecznej ``fread``/``fwrite``) - jeśli wartość jest równa ``0`` to został osiągnięty koniec pliku (używane tylko w ``read``) - wartość ujemna oznacza błąd Implementując te operacje, należy pamiętać o zachowaniu właściwej semantyki -- zwrócenie błędu mówi, że żadne bajty nie zostały odczytane/zapisane. Jeśli nasz sterownik zauważy błąd dopiero po pewnej ilości przetransferowanych bajtów (i nie ma łatwego sposobu, by ten transfer cofnąć), należy zamiast błędu zwrócić liczbę przetransferowanych bajtów -- kod błędu będzie zwrócony gdy użytkownik ponowi operację dla pozostałych bajtów. Operacja ``llseek`` -- zmiana pozycji pliku ------------------------------------------- Prototyp tej funkcji wyglada następująco:: loff_t llseek(struct file *filp, loff_t off, int whence) Operacja ``llseek`` implementuje wywołania systemowe ``lseek`` i ``llseek``. Domyślnym działaniem jądra, gdy operacja ``llseek`` nie jest wyszczególniona w operacjach sterownika, jest zmiana pola ``f_pos`` struktury file. W przypadku, gdy pojęcie zmiany pozycji pliku nie ma sensu dla urządzenia, należy wpisać tu funkcję zwracającą błąd. W jądrze jest w tym celu dostępna gotowa funkcja ``no_llseek``, zawsze zwracająca ``-ESPIPE``. Operacja ``ioctl`` -- wywołanie komend specyficznych dla urządzenia ------------------------------------------------------------------- Prototypy funkcji wygladają następująco:: long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg); long (*compat_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg); Funkcja ``unlocked_ioctl`` odpowiada wywołaniom ``ioctl`` przez "główną" architekturę jądra. Nazwa jest zaszłością historyczną z czasów big kernel locka -- niegdyś sterowniki urządzeń wymagające big kernel locka wypełniały pole ``ioctl``, podczas gdy nowsze lub skonwertowane sterowniki używające własnych blokad używały ``unlocked_ioctl``. W obecnych wersjach jądra big kernel lock i pole ``ioctl`` już nie istnieją. Funkcja ``compat_ioctl`` odpowiada wywołaniom ``ioctl`` przez programy użytkownika w trybie zgodności z 32-bitową wersją architektury -- np. programy na architekturę i386 pod jądrem na architekturę x86_64. W przypadku, gdy struktury przekazywane przez ``ioctl`` nie zawierają pól o rozmiarze zależnym od architektury, można ustawić oba pola na tą samą funkcję. Pierwszy argument odpowiada deskryptorowi pliku przekazanemu przez wywołanie systemowe. Argument ``cmd`` jest dokładnie taki, jak w wywołaniu systemowym. Opcjonalny argument ``arg`` jest przekazywany w postaci liczby typu ``unsigned long`` bez względu na typ użyty przy wywołaniu systemowym. Zazwyczaj implementacja operacji ``ioctl`` zawiera po prostu konstrukcję switch wybierającą odpowiednie zachowanie w zależności od wartości argumentu ``cmd``. Różne komendy są reprezentowane różnymi numerami, którym zazwyczaj nadaje się nazwy korzystając z definicji preprocesora. Program użytkownika powinien mieć możliwość włączenia pliku nagłówkowego z deklaracjami (zazwyczaj tego samego, który jest używany przy kompilacji modułu sterownika). Do twórcy interfejsu sterownika należy ustalenie wartości liczbowych odpowiadających komendom interpretowanym przez sterownik. Najprostszy wybór, przypisujący kolejne małe wartości poszczególnym komendom, niestety ogólnie nie jest dobrym rozwiązaniem. Komendy powinny być unikalne w skali systemu, żeby uniknąć błędów, gdy poprawną komendę wysyłamy do niepoprawnego urządzenia. Taka sytuacja może nie występować zbyt często, ale jej konsekwencje mogą by poważne. Przy różnych komendach dla wszystkich ``ioctl``, w przypadku pomyłki, zostanie zwrócone ``-ENOTTY`` zamiast wykonania niezamierzonej akcji. W ustalaniu wartości liczbowych dla komend powinno się używać następujących makr (zdefiniowanych w ``linux/ioctl.h``): ``_IO(type, nr)`` komenda ogólnego przeznaczenia (bez argumentu) ``_IOR(type, nr, dataitem)`` komenda z zapisem w przestrzeni użytkownika ``_IOW(type, nr, dataitem)`` komenda z odczytem z przestrzeni użytkownika ``_IOWR(type, nr, dataitem)`` komenda z zapisem i odczytem Oznaczenia: ``type`` unikatowy numer dla sterownika (8 bitów, wybrany po przejrzeniu ``Documentation/ioctl-number.txt``) -- numer magiczny ``nr`` kolejny numer komendy (8 bitów) ``dataitem`` struktura związana z komendą; rozmiar podanej struktury zazwyczaj nie może byc większa niż 16kb-1 (zależy to od wartości ``_IOC_SIZEBITS``). Kodowanie rozmiaru struktury zapisywanej/odczytywanej jako parametr może się przydać do wyłapywania programów skompilowanych z nieaktualnymi wersjami sterownika i pozwala uniknąć np. pisania poza buforem. Przykład:: #define DN_SETCOUNT _IOR(0, 3, int) 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; }; Literatura ========== 1. A. Rubini, J. Corbet, G. Kroah-Hartman, Linux Device Drivers, 3rd edition, O'Reilly, 2005. (http://lwn.net/Kernel/LDD3/) 2. 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 Marcelina Kościelnicka (mwk@mimuw.edu.pl) Aktualizacja: 2013-03-25 Marcelina Kościelnicka (mwk@mimuw.edu.pl) =============================================================================