Dans l'article précédent, nous avons introduit le multitâche. Aujourd'hui, il est temps d'examiner le sujet des pilotes de périphériques de caractères.
Plus précisément, nous allons aujourd'hui écrire un pilote de terminal, un mécanisme de traitement différé des interruptions, et examiner le sujet des gestionnaires pour les moitiés supérieure et inférieure des interruptions.
Nous commençons par créer une structure de périphérique, puis introduisons le support d'E / S de fichier de base, considérons la structure et les fonctions io_buf pour travailler avec des fichiers de stdio.h.
Table des matières
Construisez le système (make, gcc, gas). Démarrage initial (multiboot). Lancez (qemu). Bibliothèque C (strcpy, memcpy, strext). Bibliothèque C (sprintf, strcpy, strcmp, strtok, va_list ...). Construction de la bibliothèque en mode noyau et en mode application utilisateur. Le journal système du noyau. Mémoire vidéo Sortie vers le terminal (kprintf, kpanic, kassert). Mémoire dynamique, tas (kmalloc, kfree). Organisation de la mémoire et gestion des interruptions (GDT, IDT, PIC, syscall). Exceptions Mémoire virtuelle (répertoire de pages et table de pages). Processus. Planificateur Multitâche. Appels système (kill, exit, ps).
Pilotes de périphériques de caractères. Appels système (ioctl, fopen, fread, fwrite). Bibliothèque C (fopen, fclose, fprintf, fscanf).Le système de fichiers du noyau (initrd), elf et ses composants internes. Appels système (exec). Shell comme programme complet pour le noyau. Mode de protection utilisateur (ring3). Segment d'état de la tâche (tss).
Pilotes de personnage
Tout commence par l'apparition d'un dispositif symbolique. Comme vous vous en souvenez, dans les pilotes de périphériques Linux, la définition du périphérique ressemblait à ceci:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
L'essentiel est d'attribuer à l'appareil la mise en œuvre des fonctions d'E / S de fichiers.
Nous nous en sortirons avec une seule structure, mais la signification sera similaire:
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; };
Chaque périphérique correspond à la moitié de la liste des interruptions appelées lors de la génération des interruptions.
Sous Linux, ces moitiés sont appelées supérieures, au contraire inférieures (niveau inférieur).
Personnellement, cela me semblait plus logique et je me suis souvenu accidentellement des termes dans l'autre sens. Nous décrivons chaque élément de la liste des moitiés inférieures d'interruptions comme suit:
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
Lors de l'initialisation, le pilote enregistrera son périphérique via la fonction dev_register, en d'autres termes, ajoutera un nouveau périphérique à la liste de sonnerie:
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; }
Pour que tout cela fonctionne, nous avons besoin du rudiment du système de fichiers. Au début, nous n'aurons que des fichiers pour les appareils de caractères.
C'est-à-dire l'ouverture du fichier équivaudra à la création d'une structure FILE à partir de stdio pour le fichier de pilote correspondant.
Dans ce cas, les noms de fichiers correspondront au nom du périphérique. Nous définissons le concept d'un descripteur de fichier dans notre bibliothèque C (stdio.h).
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
Pour plus de simplicité, laissez tous les fichiers ouverts être stockés dans une liste en anneau pour l'instant. L'élément de liste est décrit comme suit:
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; };
Pour chaque fichier ouvert, nous enregistrerons un lien vers l'appareil. Nous implémentons une liste en anneau de fichiers ouverts et implémentons les appels système en lecture / écriture / ioctl.
Lors de l'ouverture d'un fichier, il nous suffit d'affecter les positions initiales des tampons de lecture et d'écriture du pilote à la structure io_buf_t et, en conséquence, d'associer les opérations sur les fichiers au pilote de périphérique.
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; }
Les opérations sur les fichiers en lecture / écriture / ioctl sont définies par un modèle utilisant l'exemple d'appel de système de lecture.
Les appels système que nous avons appris à écrire dans la dernière leçon appellent simplement ces fonctions.
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; }
En bref, ils tireront simplement les rappels de la définition de l'appareil. Nous allons maintenant écrire le pilote de terminal.
Pilote de terminal
Nous avons besoin d'un tampon de sortie d'écran et d'un tampon d'entrée de clavier, ainsi que de deux drapeaux pour les modes d'entrée et de sortie.
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;
Nous écrivons la fonction de création d'un appareil. Il supprime simplement les rappels des opérations sur les fichiers et les gestionnaires des moitiés inférieures d'interruptions, après quoi il enregistre le périphérique dans une liste de sonnerie.
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); }
Le gestionnaire d'interruption inférieur pour le clavier est défini comme suit:
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); }
Ici, nous mettons simplement le caractère entré dans le tampon du clavier. À la fin, nous enregistrons l'appel différé du processeur des moitiés supérieures des interruptions du clavier. Cela se fait en envoyant un message (IPC) au thread du noyau.
Le thread du noyau lui-même est assez simple:
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); }
En l'utilisant, le gestionnaire des moitiés supérieures de l'interruption du clavier sera appelé. Son but est de dupliquer un caractère à l'écran en copiant le tampon de sortie dans la mémoire vidéo.
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
Il reste maintenant à écrire les fonctions d'E / S elles-mêmes, appelées à partir d'opérations sur les fichiers.
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++); } }
Les opérations caractère par caractère ne sont pas beaucoup plus compliquées et je ne pense pas qu'elles aient besoin de commentaires.
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'; } }
Il ne reste plus qu'à contrôler les modes d'entrée et de sortie pour implémenter 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(); } }
Maintenant, nous implémentons la sortie d'entrée de fichier au niveau de notre bibliothèque 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); }
Eh bien, voici quelques fonctions de haut niveau:
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(); }
Afin de ne pas tromper avec la lecture de format jusqu'à présent, nous allons toujours simplement lire dans la ligne comme si l'indicateur% s était donné. J'étais trop paresseux pour introduire un nouveau statut de tâche pour attendre les descripteurs de fichiers, nous essayons donc de lire quelque chose dans une boucle infinie jusqu'à ce que nous réussissions.
C’est tout. Vous pouvez maintenant attacher des pilotes en toute sécurité à votre noyau!
Les références
Regardez le
didacticiel vidéo pour plus d'informations.
→ Code source
dans le référentiel git (vous avez besoin de la branche de leçon 8)
Les références
- James Molloy. Faites rouler votre propre système d'exploitation jouet UNIX-clone.
- Zubkov. Assembleur pour DOS, Windows, Unix
- Kalachnikov. L'assembleur est facile!
- Tanenbaum. Systèmes d'exploitation. Mise en œuvre et développement.
- Robert Love. Noyau Linux Description du processus de développement.