En el artículo anterior, aprendimos cómo trabajar con un espacio de direcciones virtual. Hoy agregaremos soporte multitarea.
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).El sistema de archivos del kernel (initrd), elf y sus componentes internos. Sistema de llamadas (exec).
Controladores de dispositivos de caracteres. Llamadas del sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
Shell como un programa completo para el kernel.
Modo de protección del usuario (anillo3). Segmento de estado de la tarea (tss).
Multitarea
Como recordará, cada proceso debe tener su propio espacio de direcciones.
Para hacer esto, debe contar las páginas utilizadas por el proceso.
Por lo tanto, debemos describir la memoria del proceso:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
Los procesos de alguna manera tienen que intercambiar información entre ellos. Para hacer esto, presentamos el concepto de un mensaje:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
Después de eso, debe describir el proceso en sí mismo como un elemento de una lista cíclica.
La lista cíclica aquí es conveniente porque en el planificador debe seleccionar la siguiente de cada tarea hasta que la lista esté vacía.
Con esto eliminamos el caso degenerado, lo que significa que simplificamos la lógica.
struct task_t { struct clist_head_t list_head; u_short tid; char name[8]; struct gp_registers_t gp_registers; struct op_registers_t op_registers; struct flags_t flags; u_int time; bool reschedule; u_short status; int msg_count_in; struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; void* kstack; void* ustack; struct task_mem_t task_mem; } attribute(packed);
Asignaremos una cuota a cada proceso, el número de interrupciones del temporizador después de las cuales se reprogramará el proceso.
#define TASK_QUOTA 3
A continuación, debe escribir una función para crear un nuevo proceso.
Si bien no tenemos niveles de privilegio, utilizaremos selectores de kernel.
Necesitaremos 2 pilas para el futuro, una para el modo de usuario y otra para el núcleo.
Estableceremos el estado inicial como TASK_UNINTERRUPTABLE para que la tarea no tenga tiempo de completarse antes de la inicialización completa.
extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; entry = clist_insert_entry_after(&task_list, task_list.head); task = (struct task_t*)entry->data; task->kstack = malloc(TASK_KSTACK_SIZE); task->ustack = malloc(TASK_USTACK_SIZE); task->tid = tid; task->name[0] = '\0'; task->status = TASK_UNINTERRUPTABLE; task->msg_count_in = 0; task->time = 0; memcpy(&task->task_mem, task_mem, sizeof(struct task_mem_t)); *(u32*)(&task->flags) = asm_get_eflags() | 0x200; memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); task->op_registers.cs = GDT_KCODE_SELECTOR; task->op_registers.ds = GDT_KDATA_SELECTOR; task->op_registers.ss = GDT_KSTACK_SELECTOR; task->op_registers.eip = (size_t)address; task->op_registers.cr3 = (size_t)task_mem->page_dir; task->op_registers.k_esp = (u32)task->kstack + TASK_KSTACK_SIZE; task->op_registers.u_esp = (u32)task->ustack + TASK_USTACK_SIZE; printf(MSG_SCHED_TID_CREATE, tid, (u_int)address, task->kstack, task->ustack); return true; }
El planificador eliminará el proceso si el estado del proceso es TASK_KILLING.
Al eliminar, solo necesitamos liberar las pilas, páginas asignadas para secciones de datos y códigos, y también destruir el directorio de páginas de proceso.
En general, para una buena pila de usuarios, puede asignar a través del administrador de memoria, pero por conveniencia, depurar mientras lo implementamos en el montón del núcleo, que siempre está en los directorios de la página.
extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; if (task->task_mem.pages_count > 0) { mm_phys_free_pages(task->task_mem.pages, task->task_mem.pages_count); task->task_mem.pages = null; task->task_mem.pages_count = 0; } if (task->task_mem.page_dir != null) { mmu_destroy_user_page_directory(task->task_mem.page_dir, task->task_mem.page_table); } clist_delete_entry(&task_list, (struct clist_head_t*)task); }
Ahora necesita escribir el planificador en sí.
Primero debemos entender si la tarea actual se está ejecutando o si esta es la primera ejecución.
Si la tarea se está ejecutando, debe verificar si su cuota se ha agotado.
Si es así, debe guardar su estado especificando explícitamente el indicador de interrupción de habilitación.
Después de eso, debe encontrar la siguiente tarea para la ejecución en el estado TASK_RUNNING.
A continuación, debe completar la tarea actual si está en el estado TASK_KILLING.
Después de eso, preparamos el marco de la pila para la siguiente tarea y cambiamos el contexto.
extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; if (current_task != null) { current_task->time += 1; if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; } current_task->time = 0; current_task->reschedule = false; current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(¤t_task->flags) = *(u32*)((size_t)ret_addr + 6) | 0x200; current_task->op_registers.u_esp = (size_t)ret_addr + 12; current_task->gp_registers.esp = current_task->op_registers.u_esp; memcpy(¤t_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } if (current_task) { next_task = task_get_next_by_status(TASK_RUNNING, current_task); } else { next_task = task_get_by_status(TASK_RUNNING); tss_set_kernel_stack(next_task->kstack); } assert(next_task != null); if (current_task && current_task->status == TASK_KILLING) { task_delete(current_task); } else { struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = (*(u16*)(&next_task->flags)) | 0x200; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.cs; next_task->op_registers.u_esp -= 4; *(u32*)(next_task->op_registers.u_esp) = next_task->op_registers.eip; next_task->gp_registers.esp = next_task->op_registers.u_esp; next_task->op_registers.u_esp -= sizeof(struct gp_registers_t); memcpy((void*)next_task->op_registers.u_esp, (void*)&next_task->gp_registers, sizeof(struct gp_registers_t)); current_task = next_task; printf(MSG_SCHED_NEXT, next_task->tid, next_task->op_registers.u_esp, *ret_addr, next_task->op_registers.eip); asm_switch_ucontext(next_task->op_registers.u_esp, next_task->op_registers.cr3); }
Queda por escribir una función de cambio de contexto de tarea.
Para hacer esto, descargue un nuevo directorio de página y restaure todos los registros generales, incluidas las banderas.
También debe cambiar a la pila de la siguiente tarea.
A continuación, debe regresar de la interrupción, ya que formamos un marco de pila especial para esto.
Por el hecho de que más adelante necesitaremos esto para cambiar los niveles de privilegio.
/* * Switch context (to kernel ring) * void asm_switch_kcontext(u32 esp, u32 cr3) */ asm_switch_kcontext: mov 4(%esp),%ebp # ebp = esp mov 8(%esp),%eax # eax = cr3 mov %cr0,%ebx # ebx = cr0 and $0x7FFFFFFF,%ebx # unset PG bit mov %ebx,%cr0 mov %eax,%cr3 or $0x80000001,%ebx # set PE & PG bits mov %ebx,%cr0 mov %ebp,%esp popal sti iretl
Implementamos el mecanismo de mensajería más simple entre procesos.
Cuando enviamos un mensaje a un proceso, debe descongelarse si está congelado porque está esperando mensajes.
Pero cuando recibimos un mensaje, debemos congelar el proceso si la cola de mensajes está vacía.
Después de eso, debe transferir el control al planificador.
extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; task = task_get_by_id(tid); task_pack_message(task, msg); if (task->status == TASK_INTERRUPTABLE) { task->status = TASK_RUNNING; } } extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; struct task_t* task_after; task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); if (task_before->msg_count_in == 0) { task_before->status = TASK_INTERRUPTABLE; } sched_yield(); task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); task_extract_message(task_after, msg); assert(msg != null); }
Ahora necesita escribir llamadas al sistema para trabajar con procesos desde aplicaciones de usuario.
Allí lanzaremos llamadas al sistema para enviar y recibir mensajes.
extern size_t ih_syscall(u_int* function) { size_t params_addr = ((size_t)function + sizeof(u_int)); size_t result = 0; struct task_t *current = sched_get_current_task(); printf(MSG_SYSCALL, *function, current->tid); asm_lock(); switch (*function) { case SYSCALL_KSEND: { u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { u_short tid = *(u_int*)params_addr; struct task_t* task = task_find_by_id(tid); if (task != null) { assert(task->tid == tid); task->status = TASK_KILLING; task->reschedule = true; result = true; } else { result = false; } break; } case SYSCALL_EXIT: { int errno = *(int*)params_addr; u_int tid = current->tid; printf(MSG_TASK_FINISHED, tid, errno); current->status = TASK_KILLING; sched_yield(); break; } case SYSCALL_TASK_LIST: { result = (size_t)task_get_task_list(); break; } default: unreachable(); } printf(MSG_SYSCALL_RET, *function); asm_unlock(); return result; }
Para usar las llamadas del sistema desde nuestra biblioteca C, necesitamos escribir una función que cause una interrupción del núcleo. En aras de la simplicidad, no utilizaremos comandos especiales de Intel, ya que no tenemos niveles de privilegio. Como argumento, pasaremos el número de la función del sistema y los argumentos que necesita.
/* * Call kernel * void asm_syscall(unsigned int function, ...) */ asm_syscall: push %ebx push %ebp mov %esp,%ebp mov %ebp,%ebx add $8,%ebx # skip registers add $4,%ebx # skip return address push %ebx # &function int $0x80 mov %ebp,%esp pop %ebp pop %ebx ret
Esto es suficiente para implementar la multitarea más simple sin el soporte de anillos de protección. ¡Ahora abre el
video tutorial y mira todo en orden!
Referencias
Detalles y explicaciones en el
video tutorial .
El código fuente
en el repositorio de git (necesita la rama de la lección 7).
Referencias
1. James Molloy. Haga rodar su propio sistema operativo de clones UNIX de juguete.
2. Dientes. 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.