Desenvolvimento de sistema operacional Unix-like - Multitarefa e chamadas de sistema (7)

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:

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

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; /* message type */ u_int len; /* data length */ u8 data[IPC_MSG_DATA_BUFF_SIZE]; /* message data */ }; 

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.

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

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.

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

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.

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

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.

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

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.

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

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.

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

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


All Articles