. Limited-Direct-Execution
Esse documento representa parte dos meus estudos acerca do Livro - Operational Systems Three Easy Pieces, de dominio publico.
De maneira geral, ao compartilhar tempo da CPU com 2 ou mais processos, encontramos com diversos problemas. Entre eles: como podemos manter o controle da CPU, mesmo apos ele ter sido passado para o processo . Podemos ter um problema de performance, visto que determinados processos podem precisar ou nao de mais tempo de CPU tendo outros em vista. (por exemplo, um loop infinito dentro em alguma rotina do processo?)
Diante destas problematicas, podemos inferir que nao se pode apenas virtualizar a CPU sem criterio. Por isso, veremos algumas tecnicas para tal.
Limited Direct Execution
Esta tecnica eh bem simples, e direta, como o nome sugere. Apenas rodamos o programa requirido diretamente na CPU, ou seja, seguimos os seguintes passos:
- crie uma entrada na lista de processos
- aloque memoria para o programa
- carregue o programa na memoria principal.
- inicializa a run-time stack com os argumentos externos (e.g argc/argv)
- limpa os registradores
- executa a rotina principal do programa
- roda o programa
- retorna um status
- limpa a memoria utilizada
- remove o processo da lista
Este metodo, do jeito que esta, mantem a problematica supracitada.
Operacoes restritas
Neste sentido, o programa precisa ter camadas de protecao. Por exemplo, teremos escopos de processos designados de escopo/modo de usuario, enquanto ha outros executados em escopo/modo kernel. Desta maneira, podemos separar os escopos de maneira mais restritiva.
No entanto, um usuario, ora ou outra, precisa fazer algumas chamadas que estao no modo kernel. Para tal, dentro do user mode, podemos criar os ditos system calls.
Para poder performar estas chamadas, o programa deve executar um instrucao de armadilha (instruction trap), que eleva os privilegios de user para kernel, conquanto nao permite o programa um user mode acessar o kernel.
Desta forma, apenas a chamada para o kernel em si eh executada em modo kernel. Quando finalizada a operacao que precisa de privilegios, o sistema chama uma chamada return-from-trap e rebaixa o privilegio novamente.
Desta forma, precisamos salvar o estado atual do programa em user mode para podermos executar a chamada de kernel, por exemplo usando um kernel stack. Esta pilha salvara o estado dos registradores em user mode e, posteriormente, em kernel mode, podendo restaurar estados de maneira precisa durante o inicio e termino de uma trap.
Durante o tempo de boot, o SO cria uma trap table, a qual permite definir previamente a qualquer syscall (system calls ou chamadas de sistema/kernel), atraves dos trap handlers, que tipo de acao sera tomada de acordo com a syscall enviada ou excecao gerada. Assim, podemos garantir que nenhuma chamada para o sistema fara algo inesperado ou quebrara a execucao do programa de modo user, consequentemente ocasionando um problema ate em kernel.
exemplo de boot e execucao de programa
| (kernel mode) | Hardware | (user mode) |
|---|---|---|
| initialize trap table | ||
| remember address of syscall handler | ||
| Create entry for process list | ||
| Allocate memory for program | ||
| Load program into memory | ||
| Setup user stack with argv | ||
| Fill kernel stack with reg/PC | ||
| return-from-trap | ||
| restore regs | ||
| from kernel stack | ||
| move to user mode | ||
| ;; TODO |
um numero eh normalmente associado a uma syscall, para facil identificacao, um indice, o qual nunca eh exatamente referente a um endereco de memoria, visto que o usuario poderia enviar o endereco dentro da syscall e nao poderiamos interferir. Virtualizando ate o indice, podemos associar um endereco a um numero, podendo assim, validar a qualidade do numero recebido.
Alternando entre processos
Se um processo toma o lugar do SO na cpu, significa, obviamente, que o SO para de executar. Desta maneira, como eh possivel o SO ganhar novamente acesso a CPU?
Existe o metodo cooperativo, onde se espera que o programa faca, eventualmente, uma syscall (como pedir para abrir um arquivo ou gerar um erro). Entretanto, este metodo eh falho pois um programa pode so meter o louco e rodar um while(1) sem syscall infinitamente.
Outro metodo, simples e eficaz, eh criar um timer no hardware que envia uma interrupcao programada e da novamente o controle da CPU ao SO. Para isso, eh necessario salvar o estado atual do processo que esta em execucao, para nao perde-lo caso seja necessario retoma-lo.
Salvando e restaurando contexto
Quando conseguimos novamente o controle da cpu pelo SO, temos que decidir se o processo que estava atuando deve permanecer atuando. Esta decisao eh tomada pelo scheduler. Tomada a decisao (veremos em capitulos futuros como prosseguir com o scheduler), o SO executa uma serie de funcoes referenciadas como parte do context switch.
Este metodo (context switch) eh responsavel por, eh claro, alterar o estado do proximo e atual programas. Para isso, ele, ou salva em registradores o estado do programa atual e restaura o estado do proximo programa, ou apenas mantem o estado e continua o programa em execucao.
O context switch salva, por exemplo, o ponteiro da pilha do kernel do programa atual, assim como o PC (program counter), assim como alguns registradores padrao, e registra os dados desta stack como sendo o proximo programa a ser executado. Assim, o context switch pode escolher outro processo que estava na lista de processos para rodar, apenas buscando o kernel stack deste processo.
Bonus
[!tip] Syscalls em C
As syscalls em C, na verdade, sao apenas chamadas de funcao padrao da linguagem, entretanto, com algumas peculiaridades. Elas recebem e armazenam os argumentos e resultados da operacao em registradores e regioes especificas da memoria (trap) referente ao processo em si, atraves de um codigo assembly ja escrito. Desta forma, nao precisamos escrever chamadas ao sistema diretamente com assembly, por exemplo.

