No artigo anterior, aprendemos como trabalhar com espaço de endereço virtual. Hoje vamos adicionar suporte a multitarefa.
Sumário
Construa o sistema (marca, gcc, gás). Inicialização inicial (inicialização múltipla). Iniciar (qemu). Biblioteca C (strcpy, memcpy, strext).
Biblioteca C (sprintf, strcpy, strcmp, strtok, va_list ...). Construindo a biblioteca no modo kernel e no modo de aplicativo do usuário.
O log do sistema do kernel. Memória de vídeo Saída para o terminal (kprintf, kpanic, kassert).
Memória dinâmica, heap (kmalloc, kfree).
Organização da memória e manipulação de interrupções (GDT, IDT, PIC, syscall). Exceções
Memória virtual (diretório e tabela de páginas).
Processo. Planejador Multitarefa. Chamadas do sistema (interrupção, saída, ps).O sistema de arquivos do kernel (initrd), elf e seus internos. Chamadas do sistema (exec).
Drivers de dispositivo de caracteres. Chamadas do sistema (ioctl, fopen, fread, fwrite). Biblioteca C (fopen, fclose, fprintf, fscanf).
Shell como um programa completo para o kernel.
Modo de proteção do usuário (anel3). Segmento de status da tarefa (tss).
Multitarefa
Como você se lembra, cada processo deve ter seu próprio espaço de endereço.
Para fazer isso, você precisa contar as páginas usadas pelo processo.
Portanto, devemos descrever a memória do processo:
struct task_mem_t { void* pages; u_int pages_count; void* page_dir; void* page_table; };
Os processos de alguma forma precisam trocar informações entre si. Para fazer isso, apresentamos o conceito de uma mensagem:
struct message_t { u_short type; u_int len; u8 data[IPC_MSG_DATA_BUFF_SIZE]; };
Depois disso, você precisa descrever o processo em si como um elemento de uma lista circular.
A lista cíclica aqui é conveniente porque no planejador você precisa selecionar o próximo de cada tarefa até que a lista esteja vazia.
Com isso, removemos o caso degenerado, o que significa que simplificamos a 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);
Alocaremos uma cota para cada processo, o número de interrupções do cronômetro após o qual o processo será reagendado.
#define TASK_QUOTA 3
Em seguida, você precisa escrever uma função para criar um novo processo.
Embora não tenhamos níveis de privilégio, usaremos seletores de kernel.
Vamos precisar de 2 pilhas para o futuro, uma para o modo de usuário e outra para o kernel.
Definiremos o status inicial como TASK_UNINTERRUPTABLE para que a tarefa não tenha tempo para concluir antes da inicialização 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; }
O processo será excluído pelo planejador se o status do processo for TASK_KILLING.
Ao excluir, basta liberar as pilhas, as páginas alocadas para as seções de dados e código e também destruir o diretório das páginas do processo.
Em geral, para uma boa pilha de usuários, você pode alocar através do gerenciador de memória, mas por conveniência, depurando enquanto a implementamos no heap do kernel, que está sempre nos diretórios da 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); }
Agora você precisa escrever o próprio planejador.
Primeiro, precisamos entender se a tarefa atual está em execução ou se é a primeira.
Se a tarefa estiver sendo executada, você precisará verificar se sua cota foi esgotada.
Nesse caso, você precisa salvar seu estado especificando explicitamente o sinalizador de interrupção de ativação.
Depois disso, você precisa encontrar a próxima tarefa para execução no status TASK_RUNNING.
Em seguida, você precisará concluir a tarefa atual se ela estiver no status TASK_KILLING.
Depois disso, preparamos o quadro da pilha para a próxima tarefa e alternamos o 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); }
Resta escrever uma função de alternância do contexto da tarefa.
Para fazer isso, você precisa baixar um novo diretório de páginas e restaurar todos os registros gerais, incluindo sinalizadores.
Você também precisa mudar para a pilha da próxima tarefa.
Em seguida, é necessário retornar a interrupção, pois formamos um quadro de pilha especial para isso.
Pelo fato de que mais tarde precisaremos disso para alterar os níveis de privilégio.
/* * 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 o mecanismo mais simples de mensagens entre processos.
Quando enviamos uma mensagem para um processo, ele precisa ser descongelado se estiver congelado porque está aguardando mensagens.
Mas quando recebemos uma mensagem, devemos congelar o processo se a fila de mensagens estiver vazia.
Depois disso, você precisa transferir o controle para o planejador.
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); }
Agora você precisa escrever chamadas do sistema para trabalhar com processos de aplicativos do usuário.
Lá, faremos chamadas do sistema para enviar e receber mensagens.
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 as chamadas de sistema da nossa biblioteca C, precisamos escrever uma função que cause uma interrupção do kernel. Por uma questão de simplicidade, não usaremos comandos especiais da Intel, pois não temos níveis de privilégio. Como argumento, passaremos o número da função do sistema e os argumentos necessários.
/* * 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
Isso é suficiente para implementar a multitarefa mais simples sem o suporte de anéis de proteção. Agora abra o
tutorial em vídeo e assista tudo em ordem!
Referências
Detalhes e explicações no
tutorial em
vídeo .
O código fonte
no repositório git (você precisa da ramificação lição7).
Referências
1. James Molloy. Role seu próprio sistema operacional clone do UNIX de brinquedo.
2. Dentes. Assembler para DOS, Windows, Unix
3. Kalashnikov. Assembler é fácil!
4. Tanenbaum. Sistemas operacionais. Implementação e desenvolvimento.
5. Robert Love. Kernel Linux Descrição do processo de desenvolvimento.