Notas da Aula 3 - Fundamentos de Sistemas Operacionais 1. Problemas de Segurança A multiprogramação traz uma série de benefícios, como a melhora do desempenho do sistema e a redução do tempo de resposta (quando utilizada com o sistema de timesharing). No entanto, existem algumas questões importantes relacionadas a segurança que devem ser levadas em consideração em um sistema no qual os recursos são compartilhados por vários processos. Quando um processo de um usuário está no estado executando ele tem controle sobre o processador. Logo, ele pode executar instruções, manipular o valor dos registradores, ter acesso a barramentos, etc. Da mesma forma, ele tem acesso à memória: pode escrever e ler valores. Como então evitar que o processo atualmente em execução consiga: 1. Monopolizar o processador? Isto é, como forçar que o processo eventualmente devolva o controle da execução para o SO? 2. Monopolizar acesso a dispositivos de E/S? Ou seja, como fazer com que processos não usem exclusivamente estes dispositivos? 3. Ler informações guardadas no espaço de endereçamento de outros processos? 4. Alterar (escrever) informações guardadas no espaço de endereçamento de outros processos? Os dois primeiros itens são relacionados ao compartilhamento justo de recursos. Se de alguma forma um processo puder monopolizar o processador, por exemplo, sistemas multiusuário provavelmente não funcionariam. Bastaria que um único usuário decidisse “sequestrar” o processador e os demais usuários não teriam mais acesso à maquina. Os dois últimos itens se relacionam a confidencialidade e integridade dos dados contidos em memória. Suponha que um processo em execução pudesse ler a memória de outro processo qualquer. Isto significa que, se um usuário do sistema está atualmente acessando sua conta de banco através da máquina, outro usuário poderia capturar todas as informações (senhas, saldos, movimentações, etc) apenas escrevendo um programa que fizesse leitura da memória. Talvez até mais sério, se um processo pudesse escrever na memória de outro processo, um usuário poderia realizar transações na conta bancária do outro, apenas manipulando valores de variáveis. Estas questões, portanto, trazem os seguintes requisitos de segurança a um Sistema Operacional: 1. O SO deve ser capaz de retomar o controle do processador, uma vez que um processo seja colocado para executar. 2. O SO deve ser capaz de impedir acessos de memória a posições externas ao espaço de endereçamento de um processo. 3. O SO deve ser capaz de impedir acesso direto a dispositivos de E/S por parte dos processos dos usuários. Infelizmente, em sistemas monoprocessados, o SO não tem como realizar qualquer tipo de ação, uma vez que um processo seja colocado para executar. Isso porque qualquer ação de controle depende da utilização do processador (que por hipótese está em posse do processo). Logo, a solução para estes problemas de segurança precisa obrigatoriamente passar por algum tipo de suporte do hardware. Este suporte será dado através de 3 mecanismos básicos: 1. Temporizadores: o processador tem um relógio ou um contador que pode ser configurado para um valor determinado. Quando o tempo pré-configurado expira, uma interrupção é disparada. 2. Validação de endereços de memória: o processador tem um circuito auxiliar que verifica o valor do endereço de memória acessado a cada instrução. Se este valor for inválido, uma interrupção é disparada. 3. Instruções restritas: o processador tem um sub-conjunto de instruções cujo acesso é permitido apenas para o SO. Se algum processo de aplicação tenta executar uma destas instruções, uma interrupção é disparada. Os 3 mecanismos utilizam a funcionalidade de interrupções. Uma interrupção é um sinal recebido pelo processador que o avisa que determinados eventos ocorreram. Em geral, existem vários tipos de interrupções diferentes, representando a ocorrência de vários tipos distintos de eventos. O hardware reserva uma região específica da memória para guardar uma tabela de tratadores de interrupção. Cada entrada desta tabela corresponde a uma interrupção específica e contém o endereço de memória inicial de uma rotina de tratamento da interrupção. Ao ser carregado, é tarefa do SO preencher as entradas desta tabela com os endereços das suas rotinas de tratamento de interrupção. Quando uma interrupção é detectada pelo processador, ele aborta a execução atual, salva o contexto básico (valores de alguns registradores), consulta a tabela de tratadores de interrupção e passa a execução para o tratador adequando (faz o registrador PC apontar para o endereço da rotina). Os tratadores de interrupção são parte do SO. Logo, uma vez que a rotina de tratamento de interrupção é chamada, o controle do processador já está de volta com o Sistema Operacional. Daí para frente, o SO pode tomar as atitudes cabíveis para controlar a situação. Por exemplo, se o SO identifica que a interrupção foi causada pelo estouro do temporizador, ele pode decidir colocar um novo processo em execução. Existem três tipos básicos de interrupção: as interrupções de hardware, as interrupções de software e as interrupções geradas por erros de execução. Interrupções de hardware são aquelas disparadas por dispositivos de hardware, como uma placa de rede ou o temporizador do processador. Interrupções de software são causadas pela execução de uma instrução específica do processador, em geral denominada INT. Esta instrução pode ser executada por processos, quando necessitam da intervenção do SO. Por fim, as interrupções causadas por erros são aquelas que indicam que algo na execução de uma instrução não ocorreu corretamente. Por exemplo, uma instrução de divisão que recebe o denominador 0 pode disparar uma interrupção para alertar sobre a condição de erro. Interrupções podem ainda ter prioridades diferentes. Em geral, dispositivos de E/S diferentes apresentam prioridades diferentes. Por exemplo, a interrupção causada pela chegada de um novo pacote no buffer da placa de rede é provavelmente mais importante que a interrupção causada pela mudança de posição de um mouse. 2. Modos de Execução do Processador Para dar suporte aos requisitos de segurança dos Sistemas Operacionais, os processadores em geral apresentam mais de um modo de execução. Tradicionalmente, existem dois modos distintos: o modo supervisor e o modo usuário. O modo supervisor é aquele de maior privilégio. Um processo executando em modo supervisor tem acesso a todas as instruções do processador (o que significa acesso a todos os dispositivos de E/S) e a qualquer posição da memória principal. Este modo é o modo padrão, quando a máquina é ligada. A ideia é que o SO comece a ser executado neste modo e possa fazer todas as suas tarefas iniciais. Este modo também é ativado quando um tratador de interrupção é chamado. Ao longo de toda a execução da máquina, quando o núcleo do SO precisa ser executado, ele sempre é executado neste modo. Quando o SO coloca um processo de um usuário para execução, ele antes altera o modo de execução do processador (isto é permitido no modo supervisor). Quando o processo do usuário executa sua primeira instrução, o processador já está em modo usuário. No modo usuário, existem instruções que não são permitidas. Por exemplo, as instruções CLI e STI, que desativam e ativam as interrupções respectivamente, não são permitidas neste modo. Além disso, todo acesso à memória passa a ser verificado pelo processador, analisando se o endereço especificado pertence ou não ao espaço de endereçamento do processo. Processadores mais modernos às vezes implementam um esquema alternativo de diferenciação de modo de execução. Ao invés de utilizarem apenas dois modos, os processadores apresentam vários rings de execução. No ring 0, todos os recursos da máquina estão disponíveis. No ring 1, alguns recursos são restritos. Nos rings sucessivos, o acesso da máquina passa a ficar cada vez mais controlado. A objetivo original da utilização dos rings era a diferenciação de classes de processos. Este esquema dá suporte, por exemplo, ao SO eleger alguns processos que tem um pouco mais de privilégios que outros. Um exemplo disso são os device drivers, ou seja, processos que realizam a manipulação de algum dispositivo de hardware específico, como uma placa de som. No modelo de dois níveis de prioridade, estes device drivers precisam executar no modo supervisor, já que eles precisam de acesso a dispositivos de E/S. No modelo de rings, estes device drivers podem ser executados em um modo um pouco menos privilegiado, aumentando a segurança do SO. Em geral, no entanto, os Sistemas Operacionais atuais não empregam esta funcionalidade, continuando a utilizar basicamente dois modos (o ring 0 para tarefas do SO e o ring de mais baixo privilégio para os demais processos). No entanto, a existência destes vários níveis trouxe benefícios para a área de virtualização de hardware. Hoje, é possível executar uma instância de um Sistema Operacional dentro de outro SO através dos softwares de virtualização. Embora estes softwares não sejam novos, apenas recentemente eles se tornaram realmente eficientes. A ineficiência da virtualização era causada pelo excesso de chamadas ao SO nativo, mesmo para tarefas relativamente simples. Como o software de virtualização era executado em modo usuário, qualquer tarefa classificada como privilegiada requeria a intervenção do SO nativo. Com o advento dos rings de execução, o virtualizador (ou parte dele) pode ser executado em um modo com mais privilégios, evitando o excesso de chamadas ao SO nativo. 3. Proteção de E/S A proteção de acesso aos dispositivos de E/S é feita de maneira muito simples. Dado que os processos dos usuários executam em modo usuário, eles não têm acesso às instruções necessárias para comunicação com os dispositivos (instruções IN e OUT). Isso faz com que os processos sejam forçados a utilizar o SO como um intermediário na utilização de tais dispositivos. Quando um processo de um usuário precisa realizar uma operação de E/S, ele faz uma chamada de sistema ao SO. Essa chamada, geralmente é feita através de uma interrupção de software (instrução INT). A execução da instrução INT faz com que o processador interrompa a execução atual e passe a execução (agora em modo supervisor) para o tratador de interrupções do SO. O tratador de interrupções, então, verifica os parâmetros da chamada, identifica a operação e, caso seja possível, executa o serviço requisitado. Possivelmente, o SO coloca o processo no estado bloqueado, enquanto o mesmo aguarda o resultado da operação. Enquanto a operação é realizada, o SO pode colocar um novo processo em execução. Quando o dispositivo termina a operação, uma interrupção é gerada, o que coloca novamente o SO em execução. Desta vez, o SO identifica qual processo havia requisitado a operação, o desbloqueia e, de alguma forma, repassa os resultados da operação. 4. Proteção de Memória Quando um processo executa em modo usuário, cada acesso à memória é verificado pelo processador antes da execução propriamente dita. Uma maneira de fazer isso é através de um sistema simples de duas comparações. Ao colocar um novo processo em execução, o SO configura o valor de dois registradores específicos: o registrador base e o registrador limite. A cada acesso à memória, um circuito auxiliar com dois comparadores faz as seguintes verificações: 1. Se o endereço a ser acessado é menor que o registrador base, uma interrupção é gerada. 2. Se o endereço a ser acessado é maior ou igual ao registrador limite, uma interrupção é gerada. Estes registradores, portanto, guardam os endereços dos extremos do espaço de endereçamento do processo. Quando o tratador de interrupção do SO é chamado para tratar este tipo de interrupção, ele se encarrega de tomar alguma atitude para corrigir o problema. Em geral, a ação tomada é simplesmente o encerramento do processo com algum código específico de erro. Para que este simples mecanismo funcione, é preciso que o espaço de endereçamento dos processos seja contíguo. Ou seja, o espaço de endereçamento do processo não pode conter “buracos”, ou estar espalhado em vários pedaços da memória. Como será visto no Capítulo 6, isso nem sempre é verdade, o que resulta na necessidade de mecanismos mais complexos. 5. Proteção do Processador O mecanismo de proteção do processador é bastante simples. Em um sistema baseado em timesharing, por exemplo, o SO escolhe um valor para a fatia de tempo, ou seja, a quantidade de tempo que cada processo tem para usar o processador. Ao colocar um novo processo em execução, o SO configura previamente um temporizador do processador para expirar ao término do slice. Se o processo termina sua execução antes do final do slice, ele simplesmente dispara uma interrupção de sotfware, que eventualmente passará a execução de volta para o SO. Por outro lado, se o slice do processo termina enquanto o processo ainda está executando, o temporizador gera uma interrupção de hardware que faz com que a execução volte ao SO. Neste caso, o SO pode decidir substituir o processo atualmente de posse do processador por outro processo na fila de aptos. Desta forma, o processo que estava no processador é colocado de volta na fila de aptos, aguardando pela oportunidade de receber um novo slice do processador para prosseguir seu processamento. Mesmo em um sistema que não utiliza timesharing (por exemplo, um sistema batch), o SO pode se utilizar do temporizador do processador para evitar que processos abusem do uso deste recurso. Por exemplo, o SO pode configurar o temporizador para, de tempos em temos, retomar o controle da execução e verificar se o tempo máximo de uso da máquina não foi ultrapassado (e.g., um processo executando por mais de uma semana é automaticamente terminado).