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; char name[8]; void* base_r; void* base_w; dev_read_cb_t read_cb; dev_write_cb_t write_cb; dev_ioctl_cb_t ioctl_cb; struct clist_definition_t ih_list; };
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; int number; ih_low_cb_t 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; entry = clist_insert_entry_after(&dev_list, dev_list.head); device = (struct dev_t*)entry->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; char* base; char* ptr; bool is_eof; void* file; }; #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; struct io_buf_t io_buf; char name[8]; int mod_rw; struct dev_t* dev; };
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; 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; } entry = clist_insert_entry_after(&file_list, file_list.head); file = (struct file_t*)entry->data; dev = dev_find_by_name(path); if (dev != null) { 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 { file->dev = null; unreachable(); } 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; if (file->dev != null) { return file->dev->read_cb(&file->io_buf, buff, size); } else { unreachable(); } 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; static char tty_output_buff[VIDEO_SCREEN_SIZE]; static char tty_input_buff[VIDEO_SCREEN_WIDTH]; char* tty_output_buff_ptr = tty_output_buff; char* tty_input_buff_ptr = tty_input_buff; bool read_line_mode = false; bool is_echo = false;
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)); 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; 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:
static void tty_keyboard_ih_low(int number, struct ih_low_data_t* data) { 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') { *tty_output_buff_ptr++ = ch; } 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:
void dq_task() { struct message_t msg; for (;;) { kreceive(TID_DQ, &msg); switch (msg.type) { case IPC_MSG_TYPE_DQ_SCHED: 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.
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.
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; } 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.
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') { *tty_output_buff_ptr++ = ch; } else { 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; } 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.
static void tty_ioctl(struct io_buf_t* io_buf, int command) { char* hello_msg = MSG_KERNEL_NAME; switch (command) { case IOCTL_INIT: if (io_buf->base == tty_output_buff) { kmode(false); 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) { 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) { tty_input_buff_ptr = tty_input_buff; io_buf->ptr = io_buf->base; io_buf->is_eof = true; } break; case IOCTL_FLUSH: 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: 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: 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.
extern FILE* fopen(const char* file, int mod_rw) { FILE* result = null; asm_syscall(SYSCALL_OPEN, file, mod_rw, &result); return result; } extern void fclose(FILE* file) { asm_syscall(SYSCALL_CLOSE, file); } extern u_int fread(FILE* file, char* buff, u_int size) { return asm_syscall(SYSCALL_READ, file, buff, size); } 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:
extern void uvnprintf(const char* format, u_int n, va_list list) { char buff[VIDEO_SCREEN_WIDTH]; vsnprintf(buff, n, format, list); uputs(buff); } extern void uscanf(char* buff, ...) { u_int readed = 0; do { readed = fread(stdin, buff, 255); } while (readed == 0); buff[readed - 1] = '\0'; 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
- James Molloy. Rollen Sie Ihr eigenes UNIX-Klon-Betriebssystem.
- Zubkov. Assembler fĂŒr DOS, Windows, Unix
- Kalaschnikow. Assembler ist einfach!
- Tanenbaum. Betriebssysteme. Implementierung und Entwicklung.
- Robert Love. Linux-Kernel Beschreibung des Entwicklungsprozesses.