Reorganizando o UNIX para ser Confiável Jorrit N. Herder, Herbert Bos, Ben Gras, Philip Homburg, and Andrew S. Tanenbaum Computer Science Dept., Vrije Universiteit Amsterdam De Boelelaan 1081a, 1081 HV Amsterdam, The Netherlands Resumo Neste artigo, discutimos a arquitetura UNIX compatível com a geração de sistema MINIX 3, que oferece confiabilidade além da maioria dos outros sistemas. Com quase todo o sistema operacional em execução no modo usuário, serviços e drivers em cima de um kernel mínimo, o sistema está completamente dividido em corpatimentos. Movendo maior parte do código de usuário sem privilégios e processos de modo a restringir os poderes de cada um, nós ganhamos isolamento de falhas e limitamos os danos que bugs podem causar. Além disso, o sistema foi projetado para sobreviver e automaticamente recuperar-se de forma transparente de falhas em módulos mais críticos, como drivers de dispositivo. 1. INTRODUÇÃO Sistemas Operacionais são projetados com a expectativa de funcionar perfeitamente, mas infelizmente a maioria dos sistemas operacionais de hoje falham com muita frequencia. Como discutido da Seção 2, muitos problemas provem da concepção monolitica dos sistemas. Todas as funcionalidades do sistema operacional, por exemplo, são executadas em modo kernel sem isolamento a falhas adequado, assim qualquer erro pode potencialmente estragar todo o sistema. Acreditamos que reduzindo o kernel do sistema operacional e executando drivers e outros componentes do núcleo em modo usuário ajudaria a minimizar os danos causados por bugs de códigos. Essa estrutura , combinada com diversos mecanismos transparentes de recuperação de erros e outras falhas, resulta em um sistema operacional multitarefa altemanete confiável, que ainda se parece com o UNIX. Para melhorar nosso conhecimento, somos os primeiros a explorar desta maneira, com uma extrema decomposição do sistema operacional que é projetado para ser confiável, enquanto fornece uma performance razoável. Algumas idéias e tecnologias foram utilizadas durante um longo tempo, mas muitas vezes abandonadas por razões de desempenho. Nós acreditamos que chegou o momento de reconsiderar as escolhas que foram feitas no projeto dos sistemas operacionais comuns. 1.1 Contribuição A contribuição deste trabalho é o projeto e implementação de um sistema operacional que leva o conceito de multi-servidores para sua conclusão lógica afim de fornecer uma plataforma de computação comfiável. O objetivo concreto desta pesquisa é criar um sistema operacional UNIX que possa sobreviver a falhas de compomentes críticos de forma transparente, tais como drivers de dispositivos. Como mencionado anteriormente, a resposta veio quebrando o sistema em unidades gerenciáveis e através de um rígido controle de cada uma delas. O objetivo final é que um erro fatal, por exemplo um driver de dispositivo, não deva gerar uma falha no sistema operacional; assim, uma falha local deve ser detectada e falhando um componente, este deve ser automaticamente e transparentemente substituído por uma nova cópia, sem afetar os processos do usuário. 2. TRABALHOS RELACIONADOS Esta sessão dá uma visão geral do projeto, que tem os sistemas monolíticos em um extremo e nosso no outro. Brevemente começamos uma pequena discussão – vinda dos sistemas monolíticos e formas de confiabilidade. Então pesquisamos cada vez mais projetos modulares que acreditamos que ajudará a tornar os sistemas operacionais mais confiáveis. Costuma-se dizer que as máquinas virtuais e os exokernels propocionam isolamento e modularidade suficientes para fazer um sistema seguro. Contudo, estas tecnologias proporcionam uma interface para um sistema operacional, mas não representa um sistema completo por si só. O sistema operacional sobre uma máquina virtual, exokernel, ou hardware puro, pode ter uma das estruturas discutidas abaixo. 2.1 Ajustando a Confiabilidade de Sistemas Legados Kernels monolíticos proporcionam uma abstração rica e poderosa. Todos os serviços do sistema operacional são prestados por um simples programa que executa em modo kernel. Projetos Monolíticos tem alguns problemas de confiabilidade. Todo código do sistema operacional, por exemplo, executa com mais alto nível de privilégio sem o adequado isolamento de falhas, de modo que qualquer erro pode, potencialmente, comprometer o sistema operacional. Com milhões de linhas de código executável (LOC) e taxas de erro acima de 16 relatadas, ou 75 erros a cada 1000 (LOC), os sistemas monolíticos são propensos ao erro. Rodando sem confiabilidade, o código de terceiros dentro do kernel também compromete a confiabilidade do sistema, evidenciado pelo fato que a taxa de erro dos controladores de dispositvo é 3 a 7 vezes superior do que em qualquer outro código e 85% de todas as falhas do Windows são causadas por drivers. Um projeto importante para melhorar a confiabilidade de sistemas como Linux é o Nooks. O Nooks guarda os drivers de dispositvos dentro do kernel mas de forma transparente envolve-os em um tipo de capa protetora para que os erros de drivers não possam se propagar para outras partes do sistema operacional. Todo tráfego entre o driver e o resto do kernel é inspecioando pela camada de confiabilidade. Outro projeto usa maquinas virtuais para isolar os drivers de dispositivos do restante do sistema. Quando um driver é chamado, uma diferente máquina virtual é executada em relação a sistema principal para que todo um erro ou uma falha não polua o sitema principal. Além do isolamento, esta técnica permite a reutilização do driver do dispositivo quando experimentado com novos sistemas operacionais. Um projeto recente rodou drivers de dispositivos Linux em modo usuário com poucas mudanças no kernel do Linux. Este trabalho mostrou que os drivers podem ser isolados em separado dos porcessos modo usuários sem degradar significavelmente o desempenho. Embora isolar os drivers de dispositivos ajude a melhorar a confiabilidade do sistema operacional, acreditamos ser adequado, um projeto totalmente modular desde o início, dando melhores resultados. Isso inclui o encapsulamento de todos os componentes do sistema operacional independentemente, em processos do modo usuário. 2.2 Projetanto Novo Design Modular Em projetos modulares, o sistema operacional é dividido em um conjunto de servidores que colaboram. Código não confiável como drivers de dispositivo podem ser executadas independentemente dos módulos em modo usuário previnindo a difusão de falhas. Uma abordagem é executar o sistema operacional em um único servidor modo-usuário em cima de um microkernel. Alguns sistemas comerciais como Symbian OS e QNX [13] são também baseados em vários projetos de servidores, mas não usam uma extrema decomposição do sistema operacional como nós fazemos. No Symbian OS, por exemplo, apenas o servidor de arquivo e a rede de trabalho e telefonia são hospedados nos servidores modo-usuário, enquanto o kernel QNX ainda contém o gerenciamento de processos e outras funções que poderiam ter sido isoladas em separado aos processo de modo usuário. Um recente sistema multi-servidor devenvolvido pela Microsoft Pesquisas é o Singularidade (Singularity) [4]. Em contraste com outros sistemas, o Singularidade (Singularity) usa proteção da linguagem ao invés da proteção do hardware oferecido pela MMU. O sistema pode ser caracterizado como um microkernel rodando uma configuração verificável segura, servidor de software isolado. Embora linguagem segura possa ser viável para construção de sistemas confiáveis, Singularidade (Singularity) não está ainda compatível com as aplicações existentes. 3. NOSSA ARQUITETURA MULTI-SERVIDORES Em nosso projeto, chamado de MINIX 3, o sistema operacional executa como configuração de modo usuário de servidores e drivers sobre um pequeno núcleo, como ilustrado na Fig. 2. O kernel é responsável pelo baixo nível e operações privilegiadas, como a programação da CPU e MMU, tratamento de interrupções, e IPC, e comtém duas tarefas (SYS and CLOCK) para dar suporte as partes modo usuário do sistema operacional. O mais simples servidor, fornece as funcionalidades de sistema de arquivos (FS), gerenciador de processos (PM), e gerenciador de memória (MM). O armazenamento de dados (DS) é um pequeno servidor de banco de dados com funcionalidades publicadas. Finalmente, o servidor de ressucitação (RS) mantém o controle de todos os servidores e drivers e pode reparar de forma transparente o sistema quando determinadas falhas ocorrerem. Cada componente do nosso projeto uma pequena e bem definida entidade com poder e responsabilidade limitada, como na filosofia original do UNIX. O kernel consiste em menos de 4000 linhas de código executável (LoC) e o tamanho dos servidores aproximadamente 1000 a 3000 LoC por servidor, que facilita a compreensão e manutenção. O pequeno tamanho também torna prático a verificação do código de forma manual ou através do uso de ferramentas de verificação. Antes de nós continuarmos com a discussão dos componentes do núcleo, ilustraremos como nosso sistema operacional multi-servidor atualmente trabalha. Quatro exemplos são dados abaixo: (1) Uma aplicação que quer criar um processo filho chama o a função fork() da biblioteca, que envia uma mensagem de solicitação ao gerenciador de processo (PM). PM verifica que um processo aberto está disponível, solicita ao gerenciador de Memória (MM) para alocar memória e instrui o kernel (SYS) para criar uma cópia do processo. Finalmente, PM envia a resposta e retorna a função da biblioteca. Todas as mensagens passadas estão ocultas para a aplicação. (2) A read() ou write() chamada para fazer I/O, é enviada para FS. Se o bloco solicitado está no cache do buffer, FS solicita ao kernel (SYS) para copiá-lo para o usuário. De outra maneira ele envia uma menssagem para o controlador de disco solicitando para recuperar o bloco de disco. O controlador configura um alarme, comandando o controlador de disco através de um pedido de I/O para o kernel (SYS), e aguarda a interrupção de hardware ou tempo limite. (3) Servidores e drivers adicionais podem ser iniciados pela solicitação do sercidor de ressucitação (RS). RS então cria um novo processo, define os provilégios do processo no kernel (SYS) e, finalmente, executa o caminho dado dentro do processo filho (não é mostrado na figura). Informações sobre o novo processo do sistema é publicado no armazenador de dados (DS), que permite que as partes do sistema operacional subescrevam as atualizações de configuração do sistema. (4)Apesar de não ser chamada de um sistema, é interessante ver o que acontece se um usuário ou processo do sistema provoca uma execeção fatal, por exemplo, devido a um inválido ponteiro. Neste caso, o manipulador de exceção do kernel notifica PM, que transforma a exceção em um sinal ou mata o processo quando manipulador não é registrado. Recuperação de falhas em servidores e drivers é tratado pelo RS e é discutido abaixo. 3.1 O Kernel O kernel pode ser caracterizado como um verdadeiro microkernel e oferece operações de baixo nível que não podem ser feitas pelo modo-usuário sem privilégios. Primeiro, o kernel é responsável pela gestão dos recursos de baixo nível e interação com o hardware. Por exemplo, isso inclui manipulação de interrupção, programação da CPU e MMU, dispositivos I/O, e escalonamento de processos. 3.2 Os Servidores do Espaço do Usuário No topo do kernel, nós temos implementado um multi-servidor operando o sistema. Todos os servidores e drivers rodam independentemente dos procesos modo usuário e são altamente restritos no que podem fazer, apenas como aplicações de usuários normais. O Gerente de Processo (PM), juntamente com FS, PM implementa a interface que está disponível para programas de aplicações. PM é responsável pelo gerenciamento do processo tais como criação e remoção dos mesmos, atribuindo IDs aos processos e prioridade, e controlando o fluxo de execução. Além disso, a PM mantém a relação entre os processos, tais como grupo de processos e linhas de parentesco pai-filhos. Este último, por exemplo, tem consequencia de sinalizar os pais de processos terminados e contabilizar o tempo de CPU. Embora o kernel forneça mecanismos de baixo-nível, por exemplo, configurar os registradores da CPU, PM implementa toda a política de gerenciamento de processo. Como o kernel está preocupado com todos os processos similares; tudo que ele faz é programar a mais alta prioridade dos processos prontos. O gerenciamento de processos mais alto nível fornecido pelo PM é responsável pela aparência do UNIX e do nosso sistema. Gerente de Memoria (MM) Cada processo tem um segmento de texto, que pode ser compartilhado com outros processsos que executam o mesmo programa. A Processos do sistema podem ser concedido acesso adicional a segmentos de memória, tais como memória de vídeo ou memória RAM. Além disso, eles são autorizados a solicitar blocos de memória livre. O segmento de texto de todos os processos tem proteção de apenas leitura, o que torna a vunerabilidade de estouro de buffer mais difícil de ser explorada por vírus e worms, já que o código injetado não pode ser executado diretamente. Embora o kernel seja responsável por esconder os detalhes de hardware como programação do MMU, MM faz o gerenciamento da memória real. MM mantém uma lista com as regiões de memória livre, e pode alocar ou criar segmentos de memória para outros serviços do sistema. Atualmente MM está integrado no PM, mas o trabalho está em andamento para dividí-lo e colocá-lo para fora, oferecendo capacidade de memória virtual. Servidor de Arquivos (FS) gerencia os arquivos do sistema. É um servidor de arquivos comum manuseia chamadas como open() (abrir), read() (ler) e write() (escrever). Atualmente, nosso sistema oferece um sistema de arquivos – nosso próprio sistema de arquivos nativo – mas o trabalho está em andamento para transformar o FS em um servidor virtual de sistema de arquivos (VFS), que surporta múltiplos sistemas de arquivos, diferente do servidor de arquivos do sistema. Ambos VFS e cada servidor de arquivo será executado com um isolado, processo de modo usuário. Servidor de Ressucitação (RS) RS é o componente central responsável pelo gerenciamento de todos os servidores do sistema operacional e drivers. Enaquanto o PM é responsável pelo gerenciamento dos processos em geral, o RS trata apenas os processos privilegiados: servidores e drivers. Ele age como um guardião e garante a vida do sistema operacional. A administração do sistema de processos também é feito através do RS. Um programa utilitário, serviço, fornece ao usuário uma interface conveniente para RS. Ele permite que o administrador inicie e pare os serviços do sistema e redefina a politica que é executada em certo evento, incluindo erros de driver. Durante a inicialização do sistema RS adota todos os processos boot como seus filhos. Processos do sistema que são inicializados mais tarde, também tornam-se filhos do RS. Isso garante uma detecção de erros imediata. Além disso, RS pode verificar a atividade do sistema. RS faz uma chegagem periódica do status, e espera uma resposta no próximo período. A ausência de resposta causará a morte do processo. Sempre que um problema é detectado, RS pode substituir o componente com defeito por uma nova cópia, mas a ação tomada pode ser diferente para cada servidor e cada driver. A política associada pode reiniciar o componente que falhou, colocar a falha no registro do sistema, efetuar um backup (cópia de segurança) da imagem do núlceo do componente que falhou para uma posterior inspeção, enviar um e-mail para um sistema remoto administrador, ou outra coisa. Se a falha ocorrer novamente, um protocolo poderia ser usado para previnir a queda do sistema com repetidas recuperações. Amazenamento de Dados (DS) DS é um pequeno servidor de banco de dados que serve para dois propósitos. Primeiro, processos do sistema podem usá-lo para armazenar alguns dados particulares. Esta redundância é útil em função a tolerância a falhas. Uma reinicialização do serviço do sistema, por exemplo, pode pedir o estado atual que perdeu quanto caiu. Esses dados não são acessíveis ao público, mas apenas para os processos que os armazenaram. Segundo, o mecanismo de assinatura pública (publish-subscribe) é a ligação entre os componentes do sistema operacional. Drivers de Dispositivos – Todo os sistemas operacionais escondem o verdadeiro hardware embaixo da camada de drivers de dispositivos. Para começar e provar nossos princípios de trabalho na prática, temos implementados drivers para ATA, S-ATA, floppy, RAM, teclados, monitores, audio, impressoras, conexão serial, várias placas Ethernet, etc. Embora os drivers de dispositivos possam ser muito exigentes, tecnicamente, eles não são muito interessantes dentro do projeto do sistema operacional. O que é importante, porém, é que cada um dos nossos são executados como um processo modo-usuário independente para previnir que falhas se espalhem e torna-se fácil substituir drivers com falhas sem necessidade de reiniciar. Estamos conscientes que nem todos os erros podem ser resolvidos, reiniciando um driver que falhou, porém os principais bugs tendem a ser erros de tempo ou vazamentos memória em vez de erros de algorítmos, e fazem reiniciar o trabalho frequentemente. Além disso, nosso sistema pode tomar outras medidas, bem como, identificar o driver que é responsável pela falha e notificar um administrador remoto. 4. CONFIABILIDADE Um dos pontos fortes do nosso sistema é que ele move os drivers de dispositivos e outras funcionalidades do sistema operacional para fora do kernel dentro dos processos de modo-usuário sem privilégios e introduz barreiras protetoras entre todos os módulos. Esta forte compartimentação melhora a confiabilidade do sistema em vários aspectos [15, 16]. Falhas são devidamente isoladas e muitas vezes o sistema pode recuperar-se por reiniciar o componente que falhou ao invés de reiniciar o computador inteiro. 4.1 Isolamento de Falhas O kernel e o hardware MMU garante que os processos são totalmente isolados. Cada servidor e driver é encapsulado num endereço privado que é protegido pelo hardware MMU. Tentativas de acesso ilegal são capturadas, assim como para as aplicações de usuários. Processos podem trocar dados de forma controlada. O modo-usuário dos componentes do sistema operacional não executa com privilégios de super-usuário. Em vez disso, eles recebem usuário sem privilégios e um identificador de grupo para restringir o acesso ao sistema de arquivo e chamadas do sistema. Além disso, cada usuário, servidor, processo de driver tem uma política de restrição, de acordo com os princcípios de menos autoridade. As políticas são definidas pelo RS e o kernel as aplica em tempo de execução. Driver para acessar portas I/O e linhas de IRQ são atribuídos quando eles são iniciados. Desta forma, digamos, o driver da impressora tenta escrever nas portas I/O de discos, o kernel negará o acesso. 4.2 Flexibilidade das Falhas Enquanto nós não reivindicamos que nosso sistema está livre de erros, em muitos casos, podemos recuperar de falhas devido a erros de programação ou de tentativas de explorar vulnerabilidades, transparente às aplicações e sem intervenção do usuário. Como discutido na Seção 3, o servidor RS executa uma política quando detecta uma falha e pode substituir automaticamente um processo do sistema que falhou por uma nova cópia. O pressuposto de nossa abordagem é que falhas são transitórias e que reiniciar o componete permite solucionar o problema. A falha defininida que trata RS, como erros internos, falhas de sincronismo, envelecimento de bugs, e falhas de ataque. Erros internos significam que um processo do sistema encontra uma exceção, por exemplo, porque ele referencia um ponteiro inválido. EM princípio, RS guarda ambos os servidores e drivers, mas nosso sistema atualmente é projetado principalmente para lidar com falhas de drivers de dispositivos. Se um dos servidores do núcleo discutidos na seção anterior de falhar, a recuperação não é ainda possível e o sistema será prejudicado. Por exemplo, um queda de PM ou FS que juntos implementam afetará diretamente os programas de aplicação. No entanto, dado que, normalmente cerca de 70% dos sistemas operacionais consistem em drivers de dispositivos e que eles tem uma taxa de erro de 3 a 7 vezes maior que o código comum [7], nós abordamos uma importante classe de problemas com nosso projeto. 5. PERFORMACE Os sistemas modulares tem sido criticados por décadas porque seu desempenho apresenta problemas. Sistemas Multiservidor modernos, no entanto, tem provado que um desempenho competitivo pode ser realizado [3, 11]. Nós temos feito medições extensivas de nossos sistemas (um um Athlon 2.2 Ghz), mostrando que a sobrecarga de desempenho em comparação com os sistemas baseados em drivers no kernel está limitado a 5-10%. A chamada de sistema mais simples, getpid(), leva 1,011 microsegundos, que inclui passar duas mensagens e duas trocas de contexto. Reconstruir o sistema completo, que é um trabalho fortemente vinculado ao disco, tem uma sobrecarga de 7% em relação ao sistema básico com driver de dispositivo no kernel. 6. CONCLUSÕES Nossa principal contribuição de pesquisa apresentada neste artigo é que ralmente construímos um sistema operacional multi-servidor UNIX-compatível altamente confiável com uma perda de desempenho se somente 5% a 10%. Para alcançar alta confiabilidade reorganizamos o projeto monolítico que é comum a vários sistemas operacionais UNIX. Nosso projeto consiste em um pequeno kernel rodando o rodando o sistema operacional inteiro como um coleção independente, isolado, de processos modo-usuário. O kernel implementa apenas os mecanismos mínimos, tais como manipulação de interrupção, IPC, aplicação de política. 7. DISPONIBILIDADE O sistema é chamado MINIX 3, porque nós começamos com o MINIX 2 como base e então foi modificado muito fortemente. É gratuito, software de cófigo aberto, disponível através da Internet. Você pode baixar o MINIX 3 da página oficial em: http://www.minix3.org/, que também contém o código-fonte, documentação, notícias, contribuiu pacotes de software, e muito mais. Mais de 75.000 pessoas fizeram o download a imagem de CD-ROM desde o lançamento (Outubro de 2005), resultando em um grande e crescente comunidade de usuários que se comunica com o newgroup USENET comp.os.minix. MINIX 3 está sendo ativamente desenvolvidas, e sua ajuda e feedback são muito bem-vindos.