Desarrollo de sistemas operativos tipo Unix: controladores de dispositivos de caracteres (8)

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

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

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

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

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

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

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

El controlador de interrupción inferior para el teclado se define de la siguiente manera:

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

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:

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

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.

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

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

Las operaciones de personaje por personaje no son mucho más complicadas y no creo que necesiten comentar.

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

Solo queda controlar los modos de entrada y salida para implementar 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(); } } 

Ahora implementamos salida de entrada de archivo a nivel de nuestra biblioteca 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); } 

Bueno, aquí hay algunas funciones de alto nivel:

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

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


  1. James Molloy Haga rodar su propio sistema operativo de clones UNIX de juguete.
  2. Zubkov Ensamblador para DOS, Windows, Unix
  3. Kalashnikov. ¡Ensamblador es fácil!
  4. Tanenbaum Sistemas operativos Implementación y desarrollo.
  5. Robert Love Kernel de Linux Descripción del proceso de desarrollo.

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


All Articles