Desarrollo de sistema operativo tipo Unix: multitarea y llamadas al sistema (7)

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:

/* * Process memory description */ struct task_mem_t { void* pages; /* task physical pages */ u_int pages_count; /* task physical pages count */ void* page_dir; /* page directory */ void* page_table; /* 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; /* message type */ u_int len; /* data length */ u8 data[IPC_MSG_DATA_BUFF_SIZE]; /* message data */ }; 

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.

 /* * Process descriptor */ struct task_t { struct clist_head_t list_head; /* should be at first */ u_short tid; /* task id */ char name[8]; /* task name */ struct gp_registers_t gp_registers; /* general purpose registers */ struct op_registers_t op_registers; /* other purpose registers */ struct flags_t flags; /* processor flags */ u_int time; /* time of task execution */ bool reschedule; /* whether task need to be rescheduled */ u_short status; /* task status */ int msg_count_in; /* count of incomming messages */ struct message_t msg_buff[TASK_MSG_BUFF_SIZE]; /* task message buffer */ void* kstack; /* kernel stack top */ void* ustack; /* user stack top */ struct task_mem_t task_mem; /* task memory */ } 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.

 /* * Api - Create new task */ extern bool task_create(u_short tid, void* address, struct task_mem_t *task_mem) { struct task_t* task; struct clist_head_t* entry; /* allocate memory */ 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); /* fill data */ 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)); /* set flags */ *(u32*)(&task->flags) = asm_get_eflags() | 0x200; /* set general purpose registers */ memset(&task->gp_registers, 0, sizeof(struct gp_registers_t)); /* set other purpose registers */ 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.

 /* * Api - Delete task by id */ extern void task_delete(struct task_t* task) { printf(MSG_SCHED_TID_DELETE, (u_int)task->tid); assert(task != null); /* free stack memory */ free(task->kstack); free(task->ustack); task->kstack = null; task->ustack = null; /* free user pages memory */ 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; } /* clear resources */ 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.

 /* * Api - Schedule task to run */ extern void sched_schedule(size_t* ret_addr, size_t* reg_addr) { struct task_t* next_task = null; /* finish current task */ if (current_task != null) { /* update running time */ current_task->time += 1; /* check quota exceed */ if (current_task->time < TASK_QUOTA && !current_task->reschedule) { return; /* continue task execution */ } /* reset quota */ current_task->time = 0; current_task->reschedule = false; /* save task state */ current_task->op_registers.eip = *ret_addr; current_task->op_registers.cs = *(u16*)((size_t)ret_addr + 4); *(u32*)(&current_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(&current_task->gp_registers, (void*)reg_addr, sizeof(struct gp_registers_t)); } /* pick next task */ 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); /* whether should kill current task */ if (current_task && current_task->status == TASK_KILLING) { /* kill current task */ task_delete(current_task); } else { /* try to kill killing tasks */ struct task_t* task; task = task_find_by_status(TASK_KILLING); if (task) { task_delete(task); } } /* prepare context for the next 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)); /* update current task pointer */ current_task = next_task; /* run 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.

 /* * Api - Send message to task */ extern void ksend(u_short tid, struct message_t* msg) { struct task_t* task; /* get target task */ task = task_get_by_id(tid); /* put message to task buffer */ task_pack_message(task, msg); /* whether task has frozen */ if (task->status == TASK_INTERRUPTABLE) { /* defrost task */ task->status = TASK_RUNNING; } } /* * Api - Receive message to task * This function has blocking behaviour */ extern void kreceive(u_short tid, struct message_t* msg) { struct task_t* task_before; /* before yield */ struct task_t* task_after; /* after yield */ /* get current task */ task_before = sched_get_current_task(); assert(tid == task_before->tid); assert(task_before->status == TASK_RUNNING); /* whether task has not incomming messages */ if (task_before->msg_count_in == 0) { /* freeze task */ task_before->status = TASK_INTERRUPTABLE; } /* wait fot message */ sched_yield(); /* get current task */ task_after = sched_get_current_task(); assert(task_after == task_before); assert(tid == task_after->tid); assert(task_after->status == TASK_RUNNING); /* fetch message from buffer */ 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.

 /* * Api - Syscall handler */ 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(); /* handle syscall */ switch (*function) { case SYSCALL_KSEND: { /* send message */ u_short tid = *(u_int*)params_addr; ksend(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KRECEIVE: { /* receive message */ u_short tid = *(u_int*)params_addr; kreceive(tid, *(struct message_t**)(params_addr + 4)); break; } case SYSCALL_KILL: { /* kill task */ 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: { /* exit task */ 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: { /* get tasks 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.

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


All Articles