En el artículo anterior, presentamos la multitarea. Hoy es hora de considerar el tema de los controladores de dispositivos de caracteres.
Específicamente, hoy escribiremos un controlador de terminal, un mecanismo para el procesamiento diferido de las interrupciones, y consideraremos el tema de los controladores para las mitades superior e inferior de las interrupciones.
Comenzamos creando una estructura de dispositivo, luego introducimos el soporte básico de E / S de archivos, consideremos la estructura y funciones de io_buf para trabajar con archivos de stdio.h.
Tabla de contenidos
Sistema de construcción (marca, gcc, gas). Arranque inicial (arranque múltiple). Lanzamiento (qemu). Biblioteca C (strcpy, memcpy, strext). Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Creación de la biblioteca en modo kernel y modo de aplicación de usuario. El registro del sistema del núcleo. Memoria de video Salida a la terminal (kprintf, kpanic, kassert). Memoria dinámica, montón (kmalloc, kfree). Organización de memoria y manejo de interrupciones (GDT, IDT, PIC, syscall). Excepciones Memoria virtual (directorio de páginas y tabla de páginas). Proceso. Planificador Multitarea Sistema de llamadas (kill, exit, ps).
Controladores de dispositivos de caracteres. Llamadas del sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).El sistema de archivos del kernel (initrd), elf y sus componentes internos. Sistema de llamadas (exec). Shell como un programa completo para el kernel. Modo de protección del usuario (anillo3). Segmento de estado de la tarea (tss).
Conductores de personajes
Todo comienza con la aparición de un dispositivo simbólico. Como recordará, en los controladores de dispositivo de Linux, la definición del dispositivo se veía así:
struct cdev *my_cdev = cdev_alloc( ); my_cdev->ops = &my_fops;
El punto principal es asignar al dispositivo la implementación de las funciones de E / S de archivo.
Nos las arreglaremos con una estructura, pero el significado será similar:
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; };
Cada dispositivo corresponde a la mitad de la lista de interrupciones que se llaman cuando se generan interrupciones.
En Linux, estas mitades se denominan superior, por el contrario, inferior (nivel inferior).
Personalmente, me pareció más lógico y accidentalmente recordé los términos al revés. Describimos cada elemento de la lista de mitades inferiores de interrupciones de la siguiente manera:
extern struct ih_low_t { struct clist_head_t list_head; int number; ih_low_cb_t handler; };
Tras la inicialización, el controlador registrará su dispositivo a través de la función dev_register, es decir, agregará un nuevo dispositivo a la lista de timbres:
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; }
Para que todo esto funcione de alguna manera, necesitamos el rudimento del sistema de archivos. Al principio, solo tendremos archivos para dispositivos de caracteres.
Es decir abrir el archivo será equivalente a crear una estructura de ARCHIVO desde stdio para el archivo del controlador correspondiente.
En este caso, los nombres de los archivos coincidirán con el nombre del dispositivo. Definimos el concepto de un descriptor de archivo en nuestra biblioteca C (stdio.h).
struct io_buf_t { int fd; char* base; char* ptr; bool is_eof; void* file; }; #define FILE struct io_buf_t
Para simplificar, deje que todos los archivos abiertos se almacenen en una lista de timbre por ahora. El elemento de la lista se describe de la siguiente manera:
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; };
Para cada archivo abierto, almacenaremos un enlace al dispositivo. Implementamos una lista de anillo de archivos abiertos e implementamos las llamadas al sistema de lectura / escritura / ioctl.
Al abrir un archivo, solo necesitamos asignar las posiciones iniciales de las memorias intermedias de lectura y escritura desde el controlador a la estructura io_buf_t y, en consecuencia, asociar las operaciones de archivo con el controlador del dispositivo.
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; }
Las operaciones de archivo de lectura / escritura / ioctl se definen mediante un patrón que utiliza la llamada al sistema de lectura como ejemplo.
Las mismas llamadas al sistema que aprendimos a escribir en la última lección simplemente llamarán a estas funciones.
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 resumen, simplemente extraerán devoluciones de llamada de la definición del dispositivo. Ahora escribiremos el controlador de terminal.
Conductor terminal
Necesitamos un búfer de salida de pantalla y un búfer de entrada de teclado, así como un par de banderas para los modos de entrada y salida.
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;
Escribimos la función de crear un dispositivo. Simplemente anula las devoluciones de llamadas de las operaciones de archivo y los controladores de las mitades inferiores de las interrupciones, después de lo cual registra el dispositivo en una lista de timbres.
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); }
El controlador de interrupción inferior para el teclado se define de la siguiente manera:
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); }
Aquí simplemente colocamos el carácter ingresado en el búfer del teclado. Al final, registramos la llamada diferida del controlador de las mitades superiores de las interrupciones del teclado. Esto se hace enviando un mensaje (IPC) al hilo del núcleo.
El hilo del núcleo en sí es bastante 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); }
Al usarlo, se llamará al controlador de las mitades superiores de la interrupción del teclado. Su propósito es duplicar un carácter en la pantalla copiando el búfer de salida a la memoria de video.
static void tty_keyboard_ih_high(struct message_t *msg) { video_flush(tty_output_buff); }
Ahora queda por escribir las funciones de E / S, llamadas desde las operaciones de archivo.
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++); } }
Las operaciones de personaje por personaje no son mucho más complicadas y no creo que necesiten comentar.
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'; } }
Solo queda controlar los modos de entrada y salida para implementar 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(); } }
Ahora implementamos salida de entrada de archivo a nivel de nuestra biblioteca 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); }
Bueno, aquí hay algunas funciones de alto nivel:
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(); }
Para no engañar con la lectura de formatos hasta ahora, siempre leeremos en la línea como si se hubiera dado el indicador% s. Fui demasiado vago como para presentar un nuevo estado de tarea para esperar los descriptores de los archivos, así que tratamos de leer algo en un bucle infinito hasta que lo logramos.
Eso es todo. ¡Ahora puede sujetar los controladores de forma segura a su núcleo!
Referencias
Mire el
video tutorial para más información.
→ Código fuente
en el repositorio de git (necesita la rama de la lección 8)
Referencias
- James Molloy Haga rodar su propio sistema operativo de clones UNIX de juguete.
- Zubkov Ensamblador para DOS, Windows, Unix
- Kalashnikov. ¡Ensamblador es fácil!
- Tanenbaum Sistemas operativos Implementación y desarrollo.
- Robert Love Kernel de Linux Descripción del proceso de desarrollo.