Développement de système d'exploitation de type Unix - Pilotes de périphériques de caractères (8)

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; /* 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 */ }; 

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; /* should be at first */ int number; /* interrupt number */ ih_low_cb_t handler; /* interrupt 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; /* 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; } 

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; /* 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 

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; /* 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 */ }; 

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; /* 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; } 

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; /* 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; } 

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; /* 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 */ 

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)); /* 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); } 

Le gestionnaire d'interruption inférieur pour le clavier est défini comme suit:

 /* * 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); } 

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:

 /* * 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); } 

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.

 /* * Key press high half handler */ 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.

 /* * 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++); } } 

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.

 /* * 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'; } } 

Il ne reste plus qu'à contrôler les modes d'entrée et de sortie pour implémenter 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(); } } 

Maintenant, nous implémentons la sortie d'entrée de fichier au niveau de notre bibliothèque 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); } 

Eh bien, voici quelques fonctions de haut niveau:

 /* * 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(); } 

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


  1. James Molloy. Faites rouler votre propre système d'exploitation jouet UNIX-clone.
  2. Zubkov. Assembleur pour DOS, Windows, Unix
  3. Kalachnikov. L'assembleur est facile!
  4. Tanenbaum. Systèmes d'exploitation. Mise en œuvre et développement.
  5. Robert Love. Noyau Linux Description du processus de développement.

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


All Articles