Podstawowy sterownik znaku w Linux

Podstawowy sterownik znaku w Linux
Przejdziemy przez Linux sposób wdrożenia sterownika postaci. Najpierw spróbujemy zrozumieć, czym jest sterownik postaci i jak Linux Framework umożliwia nam dodanie sterownika znaku. Następnie wykonamy przykładową aplikację przestrzeni użytkowników testowych. Ta aplikacja testowa wykorzystuje węzeł urządzenia ujawniony przez sterownik do pisania i odczytu danych z pamięci jądra.

Opis

Rozpocznijmy dyskusję z sterownikiem postaci w Linux. Kernel podziela kierowców na trzy kategorie:

Sterowniki postaci - To są sterowniki, które nie mają zbyt dużej ilości danych. Kilka przykładów sterowników znaków to sterownik ekranu dotykowego, sterownik UART itp. Wszystkie są sterownikami znaków, ponieważ przesyłanie danych odbywa się przez charakter według charakteru.

Blokuj sterowniki - Są to sterowniki, które zajmują się zbyt dużą ilością danych. Transfer danych odbywa się blok według bloku, ponieważ zbyt wiele danych musi zostać przeniesione. Przykładem sterowników blokowych są SATA, NVME itp.

Sterowniki sieciowe - Są to sterowniki, które funkcjonują w sieciowej grupie sterowników. Tutaj przesyłanie danych odbywa się w postaci pakietów danych. Kierowcy bezprzewodowe, takie jak Atheros w tej kategorii.

W tej dyskusji skupimy się tylko na sterowniku postaci.

Na przykład podejmiemy proste operacje odczytu/zapisu, aby zrozumieć podstawowy sterownik znaku. Zasadniczo każdy sterownik urządzenia ma te dwie minimalne operacje. Dodatkowa operacja może być otwarta, blisko, IOCTL itp. W naszym przykładzie nasz sterownik ma pamięć w przestrzeni jądra. Ta pamięć jest przydzielana przez sterownik urządzenia i może być uważana za pamięć urządzenia, ponieważ nie wiąże się to z komponentem sprzętowym. Sterownik tworzy interfejs urządzenia w katalogu /dev, który może być używany przez programy przestrzeni użytkowników w celu uzyskania dostępu do sterownika i wykonywania operacji obsługiwanych przez sterownik. W przypadku programu przestrzeni użytkowników te operacje są jak każde inne operacje plików. Program przestrzeni użytkowników musi otworzyć plik urządzenia, aby uzyskać instancję urządzenia. Jeśli użytkownik chce wykonać operację odczytu, do tego można użyć wywołania systemu odczytu. Podobnie, jeśli użytkownik chce wykonać operację zapisu, wywołanie systemu zapisu można użyć do osiągnięcia operacji zapisu.

Sterownik postaci

Rozważmy wdrożenie sterownika znaku z operacjami danych odczytu/zapisu.

Zaczynamy od przyjmowania instancji danych urządzenia. W naszym przypadku jest to „struct cdrv_device_data”.

Jeśli widzimy pola tej struktury, mamy CDEV, bufor urządzenia, rozmiar bufora, instancję klasową i obiekt urządzenia. Są to minimalne pola, w których powinniśmy zaimplementować sterownik znaku. Zależy to od implementarza, od których dodatkowe pola chce dodać, aby poprawić funkcjonowanie sterownika. Tutaj staramy się osiągnąć minimalne funkcjonowanie.

Następnie powinniśmy utworzyć obiekt struktury danych urządzenia. Używamy instrukcji, aby przydzielić pamięć w sposób statyczny.

struct cdrv_device_data char_device [cdrv_max_minors];

Pamięć tę można również podzielić dynamicznie za pomocą „Kmalloc”. Zachowajmy implementację tak prostą, jak to możliwe.

Powinniśmy przyjąć implementację funkcji odczytu i zapisu. Prototyp tych dwóch funkcji jest zdefiniowany przez strukturę sterownika urządzenia Linux. Implementacja tych funkcji musi być zdefiniowana przez użytkownika. W naszym przypadku rozważyliśmy następujące:

Przeczytaj: Operacja, aby uzyskać dane z pamięci sterownika do przestrzeni użytkownika.

static ssize_t cdrv_read (plik struct *plik, char __User *user_buffer, rozmiar size_t, loff_t *offset);

Napisz: Operacja przechowywania danych do pamięci sterownika z przestrzeni użytkownika.

static ssize_t cdrv_write (plik struct *, const char __User *user_buffer, rozmiar size_t, loff_t *offset);

Zarówno operacje, odczyt i zapisz, muszą być zarejestrowane jako część struct file_operations cdrv_fops. Są one zarejestrowane w ramach sterownika urządzenia Linux w init_cdrv () sterownika. Wewnątrz funkcji init_cdrv () wszystkie zadania konfiguracyjne są wykonywane. Niewiele zadań jest następujące:

  • Utwórz klasę
  • Utwórz instancję urządzenia
  • Przydzielić główną i drobną liczbę dla węzła urządzenia

Pełny przykładowy kod podstawowego sterownika urządzenia znakowego jest następujący:

#włączać
#włączać
#włączać
#włączać
#włączać
#włączać
#włączać
#Define CDRV_MAJOR 42
#Define cdrv_max_minors 1
#Define BUF_LEN 256
#Define cdrv_device_name „cdrv_dev”
#Define cdrv_class_name „cdrv_class”
struct cdrv_device_data
struct CDEV CDEV;
Char Buffer [buf_len];
rozmiar size_t;
struct class* cdrv_class;
Urządzenie struct* cdrv_dev;
;
struct cdrv_device_data char_device [cdrv_max_minors];
static ssize_t cdrv_write (plik struct *, plik, const char __User *użytkownik_buffer,
Rozmiar size_t, przesunięcie loff_t *)

struct cdrv_device_data *cdrv_data = & char_device [0];
ssize_t len ​​= min (cdrv_data-> rozmiar - *przesunięcie, rozmiar);
printK („pisanie: bajty =%d \ n”, rozmiar);
if (len buffer + *offset, user_buffer, len))
return -efault;
*offset += len;
powrót len;

static ssize_t cdrv_read (plik struct *, plik, char __User *użytkownik_buffer,
Rozmiar size_t, przesunięcie loff_t *)

struct cdrv_device_data *cdrv_data = & char_device [0];
ssize_t len ​​= min (cdrv_data-> rozmiar - *przesunięcie, rozmiar);
if (len buffer + *offset, len))
return -efault;
*offset += len;
printK („czytaj: bajty =%d \ n”, rozmiar);
powrót len;

static int cdrv_open (struct inode *inode, struct plik *plik)
printK (kern_info "cdrv: urządzenie otwarte \ n");
powrót 0;

static int cdrv_release (struct inode *inode, struct plik *plik)
printK (kern_info "cdrv: urządzenie zamknięte \ n");
powrót 0;

const struct file_operations cdrv_fops =
.właściciel = this_module,
.Open = cdrv_open,
.Read = cdrv_read,
.zapis = cdrv_write,
.release = cdrv_release,
;
int init_cdrv (void)

int count, ret_val;
printK („Init podstawowy sterownik znaku… start \ n”);
ret_val = Register_chrdev_region (mkdev (cdrv_major, 0), cdrv_max_minors,
„cdrv_device_driver”);
if (ret_val != 0)
printK („Register_chrdev_region (): nie powiodło się z kodem błędu:%d \ n”, ret_val);
return ret_val;

dla (count = 0; liczba < CDRV_MAX_MINORS; count++)
CDEV_INIT (& char_device [Count].CDEV i CDRV_FOPS);
CDEV_ADD (& char_device [Count].CDEV, MKDEV (CDRV_MAJOR, Count), 1);
Char_device [Count].cdrv_class = class_create (this_module, cdrv_class_name);
if (is_err (char_device [count].cdrv_class))
printK (kern_alert "cdrv: rejestr klasa urządzenia nie powiodła się \ n");
return ptr_err (char_device [count].cdrv_class);

Char_device [Count].size = buf_len;
printK (kern_info „klasa urządzenia CDRV zarejestrowana pomyślnie \ n”);
Char_device [Count].cdrv_dev = device_create (char_device [count].CDRV_CLASS, NULL, MKDEV (CDRV_MAJOR, Count), NULL, CDRV_DEVICE_NAME);

powrót 0;

void CleanUp_cdrv (void)

Int Count;
dla (count = 0; liczba < CDRV_MAX_MINORS; count++)
device_destroy (char_device [count].CDRV_CLASS i char_device [Count].cdrv_dev);
class_destroy (Char_Device [Count].cdrv_class);
CDEV_DEL (& char_device [Count].CDEV);

Unregister_chrdev_region (mkdev (cdrv_major, 0), cdrv_max_minors);
printk („Wychodzenie z podstawowego sterownika znaku… \ n”);

module_init (init_cdrv);
module_exit (cleanUp_cdrv);
Module_license („gpl”);
Module_author („sushil rathore”);
Module_description („przykładowy sterownik znaku”);
Module_version ("1.0 ”);

Tworzymy przykładowy makefile, aby skompilować podstawowy sterownik znaku i aplikację testową. Nasz kod sterownika jest obecny w CRDV.C i kod aplikacji testowej jest obecny w CDRV_APP.C.

obj-m+= cdrv.o
Wszystko:
Make -c/lib/modules/$ (shell uname -r)/build/m = $ (pwd) moduły
$ (Cc) cdrv_app.C -O cdrv_app
czysty:
make -c/lib/modules/$ (shell uname -r)/build/m = $ (PWD) Clean
rm cdrv_app
~

Po wydaniu Makefile powinniśmy uzyskać następujące dzienniki. Dostajemy również CDRV.KO i wykonywalne (CDRV_App) dla naszej aplikacji testowej:

root@haxv-srathore-2:/home/cinauser/kernel_articles# Make
Make -c/lib/moduły/4.15.0-197-generyka/build/m =/home/cinauser/kernel_articles moduły
Make [1]: Wprowadzanie katalogu '/usr/src/linux-headers-4.15.0-197-generalny '
CC [M]/Home/Cinauser/Kernel_articles/CDRV.o
Moduły budowlane, etap 2.
Modpost 1 moduły
CC/Home/Cienauser/Kernel_articles/CDRV.mod.o
Ld [m]/home/cinauser/kernel_articles/cdrv.Ko
Make [1]: opuszczenie katalogu '/usr/src/linux-headers-4.15.0-197-generalny '
CC CDRV_APP.C -O cdrv_app

Oto przykładowy kod dla aplikacji testowej. Ten kod implementuje aplikację testową, która otwiera plik urządzenia utworzony przez sterownik CDRV i zapisuje do niego „dane testowe”. Następnie odczytuje dane z sterownika i drukuje je po odczytaniu danych do wydrukowania jako „dane testowe”.

#włączać
#włączać
#Define Device_file "/dev/cdrv_dev"
char *data = "testowe dane";
Char Read_Buff [256];
int main ()

int fd;
int rc;
fd = Open (Device_file, O_Wronly, 0644);
if (fd<0)

Perror („Otwarcie pliku: \ n”);
zwrot -1;

rc = zapis (fd, dane, strlen (dane) +1);
if (rc<0)

Perror („Pisanie pliku: \ n”);
zwrot -1;

printf („pisemne bajty =%d, data =%s \ n”, rc, data);
Zamknij (FD);
fd = Open (Device_file, O_rdonly);
if (fd<0)

Perror („Otwarcie pliku: \ n”);
zwrot -1;

rc = read (fd, read_buff, strlen (data) +1);
if (rc<0)

Perror („Czytanie pliku: \ n”);
zwrot -1;

printf („odczyt bajtów =%d, data =%s \ n”, rc, read_buff);
Zamknij (FD);
powrót 0;

Gdy weźmiemy wszystkie rzeczy, możemy użyć następującego polecenia, aby wstawić podstawowy sterownik znaku do jądra Linux:

root@haxv-sathore-2:/home/cinauser/kernel_articles# insmod cdrv.Ko
root@haxv-sathore-2:/home/cinauser/kernel_articles#

Po włożeniu modułu otrzymujemy następujące wiadomości z DMESG i tworzymy plik urządzenia utworzony w /Dev as /dev /cdrv_dev:

root@haxv-sathore-2:/home/cinauser/kernel_articles# dmesg
[160.015595] CDRV: Załadowanie jądra modułu poza drzewem.
[160.015688] CDRV: Weryfikacja modułu nie powiodła
[160.016173] Inicjować podstawowy sterownik znaku… Rozpocznij
[160.016225] klasa urządzenia CDRV zarejestrowana pomyślnie
root@haxv-sathore-2:/home/cinauser/kernel_articles#

Teraz wykonaj aplikację testową z następującym poleceniem w powładzie Linux. Ostateczna wiadomość drukuje dane odczytu z sterownika, które są dokładnie takie samo, jak to, co napisaliśmy podczas operacji zapisu:

root@haxv-sathore-2:/home/cinauser/kernel_articles# ./cdrv_app
pisemne bajty = 10, dane = dane testowe
odczyt bajtów = 10, dane = dane testowe
root@haxv-sathore-2:/home/cinauser/kernel_articles#

Mamy kilka dodatkowych wydruków na ścieżce zapisu i odczytu, które można zobaczyć za pomocą polecenia DMESG. Kiedy wydawamy polecenie DMESG, otrzymujemy następujące dane wyjściowe:

root@haxv-sathore-2:/home/cinauser/kernel_articles# dmesg
[160.015595] CDRV: Załadowanie jądra modułu poza drzewem.
[160.015688] CDRV: Weryfikacja modułu nie powiodła
[160.016173] Inicjować podstawowy sterownik znaku… Rozpocznij
[160.016225] klasa urządzenia CDRV zarejestrowana pomyślnie
[228.533614] CDRV: Otwarte urządzenie
[228.533620] Pisanie: bajty = 10
[228.533771] CDRV: Urządzenie zamknięte
[228.533776] CDRV: Otwarte urządzenie
[228.533779] Czytaj: bajty = 10
[228.533792] CDRV: Urządzenie zamknięte
root@haxv-sathore-2:/home/cinauser/kernel_articles#

Wniosek

Przeszliśmy przez podstawowy sterownik znaków, który implementuje podstawowe operacje zapisu i odczytu. Omówiliśmy również przykładowy makefile, aby skompilować moduł wraz z aplikacją testową. Aplikacja testowa została napisana i omówiona w celu wykonania operacji zapisu i odczytu z miejsca użytkownika. Wykazaliśmy również kompilację i wykonanie modułu i aplikacji testowej z dziennikami. Aplikacja testowa zapisuje kilka bajtów danych testowych, a następnie odczytuje je z powrotem. Użytkownik może porównać oba dane, aby potwierdzić prawidłowe funkcjonowanie sterownika i aplikacji testowej.