PODSYSTEM WEJŚCIA WYJŚCIA
Spis treści
1 Wstęp
-
Podsystem IO to zespół mechanizmów służących do komunikacji systemu ze
światem zewnętrznym.
-
Urządzenia są widziane jednolicie - jako pilki specjalne.
-
Sterownik - program tłumaczący polecenia systemu na sygnały zrozumiałe
dla sprzętu.
-
Funkcja ochronna sterownika - na sprzęcie dozwolone są tylko ściśle wyspecyfikowane
operacje.
Spis treści
2 Pojęcia podstawowe
2.1 Numery główne i drugorzędne.
-
Przyjęta w Linuksie polityka identyfikacji urządzeń - para numerów + typ
urządzenia.
-
Numer nadrzędny (ang. major) - określa rodzaj urządzenia; każdemu
numerowi nadrzędnemu przydzielony jest jeden sterownik; oficjalna lista
przydzielonych numerów w Documentation/devices.txt.
-
Numer podrzędny (ang. minor) - numer urządzenia w obrębie jednego
rodzaju.
-
Obsługa poprzez makra, zdefiniowane w include/linux/kdev_t.h
Spis treści
2.2 Pliki specjalne.
-
Podział urządzeń na znakowe i blokowe - to jest właśnie typ urządzenia,
trzeci parametr przy identyfikacji.
-
Zasadnicze różnice między nimi:
-
do znakowych mamy dostęp sekwencyjny, konieczność zastosowania bufora;
intuicja - nieskończony strumień; przykład - karty dźwiękowe;
-
do blokowych mamy dostęp swobodny, można jednak używać mechanizmu
cache,
aby zwiększyć wydajność; intuicja - skończony obszar podzielony na bloki;
przykład - systemy plików;
-
Dlaczego pliki - standaryzacja obsługi urządzenia, stosunkowo duża przenośność
pomiędzy Uniksami, system plików jako pośrednik, zajmujący się ochroną
na najwyższym poziomie.
-
/dev - standardowy katalog dla plików urządzeń.
-
Informacje, które możemy uzyskać nt. pliku specjalnego przy pomocy polecenia
ls
- numer nadrzędny i podrzędny, rodzaj ( c - znakowe, b - blokowe); informacje
te pochodzą z i-węzła.
-
Operacje na plikach specjalnych - analogiczne do tych na zwykłych plikach,
jednak ich obsługa jest bardziej skomplikowana - o tym później.
Spis treści
2.3 Tworzenie
i usuwanie urządzeń.
-
Polecenie mknod - tworzenie.
-
Składnia: mknod /dev/nazwa [b|c] nr_nadrzędny nr_podrzędny
-
Znaczenie poszczególnych parametrów.
-
Polecenie rm - usuwanie, tak samo jak zwykłe pliki.
-
Oprócz stworzenia pliku urządzenia musimy także załadować odpowiedni program
obsługi do jądra.
Spis treści
2.4 Porty wejścia/wyjścia.
-
Porty - niskopoziomowy interfejs komunikacji systemu operacyjnego z urządzeniami.
-
Architektura i386 - 16-bitowa przestrzeń adresowa dla urządzeń,
co daje 65536 portów.
-
Istnieje mechanizm rejestracji używanych portów w celu uniknięcia konfliktów
- kernel/resource.c oraz
include/ioports.h.
-
Obsługa portów:
-
int check_region(unsigned long from, unsigned long extent);
- sprawdzenie, czy obszar portów o długości extent, począwszy
od from jest używany.
-
void request_region(unsigned long from, unsigned long extent,const
char *name);
- zarezerwowanie podanego obszaru pod nazwą name.
-
void release_region(unsigned long from, unsigned long extent);
- zwolnienie podanego obszaru.
-
int get_ioport_list(char *);
- pobranie listy zarezerwowanych portów w postaci tekstu.
-
/proc/ioports - zawiera listę aktualnie wykorzystanych portów.
-
Używanie portów:
-
void outb(unsigned char byte, unsigned port);
- odczyt i zapis bajtu z/do podanego portu
-
void outw(unsigned short word, unsigned port);
- j.w., dla słowa 16-bitowego
-
void outl(unsigned doubleword, unsigned port);
- j.w., dla słowa 32-bitowego
-
Istnieją też odpowiedniki powyższych funkcji, operujące na większych ilościach
danych.
Spis treści
2.5 Tablice rozdzielcze urządzeń
znakowych i blokowych.
-
Tablice rozdzielcze - struktury przechowujące adresy funkcji implementujących
poszczególne operacje, realizowane przez sterownik danego urządzenia; istnieją
dwie osobne tablice:
-
struct device_struct chrdevs[MAX_CHRDEV] - dla urządzeń znakowych
-
struct device_struct blkdevs[MAX_BLKDEV] - dla urządzeń blokowych
-
Każda komórka tablicy to struktura, odpowiadająca sterownikowi urządzeń
o numerze nadrzędnym równym indeksowi.
struct device_struct {
const char * name;
struct file_operations * fops;
};
W strukturze file_operations przechowywane są adresy odpowiednich
funkcji, dokładny opis za chwilę.
Standardowo oba pola struktury są inicjalizowane NULLami; deklaracje struktur
- fs/devices.c.
Uwaga do urządzeń blokowych: przy operacjach odczytu/zapisu korzystamy
z trzeciej tablicy, struct blk_dev_struct blk_dev[MAX_BLKDEV],
zawierającej adresy tzw. funkcji strategii, kolejkującej żądania. Dokładna
specyfikacja w include/linux/blkdev.h, deklaracja w devices/block/ll_rw_blk.c.
Spis treści
2.6 Inicjowanie urządzeń
w jądrze.
-
Rejestracji zestawu funkcji:
int register_chrdev(unsigned int major,
const char *name,
struct file_operations *fops);
dla urządzeń blokowych:
int register_blkdev(unsigned int major,
const char *name,
struct file_operations *fops);
Rejestracja może się nie udać, jeśli dany numer nadrzędny jest już zajęty
i zestaw funkcji dla niego zarejestrowanych różni się od nowego.
Istnieje możliwość zażądania przydziału pierwszego wolnego numeru nadrzędnego
- wtedy major = 0. Przydatne, gdy piszemy sterownik dla własnego
urządzenia, które nigdy nie dostanie oficjalnego numeru nadrzędnego.
Dla urządzeń blokowych dodatkowo instalujemy procedury strategii, poprzez
jawne wpisanie ich adresu do tablicy blk_dev[], w pole request_fn.
Spis treści
2.7 Podprogramy obsługi
urządzeń.
-
Struktura file_operations (podkreślone zostały zmiany w
stosunku do 2.0):
struct file_operations {
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *);
int (*fasync) (int, struct file *, int);
int (*check_media_change) (kdev_t dev);
int (*revalidate) (kdev_t dev);
int (*lock) (struct file *, int, struct file_lock *);
};
Ta sama struktura opisuje operacje na zwykłych plikach, stąd pewna nadmiarowość,
np. funkcja readdir() nie ma zastosowania w przypadku plików urządzeń,
kilka innych funkcji (np. fasync()) spotyka się bardzo rzadko.
Spis treści
2.8 Obsługa funkcji systemowych.
-
open()
-
Ustaw file_operations na domyślną dla danego rodzaju pliku, korzystając
z informacji zawartej w i-węźle.
-
Wywołaj funkcję open() z ustawionej właśnie struktury; dla plików
specjalnych w tym momencie nastąpi podmiana
file_operations na
zawarte w sterowniku urządzenia i wywołanie open() sterownika.
-
close()
-
Wywołanie funkcji flush() z file_operations, o ile istnieje.
-
Usunięcie ewentualnych blokad (posix_locks).
-
Zmniejszenie licznika odwołań w strukturze file.
-
O ile licznik ten został wyzerowany, wywołanie release() z file_operations.
-
ioctl()
-
Jeśli wywoływana operacja jest standardowo obsługiwana przez system (FIOCLEX,
FIONCLEX, FIONBIO) - obsługa standardowa, w p.p.
-
Jeśli wywoływana operacja to FIOASYNC - wywołanie funkcji fasync()
z file_operations, o ile flaga FASYNC właśnie jest zmieniana,
w p.p.
-
Wywołanie funkcji ioctl() z file_operations, o ile istnieje.
-
read() / write()
-
Wywołanie odpowiedników z file_operations, o ile istnieją.
-
llseek()
-
Jeśli istnieje odpowiednik w file_operations - wywołanie go, w
p.p. wywołanie systemowego default_llseek (nie obsługuje przesunięć
względem końca pliku).
Spis treści
3 Urządzenia blokowe
3.1 Wprowadzenie
3.1.1 Charakterystyczne cechy urządzeń blokowych
-
urządzenia blokowe to urządzenia o dostępie bezpośrednim
-
urządzenia te są podzielone na bloki określonej wielkości
-
fizycznie zapisywane lub odczytywane są tylko całe bloki
3.1.2 Urządzenia blokowe w jądrze
-
tablica blk_devs przechowuje nazwę i zbiór
operacji dla każdego urządzenia blokowego
-
tablica blk_dev zawiera wskaźnik na funkcję
obsługi żądania oraz wskaźnik na pierwsze żądanie
-
tablica blk_sizezawiera
wielkość urządzeń w blokach
-
tablica blksize_sizeopisuje
wielkość bloku urządzenia
Spis treści
3.2 Funkcje block_read, block_write
-
block_read i block_write są wspólne dla wszystkich urządzeń
-
zapis i odczyt jest buforowany
Spis treści
3.3 Buforowanie
spis treści
3.4 Szeregowanie zadań
3.4.1 Struktury danych
W pliku include/linux/blkdev.h zdefiniowano strukturę request.
Opisuje ona każde żądanie odczytu i zapisu z urządzenia blokowego
struct request {
volatile int rq_status;
#define RQ_INACTIVE (-1)
#define RQ_ACTIVE 1
#define RQ_SCSI_BUSY 0xffff
#define RQ_SCSI_DONE 0xfffe
#define RQ_SCSI_DISCONNECTING 0xffe0
kdev_t rq_dev;
/*do tego urządzenia skierowane jest żądanie*/
int cmd;
/* komenda, może przyjmować wartości READ or WRITE */
int errors;
/* zlicza błędy powstałe przy komunikacji z urządzeniem */
unsigned long sector;
/* pierwszy sektor, którego dotyczy żądanie */
unsigned long nr_sectors;
/*liczba
sektorów (rozmiar) bieżącego żądanie. Gdy piszemy sterownik, powinien on
sie odwoływać do zmiennej current_nr_sectors
ignorować nr_sectors (została ona podana tylko dla zachowania pełności
opisu).*/
unsigned long nr_segments;
unsigned long current_nr_sectors;
char * buffer;
/* bufor do zapisu lub odczytu */
struct semaphore * sem;
/* semafor używany przy korzystaniu z urządzeń SCSI oraz pliku wymiany*/
struct buffer_head * bh;
/* struktura opisująca pierwszy bufor na liście danego żądania */
struct buffer_head * bhtail;
struct request * next;
};
3.4.2 Funkcje
-
Wszystkie żądania do danego urządzenia są trzymane w tablicy
all_requests (Jej rozmiar dany jest stałą NR_REQUEST,
zdefiniowaną w pliku blk.h. W wersji jądra 2.2.12 stała przyjmuje
wartość 128)
-
W tablicy all_requests żądania zapisu mogą zajmować
co najwyżej 2/3. Jeżeli dla danego żądania nie będzie miejsca w all_requests
to proces zostanie powieszony na kolejce wait_for_request.
-
Funkcja ll_rw_block, zdefiniowana w /drivers/block/ll_rw_block.c
służy do spełnienia żądań skierowanych do urządzeń blokowych. Powoduje
wstawienie żądania do tablicy all_requests oraz do odpowiedniej
kolejki żądań. Żądania są zgrupowane w kolejki według numeru głównego urządzenia,
którego dotyczą. Lista ta jest posortowana według drugorzędnego numeru
urządzenia oraz pierwszego sektora, którego dotyczy żądanie.
-
Funkcja ll_rw_block wywołuje dla każdego żądania funkcję make_request.
Funkcja ta zajmuje się utworzeniem nowego zadania i wstawieniem go do tablicy
all_requests.
-
Funkcja end_request zarówno porządkuje bieżące żądanie jak
i przygotowuje obsługę następnego. Może też przygotowywać się do obsłużenia
następnego bufora w danym żądaniu. Dzięki temu grupowanie jest przezroczyste
dla sterownika urządzenia.
Spis treści
3.5 Sterowniki dysku twardego ide-disk v. 1.08
3.5.1 Garść informacji na temat sterownika
ide.c-disk
v.1.08
-
obsługuje do czterech portów IDE,
-
może zarządzać dowolnym zestawem maksymalnie ośmiu urządzeń (dwa na każdym
porcie),
-
automatycznie wykrywa porty, napędy, geometrie dysków,
-
może współistnieć ze "starym" sterownikiem - hd.c, wtedy ten drugi
obsługuje pierwszy port IDE (nowsze dyski, z którymi nie radzi sobie hd.c,
mogą być podłączone do pozostałych portów i obsługiwane przez ide.c),
-
ide.c może być ładowany jako moduł (to samo dotyczy jego "podsterownikow"
- ide-cd.c, ide-floppy.c, ide-disk.c),
-
zakłada, że urządzenie podłączone do portu ide0 ma numer główny 3, na porcie
ide1 numer 22, na porcie ide2 numer 33, a na porcie ide3 numer 34.
3.5.1 Najważniejsze struktury danych
Oto opis najważniejszych struktur utrzymywanych przez
ide.c oraz
niektórych z ich pól.
ide_drive - dane o pojedynczym napędzie
typedef struct ide_drive_s {
struct request
*queue; - kolejka żądań do urządzenia
special_t
special; - flagi poleceń
specjalnych (np. rekalibracji)
byte
media; - rodzaj
urządzenia (disk, floppy, ...)
byte
usage; - liczba
operacji open() na napędzie
byte
head; -
liczba głowic
byte
sect; -
liczba sektorów
unsigned short
cyl; -
liczba cylindrów
void
*hwif; - wskaźnik do struktury
hwif
struct hd_driveid *id;
- dokładne dane na temat napędu
struct hd_struct
*part; - tablica partycji
char
name[4]; - nazwa napędu
(np. "hda")
void
*driver; - sterownik urządzenia
(ide_driver)
[...]
} ide_drive_t;
hwif - jedna struktura odpowiada jednemu interfejsowi
IDE
typedef struct hwif_s {
void
*hwgroup; - wskaźnik do struktury
hwgroup
ide_drive_t drives[MAX_DRIVES];
-
napędy podłączone do portu (MAX_DRIVES = 2)
int
irq; -
numer przerwania obsługującego interfejs
byte
major; - numer
główny
char
name[6]; - nazwa interfejsu
(np. "ide0")
[...]
} ide_hwif_t;
hwgroup - struktura wspólna dla portów obsługiwanych
przez to samo przerwanie
typedef struct hwgroup_s {
ide_handler_t *handler;
- procedura obsługi przerwania (może być NULL)
ide_drive_t *drive;
- aktualnie obsługiwany napęd
ide_hwif_t *hwif;
- wskaźnik do struktury interfejsu
struct request *rq;
- aktualnie realizowane żądanie
[...]
} ide_hwgroup_t;
ide_driver - opis "podsterownika" obsługującego
konkretny typ urządzenia
typedef struct ide_driver_s {
const char *name;
- nazwa sterownika (np. ide-disk)
const char *version;
- wersja sterownika
byte
media;
- typ urządzenia (disk, floppy, ...)
ide_do_request_proc *do_request;
- funkcja rozpoczynająca obsługę żądania
ide_end_request_proc *end_request;
-
funkcja kończąca obsługę żądania
[...]
} ide_driver_t;
ide_module - informacje o module
typedef struct ide_module_s {
int type;
- typ modułu
ide_module_init_proc
*init; - funkcja inicjalizująca moduł
void *info;
- dodatkowe informacje o module
struct ide_module_s *next;
- wskaźnik do następnego modułu
} ide_module_t;
ide_hwif_t hwifs[] - struktura
będąca głównym repozytorium danych o wszystkim co dzieje się w ide.c
ide_module_t *ide_modules-
lista modułów
3.5.3 Działanie ide-disk.c
Sterownik ide-disk.c jest częścią ("podsterownikiem") sterownika
ide.c,
który zajmuje się obsługą wszelkich urządzeń podłączonych do interfejsu
IDE, np.: CD-ROM'u, dysku twardego.
Funkcja zajmująca się strategią szeregowania po wstawieniu żądania
do pustej kolejki wywołuje przypisaną urządzeniu w strukturze blk_dev_struct
funkcję strategii. W naszym przypadku jest to funkcja do_ideX_request(),
gdzie X jest numerem portu IDE, zdefiniowana w ide.c. Ide.c zajmuje się
obsługą żądania tak długo jak jest to możliwe, natomiast gdy jest to konieczne
sterowanie przejmuje ide-disk.c. Współpracę miedzy tymi modułami przedstawia
poniższy rysunek.
Spis treści
3.6 Źródła informacji
1. Kod źródłowy Linuxa, pliki:
include/linux/blk.h
include/linux/blkdev.h
drivers/block/ll_rw_blk.c
fs/devices.c
fs/block_dev.c
2. Michael
K. Johnson: Kernel Hackers' Guide -urzadzenia blokowe
3. M.Beck, H.Bohme, M.Dziadzka, U.Kunitz, R.Magnus, D.Verworner "Linux
kernel - jądro systemu"
4. M.J.Bach "Budowa systemu operacyjnego Unix"
Spis treści
4 Urządzenia znakowe
4.1 Terminal
Rodzaje urządzeń terminalowych:
Urządzenie terminalowe odpowiada za komunikację:
-
systemu z ludźmi.
-
systemy z innymi systemami
Schemat działania terminala:
Uwaga: w trybie surowym (raw mode) komunikacja odbywa się
z pominięciem dyscypliny lini.
Struktura danych urządzenia terminalowego (deklaracja w
include/linux/tty.h):
struct tty_struct {
...
struct tty_driver driver;
/*
* Interfejs pomiędzy sterownikiem niskopozimowym terminala,
* a procedurami tty.
*/
struct tty_ldisc ldisc;
/*
* Interfejs pomiędzy implementacją dyscypliny lini,
* a procedurami tty.
*/
struct termios *termios;
/*
* Parametry pracy urządzenia terminalowego.
*/
int pgrp;
/*
* Identyfikator grupy terminalowej.
* Procesy z grupy terminalowej: o numerze grupy równej pgrp.
*/
int session;
/*
* Numer sesji terminala.
*/
...
/*
* Tutaj także znajdują się dane dla dyscypliny lini N_TTY
*/
};
Terminal jest odpowiedzialny za wysyłanie odpowiednich sygnałów do procesów
należący do grupy terminalowej np.:
-
SIGINT - przy wciśnięciu ^C
-
SIGSTOP - przy wciśnięciu ^Z
-
SIGHUP - przy zakończenia lidera grupy terminalowej (zazwyczaj
jest to shell)
Źródła:
Spis treści
4.2 Pseudoterminal
Pseudoterminal to para terminali: master-slave
master /dev/ptyxx
slave /dev/ttyxx
Typowe programy dla terminala master:
Dokumentajca: Documentation/devices.txt
Spis treści
4.3 Konsola
Jest to urządzenie znakowe odpowiadające za lokalną komunikację
z użytkownikiem.
Po starcie systemu naszym terminalem staje się właśnie konsola.
W Linuksie mamy 63 konsole, (tty1..63).
Dzielą one ten sam ekran - są więc wirtualne (ang. virtual
consoles).
Tylko jedna z nich może być w jednej chwili widoczna.
Ta aktywna konsola odpowiada urządzeniu tty0.
/dev/console. - odrębne urządzenie - aktualna konsola
Używane przez urządzenia z buforem ramki (ang frame buffer)
Obsługa konsoli:
sterownik urządzeń terminalowych
Jedną z ważniejszych właściwości programów obsługi konsoli jest emulacja
terminali
VT100.
Działanie
-
wysłanie znaku na konsolę,
-
analiza charakterystyczna dla tty (patrz. dyscyplina
linii) ,
-
wynik jest przekazywany do sterownika konsoli.
-
sterownik przetwarza dane i analizuje sekwencje z kodami ESC (ruchy kursorów,
czyszczenie ekranu, etc)
-
znaki, które nie są częscią sekwencji sterujących, są konwertowane
na UNICODE przy użyciu 1 z 4 tablic
mapujących (jeśli konsola nie byla w trybie UTF-8
-
na podstawie uzyskanego kodu określany jest faktyczny kod znaku i
przesyłany do pamięci karty graficznej.
Odczyt
-
po naciśnięciu klawisza generowane są przerwania klawiatury
-
w procedurze obsługi odczytywane są scancody
-
scancode jest konwertowany w postać 16 bitową (ostatnie bity odpowiadają
za typ klawisza (jest 16 typów))
-
sprawdzamy typ klawisza, który został naciśniety i wywołujemy jedną z 16
funkcji odpowiedzialnych za interpretację typu klawisza.
Efekty
-
klawisze np.: po "X-ami" zachowują się inaczej niż na "gołej", tekstowej
konsoli.
-
Linux ukrywa specjalne działanie niektórych klawiszy np ALT-Fn i
ALT-SysRQ-
.
Magic SysRQ Key.
Zestaw dosyć zlożonych komend uruchamianych przez kombinacje klawiszy
(SysRQ to najczęściej Print Screen na klawiaturze).
Magic SysRQ dostępne jest (są??) dopiero w jadrach od 2.y.x gdzie y>=1,
a x jest dowolne.
Przykład komend (pełny wykaz można znaleźć w pliku Documentation/sysrq.txt
- źrodła kernela):
-
'k' - zabija wszystkie procesy na wirtualnej konsoli
-
'u' - remount wszystkich filesystemow
-
'p' - wypisuje aktualny stan wszystkich rejestrów i flag procesora
-
't' - wyświetla na konsoli wszystkie zadania aktualnie uruchomione
w systemie
-
'm' - przeróżne informacje o pamięci
-
Można robic inne ciekawe rzeczy, np. wysłac SIGKILL do wszystkich procesów
łącznie z init-em.
Obsługa Magicznych Klawiszy jest zaimplementowana w linux/drivers/char/sysrq.c.
przeglądanie wcześniejszych ekranów
(standardowe klawisze: Shift+ PgUp, PgDn)
Sterownik konsoli wykorzystuje możliwości tzw. hard scrollingu, dotępnego
na kartach graficznych.
Ekrany widoczne poprzednio na konsoli, są pamiętane tylko w pamieci
karty graficznej, a przegladanie ich (Shift +....) to po prostu zmiana
adresu początku wyświetlanego obrazu. Z tego powodu, jeżeli zapragniemy
zmienić konsolę na inną, program obsługujący konsolę wyczyści zawartość
karty graficznej (żeby zachować sens działania Shift+....) i po powrocie
na pierwotną konsolę nie bedziemy mogli obejrzeć tego, co poprzednio było
wyświetlone.
Zmiana i tworzenie
konsol
Zmieniac aktywną można na kilka sposobów:
-
Alt-Fn lub Alt-Ctrl-Fn
-
Prawy ALT+Fn - zmieniamy na konsole o numerze n+12
-
Alt + strzałki w bok
-
programem chvt (man chvt)
Podane kombinacje obowiązują przy domyślnym mapowaniu klawiszy.
Zmiana mapowania - loadkeys
Konsolę tworzy się za pomocą chvt, a usuwa za pomocą deallocvt.
Na utworzonej konsoli można uruchomić program używając polecenia openvt.
Źródła informacji:
-
Unicode jest to "zunifikowany 16-bitowy zestaw
znaków". Zawiera znaki z prawie wszystkich języków. Więcej (naprawdę sporo)
informacji można znaleźć na stronach man unicode
UTF-8 kodowanie Unikodowe, kompatybilne
z ascii. Patrz man 7 utf-8
-
Zainsteresowanych szczgółami kbd.FAQ.txt
-
Tu znajduje się
lista wszystkich urządzeń w Linuxie.
-
Projekt Linux
-
Żródła: : console.c
,
console.h
,
consolemap.c,
consolemap.h
,
console_struct.h
Spis treści
4.4 Dyscyplina linii
Dyscyplina linii polega na tłumaczeniu znaków przesyłanych przez
terminal do procesu. Jest to przydatne, aby mieć pewien standardowy sposób
reagowania procesu na wejście ( np. Ctrl-Z zatrymanie procesu ).
Interfejs dyscypliny linii zdefiniowany jest w pliku include/linux/tty_ldisc.h
Wersja 2.2.x jądra wyróżnia 2 dyscypliny linii, są to:
-
N_TTY - używana przez konsole tty
-
N_HDLC - używana przez urządzenia komunikacjyjne, np modemy.
Jest zorientowana na przesyłanie całych ramek (frames), tzn można wysłać
lub odebrać pełną ramkę. Jej cechą jest to, że nie wykonuje żadnej konwersji.
Dyscyplina N_TTY obsługuje następujące konwersje:
-
koniec linii \n
-
powrót karetki \r
-
tabulator \t
i wiele innych. Zainteresowanych odsyłam do źródeł: drivers/char/n_tty.c
Interfejs
W pliku include/linux/tty_ldisc.h
jest
zdefiniowany interfejs pomiędzy tty_discipline a procedurami
obsługi
tty
Funkcje wywoływane ``z góry'', czyli przez użytkownika
-
open
-
close
-
flush_buffer
-
chars_in_buffer
-
read
-
write
-
ioctl
wywoływana, jeśli nie udało jej się wykonać w poprzedniej warstwie
Porządek warstw jest następujący
-
tty
-
sterownik niskopoziomowy tty
-
dyscplina linii
-
set_termios
-
poll
Funkcje wywoływane ``z dołu'' - przez sterownik niskopoziomowy
-
receive_buf
-
receive_room
-
write_wakeup
Źródła informacji:
Spis treści
4.5 Urządzenie mem
Wstęp
Pamięć jest dostępna jako urządzenie znakowe o numerze głównym 1.
Wyróżniamy kilka podurządzeń mem:
Podnumer(minor) |
lokalizacja w systemie plików |
cel |
1 |
/dev/mem |
pamięć fizyczna |
2 |
/dev/kmem |
pamięć wirtualna widziana przez jądro |
3 |
/dev/null |
czarna dziura , wszystko tam wrzucone przepada |
4 |
/dev/port |
dostęp do portów |
5 |
/dev/zero |
generator zer |
6 |
/dev/core |
|
7 |
/dev/full |
urzadzenie zwracajace ENOSPC przy próbie zapisu |
8 |
/dev/random |
generator liczb pseudolosowych |
9 |
/dev/urandom |
j.w. tyle że nie blokujące |
mem i kmem
Na /dev/mem i /dev/kmem można wykonywać operacje:
-
open
-
write
-
read
-
lseek
-
map
Funkcje
Funkcje obsługi mem są zaimplementowane w pliku drivers/char/mem.c
.
Do najważniejszych można zaliczyć :
-
open_port
służy do otwierania zarówno io jaki i mem,kmem
-
do_write_mem
-
read_mem -czytanie z pamieci
-
write_mem -sprawdza parametry i woła odpowiednio do_write_mem
-
mmap_mem- zamapowanie urzadzenia do przestrzeni adresowej użytkownika /dev/mem
i /dev/kmem są wyjatkiem od reguły, że urządzenia znakowe nie potrzebuja
mmap.
Mogą być mapowane ponieważ są skończone (mają określony rozmiar ) i
dają się adresować.
-
memory_lseek uwaga : nie można przesuwać względem końca pamięci
Zmienna globalna
Jedną z zmiennych globalnych często wykorzystywaną w powyższych funkcjach
to
high_memory (wskazuje poczatek pamieci)
jest zadeklarowana w include/linux/mm.h
void * high_memory = NULL;
Różnice między mem a kmem
Mem i kmem różnią o tę częćś pamięci, którą zajmuje jądro
Zatem mem inaczej przelicza się offset w obu tych przypadkach
służą do tego makra:__pa,__va
Spis treści
5 Jak napisać program obsługi urządzenia
5.1 Wiadomości wstępne
5.1.1 Inicjowanie programu obsługi urządzenia
Funkcja inicjująca program obsługi urządzenia odpowiada za:
-
wykrycie urządzenia
-
rejestrację w jądrze
-
stworzenie wewnętrznych struktur danych programu
Postać funkcji inicjującej:
int init_mydrv (void)
Wywołanie funkcji należy dopisać do któregoś z plików:
-
drivers/char/mem.c - dla urządzeń znakowych (funkcja
char_dev_init)
-
drivers/block/ll_rw_bl.c - dla urządzeń blokowych (funkcja
blk_dev_init)
Inicjowanie programu obsługi, który będzie w postaci modułu odbywa sie
w standardowej funkcji inicjującej moduł:
int init_module (void)
Spis treści
5.1.2 Odczyt parametrów
Funkcja odczytująca parametry jest wywoływana przed funkcją inicjującą.
Postać funkcji:
int mydrv_setup (char *s, int *p)
-
Tablica p (o rozmiarze p[0])
-
zawiera parametry przekazane do programu, które udało się jądru zamienić
na liczby (próba zamiany dotyczy pierwszych dziesięciu parametrów).
-
Ciąg s
-
składa się z pozostałych argumentów.
Funkcję należy dopisać do pliku <init/main.c> w cooked_params:
static struct kernel_param cooked_params[] __initdata = {
......
{"mydrv=", mydrv_setup },
......
{0, 0}
};
Istnieje jeszcze tablica raw_params[] (dla jej programów obsługi
jądro nie próbuje zamieniać pierwszych parametrów na liczby).
W module zadeklarowanie zmiennej jako parametru
odbywa się za pomocą makra:
MODULE_PARM(variable, type-description);
-
variable
-
nazwa zmiennej-parametru.
-
type-description
-
jej opis (dokumentacja w pliku <linux/module.h>).
Spis treści
5.1.3 Dostęp do przestrzeni użytkownika
Funkcje realizujące dostęp do przestrzeni użytkownika:
int access_ok (int type, unsigned long addr, unsigned long size)
-
type - VERIFY_READ lub VERIFY_WRITE
-
addr - początek bloku pamięci
-
size - długość bloku
int get_user (lvalue, address)
int __get_user(lvalue, address)
Przypisanie na lvalue wartości spod adresu address.
int put_user (expression, address)
__put_user (expression, address)
Zapisanie wartości expression pod adres address.
unsigned long copy_from_user (unsigned long to, unsigned long from,
unsigned long len)
unsigned long copy_to_user (unsigned long to, unsigned long from,
unsigned long len)
Funkcje kopiujące dane pomiędzy przestrzeniami użytkownika i jądra. Spis
treści
5.1.4 Alokacja pamięci
Dynamiczna alokacja i dealokacja pamięci w jądrze.
void *kmalloc (size_t size, int flags)
void kfree (const void *objp)
-
size - rozmiar w stronach (maksymalnie 32)
-
flags - sposób przydzielenia pamięci
Niektóre możliwe wartości pola flags:
-
GFP_KERNEL
-
W przypadku braku wolnej pamięci jądro "czeka" na zwolnienie się jakiejś
strony wstrzymując aktualnie wykonujący się proces.
-
GFP_ATOMIC
-
Jeśli zabraknie pamięci od razu zwracany jest błąd.
-
GFP_DMA
-
Flaga używana razem z poprzednimi. Zaznacza, że dany obszar będzie służyć
do transmisji DMA.
Dla alokacji obszarów pamięci wirtualnej służą funkcje:
void *vmalloc (unsigned long size)
void vfree (void *addr)
Spis treści
5.1.5 Wsparcie dla mechanizmów sprzętowych
Kilka najbardziej popularnych mechanizmów sprzętowych ma wsparcie ze strony
jądra. Oznacza to, ze programista, zamiast komunikować się ze sprzętem
przy pomocy portów lub BIOS32, może używać zestawu funkcji i struktur danych,
które tworzą wygodny interface i pomagają tworzyć przenośne programy obsługi
urządzeń.
Do najlepiej wspieranych mechanizmów należy magistala PCI. Opis interface'u
można znaleźć w pliku Documentation/pci.txt w źródłach jądra.
Spis treści
5.2 Jak napisać program obsługi urządzenia znakowego
5.2.1 Rejestrowanie programu obsługi urządzenia
Do rejestracji programu obsługi urządzenia znakowego służy funkcja:
int register_chrdev (unsigned int major, const char *name,
struct file_operations *fops)
-
major
-
numer nadrzędny pod jakim chcemy zarejestrować program.
-
name
-
nazwa urządzenia.
-
fops
-
wskaźnik do struktury operacji plikowych.
W programie w postaci modułów można także korzystać z funkcji zwalniającej:
int unregister_chrdev (unsigned int major, const char *name)
Spis treści
5.2.2 Operacje plikowe
Struktura file_operations
Przykładowa struktura file_operations dla urządzenia znakowego:
struct file_operations testdrv_fops {
testdrv_llseek, /* llseek */
testdrv_read, /* read */
testdrv_write, /* write */
NULL, /* readdir */
NULL, /* poll */
testdrv_ioctl, /* ioctl */
NULL, /* mmap */
testdrv_open, /* open */
NULL, /* flush */
testdrv_release, /* release */
NULL, /* fsync */
NULL, /* fasync */
NULL, /* check_media_change */
NULL, /* revalidate */
NULL /* lock */
};
Struktura file
Struktura struct file zdefiniowana w pliku <include/linux/fs.h>
-
struct file_operations *f_op;
operacje związane z plikiem (możemy je modyfikowac w trakcie pracy
urządzenia)
-
mode_t f_mode;
tryb otwarcia pliku (FMODE_READ i FMODE_WRITE)
-
loff_t f_pos;
Bieżąca pozycja czytania lub pisania
-
unsigned int f_flags;
flagi
-
void *private_data;
pole to program obsługi może wykorzystać do własnych(specyficznych)
celów.
Otwieranie i zamykanie urządzenia
int (*testdrv_open) (struct inode * inode, struct file *filp);
int (*testdrv_release) (struct inode *inode, struct file *filp);
Funkcja open służy programowi obsługi do wstępnego przygotowania dla kolejnych
operacji. Zazwyczaj powinna wykonywać następujące działania:
-
Sprawdzenie błedów specyficznych dla urządzenia
-
Alokacja i wypełnienie struktur danych, które mają być umieszczone
w filp->private_data
-
Zwiększenie licznika wykorzystania za pomocą makra MOD_INC_USE_COUNT
Funkcja release powinna realizować:
-
Zmniejszenie licznika wykorzystania za pomocą makra MOD_DEC_USE_COUNT
-
Zdealokowanie wszystkiego co zostało zaalokowane w open w filp->private_data
Czytanie i pisanie do/z urządzenia
ssize_t (*read) (struct file *filp, char *buf,
size_t size, loff_t *loff);
ssize_t (*write) (struct file *filp, const char *buf,
size_t size, loff_t *loff);
Funkcje implementują czytanie danych z urządzenia i pisanie do urządzenia.
Powinny zwracać ilość przeczytanych/zapisanych danych.
Do przesyłania danych między przestrzenią użytkownika a przestrzenią
jądra używamy makr copy_from_user, copy_to_user, get_user
i put_user.
Funkcja kontroli urządzenia
int (*ioctl) (struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg);
Zaimplementowanie tej metody pozwoli użytkownikowi urządzenia na dodatkowe,
specyficzne tylko dla tego urządzenia operacje.
-
cmd - numer komendy
-
arg - argument
Należy wybrać unikatowe numery komend, aby uniemożliwic wydanie poprawnej
komendy błędnemu urządzeniu. Służą do tego makra:
_IO(type, nr)
_IOR(type, nr, size) // czyta do przestrzeni jądra
_IOW(type, nr, size) // zapisuje do przestrzeni jądra
_IOWR(type, nr, size) // czyta i zapisuje
Kolejne parametry oznaczają:
-
type - numer magiczny (magic number), liczba ośmiobitowa ktorą należy
wybrać i używać we wszystkich makrach. Spis zarezerwowanych numerów magicznych
(nie należy ich używać) znajduje się w pliku Documentation/ioctl-number.txt
-
nr - liczba osmiobitowa (po prostu kolejna liczba naturalna)
-
size - rozmiar danych, które będą przesyłane
Oto przykład wycięty z pliku include/linux/random.h
/* Get the entropy count. */
#define RNDGETENTCNT _IOR( 'R', 0x00, int )
/* Add to (or subtract from) the entropy count. (Superuser only.) */
#define RNDADDTOENTCNT _IOW( 'R', 0x01, int )
/* Get the contents of the entropy pool. (Superuser only.) */
#define RNDGETPOOL _IOR( 'R', 0x02, int [2] )
W podanym przykładzie numerem magicznym jest 'R'. Należy zwrócic uwagę,
że jako size podajemy typ danych, a nie sizeof(typ).
Spis treści
5.3 Jak napisać program obsługi urządzenia blokowego
5.3.1 Rejestracja i operacje plikowe
Do rejestracji programu obsługi urządzenia blokowego służy funkcja:
int register_blkdev (unsigned int major, const char *name,
struct file_operations *fops)
-
major
-
numer nadrzędny pod jakim chcemy zarejestrować program.
-
name
-
nazwa urządzenia.
-
fops
-
wskaźnik do struktury operacji plikowych.
Programu w postaci modułów mogę także korzystać z funkcji zwalniającej:
int unregister_blkdev (unsigned int major, const char *name)
Przykładowe wypełnienie struktury operacji plikowych (fops):
struct file_operations mydrv_fops {
NULL, /* llseek */
block_read, /* read */
block_write, /* write */
NULL, /* readdir */
NULL, /* poll */
mydrv_ioctl, /* ioctl */
NULL, /* mmap */
mydrv_open, /* open */
NULL, /* flush */
mydrv_release, /* release */
block_fsync, /* fsync */
NULL, /* fasync */
mydrv_check_media_change, /* check_media_change */
mydrv_revalidate, /* revalidate */
NULL /* lock */
};
Nie trzeba implementować własnych funkcji czytających i piszących. Używamy
standardowe funkcje block_* korzystające z mechanizmu podręcznej
pamięci buforowej. Za rzeczywiste przesłania danych (kiedy bufory nie są
w stanie obsłuzyć żądania) odpowiada procedura strategii.
Spis treści
5.3.2 Dodatkowe struktury danych
Każde urządzenie blokowe opisane jest przez strukturęblk_dev_struct:
struct blk_dev_struct {
request_fn_proc *request_fn;
queue_proc *queue;
void *data;
struct request *current_request;
struct request plug;
struct tq_struct plug_tq;
};
gdzie,
typedef void (request_fn_proc) (void);
Jądro utrzymuje tablicę tych struktur (indeksowaną numerami nadrzędnymi):
extern struct blk_dev_struct blk_dev[MAX_BLKDEV];
Musimy wypełnić pole
request_fn. Jest to wskaźnik na procedurę
strategii (gdzie są zaimplementowane lub zlecane niskopoziomowe operacje
specyficzne dla danego urządzenia).
Inne dodatkowe struktury danych to:
-
int *blk_size [MAX_BLKDEV]
-
Opisuje rozmiar każdego urządzenia w kilobajtach. Pierwszy indeks to numer
nadrzędny urządzenia, a drugi to numer drugorzędny.
-
int *blksize_size [MAX_BLKDEV]
-
Rozmiar jednego bloku urządzenia w bajtach.
-
int *hardsect_size [MAX_BLKDEV]
-
Rozmiar sektora urządzenia.
-
int read_ahead [MAX_BLKDEV]
-
Liczba sektorów, które mają zostać przeczytane z wyprzedzeniem, gdy plik
będzie czytany sekwencyjnie.
Spis treści
5.3.3 Interfejs programisty, plik <linux/blk.h>
W pliku <linux/blk.h> znajduje się interfejs wspólnego kodu
dla programów obsługi urządzeń blokowych.
Aby z niego korzystać trzeba wcześniej zdefiniować pewne symbole wymagane
przez makrodefinicje z tego pliku:
-
MAJOR_NR
-
Numer nadrzędny urządzenia (nie musi być zdefinowany jako stała).
-
DEVICE_NAME
-
Nazwa urządzenia.
-
DEVICE_NR(device)
-
Służy do określenia numeru fizycznego urządzenia na podstawie drugorzędnego
numeru urządzenia (często oba numery są sobie równe).
-
DEVICE_ON i DEVICE_OFF
-
Wykorzystywane w urządzeniach wyłączanych i włączanych (np stacja dyskietek).
Definiujemy je puste.
Przykładowy kod dla programu obsługi urządzenia mydrv:
#define MAJOR_NR mydrv_major
#define DEVICE_NAME "mydrv"
#define DEVICE_NR(device) MINOR(device)
/* numer fizycznego urządzenia aktualnie obsługiwanego
przez program jest równy numerowi drugorzędnemu */
#define DEVICE_ON(device) // nie obsługiwane
#define DEVICE_OFF(device) // nie obsługiwane
int mydrv_major;
......
#include <linux/blk.h>
Spis treści
5.3.4 Obsługa żądań, procedura strategii
Postać funkcji:
void mydrv_request (void)
Jest to funkcja, która odpowiada za zlecanie (ew. wykonanie) niskopoziomowych
instrukcji wejścia/wyjścia (na nią ustawiamy wskaźnik
request_fn
ze struktury blk_dev_struct).
Dla zwiększenia wydajności systemu jądro kolejkuje żądania przesłania
danych do urządzenia (szeregowanie)
i później przekazuje do programu obsługi całą listę żądań.
Dostęp do kolejki żądań odbywa się przy pomocy:
-
INIT_REQUEST - makro, sprawdza
poprawność żądania i "pobiera" pierwsze z listy żądanie.
-
CURRENT
- makro, wskaźnik na strukturę request bieżącego
żądania.
-
end_request(int) - funkcja wywoływana po zakończeniu obsługi żądania
(argument 0 - błąd, 1 - ok). Bez jej wywołania makro INIT_REQUEST
nie ustawi CURRENT na następne żądanie.
Pola struktury request przydatne dla funkcji żądania to:
-
kdev_t rq_de
-
Urządzenie, do którego następuje dostęp przez to żądanie.
-
int cmd
-
Rodzaj zlecenia. Może przybierać wartości READ lub WRITE.
-
unsigned long sector
-
Pierwszy sektor, którego dotyczy żądanie.
-
unsigned long current_nr_sectors
-
Liczba sektorów bieżącego żądania.
-
char *buffer
-
Obszar w pamięci podręcznej, do którego powinny być zapisane (READ)
lub z którego powinny być odczytane dane (WRITE).
Schemat funkcji obsługi żądania może wyglądać tak:
void mydrv_request (void)
{
while (1) {
/*
w pętli nieskończonej bo INIT_REQUEST sam
zakończy wykonywanie naszej funkcji jeśli
nie będzie już żądań
*/
INIT_REQUEST;
......
/* sprawdzenie poprawności */
......
switch (CURRET->cmd) {
case READ :
/* czytanie */
break;
case WRITE :
/* pisanie */
break;
default:
/* oops */
end_request(0); /* błąd */
continue;
}
end_request(1); /* ok, udało się */
}
}
W powyższym przykładzie blokujemy jednak procesor na czas wykonywania operacji
czytania/pisania.
Schemat działania programu obsługi urządzenia korzystającego z przerwań:
-
Procedura strategii zleca niskopoziomową transmisję danych bez wywołania
end_request.
-
Zachodzi jedna z możliwości:
-
Funkcja obsługi przerwania sprawdza poprawność realizacji żądania i wywołuje
end_request.
-
Po przekroczeniu limitu czasu funkcja obsługi licznika wywołuje
end_request(0)
(jeśli korzystamy z liczników czasu).
-
Wywołanie procedury strategii jeśli są jeszcze żądania w kolejce.
Spis treści
5.4 Moduły
Opcjonalne części jądra, np. programy obsługi urządzeń, mogą być kompilowane
w postaci modułów. Moduł to kod obiektowy, który można włączać do działającego
jądra i z niego usuwać.
Korzyści, jakie daje możliwość stosowania modułów:
-
Łatwe rozszerzanie funkcjonalności jądra (bez konieczności rekompilacji).
-
Oszczędność zasobów.
-
Można stworzyć jedno podstawowe jądro i dopasowywać je do różnych komputerów
przez ładowanie modułów.
-
Poprawianie modułu jest wygodniejsze niż poprawianie kodu wkompilowanego
w jądro.
Pakiet modutils:
lsmod - pokazuje załadowane moduły, ich liczniki użyć
i odwołania między nimi. Jest to prawie dokładnie to samo, co w pliku /proc/modules
insmod - instalowanie modułów w jądrze
rmmod - usuwanie modułów z jądra
modprobe - narzędzie silniejsze niż insmod
- znając zależności między modułami (modules.dep), potrafi załadować
od razu stos modułów, ma wygodny plik konfiguracyjny (/etc/conf.modules).
depmod - oblicza zależności między modułami dla modprobe
5.4.1 Funkcje init_module() i cleanup_module()
Te dwie funkcje moduł musi posiadać.
-
int init_module(void) - inicjacja modułu (zwraca: 0 - ok, 1 -
błąd).
-
void cleanup_module(void) - czyszczenie po module, wywoływana
w momencie usuwania modułu.
5.4.2 Symbole eksportowane przez jądro
Podczas ładowania jądro próbuje dowiązać niezdefiniowane symbole w module
do własnych (eksportowanych) funkcji i zmiennych.
Moduł może eksportować własne zmienne i funkcje.
Listę symboli eksportowanych przez jądro można obejrzeć w /proc/ksyms.
Przykład:
...
c382004c ne_probe [ne]
c381d04c ei_open [8390]
c381d0ac ei_close [8390]
c381d578 ei_interrupt [8390]
c381dfa4 ethdev_init [8390]
c381e014 NS8390_init [8390]
c0239b40 drive_info
...
c01222e0 kmalloc
c0122474 kfree
c0122644 kfree_s
c0121390 vmalloc
c0121328 vfree
...
Makra:
-
EXPORT_SYMTAB - to makro należy umieścić w kodzie modułu, jeśli
chcemy używać makra EXPORT_SYMBOL.
-
EXPORT_SYMBOL(nazwa_zmiennej_lub_funkcji) - wyeksportowanie nazwy
zmiennej lub funkcji.
-
EXPORT_NO_SYMBOLS - moduł nie eksportuje żadnych symboli.
5.4.3 Liczniki użyć modułu i referencje
Moduł można usunąć z jądra tylko wtedy, kiedy nie jest używany.
Najczęstsze sposoby użycia modułu:
-
Inny załadowany moduł używa jego symboli.
-
Inny moduł zarejestrował użycie czegoś, co udostępnia moduł. Wtedy zachodzi
też poprzedni przypadek.
-
Urządzenie udostępniane przez moduł jest otwarte dla jednego lub więcej
procesów.
-
Zamontowany jest system plików udostępniany przez moduł.
Jądro śledzi odwołania do symboli między modułami i nie pozwala usunąć
modułu, do którego odwołuje się inny moduł.
W pozostałych przypadkach używa się liczników użycia modułu. Po załadowaniu
modułu licznik ma wartość 0. Do zwiększania i zmniejszania jego wartości
służą makra:
MOD_INC_USE_COUNT - zwiększa wartość licznika
MOD_DEC_USE_COUNT - zmniejsza wartość licznika
Moduł można usunąć tylko wtedy, kiedy nie ma żadnych referencji do niego
i jego licznik użyć ma wartość 0.
5.4.4 Parametry modułu
Przekazanie parametrów do ładowanego modułu:
insmod ne.o io=0x300 irq=10
Do zaznaczenia zmiennej jako parametr służy makro:
MODULE_PARM(zmienna, typ);
Typ jest ciągiem formatu "[min[-max]]{b,h,i,l,s}", gdzie min i
max
są opcjonalnymi ograniczeniami długości tablicy (domyślnie 1, czyli pojedyncza
zmienna), a litera oznacza:
b - bajt
h - słowo (2 bajty)
i - int
l - long
s - string
5.4.5 Przykładowy moduł
Oto kod przykładowego modułu. Eksportuje jedną funkcję, ma parametr i wypisuje
kilka komunikatów podczas ładowania i usuwania.
/*
* Kompilacja:
* gcc -O2 -D__KERNEL__ -DMODULE -c modul.c
*
* Opcja -O2 powoduje m. in. rozwijanie funkcji inline w miejscu wywołania.
* Niektóre funkcje nie są udostępniane przez jądro i muszą zostać skompilowane
* właśnie w ten sposób.
*/
#include <linux/kernel.h> /* printk */
#define EXPORT_SYMTAB
#include <linux/module.h>
int moja_funkcja(void);
EXPORT_SYMBOL(moja_funkcja);
int parametr = 0;
MODULE_PARM(parametr, "i");
int init_module(void)
{
printk("<1>modul: parametr == %d\n", parametr);
printk("<1>modul: moduł załadowany\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>modul: moduł jest usuwany\n");
}
int moja_funkcja(void)
{
return 0;
}
Spis treści