Unix-Àhnliche Betriebssystementwicklung - ZeichengerÀtetreiber (8)

Im vorherigen Artikel haben wir Multitasking eingefĂŒhrt. Heute ist es Zeit, sich mit dem Thema ZeichengerĂ€tetreiber zu befassen.

Insbesondere werden wir heute einen Terminaltreiber schreiben, einen Mechanismus fĂŒr die verzögerte Verarbeitung von Interrupts, und das Thema Handler fĂŒr die obere und untere HĂ€lfte von Interrupts betrachten.

Wir beginnen mit der Erstellung einer GerĂ€testruktur, fĂŒhren dann die grundlegende UnterstĂŒtzung fĂŒr Datei-E / A ein und berĂŒcksichtigen die io_buf-Struktur und -Funktionen fĂŒr die Arbeit mit Dateien aus stdio.h.

Inhaltsverzeichnis


Build-System (make, gcc, gas). Erster Start (Multiboot). Starten Sie (qemu). C-Bibliothek (strcpy, memcpy, strext). C-Bibliothek (sprintf, strcpy, strcmp, strtok, va_list ...). Erstellen der Bibliothek im Kernelmodus und im Benutzeranwendungsmodus. Das Kernel-Systemprotokoll. Videospeicher Ausgabe an das Terminal (kprintf, kpanic, kassert). Dynamischer Speicher, Heap (kmalloc, kfree). Organisation der Speicher- und Interrupt-Behandlung (GDT, IDT, PIC, Syscall). Ausnahmen Virtueller Speicher (Seitenverzeichnis und Seitentabelle). Prozess. Planer Multitasking. Systemaufrufe (kill, exit, ps).

ZeichengerÀtetreiber. Systemaufrufe (ioctl, fopen, fread, fwrite). C-Bibliothek (fopen, fclose, fprintf, fscanf).

Das Dateisystem des Kernels (initrd), elf und seiner Interna. Systemaufrufe (exec). Shell als komplettes Programm fĂŒr den Kernel. Benutzerschutzmodus (Ring3). Aufgabenstatussegment (tss).

Charaktertreiber


Alles beginnt mit dem Erscheinen eines symbolischen GerĂ€ts. Wie Sie sich erinnern, sah die GerĂ€tedefinition in Linux-GerĂ€tetreibern folgendermaßen aus:

struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops; 

Der Hauptpunkt besteht darin, dem GerÀt die Implementierung von Datei-E / A-Funktionen zuzuweisen.

Wir werden mit einer Struktur auskommen, aber die Bedeutung wird Àhnlich sein:

 extern struct dev_t { struct clist_head_t list_head; /* should be at first */ char name[8]; /* device name */ void* base_r; /* base read address */ void* base_w; /* base write address */ dev_read_cb_t read_cb; /* read handler */ dev_write_cb_t write_cb; /* write handler */ dev_ioctl_cb_t ioctl_cb; /* device specific command handler */ struct clist_definition_t ih_list; /* low half interrupt handlers */ }; 

Jedes GerÀt entspricht der HÀlfte der Liste der Interrupts, die beim Generieren von Interrupts aufgerufen werden.

Unter Linux werden solche HĂ€lften als obere, im Gegenteil als untere (untere Ebene) bezeichnet.

Persönlich schien es mir logischer und ich erinnerte mich versehentlich an die Begriffe umgekehrt. Wir beschreiben jedes Element der Liste der unteren HÀlften der Unterbrechungen wie folgt:

 extern struct ih_low_t { struct clist_head_t list_head; /* should be at first */ int number; /* interrupt number */ ih_low_cb_t handler; /* interrupt handler */ }; 

Bei der Initialisierung registriert der Treiber sein GerĂ€t ĂŒber die Funktion dev_register. Mit anderen Worten, fĂŒgen Sie der Ringliste ein neues GerĂ€t hinzu:

 extern void dev_register(struct dev_t* dev) { struct clist_head_t* entry; struct dev_t* device; /* create list entry */ entry = clist_insert_entry_after(&dev_list, dev_list.head); device = (struct dev_t*)entry->data; /* fill data */ strncpy(device->name, dev->name, sizeof(dev->name)); device->base_r = dev->base_r; device->base_w = dev->base_w; device->read_cb = dev->read_cb; device->write_cb = dev->write_cb; device->ioctl_cb = dev->ioctl_cb; device->ih_list.head = dev->ih_list.head; device->ih_list.slot_size = dev->ih_list.slot_size; } 

Damit dies alles irgendwie funktioniert, brauchen wir das Rudiment des Dateisystems. ZunĂ€chst werden wir nur Dateien fĂŒr ZeichengerĂ€te haben.

Das heißt, Das Öffnen der Datei entspricht dem Erstellen einer DATEI-Struktur aus stdio fĂŒr die entsprechende Treiberdatei.

In diesem Fall stimmen die Dateinamen mit dem GerĂ€tenamen ĂŒberein. Wir definieren das Konzept eines Dateideskriptors in unserer C-Bibliothek (stdio.h).

 struct io_buf_t { int fd; /* file descriptor */ char* base; /* buffer beginning */ char* ptr; /* position in buffer */ bool is_eof; /* whether end of file */ void* file; /* file definition */ }; #define FILE struct io_buf_t 

Lassen Sie der Einfachheit halber alle geöffneten Dateien vorerst in einer Ringliste gespeichert werden. Das Listenelement wird wie folgt beschrieben:

 extern struct file_t { struct clist_head_t list_head; /* should be at first */ struct io_buf_t io_buf; /* file handler */ char name[8]; /* file name */ int mod_rw; /* whether read or write */ struct dev_t* dev; /* whether device driver */ }; 

FĂŒr jede geöffnete Datei speichern wir einen Link zum GerĂ€t. Wir implementieren eine Ringliste offener Dateien und implementieren die Systemaufrufe read / write / ioctl.

Beim Öffnen einer Datei mĂŒssen wir nur die Anfangspositionen der Lese- und Schreibpuffer vom Treiber der io_buf_t-Struktur zuweisen und dementsprechend dem GerĂ€te-Treiber DateivorgĂ€nge zuordnen.

 extern struct io_buf_t* file_open(char* path, int mod_rw) { struct clist_head_t* entry; struct file_t* file; struct dev_t* dev; /* try to find already opened file */ entry = clist_find(&file_list, file_list_by_name_detector, path, mod_rw); file = (struct file_t*)entry->data; if (entry != null) { return &file->io_buf; } /* create list entry */ entry = clist_insert_entry_after(&file_list, file_list.head); file = (struct file_t*)entry->data; /* whether file is device */ dev = dev_find_by_name(path); if (dev != null) { /* device */ file->dev = dev; if (mod_rw == MOD_R) { file->io_buf.base = dev->base_r; } else if (mod_rw == MOD_W) { file->io_buf.base = dev->base_w; } } else { /* fs node */ file->dev = null; unreachable(); /* fs in not implemented yet */ } /* fill data */ file->mod_rw = mod_rw; file->io_buf.fd = next_fd++; file->io_buf.ptr = file->io_buf.base; file->io_buf.is_eof = false; file->io_buf.file = file; strncpy(file->name, path, sizeof(file->name)); return &file->io_buf; } 

Die Dateioperationen read / write / ioctl werden durch ein Muster definiert, wobei der Aufruf des Lesesystems als Beispiel dient.

Die Systemaufrufe, die wir in der letzten Lektion gelernt haben zu schreiben, rufen diese Funktionen einfach auf.

 extern size_t file_read(struct io_buf_t* io_buf, char* buff, u_int size) { struct file_t* file; file = (struct file_t*)io_buf->file; /* whether file is device */ if (file->dev != null) { /* device */ return file->dev->read_cb(&file->io_buf, buff, size); } else { /* fs node */ unreachable(); /* fs in not implemented yet */ } return 0; } 

Kurz gesagt, sie ziehen einfach RĂŒckrufe aus der GerĂ€tedefinition. Jetzt werden wir den Terminaltreiber schreiben.

Terminaltreiber


Wir benötigen einen Bildschirmausgabepuffer und einen Tastatureingabepuffer sowie einige Flags fĂŒr den Eingabe- und Ausgabemodus.

 static const char* tty_dev_name = TTY_DEV_NAME; /* teletype device name */ static char tty_output_buff[VIDEO_SCREEN_SIZE]; /* teletype output buffer */ static char tty_input_buff[VIDEO_SCREEN_WIDTH]; /* teletype input buffer */ char* tty_output_buff_ptr = tty_output_buff; char* tty_input_buff_ptr = tty_input_buff; bool read_line_mode = false; /* whether read only whole line */ bool is_echo = false; /* whether to put readed symbol to stdout */ 

Wir schreiben die Funktion zum Erstellen eines GerĂ€ts. Es werden einfach RĂŒckrufe von Dateioperationen und Handler der unteren HĂ€lfte von Unterbrechungen abgelegt, wonach das GerĂ€t in einer Ringliste registriert wird.

 extern void tty_init() { struct clist_head_t* entry; struct dev_t dev; struct ih_low_t* ih_low; memset(tty_output_buff, 0, sizeof(VIDEO_SCREEN_SIZE)); memset(tty_input_buff, 0, sizeof(VIDEO_SCREEN_WIDTH)); /* register teletype device */ strcpy(dev.name, tty_dev_name); dev.base_r = tty_input_buff; dev.base_w = tty_output_buff; dev.read_cb = tty_read; dev.write_cb = tty_write; dev.ioctl_cb = tty_ioctl; dev.ih_list.head = null; /* add interrupt handlers */ dev.ih_list.slot_size = sizeof(struct ih_low_t); entry = clist_insert_entry_after(&dev.ih_list, dev.ih_list.head); ih_low = (struct ih_low_t*)entry->data; ih_low->number = INT_KEYBOARD; ih_low->handler = tty_keyboard_ih_low; dev_register(&dev); } 

Der untere Interrupt-Handler fĂŒr die Tastatur ist wie folgt definiert:

 /* * Key press low half handler */ static void tty_keyboard_ih_low(int number, struct ih_low_data_t* data) { /* write character to input buffer */ char* keycode = data->data; int index = *keycode; assert(index < 128); char ch = keyboard_map[index]; *tty_input_buff_ptr++ = ch; if (is_echo && ch != '\n') { /* echo character to screen */ *tty_output_buff_ptr++ = ch; } /* register deffered execution */ struct message_t msg; msg.type = IPC_MSG_TYPE_DQ_SCHED; msg.len = 4; *((size_t *)msg.data) = (size_t)tty_keyboard_ih_high; ksend(TID_DQ, &msg); } 

Hier legen wir einfach das eingegebene Zeichen in den Tastaturpuffer. Am Ende registrieren wir den verzögerten Anruf des Handlers der oberen HÀlften der Tastaturunterbrechungen. Dies erfolgt durch Senden einer Nachricht (IPC) an den Kernel-Thread.

Der Kernel-Thread selbst ist ziemlich einfach:

 /* * Deferred queue execution scheduler * This task running in kernel mode */ void dq_task() { struct message_t msg; for (;;) { kreceive(TID_DQ, &msg); switch (msg.type) { case IPC_MSG_TYPE_DQ_SCHED: /* do deffered callback execution */ assert(msg.len == 4); dq_handler_t handler = (dq_handler_t)*((size_t*)msg.data); assert((size_t)handler < KERNEL_CODE_END_ADDR); printf(MSG_DQ_SCHED, handler); handler(msg); break; } } exit(0); } 

Mit ihm wird der Handler der oberen HĂ€lften des Tastaturinterrupts aufgerufen. Der Zweck besteht darin, ein Zeichen auf dem Bildschirm zu duplizieren, indem der Ausgabepuffer in den Videospeicher kopiert wird.

 /* * Key press high half handler */ static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); } 

Jetzt mĂŒssen die E / A-Funktionen selbst geschrieben werden, die aus DateivorgĂ€ngen aufgerufen werden.

 /* * Read line from tty to string */ static u_int tty_read(struct io_buf_t* io_buf, void* buffer, u_int size) { char* ptr = buffer; assert((size_t)io_buf->ptr <= (size_t)tty_input_buff_ptr); assert((size_t)tty_input_buff_ptr >= (size_t)tty_input_buff); assert(size > 0); io_buf->is_eof = (size_t)io_buf->ptr == (size_t)tty_input_buff_ptr; if (read_line_mode) { io_buf->is_eof = !strchr(io_buf->ptr, '\n'); } for (int i = 0; i < size - 1 && !io_buf->is_eof; ++i) { char ch = tty_read_ch(io_buf); *ptr++ = ch; if (read_line_mode && ch == '\n') { break; } } return (size_t)ptr - (size_t)buffer; } /* * Write to tty */ static void tty_write(struct io_buf_t* io_buf, void* data, u_int size) { char* ptr = data; for (int i = 0; i < size && !io_buf->is_eof; ++i) { tty_write_ch(io_buf, *ptr++); } } 

Zeichenweise Operationen sind nicht viel komplizierter und ich denke nicht, dass sie kommentiert werden mĂŒssen.

 /* * Write single character to tty */ static void tty_write_ch(struct io_buf_t* io_buf, char ch) { if ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff + 1 < VIDEO_SCREEN_SIZE) { if (ch != '\n') { /* regular character */ *tty_output_buff_ptr++ = ch; } else { /* new line character */ int line_pos = ((size_t)tty_output_buff_ptr - (size_t)tty_output_buff) % VIDEO_SCREEN_WIDTH; for (int j = 0; j < VIDEO_SCREEN_WIDTH - line_pos; ++j) { *tty_output_buff_ptr++ = ' '; } } } else { tty_output_buff_ptr = video_scroll(tty_output_buff, tty_output_buff_ptr); tty_write_ch(io_buf, ch); } io_buf->ptr = tty_output_buff_ptr; } /* * Read single character from tty */ static char tty_read_ch(struct io_buf_t* io_buf) { if ((size_t)io_buf->ptr < (size_t)tty_input_buff_ptr) { return *io_buf->ptr++; } else { io_buf->is_eof = true; return '\0'; } } 

Es bleibt nur die Steuerung der Eingabe- und Ausgabemodi zur Implementierung von ioctl.

 /* * Teletype specific command */ static void tty_ioctl(struct io_buf_t* io_buf, int command) { char* hello_msg = MSG_KERNEL_NAME; switch (command) { case IOCTL_INIT: /* prepare video device */ if (io_buf->base == tty_output_buff) { kmode(false); /* detach syslog from screen */ tty_output_buff_ptr = video_clear(io_buf->base); io_buf->ptr = tty_output_buff_ptr; tty_write(io_buf, hello_msg, strlen(hello_msg)); video_flush(io_buf->base); io_buf->ptr = tty_output_buff_ptr; } else if (io_buf->base == tty_input_buff) { unreachable(); } break; case IOCTL_CLEAR: if (io_buf->base == tty_output_buff) { /* fill output buffer with spaces */ tty_output_buff_ptr = video_clear(io_buf->base); video_flush(io_buf->base); io_buf->ptr = tty_output_buff_ptr; } else if (io_buf->base == tty_input_buff) { /* clear input buffer */ tty_input_buff_ptr = tty_input_buff; io_buf->ptr = io_buf->base; io_buf->is_eof = true; } break; case IOCTL_FLUSH: /* flush buffer to screen */ if (io_buf->base == tty_output_buff) { video_flush(io_buf->base); } else if (io_buf->base == tty_input_buff) { unreachable(); } break; case IOCTL_READ_MODE_LINE: /* read only whole line */ if (io_buf->base == tty_input_buff) { read_line_mode = true; } else if (io_buf->base == tty_output_buff) { unreachable(); } break; case IOCTL_READ_MODE_ECHO: /* put readed symbol to stdout */ if (io_buf->base == tty_input_buff) { is_echo = true; } else if (io_buf->base == tty_output_buff) { unreachable(); } break; default: unreachable(); } } 

Jetzt implementieren wir die Ausgabe von Dateieingaben auf der Ebene unserer Bibliothek C.

 /* * Api - Open file */ extern FILE* fopen(const char* file, int mod_rw) { FILE* result = null; asm_syscall(SYSCALL_OPEN, file, mod_rw, &result); return result; } /* * Api - Close file */ extern void fclose(FILE* file) { asm_syscall(SYSCALL_CLOSE, file); } /* * Api - Read from file to buffer */ extern u_int fread(FILE* file, char* buff, u_int size) { return asm_syscall(SYSCALL_READ, file, buff, size); } /* * Api - Write data to file */ extern void fwrite(FILE* file, const char* data, u_int size) { asm_syscall(SYSCALL_WRITE, file, data, size); } 

Nun, hier sind einige Funktionen auf hoher Ebene:

 /* * Api - Print user message */ extern void uvnprintf(const char* format, u_int n, va_list list) { char buff[VIDEO_SCREEN_WIDTH]; vsnprintf(buff, n, format, list); uputs(buff); } /* * Api - Read from file to string */ extern void uscanf(char* buff, ...) { u_int readed = 0; do { readed = fread(stdin, buff, 255); } while (readed == 0); buff[readed - 1] = '\0'; /* erase new line character */ uprintf("\n"); uflush(); } 

Um das bisherige Lesen des Formats nicht zu tĂ€uschen, lesen wir immer nur in die Zeile, als ob das% s-Flag gegeben wĂ€re. Ich war zu faul, um einen neuen Aufgabenstatus einzufĂŒhren, um auf Dateideskriptoren zu warten, also versuchen wir einfach, etwas in einer Endlosschleife zu lesen, bis wir erfolgreich sind.

Das ist alles. Jetzt können Sie Treiber sicher an Ihrem Kernel befestigen!

Referenzen


Weitere Informationen finden Sie im Video-Tutorial .

→ Quellcode im Git-Repository (Sie benötigen eine Lektion8-Verzweigung)

Referenzliste


  1. James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
  2. Zubkov. Assembler fĂŒr DOS, Windows, Unix
  3. Kalaschnikow. Assembler ist einfach!
  4. Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
  5. Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.

Source: https://habr.com/ru/post/de468509/


All Articles