Introdução aos Sistemas Operacionais Eleri Cardozo EA876/EA879 - 2014 Sumário 1 2 Introdução 1.1 O que é um sistema operacional? . . . . . . . . . . . . . . . . 5 1.2 Sistema operacionais embarcados . . . . . . . . . . . . . . . . 7 1.3 Relacionamento com o hardware . . . . . . . . . . . . . . . . . 8 1.4 Componentes de um sistema operacional . . . . . . . . . . . . 10 1.5 Chamadas de sistema . . . . . . . . . . . . . . . . . . . . . . . 12 1.6 Sistemas operacionais e linguagens de programação . . . . . . 14 1.7 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Subsistema de Processos 17 2.1 Estrutura de um processo . . . . . . . . . . . . . . . . . . . . 17 2.2 Criação de processos . . . . . . . . . . . . . . . . . . . . . . . 22 2.3 Troca de contexto . . . . . . . . . . . . . . . . . . . . . . . . . 26 Escalonamento de processos 29 2.4 3 5 . . . . . . . . . . . . . . . . . . . 2.4.1 Processos interativos e servidores 2.4.2 Processos de tempo real . . . . . . . . . . . . 30 . . . . . . . . . . . . . . . . . 31 2.5 Sinalização de processos . . . . . . . . . . . . . . . . . . . . . 32 2.6 Comunicação e sincronização interprocesso . . . . . . . . . . . 36 2.6.1 Comunicação interprocesso . . . . . . . . . . . . . . . . 36 2.6.2 Sincronização interprocesso 38 2.6.3 O problema da inversão de prioridades . . . . . . . . . . . . . . . . . . . . . . . . 41 2.7 A tabela de processos . . . . . . . . . . . . . . . . . . . . . . . 42 2.8 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42 2.9 Exercícios 43 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Threads 3.1 Motivações para threads 3.2 Gerenciamento de threads 47 . . . . . . . . . . . . . . . . . . . . . 47 . . . . . . . . . . . . . . . . . . . . 48 1 3.3 3.4 4 Comunicação e sincronização interthreads 50 . . . . . . . . . . . 51 3.4.1 Variáveis de condição . . . . . . . . . . . . . . . . . . . 52 3.4.2 Threads e a chamada . . . . . . . . . . . . . . . . 54 3.4.3 Threads em sistemas de tempo real . . . . . . . . . . . 55 fork 3.5 Escalonamento de threads 3.6 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.7 Exercícios 57 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55 Subsistema de Memória 59 4.1 O mapeamento de endereços . . . . . . . . . . . . . . . . . . . 61 4.2 Unidade de gerenciamento de memória . . . . . . . . . . . . . 66 4.3 5 Utilização de threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.3.1 Paginação O princípio da localidade . . . . . . . . . . . . . . . . . 69 4.3.2 Gerenciamento do espaço virtual 4.3.3 Gerenciamento do espaço físico 4.3.4 Gerenciamento de páginas . . . . . . . . . . . . 72 . . . . . . . . . . . . . 72 . . . . . . . . . . . . . . . . 74 4.4 Paginação e sistemas de tempo real . . . . . . . . . . . . . . . 78 4.5 Comentários nais 79 4.6 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80 4.7 Exercícios 81 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Subsistema de Arquivos 84 5.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84 5.2 Dispositivos de armazenamento . . . . . . . . . . . . . . . . . 85 5.3 Gerenciamento do espaço físico . . . . . . . . . . . . . . . . . 86 5.4 Organização de arquivos . . . . . . . . . . . . . . . . . . . . . 87 5.5 Organização de diretórios . . . . . . . . . . . . . . . . . . . . . 87 Esquemas de cache de disco 5.6 . . . . . . . . . . . . . . . . . . . 89 5.6.1 O cache de buers . . . . . . . . . . . . . . . . . . . . 89 5.6.2 O cache de páginas . . . . . . . . . . . . . . . . . . . . 91 5.7 Arquivos mapeados em memória . . . . . . . . . . . . . . . . . 92 5.8 Suporte a múltiplos sistemas de arquivos . . . . . . . . . . . . 93 5.9 Conabilidade do sistema de arquivos . . . . . . . . . . . . . . 95 5.9.1 Journaling . . . . . . . . . . . . . . . . . . . . . . . . . 95 5.9.2 RAID . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 5.9.3 Vericadores de consistência . . . . . . . . . . . . . . . 97 5.9.4 Backups . . . . . . . . . . . . . . . . . . . . . . . . . . 99 2 5.10 Sistemas de arquivos e sistemas de tempo real . . . . . . . . . 99 5.11 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 5.12 Exercícios 6 7 Subsistema de Entrada e Saída 103 6.1 Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 6.2 Acionadores de dispositivos 6.3 Acesso direto à memória 6.4 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 6.5 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110 Virtualização 112 . . . . . . . . . . . . . . . . . . . 105 . . . . . . . . . . . . . . . . . . . . . 108 7.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112 7.2 Requisitos para virtualização . . . . . . . . . . . . . . . . . . . 113 7.3 Virtualização com suporte do hardware . . . . . . . . . . . . . 115 7.4 7.5 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 7.3.1 Virtualização da memória 7.3.2 Virtualização da CPU . . . . . . . . . . . . . . . . 115 7.3.3 Virtualização de entrada e saída . . . . . . . . . . . . . 119 . . . . . . . . . . . . . . . . . . 117 Modelos de virtualização . . . . . . . . . . . . . . . . . . . . . 121 7.4.1 Virtualização plena . . . . . . . . . . . . . . . . . . . . 122 7.4.2 Virtualização assistida pelo hardware . . . . . . . . . . 123 7.4.3 Paravirtualização . . . . . . . . . . . . . . . . . . . . . 124 7.4.4 Virtualização suportada pelo sistema operacional Exemplo de soluções de virtualização . . . 125 . . . . . . . . . . . . . . 126 7.5.1 VirtualBox . . . . . . . . . . . . . . . . . . . . . . . . 126 7.5.2 KVM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 7.5.3 Xen 7.5.4 LXC . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127 7.6 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128 7.7 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130 Segurança em Sistemas Operacionais 131 8.1 Introdução . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131 8.2 Ataques à Segurança 8.3 Criptograa . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133 8.4 Certicados e assinaturas digitais . . . . . . . . . . . . . . . . 135 8.5 Segurança do sistema operacional . . . . . . . . . . . . . . . . 138 8.5.1 . . . . . . . . . . . . . . . . . . . . . . . 132 Assinatura de código . . . . . . . . . . . . . . . . . . . 139 3 9 8.5.2 Cifragem do sistema de arquivos . . . . . . . . . . . . . 140 8.5.3 Sandboxes . . . . . . . . . . . . . . . . . . . . . . . . . 141 8.5.4 Biometria 8.5.5 Armazenamento de senhas e chaves criptográcas 8.5.6 Ações adicionais de proteção . . . . . . . . . . . . . . . 142 . . . . . . . . . . . . . . . . . . . . . . . . . 141 . . . 142 8.6 Na prática . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 8.7 Exercícios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144 Desenvolvimento de Aplicações Embarcadas 146 9.1 Aplicações de laço único 9.2 Aplicações controladas por interrupções . . . . . . . . . . . . . 147 9.3 Aplicações multitarefa cooperativas . . . . . . . . . . . . . . . 148 9.4 Aplicações multitarefa preemptivas 9.5 Arquitetura em camadas . . . . . . . . . . . . . . . . . . . . . 150 9.6 . . . . . . . . . . . . . . . . . . . . . 147 . . . . . . . . . . . . . . . 149 9.5.1 Camada de controle de tempo real 9.5.2 Camada executiva 9.5.3 Camada de aplicação . . . . . . . . . . . . . . . . . . . 153 Exercícios . . . . . . . . . . . 151 . . . . . . . . . . . . . . . . . . . . 152 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153 4 Capítulo 1 Introdução Neste capítulo introdutório apresentaremos os principais conceitos relacionados aos sistemas operacionais. Inicialmente, deniremos um sistema operacional e analisaremos sua relação com o hardware sobre o qual o sistema operacional é instalado. A seguir apresentaremos uma visão arquitetural do sistema operacional, ou seja, seus principais componentes e interrelações. Na sequência, deniremos chamadas de sistema e avaliaremos o papel das linguagens de programação no desenvolvimento e utilização de sistemas operacionais. 1.1 O que é um sistema operacional? Na literatura encontramos várias denições de sistema operacional. Tanenbaum [1] dene um sistema operacional como um gerenciador de recursos de hardware ou como uma máquina virtual que oferece uma interface mais simples para interagir com os recursos de hardware. Silberschatz [2] dene um sistema operacional como um intermediário entre o usuário e o harware de modo que o usuário possa executar seus programas de maneira eciente e conveniente. Vahalia [3] dene um sistema operacional como um ambiente de execução no qual programas do usuário podem executar. A Wikipedia [4] dene um sistema operacional como um conjunto de programas que gerencia o hardware e provê serviços para os programas manipulados pelo usuário (aplicativos). Destas denições podemos concluir que um sistema operacional gerencia os recursos de hardware e facilita o desenvolvimento e a utilização de progra- 5 mas aplicativos. Precisamos de mais uma denição de sistema operacional? Talvez possamos fornecer um denição mais geral: um sistema operacional é um software que facilita o processamento, armazenamento e transferência de informação. Note que nesta denição não aparecem os termos hardware, usuário ou programa aplicativo, sendo centrada na informação. De fato, na atualidade, um sistema operacional pode executar sobre um hardware virtualizado, ou seja, sobre um outro sistema de software. Nem sempre um usuário interage com o sistema operacional como quando manipulamos arquivos ou executamos programas aplicativos. Em um roteador de rede, por exemplo, o sistema operacional se dedica a transferir informação (na forma de pacotes de rede) de uma interface de rede para outra, sem nenhuma intervenção de usuário. Finalmente, o conceito de programa aplicativo pode ser visto também como um software de processamento, armazenamento e transferência de informação, mas com operações de mais alto nivel comparadas àquelas fornecidas pelo sistema operacional. Por exemplo, o sistema operacional fornece a abstração de arquivos e diretórios para o armazenamento de informação, enquanto um aplicativo de banco de dados fornece abstrações de mais alto nível como tabelas, relacionamentos e consultas parametrizadas. Desta forma, o processamento, armazenamento e transferência de informação se dá, recursivamente, em vários níveis, desde o nível do harware até o nível visível ao usuário. O sistema operacional é um componente de manipulação da informação situado imediatamente acima de um harware real ou virtual. Em sendo um software de alta complexidade, sistemas operacionais são compostos de diversos subsistemas interrelacionados. Alguns destes subsistemas são mais voltados ao processamento da informação, como o subsistema de processos. Outros subsistemas são mais voltados ao armazenamento da informação, como o subsistema de arquivos, e outros à transferência da informação, como o subsistema de entrada e saída. Ainda devido à alta complexidade, sistemas operacionais devem passar por constantes aprimoramentos, por exemplo, correção de falhas ( bugs ), otimização de desempenho e eliminação de funcionalidades redundantes. Com a internet, estes aprimoramentos passaram a ser feitos de forma automática, sem a necessidade de intervenção direta do usuário. 6 1.2 Sistema operacionais embarcados Certamente o leitor já teve contato com um ou mais sistemas operacionais, seja em seu computador de mesa ou portátil, em seu tablet smartphone, em seu ou em uma máquina virtual (capítulo 7) mantida por um provedor de computação em nuvem. Sistemas como o Windows da Microsoft, o iOS da Apple, o Linux e o Android são exemplos de sistemas operacionais que utilizamos no nosso dia a dia. Estes sistemas são instalados em dispositivos que têm como função precípua processamento, armazenamento e transferência de informação. Sistemas operacionais são também empregados em dispositivos que não têm esta função precípua, por exemplo, TVs, automóveis, aeronaves, robôs, brinquedos, e uma innidade de outros artefatos. Estes sistemas operacionais são denominados embarcados (ou embutidos) por serem integralmente dedicados aos dispositivos que os abrigam. O projeto destes sistemas operacionais e as aplicações que controlam e supervisionam o dispositivo apresentam um grande número de desaos. Tais desaos, em última análise, são devidos ao limitado poder computacional dos processadores onde estes sistemas operacionais executam (os processadores embarcados). Tal limitação se deve principalmente a: • custo: produtos de consumo de massa têm no preço sua principal restrição de projeto, o que limita a capacidade do processador embarcado; • tamanho: dispositivos altamente portáteis requerem processadores de dimensões reduzidas, o que implica capacidade também reduzida; • consumo de energia: dispositivos alimentados por baterias devem limitar o consumo de energia nos seus processadores, ou seja, empregar processadores com menos componentes integrados; • conabilidade: a atualização de processadores em sistemas críticos 1 demandam onerosos procedimentos de testes e homologações, o que torna sua atualização muito menos frequente. O investimento no desenvolvimento de aplicações que executam em sistemas embarcados (as aplicações embarcadas) superam em muito o desenvolvimento de aplicações convencionais tais como editores de texto, navegadores 1 Sistemas que requerem alta conabilidade, por exemplo, aplicações nas áreas aeroespacial, médica e nuclear. 7 Web, planilhas, reprodutores de mídia, etc. Um indicativo é o fato que que mais de 90% dos processadores fabricados se destinam a sistemas embarcados. Obviamente, nem todos estes processadores utilizam sistemas operacionais. 2 A plataforma Arduino , por exemplo, não utiliza sistema operacional, o que signica que nesta plataforma as aplicações executam diretamente sobre o hardware. Entretanto, com a disponibilidade de processadores cada vez mais poderosos, compactos e ecientes, a utilização de sistemas operacionais em sistemas embarcados é uma tendência irreversível. Por exemplo, a plataforma 3 Raspberry Pi e variantes utilizam uma versão do sistema Linux como sistema operacional. 1.3 Relacionamento com o hardware Para entender como funciona um sistema operacional, é necessário entender como o mesmo se relaciona com o hardware. Felizmente, para o desenvolvedor de sistemas opeacionais o hardware contribui para tornar este relacionamento menos complicado. Vamos examinar inicialmente como o hardware executa um programa (sequência de instruções em linguagem de máquina). Inicialmente o programa deve ser carregado na memória. Vamos considerar por ora a memória como um espaço de armazenamento linear de de zero a N −1. N bytes endereçado Uma vez na memória, um registrador especial, o contador de programa, é inicializado com o endereço da primeira instrução a ser executada. Em um programa compilado a partir da linguagem C este endereço é o endereço da função main. A seguir, o ciclo de busca-decodicação-execução de instruções é iniciado com a transferência da primeira instrução em outro registrador, o registrador de instruções. Após a decodicação e execução desta instrução, o contador de programa é incrementado para o endereço da próxima instrução do programa (caso a instrução corrente seja uma instrução de desvio, o endereço da próxima instrução pode não ser o da instrução subsequente). A instrução apontada pelo contador do programa é então carregada para o registrador de instruções e o processo se repete até que a última instrução seja executada, uma instrução de parada é executada (o retorno da função main, por exemplo) ou um evento reportado pelo hardware impede a continuação do programa. Eventos reportados pelo hardware incluem exceções aritméticas como 2 http://arduino.cc 3 http://www.raspberrypi.org/ 8 over/underows e divisão por zero, instruções que não podem ser decodi- cadas (que não pertencem ao jogo de instruções do processador), acesso a posições inválidas de memória, dentre outros. eventos por meio de interrupções. O hardware reporta estes Cada processador tem um conjunto de eventos que é capaz de monitorar e reportar, denominado rupções. vetor de inter- Cada posição neste vetor dene o evento e o seu conteúdo é um endereço de memória que contém uma rotina que é invocada assim que o evento ocorrer. O processador invoca esta rotina independentemente de qualquer ação do programa sendo executado. Estas rotinas são denominadas manipuladores de interrupção. Um manipulador de interrupção deve realizar uma ação compatível com o evento reportado. Por exemplo, pode-se denir um manipulador de interrupções que força o término do programa face à ocorrência de uma exceção aritmética. Manipuladores de interrupção muitas vezes necessitam acessar registradores do processador para desempenhar suas funções. Por exemplo, o manipulador de interrupção que termina o programa face a exceções aritméticas deve acessar o contador de programas para informar qual instrução causou a interrupção ou, melhor ainda, o registrador de instruções para identicar qual a operação e operandos que provocaram tal exceção. Outro conceito importante de hardware para os sistemas operacionais é o chamado modo do processador. Por simplicidade, vamos considerar dois modos: restrito (ou usuário) e irrestrito (ou supervisor). No modo usuário algumas instruções, denominadas privilegiadas, são proibidas, o que não ocorre no modo supervisor. Instruções privilegiadas incluem desabilitação de interrupções, manipulação de certos registradores e adição de endereços de manipuladores de interrupções no vetor de interrupções. Ao iniciar o tratamento de interrupções o processador altera o modo de execução para supervisor. Isto signica que manipuladores de interrupções, por executar no modo supervisor, têm acesso às instruções privilegiadas. Antes de nalizar, o manipulador de interrupções pode alterar o modo do processador para usuário, caso necessário. A passagem do modo usuário para o modo supervisor se dá quando ocorre: • uma interrupção assíncrona, por exemplo, gerada por um periférico de entrada/saída; • uma exceção durante a execução de um programa, por exemplo, exceções ariméticas como divisão por zero; 9 • um trap. Traps são instruções de máquina que alteram o modo do pro- cessador de usuário para supervisor. Na arquitetura x86 esta alteração se dá pela instrução INT. Ao ser executada, esta instrução gera uma interrupção de hardware. Com os conceitos de interrupções e modos do processador podemos responder a uma importante questão: como o sistema operacional toma conta do hardware impedindo, por exemplo, que um programa malicioso assuma o seu controle? A resposta reside em duas ações: 1. Apenas o sistema operacional pode manipular interrupções geradas pelo hardware. 2. Apenas o sistema operacional (a rigor, uma parte deste) pode executar em modo supervisor. Com estas ações um programa do usuário jamais poderá alterar o vetor de interrupções registrando um manipulador de interrupções próprio pois este programa jamais executará no modo supervisor. Desta forma, um programa do usuário não poderá monopolizar o hardware pois, como veremos no próximo capítulo, para tal ação ocorrer o programa deverá desabilitar as interrupções ou tratá-las com seus próprios manipuladores de interrupção. 1.4 Componentes de um sistema operacional A gura 1.1 ilustra a arquitetura de um sistema operacional. O núcleo do sistema operacional executa funções de controle direto sobre o hardware, notadamente tratamento de interrupções e funções básicas de gerência de recursos (memória, disco, etc.) e de entrada e saida. Os processos de sistema complementam as funcionalidades providas pelo núcleo, por exemplo, fornecendo funções auxiliares de gerenciamento de memória, serviços de rede, segurança e ambiente gráco. As chamadas de sistema são bibliotecas para acesso às funcionalidades oferecidas pelo núcleo, detalhadas na próxima seção. Podemos identicar quatro subsistemas importantes cujas funções são desempenhadas pelo núcleo e por processos de sistema: 1. Subsistema de processos. 2. Subsistema de memória. 10 Figura 1.1: Arquitetura típica de um sistema operacional. 3. Subsistema de arquivos. 4. Subsistema de entrada e saida. Note que os manipuladores de interrupções agem sobre todos estes subsistemas, por exemplo: • No subsistema de processos uma interrupção de relógio faz com que o sistema operacional comute o processo (programa) que detem a posse da CPU (Unidade Central de Processamento). • No subsistema de memória uma interrupção gerada pela CPU pode indicar que o programa em execução tentou acessar uma região inválida de memória. • No subsistema de arquivos uma interrupção gerada pelo controlador de acesso direto à memória (DMA - Direct Memory Access ) pode indicar que a cópia de um bloco do disco para a memória foi concluída. • No subsistema de entrada e saída uma interrupção gerada por um device driver ) pode indicar que uma operação acionador de dispositivo ( de entrada e saída foi concluída. 11 1.5 Chamadas de sistema Para facilitar o processamento, armazenamento e transferência de informação, o sistema operacional deve expor aos programas aplicativos um conjunto de serviços. calls ). Estes serviços são denominados chamadas de sistema (system Chamadas de sistema são funções em uma dada linguagem de progra- mação, usualmente na linguagem C. Estas funções permitem obter serviços dos subsistemas que compõem um sistema operacional, por exemplo: • Iniciar, terminar, suspender e retomar a execução programas aplicativos (subsistema de processos). • Alocar, realocar e desalocar segmentos de memória durante a execução de um programa (subsistema de memória). • Criar, renomear e remover arquivos e diretórios (subsistema de arquivos). • Ler e escrever dados em arquivos e conexões de rede (subsistema de entrada e saída). O que chamadas de sistema têm de especial em relação às funções denidas pelos programas aplicativos? Simplesmente, chamadas de sistema são executadas em modo supervisor, ou seja, são capazes de executar instruções privilegiadas do processador, o que não ocorre com as funções denidas pelos aplicativos. Chamadas de sistema são consideradas operações de baixo nível de abstração. Por este motivo, os sistemas operacionais usualmente disponibili- zam bibliotecas que oferecem funções de mais alto nível que as chamadas de sistema. Estas funções são comumente confundidas com chamadas de sistemas, talvez por terem um correspondência muito próxima em certos casos. Por exemplo, a chamada de sistema arquivos, enquanto a função fprintf write da biblioteca permite escrever bytes em libc permite escrever dados formatados em arquivos (números inteiros e em ponto utuante, cadeias de caracteres e ponteiros). É usual dizer que um programa que efetua uma chamada de sistema incorre em um ( trap ). Isto se dá pelo fato da chamada de sistema executar a instrução que altera o modo do processador de usuário para supervisor trap ). ( O uxo de execução de uma chamada de sistema é dado abaixo: 12 1. O programa do usuário efetua a chamada de sistema (o processador encontra-se no modo usuário). 2. A interface de chamadas de sistema identica a chamada e armazena seu índice (número inteiro) em um registrador da CPU. Em seguida executa um trap alterando o modo do processador para supervisor. 3. O tratador de interupção associado ao trap (código do sistema operara- cional) busca o índice da chamada no registrador e executa a chamada de sistema correspondente (em modo supervisor). 4. Quando a chamada de sistema retorna, o tratador de interrupção comuta o modo do processador de supervisor para usuário e retorna. 5. O programa do usuário retoma sua execução em modo usuário como se tivesse executado uma função de seu próprio código. Muitas chamadas de sistema são demoradas em comparação com a velocidade da CPU, como no caso de chamadas que efetuam operações de entrada e saída. Ao efetuar estas chamadas de sistema (ditas bloqueantes) o programa é bloqueado, ou seja, perde a posse da CPU. A posse da CPU somente será retomada quando a operação que causou o bloqueio é nalizada. Além da execução em modo supervisor, a perda da posse da CPU é outro fator que diferencia as chamadas de sistema das funções do próprio programa. Os núcleos dos sistemas operacionais atuais possuem duas propriedades: 1. Preempção: o núcleo é capaz de interromper a qualquer momento qualquer programa em execução e retomar a CPU para, por exemplo, proceder a troca de contexto (alocá-la a outro programa). 2. Reentrância: o núcleo é capaz de interromper processos executando em modo supervisor (ou seja, durante o processamento de uma chamada de sistema) e retomar para si a CPU. Reentrância requer chamadas de sistemas reentrantes, ou seja, podem existir várias cópias da mesma chamada executando independentemente. Chamadas de sistema reentrantes não devem armazenar estado no seu próprio código. O estado da execução deve ser armazenado no programa que chamou a função, por exemplo, na área de pilha do processo, como veremos no capítulo seguinte. 13 1.6 Sistemas operacionais e linguagens de programação Um sistema operacional é escrito em uma linguagem de alto nivel e uma pequena porção em linguagem de montagem do processador sobre o qual executa. Esta pequena porção refere-se a operações especícas do processador, por exemplo, operações sobre os registradores da CPU, alterar o modo de execução do processador e desabilitar interrupções. Tais operações não são suportadas pela linguagem de alto nível e são necessárias principalmente no tratamento de interrupções. Praticamente a linguagem de alto nível empregada no desenvolvimento de sistemas operacionais é a linguagem C. Esta linguagem é estável, eciente e portável entre diferentes processadores. Por ter seu código escrito em C, é natural que a interface para com o sistema operacional (chamadas de sistema) também seja oferecida nesta linguagem. Entretanto, funções similares às chamadas de sistema podem ser oferecidas em outras linguagens tais como Java, Python e C#. Por serem escritos em C ou C++, os sistemas de time run destas linguagens têm pleno acesso às chamadas de sistema do sistema operacional e, desta forma, são capazes de oferecer construções de mais alto nível que as chamadas de sistema. É o caso, por exemplo, da classe File da linguagem Java que oferece métodos para a manipulação de arquivos e diretórios de forma mais abstrata que as chamadas de sistema do subsistema de arquivos. Um caso interessante de sistema operacional que esconde as chamadas de sistema de baixo nível é o Android [5]. Este sistema é construído sobre o núcleo do sistema operacional Linux. máquina virtual Java. Sobre este núcleo é instalada uma A máquina virtual Java é um interpretador, mas não um interpretador direto da linguagem como no caso do interpretador Python. A máquina virtual interpreta um código intermediário entre o código fonte e o código executável. Este código, denominado bytecode, é produzido pelo compilador Java e interpretado pela máquina virtual Java. A vantagem deste código intermediário é a portabilidade, pois o mesmo código pode ser produzido por compiladores distintos e interpretado por máquinas virtuais Java também distintas. De volta ao Android, o desenvolvedor para este sistema operacional manipula apenas as classes Java disponibilizadas pelo sistema, não interagindo diretamente com as chamadas de sistema oferecidas pelo núcleo. 14 1.7 Exercícios 1. Explique porque denir sistemas operacionais em termos de hardware, usuário e aplicativo não é mais apropriado. 2. Quais as características encontradas nos processadores atuais que são fundamentais para o desenvolvimento de sistemas operacionais? 3. Descreva quatro interrupções que o hardware comumente gera. 4. O que é um vetor de interrupções? 5. Sob que condições o modo do processador muda de supervisor para usuário? 6. O que é um trap ? 7. Cite algumas extensões da linguagem C que permitiriam codicar um sistema operacional integralmente nesta linguagem (sem necessidade de código em linguagem de montagem do processador). 8. O que as chamadas de sistema têm de especial em relação às funções denidas em um programa? 9. Sob quais condições é possível trocar o núcleo de um sistema operacional sem afetar os seus demais componentes e aplicativos do usuário? 10. Cite uma maneira pela qual um virus de computador poderia assumir plenamente o controle do hardware. Como o sistema operacional impede esta situação? 11. As propriedades preempção e reentrância do núcleo estão relacionadas? Caso estejam, como? 12. É comum programadores utilizarem chamadas de sistema sem vericar o seu retorno com base no seguinte argumento: como chamadas de sistema são implementadas pelo sistema operacional sua correta execução é sempre garantida. Você concorda com este argumento? Justique. 13. Muitos super-computadores atuais utilizam o mesmo sistema operacional presente no seu computador pessoal. Como isto é possível? 15 14. Para cada um dos quatro subsistemas de um sistema operacional (processos, memória, arquivos e entrada/saída), forneça um exemplo de interrupção de hardware que afeta o subsistema. Descreva também uma chamada de sistema típica do subsistema. 15. O que acontece se um programa executar um código? 16 trap em seu próprio Capítulo 2 Subsistema de Processos Neste capítulo apresentaremos processo, um conceito central em sistemas operacionais. Um processo é um programa em execução sob controle do sistema operacional. A estrutura dos processos, seu gerenciamento por parte do sistema operacional e os mecanismos de comunicação e sincronização interprocesso são os temas principais abordados neste capítulo. 2.1 Estrutura de um processo Além de um programa em execução, podemos denir mais precisamente processo como uma unidade de controle, alocação e compartilhamento de recursos (CPU, memória, etc.) processo • não por parte do sistema operacional. Um é: Um programa residente em memória primária ou secundária (disco, memória USB, etc.) que não iniciou sua execução. • Um componente do núcleo do sistema operacional como um manipula- device driver ). dor de interrupção ou um acionador de dispositivo ( • Um componente de software como uma biblioteca ou um Um processo é também denominado corrente. processo sequencial ou shell script. processo con- O primeiro termo enfatiza a execução de instrução após instrução, uma por vez, enquanto o segundo enfatiza a execução concorrente (ao mesmo tempo) de vários processos por parte do sistema operacional. 17 Um sistema operacional capaz de executar processos de forma concorrente é denominado sistema multitarefa. Inicialmente, vamos considerar que um processo ocupa uma área contínua de memória. O conteúdo desta área de memória foi preenchido por um software de sistema, o carregador, a partir de um código executável armazenado em memória secundária, tipicamente disco. Durante o carregamento, o carregador transforma endereços relativos presentes nas instruções de máquina em endereços absolutos. Endereços relativos são produzidos pelo compilador e ligador e têm como base o início de um segmento de código. Endereço absoluto é um endereço físico na memória. Um processo em memória pode ser dividido em três áreas (regiões de memória): texto, dados e pilha. A área de texto armazena as instruções de máquina do programa em execução. A área de dados armazena os dados manipulados pelo programa. Comumente esta área é dividida em três partes: 1. Data: dados inicializados, por exemplo, a variável k na declaração int k = 10. 2. Bss ( Block Started by Symbol - nome histórico): dados não inicializados no programa que recebem o valor inicial zero quando carregados em memória, por exemplo o vetor v na declaração oat v[20]. 3. Heap: área reservada à alocação dinâmica de memória, por exemplo, via funções malloc em C ou operador new em C++. Convém observar que as áreas de data e bss são estáticas enquanto a área heap pode crescer durante a execução do programa. Vamos considerar doravante as áreas data, bss e heap como uma única área de dados. A área de pilha é necessária durante a chamada de funções. Nela são armazenados os parâmetros passados à função e o endereço da próxima instrução a ser executada quando a função retornar. Esta área também cresce durante a execução do programa, notadamente durante chamadas recursivas ou encadeadas de funções. A gura 2.1 ilustra um processo em memória. A área sombreada é reservada ao crescimento das áreas de heap e pilha. A região que o processo ocupa na memória é denominada espaço de endereçamento do processo. Ao processo é permitido acessar posições de memória apenas neste espaço. Na realidade, o que a gura 2.1 mostra é um espaço de endereçamento ctício ou virtual. Este espaço de endereçamento é grande (por exemplo, 18 Figura 2.1: Estrutura de um processo em memória. de tamanho 3 Gigabytes) e contínuo. Por ser virtual, o sistema operacional concede este espaço de endereçamento a cada processo, independentemente da quantidade de memória física existente no computador. Mas, também por ser virtual, este espaço de endereçamento não pode ser acessado pela CPU. O truque, detalhado no capítulo dedicado ao subsistema de memória, é associar porções do espaço de endereçamento virtual ao espaço de endereçamento físico. Por ora é suciente o seguinte modelo simplicado. Vamos dividir o espaço de endereçamento virtual e o físico (memória física) em blocos contínuos de tamanho xo (por exemplo, de 4 KBytes) denominados páginas. Vamos agora substituir o espaço de endereçamento virtual por uma tabela de páginas por processo como ilustra a gura 2.2. Nesta tabela: • A i-ésima entrada na tabela corresponde à página virtual • Cada entrada na tabela possui um campo com o número da página i. correspondente na memória física. Por exemplo, a página i do espaço de endereçamento virtual pode estar associada (mapeada) à página j da memória física. mapeamento i−j Nesta gura a ag P (presente) indica que o é válido. 19 • Cada entrada na tabela possui também um campo com um conjunto de ags, indicando, por exemplo, se a região é de leitura apenas, como á área de texto, ou de leitura e gravação, como as demais áreas do processo. Figura 2.2: Mapeamento página virtual - página física por meio da tabela de páginas por processo. O processo referencia o espaço de endereçamento virtual. Este endereço pode ser decomposto em página e deslocamento na página ( oset ). Por exemplo, supondo páginas de 4 KBytes, o endereço virtual 8358 corresponte à página 3 (endereços de 8192 a 12287) e um deslocamento de 166 bytes do início da página. Ainda neste exemplo, para transformar o endereço virtual em endereço físico, o hardware determina qual página de memória física a página 3 do espaço de endereçamento virtual está mapeada, uma simples consulta à tabela de páginas por processo. Supondo que esta página esteja mapeada na página 7 do espaço de endereçamento físico, o endereço físico é 28838 (7 × 4096 + 166). Este procedimento é computado para cada instrução do processo que acessa a memória. Felizmente, esta computação é realizada com a assistência de hardware dedicado como veremos no capítulo 4. 20 Como temos muito mais memória virtual que física, é natural que apenas um subconjunto de páginas do espaço de endereçamento virtual estejam mapeadas em páginas da memória física. Vamos postergar a resolução deste problema até o capítulo que trata do subsistema de memória. Por ora, consideremos que todas as páginas do espaço de endereçamento virtual de um processo estão mapeadas em páginas da memória física. A gura 2.3 ilustra como o sistema Linux mantem controle sobre as áreas de texto, dados e pilha dos processos. As áreas (regiões contínuas) que um processo utiliza em seu espaço de endereçamento virtual é identicada por uma estrutura de dados tipo lista ligada denominada vm_struct_area. Esta estrutura aponta para o início e m de de uma região, ags relativos a região (por exemplo, permissões) e ponteiro para a estrutura que identica a próxima região. Cada página virtual destas regiões são mapeadas nas suas respectivas páginas físicas por meio da tabela de páginas por processo. Figura 2.3: Manutenção das áreas de memória de um processo no Linux. 21 Esta breve introdução à tecnica de memória virtual é necessária para entendermos a criação de processos e o compartilhamento de memória por processos distintos. 2.2 Criação de processos O gerenciamento de processos por parte do sistema operacional compreende atividades relacionadas ao ciclo de vida de um processo, deste a sua criação até o seu término, passando por sua execução. Um processo sofre ações de gerenciamento iniciadas pelo núcleo do sistema operacional ou por outro processo em execução, neste caso via chamadas de sistema. Exemplo de ação iniciada pelo nucleo é a mudança de contexto. Exemplo de ação iniciada por um processo é a criação de um novo processo. O sistema operacional mantém as informações necessárias para gerenciar os processos em uma tablela, a tabela de processos. Cada processo possui uma entrada nesta tabela que armazena os recursos utilizados pelo processo tais como arquivos abertos, memória utilizada, tempo de uso da CPU, etc. Processos são criados via chamadas de sistemas especícas, por exemplo, exec no Linux e createProcess no Windows. Usualmente estas chamadas rece- bem o nome do arquivo executável e os parâmetros a serem passados à função main, pela qual o programa inicia sua execução. A chamada exec 1 o programa que a chamou por outro especicado na chamada. substitui O sistema operacional utiliza a mesma entrada na tabela de processos e a mesma tabela de páginas por processo do processo original. Portanto, o novo processo possui o mesmo identicador de processo e recursos atribuídos ao processo original, por exemplo, arquivos abertos, prioridade, etc. Entretanto, o novo processo não tem como recuperar estes recursos, por exemplo, o descritor createProcess, ao exec, cria um processo novo, preservando o processo que efetuou de um arquivo aberto pelo processo original. A chamada contrário de a chamada. Neste caso é criada uma nova entrada na tabela de processos e uma nova tabela de páginas para o novo processo. Outra forma de criar um processo é via clonagem. A chamada fork, presente desde as primeiras versões do Unix, efetua as seguintes operações: • Cria uma nova entrada na tabela de processos para o novo processo (denominado processo lho). Copia o conteudo da entrada da tabela 1 Na realidade exec é uma família de chamadas cada qual com parâmetros diferentes. 22 de processos do processo que efetuou a chamada (denominado processo pai) para a entrada correspondente ao processo lho. • Cria uma nova tabela de páginas por processo para o processo lho. Copia as entradas da tabela de páginas do processo pai para as entradas correspondentes ao processo lho. • Marca uma ag nas tabelas de páginas dos processos pai e lho como copy-on-write (CoW). A gura 2.4 ilustra a criação de um processo via chamada Figura 2.4: Criação de processos via chamada fork. fork. O processo lho compartilha o espaço de endereçamento virtual do processo pai e, portanto, herda todo o seu histórico de execução presente em suas áreas de texto, dados e pilha. Entretanto, a partir do retorno da chamada fork temos dois processos idênticos, exceto pelo fato que o retorno da chamada no pai é o identicador de processo lho e no recém criado processo lho a chamada retorna um identicador nulo. Apesar do processo lho ser criado idêntico ao processo pai, ambos podem seguir caminhos de execução independentes. Neste caso, a alteração de uma variável pelo processo pai não deve ser visível ao processo lho e vice- copy-on-write. Ao acessar uma página para escrita o hardware verica se a ag copy-on-write está ativado. versa. Como isto é garantido? Pela ag 23 Caso esteja, é gerada uma interrupção e o sistema operacional desmembra a página. O sistema operacional aloca uma nova página na memória física (não mapeada ainda), copia o conteúdo da página a ser desmembrada para esta nova página e refaz o mapeamento conforme ilustrado na gura 2.5. Após o desmembramento, ambas as páginas têm a ag copy-on-write ressetada. Agora a escrita é realizada apenas na página que foi desmembrada. Figura 2.5: Desmembramento da página física i j após escrita na página virtual do processo lho (veja gura 2.4). Para se atingir um comportamento similar à chamada createProcess do Windows, nos sistemas Unix é comum empregar-se uma combinação das fork e exec, esta invocada no processo lho logo após o retorno da chamada fork. Neste caso é recomendado o uso da chamada vfork. vfork é similar à chamada fork exceto que as tabelas de páginas dos processos pai e lho não são copiadas nem marcadas como copy-on-write. Isto evita uma chamada série enorme de desmembramentos de páginas durante o carregamento do exec já que todas as páginas do processo lho serão sobrescritas com o carregamento do novo processo. A chamada vfork mantém novo processo na chamada a tabela de páginas do processo pai intacta e ativa uma ag nas entradas da tabela de páginas do processo ho marcando-as como não mapeadas (ag A - ausente). Isto signica que quando estas páginas forem acessadas pelo processo criado por exec o sistema operacional deve mapeá-las em páginas novas da memória física e copiar o conteúdo relativo à esta página a partir do do código executável. Fica aqui uma observação pouco intuitiva: 24 um programa pode iniciar sua execução com o carregamento de uma única página na memória, a que contém o início da função main. A gura 2.6 ilustra a criação de um processo via chamada quadro 2.1 ilustra o uso desta chamada. Figura 2.6: Criação de processos via chamada 25 vfork. vfork e o Quadro 2.1: Chamada de sistema vfork. i n t pid = v f o r k ( ) ; i f ( pid == 0) { // p r o c e s s o f i l h o execve (" novo_prog " , NULL, NULL) ; p r i n t f (" Erro −− execve nao p o d e r i a r e t o r n a r " ) ; } // f l u x o normal do p r o c e s s o p a i 2.3 Troca de contexto O gerenciamento de processos tem como meta garantir que os processos evoluam. Para tanto, o sistema operacional deve alocar os recursos necessários à execução dos processos. Portanto, a alocação de recursos é uma atividade central no gerenciamento de processos, notadamente os recursos de processamento (CPU) e armazenamento primário (memória). O compartilhamento de CPU demanda um procedimento denominado troca de contexto. Vamos iniciar com um sistema operacional capaz de executar um único programa por vez, como o antigo MS-DOS. No interpretador de comandos (interface de texto do sistema operacional) o usuário digita o nome do programa. O sistema operacional invoca o carregador passando o endereço de memória a partir do qual o programa será carregado. Efetivado o carregamento, o sistema operacional atualiza o contador de programa para a primeira execução do programa carregado e transfere esta primeira instrução para o registrador de instruções, iniciando o ciclo busca-decodicação-execução de instruções, até o programa terminar. Com o termino do programa, o interpretador de comando espera a próxima ação do usuário. este modelo se presta a um sistema operacional monotarefa. Obviamente Vamos agora considerar um sistema multitarefa como os atuais. Uma primeira estratégia seria executar os programas na sua integridade, em sequência, como nos antigos computadores mainframes. Entretanto, o que acontece se um program não termina, por exemplo, na presença de erros como um laço innito. Nos mainframes o operador humano abortava este programa e o próximo iniciava sua execução. Mesmo considerando que todos os programas terminem em um tempo nito, surgiram dois tipos de programas ausentes na era dos • mainframes : Programas servidores que executam permanentemente a espera de re- 26 quisições. • Programas interativos que devem responder de pronto às ações do usuário. Para estes tipos de programa, os sistemas operacionais multitarefa são capazes de executar os programas um pouquinho por vez (durante algumas dezenas de milisegundos). temporal da CPU, ou Esta técnica é denominada compartilhamento time sharing. O sistema operacional aloca a CPU para um processo durante um determinado tempo e, expirado este tempo, aloca a CPU para outro processo. processo é denominado quantum O tempo que a CPU é alocada a um de CPU e a troca de um processo por outro é denominada troca de contexto. Neste esquema, o sistema operacional não aloca a CPU para processos que não tem tarefa a realizar no momento, por exemplo, um servidor sem requisições para atender. Como o compartilhamento temporal é realizado pelo sistema operacional? Resposta, via interrupções. Vamos considerar que o quantum de CPU acabou de expirar para o processo Pi . Neste momento acorre uma interrupção de relógio que será tratada pelo sistema operacional (lembremos que somente o sistema operacional tem o poder de tratar interrupções de hardware). O manipulador de interrupções de relógio inicialmente salva o estado da CPU (contexto do processo) na entrada correspondente da tabela de processos. Este estado é composto dos registradores da CPU, incluindo o contador de Program Status Word ). programa e PSW ( A seguir o sistema decide qual processo deve assumir a CPU. Esta decisão é denominada processos escalonamento de e será detalhada na seção 2.4. Escolhido o processo Pj , o sistema operacional carrega a partir da tabela de processos o contexto deste processo (salvo no momento em que Pj perdeu a posse da CPU da última vez). O sistema operacional programa o relógio para gerar uma interrupção daqui a um quantum de tempo e desvia a execução para a instrução apontada pelo contador de programa. Neste momento o programa que retomou a CPU inicia sua execução exatamente do ponto em que estava quando a perdeu. Note que para o processo Pj é como se o tempo tivesse parado entre a perda e a retomada da CPU. A gura 2.7 ilustra o esquema de compartilhamento temporal da CPU. Durante a posse da CPU por um processo, alguns eventos podem ocorrer. O processo pode terminar, executar uma instrução ou operação ilegal, ou efetuar uma chamada de sistema bloqueante. O término de um processo se dá por uma chamada de sistema (a chamada 27 exit ), invocada explicitamente Figura 2.7: Time sharing entre os processo Pi e Pj . main ). pelo programador ou implicitamente pelo retorno da função principal ( Portanto, a implementação da chamada exit inicia a troca de contexto, além de outras ações tais como atualização da tabela de processos, liberação de páginas de memória ocupadas pelo processo, liberação de recursos mantidos pelo processo tais como arquivos abertos, conexões de rede, dentre outros. Uma instrução ilegal (não suportada pelo processador) ou a execução de uma operação ilegal tal como uma divisão por zero ou ainda um acesso à uma região de memória fora do espaço de endereçamento do processo causa uma interrupção de hardware. O tratamento desta interrupção usualmente força o término do processo, com ações similares às descritas para a chamada exit. O caso mais interessante é quando o processo efetua uma chamada de sistema bloqueante, por exemplo, uma chamada para ler dados de um arquivo em disco (chamada read ). Por ser uma chamada de sistema, o sistema operacional verica se a chamada é bloqueante ou não. Esta decisão depende do tempo para completar a chamada. Por exemplo, a chamada read, pode ser bloqueante ou não. Se o dado a ser lido do disco já estiver em memória (como veremos no capítulo referente ao subsistema de entrada e saída) a chamada é considerada não bloqueante. Se o dado necessita ser acessado do disco, a chamada é considerada bloqueante. Neste caso o processo perde a posse da CPU para evitar que a mesma que ociosa até a informação ser acessada (o que provavelmente ocorreria após o término do quantum para este processo). Assim podemos visualisar três estados para um processo (Figura 2.8): 1. Em execução: o processo está de posse da CPU executando suas instruções ou chamadas de sistema não bloqueantes. 28 2. Bloqueado: o processo efetuou uma chamada de sistema bloqueante e está aguardando a conclusão da operação que causou o bloqueio para prosseguir sua execução. 3. Pronto: o processo está apto a retomar a CPU, dependendo apenas de sua escolha por parte do sistema operacional. Figura 2.8: Estados de um processo e eventos que causam as transições de estado. Como o processo transita do estado de bloqueio para pronto? mente, via o tratamento de interrupções. Nova- Operações de entrada e saída são programadas em hardware (usualmente por meio de DMA) e, quando concluída, o hardware gera uma interrupção. Por exemplo, o controlador de DMA gera uma interrupção assim que um dado do disco foi copiado para uma área de memória. o processo seja desbloqueado. O tratamento desta interrupção faz com que Na tabela de processos são armazenados os eventos (interrupções) que o processo está aguardando para ser desbloqueado. 2.4 Escalonamento de processos Escalonamento de processos é uma decisão que retorna qual processo, dentre os processos prontos, deve ter a posse da CPU. Esta decisão deve ser rápida pois é efetuada a cada troca de contexto. Uma estratégia simples, caso não hajam processos interativos ou servidores, é manter os processos prontos em uma la. O primeiro processo da la é escolhido e, após consumir seu 29 quantum de tempo e perder a posse da CPU, o processo é deslocado para a nal da la. Esta estratégia de escalonamento é denominada round robin e foi utilizada nos primeiros sistemas multitarefa. 2.4.1 Processos interativos e servidores Processos interativos passam a maior parte do tempo bloqueados aguardando uma ação do usuário como pressionar uma tecla ou clicar o mouse. En- tretanto, assim que a ação for realizada, o processo deve assumir a CPU prontamente, de preferência já no próximo quantum. Isto para propiciar uma resposta rápida à ação do usuário tal como ecoar a tecla pressionada ou mover o ponteiro do mouse na tela. Processos servidores também permanecem bloqueados até que uma requisição seja recebida. Também neste caso, a requisição deve ser processada o quanto antes. Uma estratégia de escalonamento que privilegia processos interativos e servidores é empregar múltiplas las, atribuindo-se a cada la uma prioridade. Um percentual do tempo da CPU é dedicada aos processos na la de acordo com a prioridade da la, quanto maior a prioridade maior o percentual da CPU atribuido aos processos da la. A gura 2.9 ilustra este esquema. Figura 2.9: Escalonamento com múltiplas las. A estratégia de múltiplas las opera da seguinte maneira. Quando um processo passa do estado de bloqueado para o estado de pronto o sistema operacional analisa o evento que causou a transição. O tipo de evento dá uma boa idéia do tempo que o processo cou no estado de bloqueado, por 30 exemplo, uma espera por entrada do usuário é bem mais longa que uma espera por uma leitura do disco. Com base no evento, o sistema operacional adiciona o processo em uma dada la de prioridade. Quanto maior a espera maior a prioridade da la que o processo será inserido, ou seja, processos que não consumiram tempo de CPU no passado recente, têm maior prioridade. Toda a informação necessária para se determinar qual la um processo pronto está disponível na tabela de processos. Obviamente, um processo que está em uma la de alta prioridade não deve permanecer nesta la indenidamente. Caso o processo bloqueie novamente, o problema está resolvido pois ele sairá da la até se tornar pronto novamente. Mas, e se o processo após bloquear uma vez não bloqueia mais? A estratégia de múltiplas las, da mesma forma que privilegia processos que não consumiram CPU recentemente, penaliza os processos que a consumiram. Desta forma, os processos são deslocados para uma la de menor prioridade de acordo com a quantidade de CPU que consumiram recentemente. Esta mudança de la é realizada periodicamente (este período é denominado época). Dado que a prioridade dos processos variam com o tempo, esta prioridade é denominada prioridade dinâmica. 2.4.2 Processos de tempo real Processos de tempo real são aqueles que desempenham tarefas que exigem certa previsibilidade, por exemplo, tempo máximo de acesso à CPU quando se tornam prontos, tempo máximo para realizar uma tarefa antes de bloquear, etc. Exceto em sistemas operacionais projetados para aplicações de tempo real, processos de tempo real convivem com outros tipos de processos. Entretanto, processos de tempo real devem ter tratamento especial por parte do escalonador. A estratégia comumente empregada para o escalonamento de processos de tempo real é baseada em prioridades estáticas. Estas prioridades podem ser atribuídas apenas a processos executando com privilégio de superusuário. Uma prioridade estática é maior que qualquer prioridade dinâmica. O sistema operacional mantém processos de mesma prioridade estática em las correspondentes a esta prioridade. Uma la de dada prioridade estática é servida apenas quando as las de prioridade superior estiverem vazias (ou seja, todos os processos nestas las superiores terminaram ou bloquearam). Processos de prioridade dinâmica são escalonados apenas quando todas as las de prioridade estática estiverem vazias, ou seja, quando não existe processo de tempo real pronto. No escalonamento por prioridades, assim 31 que um processo de prioridade mais alta em relação àquele que tem a posse da CPU torna-se pronto, o sistema operacional realiza a troca de contexto imediatamente a m de que o processo de mais alta prioridade assuma a CPU de imediato. Esta tomada da CPU antes do quantum expirar é denominada preempção. O sistema Linux oferece duas opções para a manipulação das las de prioridade estática: • FIFO ( First In First Out ): um processo que assume a CPU é executado até bloquear, mesmo que haja outros processos prontos em sua la de prioridades. • Round Robin ): processos nas la de prioridade são escalonados por Round Robin, ou seja, após executar um quantum de CPU, o processo RR ( é posicionado no nal de sua la de prioridade e o primeiro processo da la terá a posse da CPU. Conforme mencionado anteriormente, atribuir prioridades estáticas e denir como as las de prioridade estática serão manipuladas (FIFO ou RR) exige que o processo execute com privilégio de super-usuário. Linux estas operações são realizadas com a chamada No sistema sched_setscheduler. No manual desta chamada há um conselho sábio: Como um laço innito em um processo de tempo real escalonado com FIFO ou RR bloqueará todos os processos de menor prioridade, um desenvolvedor de software deve sempre manter disponível um interpretador de comandos com prioridade estática mais alta que a aplicação em teste. Isto permitirá um término forçado da aplicação de tempo real que não terminou ou bloqueou como o esperado. É importante notar que o escalonamento por prioridade estática apenas não garante as restrições de tempo real impostas pelos sistemas físicos serão atendidas pelo sistema operacional. Da parte do sistema operacional, o problema da inversão de prioridades deve ser equacionado (seção 2.6.3). Da parte do desenvolvedor de software, algumas ações relacionadas com o gerenciamento de threads e com o gerenciamento de memória devem ser observadas. Estes ações serão detalhadas nos capítulos subsequentes. 2.5 Sinalização de processos A sinalização de processos é um mecanismo por meio do qual processos são noticados da ocorrência de eventos gerados pelo núcleo do sistema opera- 32 cional ou por outros processos. Exemplos de tais eventos incluem exceções aritméticas, requisições de mudança de estado (suspenção da temporária da execução, por exemplo) e noticação de falhas de hardware. Processos podem denir funções para o tratamento de cada evento sinalizado denominadas manipuladores de sinais. A base deste mecanismo de noticação são os sinais. Um sinal pode ser entendido como uma interrupção gerada por software. O próprio sistema operacional faz uso de sinais para informar aos processos a ocorrência de eventos. Nos sistemas Unix estes eventos são identicados por números inteiros positivos denidos como constantes (em letras maiúsculas), por exemplo: • SIGCHLD: informa ao processo pai o término ou suspensão de um processo lho. overow ). • SIGFPE: exceção aritmética (divisão por zero, • SIGSEGV: exceção de segmentação (tentativa de acesso à posição de memória fora de seu espaço de endereçamento). • SIGTRAP: informa que um breakpoint de depuração foi atingido. Para o envio de sinais a partir de um processo utiliza-se uma chamada de sistema, por exemplo, a chamada kill do Unix. Esta chamada, apesar do nome, não necessáriamente causa o término forçado (morte) do processo. Os sinais que podem ser gerados por processo incluem: • SIGKILL: termina o processo. • SIGSTOP: suspende (bloqueia) a execução de um processo. • SIGCONT: retoma a execução de um processo. • SIGINT: interrupção gerada pelo teclado (control-C). Para cada sinal é denida uma ação padrão para o processo que recebeu o sinal. Para os sistemas Unix as ações são: • abort : termina o processo gerando um mapa de mapa de memória ( dump ) para ns de depuração. • exit : termina o processo sem gerar o mapa de memória. 33 core • ignore : • stop : ignora o sinal. suspende a execução do processo. • continue : retoma a execução do processo. Com o uso de sinais um processo pode terminar, bloquear e retomar (tornar pronto) outros processos, dependendo de permissões. Processos com permissão de super-usuário são capazes de sinalizar qualquer processo enquanto processos com permissão de usuário comum são capazes de sinalizar apenas processos de seu grupo (tipicamente processos que pertencem ao mesmo usuário). Ao receber um sinal a ação padrão é executada pelo sistema operacional, a menos que o processo dena um manipulador de sinal (não confundí-los com os manipuladores de interrupção do sistema operacional). O manipulador de sinal é uma função invocada assincronamente para o processo. O processo retoma seu uxo normal de execução quando o manipulador de sinal retornar. Quando um sinal é enviado a um processo, o mesmo é armazenado na entrada deste processo na tabela de processos. O manipulador para este sinal é invocado nos seguintes instantes em que o sistema operacional tem a posse da CPU: • o processo retorna da execução em modo usuário após terminar uma chamada de sistema; • imediatamente antes do processo de iniciar uma chamada de sistema; • quando o processo iniciar um novo quantum de CPU. Nota-se pelos momentos acima que, contrário às interrupções de hardware, interrupções de software (sinais) não são tratadas de imediato. Nem todos os sinais podem ser tratados, por exemplo o sinal SIGKILL. Outros, mesmo que tratados, podem causar um comportamento imprevisível ao processo, por exemplo os sinais SIGFPE (exceção aritmética) e SIGSEGV (exceção de segmentação). Um manipulador de sinal é uma função que recebe um inteiro e nada retorna (void). A incorporação de um manipulador ao processo se dá com a chamada signal que recebe o número do sinal e um ponteiro para a função de manipulação do sinal. Por exemplo, o quadro 2.2 ilustra um programa que solicita uma conrmação quando recebe uma interrupção de teclado (SIGINT gerado por control-C). 34 Quadro 2.2: Exemplo de manipulador de sinal. #i n c l u d e <s i g n a l . h> void confirma ( i n t i ) { p r i n t f ("Tem c e r t e z a que quer t e r m i n a r [ s /n ] ? " ) ; i f ( g e t c h a r ( ) == ' s ' ) e x i t ( 0 ) ; } main ( ) { s i g n a l ( SIGINT , confirma ) ; ... } // a s s o c i a manipulador ao s i n a l Duas observações quanto a sinais são importantes para o desenvolvedor de sistemas de software: 1. Em alguns sistemas a manipulação de sinais não é recursiva, ou seja, se durante a manipulação de um sinal outro sinal for recebido, o sistema operacional executará a ação padrão para o novo sinal. 2. Chamadas de sistema bloqueantes podem ser interrompidas por sinais. Por exemplo, suponha um processo servidor aguardando uma conexão de rede (bloqueado na chamada accept ). Se um processo lho terminar o sistema operacional irá enviar ao processo o sinal SIGCHLD que fará com que a chamada accept retorne um erro (-1). O sistema operacional interrompe a chamada de sistema para dar chance ao processo de tratar o sinal. Devido à primeira observação, diz-se que o mecanismo de sinais é não conável caso o sistema operacional não utilize recursaão no tratamento de sinais. De fato, ao executar a ação padrão, pode ocorrer duas situações altamente indesejáveis para o processo que recebeu sinais em sequência. A primeira é o término do processo se a ação padrão for esta como no caso do sinal SIGINT. A segunda é a perda do sinal se a ação padrão for ignorar o sinal como no caso de SIGCHLD. Devido à segunda observação, o desenvolvedor de sistema deve sempre vericar se o erro retornado por uma chamada de sistema foi causado por uma interrupção. No Unix, basta vericar se o valor da variável global errno é a constante EINTR. Se for, a chamada de sistema deve ser repetida. variável errno A fornece o motivo que causou a falha em chamadas de sistema. 35 2.6 Comunicação e sincronização interprocesso Comunicação e sincronização interprocessos são facilidades oferecidas pelo sistema operacional para que processos possam cooperar, trocando informações e sincronizando suas ações. Comunicação e sincronização são necessárias quando uma aplicação é composta de vários processos, estrutura esta utilizada pelo próprio sistema operacional. 2.6.1 Comunicação interprocesso Como dois ou mais processos podem trocar informação? Existem basica- mente duas possibilidades. A primeira emprega meios diretamente disponíveis às aplicações tais como arquivos em disco e banco de dados. A segunda emprega mecanismos nativos de comunicação interprocesso oferecidos pelo sistema operacional por meio de chamadas de sistema. Os mecanismos nativos mais comuns são troca de mensagens e compartilhamento de memória. Troca de mensagens pode se dar por meio de um canal de comunicação estabelecido entre dois processos ou por meio de las de mensagens instaladas pelos processos. Um canal de comunicação deve ser estabelecido por ambos os processos e permite que um processo escreva dados (cadeia de bytes) no canal e outro processo leia estes dados do canal. O mecanismo de pipe, oferecido por vários sistemas operacionais, permite estabelecer este canal de comunicação entre dois processos (tipicamente entre processos pai e lho). A escrita e leitura de dados no canal se dá por meio das chamadas padrão write e read. O mecanismo de pipe é mais comumente empregado em comandos pro- cessados pelo interpretador de comandos. O interpretador de comandos cria pipe um conectando a saída padrão de um processo com a entrada padrão de outro processo por meio da barra vertical. Por exemplo, o comando ls | grep "*.pdf" faz com que a saída padrão produzida pelo comando (listagem de diretório) seja a entrada padrão do comando grep ls que, neste exemplo, listará apenas as linhas que contenham a sequência .pdf . Filas de mensagens são buers alocados pelo sistema operacional aos processos e identicados unicamente no sistema por meio de um identicador numérico. Conhecendo o identicador de uma la de mensagem de outro processo, um processo pode enviar uma mensagem (cadeia de bytes) para esta la. O envio e recuperação de mensagens se dá por meio de chamadas de sistema especícas tipo send e receive. 36 Atualmente, aplicações que utilizam troca de mensagens comumente em- sockets. sockets são canais sockets tipo via rede (sockets tipo INET). pregam as chamadas de sistema denominadas de comunicação que interligam processos na mesma máquina ( UNIX) ou em máquinas distintas conectadas Sockets tipo UNIX empregam o próprio sistema de arquivo para armazenar temporariamente as mensagens trocadas entre os processos. O compartilhamento de memória se dá quando uma área de memória alocada pelo sistema operacional é incorporada à área de dados de dois ou mais processos como ilustrado na gura 2.10. Neste caso dois processos possuem entradas na tabela de páginas por processo mapeadas na mesma página de memória física sem a marcação copy-on-write. Figura 2.10: Compartilhamento de memória: páginas virtuais P1 e j do processo P2 mapeadas na mesma página física i do processo k. Se troca de mensagens é uma comunicação do tipo um-para-um, memória compartilhada é do tipo um-para-muitos no sentido que um dado armazenado por um processo pode ser acessado por vários outros processos. A comunicação por troca de mensagens pode ser síncrona no sentido que um processo pode bloquer até que uma mensagem seja enviada a ele. Memória compartilhada é uma comunicação sempre assíncrona, ou seja, processos nunca bloqueiam no acesso à memória compartilhada. Para operar memória compartilhada o sistema operacional oferece chamadas de sistema para alocar um bloco de memória compartilhada e associar este bloco ao espaço de endereçamento de processos. Como no caso de la de mensagens, um bloco de memória compartilhada alocado pelo sistema 37 operacional possui um identicador único no sistema. Após incorporada ao espaço de endereçamento do processo, a memória compartilhada pode ser operada como uma área de memória do próprio processo, por exemplo, via memcpy ), operação com ponteiros, etc. cópia binária ( 2.6.2 Sincronização interprocesso Não raro processos devem tem suas ações sincronizadas. É o caso, por exemplo, de processos que operam sobre um mesmo recurso, seja um arquivo em disco, um dispositivo de entrada e saída ou uma estrutura de dados em uma região de memória compartilhada. O compartilhamento temporal da CPU garante a integridade dos processos mas não garante a integridade dos recursos manipulados concorrentemente pelos processos. Para ilustrar isso vamos considerar dois processos como na Figura 2.11 onde o processo P1 (produtor) captura imagens de uma câmera e as armazena em um buer em memória compartilhada. Um processo P2 (consumidor) lê este buer e apresenta a imagem após algum processamento. Figura 2.11: Arranjo produtor-consumidor. Neste exemplo as operações de escrita e leitura no buer devem ser sincronizadas sob pena de processo P1 cópia da imagem no buer e o processo apenas em parte. perder a CPU antes de completar a P2 ler todo o buer, este autualizado Esta competição pelo recurso, denominada corrida (reace condition ), pode tornar seu estado inconsistente. condição de Condições de corrida podem ser evitadas operando o recurso compartilhado atomicamente com o auxílio de técnicas de sincronização. Esta sincronização é feita via a introdução de um conceito denominado região crítica. Uma região crítica é um trecho de código que protege um recurso compartilhado. Regiões críticas protegendo um mesmo recurso compartilhado têm a propriedade de exclusão mútua, ou seja, um único processo 38 pode estar executando a região crítica em um dado instante de tempo. Regiões críticas são delimitadas por um protocolo de entrada na região e um protocolo de saída da região. Estes protocolos, implementados em chamadas de sistema, apresentam as seguintes propriedades: • O protocolo de entrada, quando retorna, garante que nenhum outro processo está executando uma região crítica protegendo o mesmo recurso compartilhado. • O protocolo de entrada bloqueia o processo até que a condição anterior seja satisfeita. Este bloqueio não pode durar indenidamente. • O protocolo de saída nunca bloqueia o processo que o invocou. Uma questão crucial é porque os protocolos de entrada e saída devem ser implementados na forma de chamadas de sistema, e não funções comuns. A razão é que, para garantir as propriedades acima, os protocolos de entrada e saída devem executar atomicamente. Imagine que um processo P1 executando um procedimento de entrada perca a CPU no momento em que verica que acesso é possível, mas antes que possa atualizar as estruturas de dados indicando que este processo ingressará na região crítica. Suponha que um processo P2 assuma a CPU e execute integralmente o mesmo protocolo de entrada e ingressa na região crítica, perdendo a CPU no interior da mesma. Ao retomar a CPU o processo P1 completa o procedimento de entrada e ingressa na região crítica, violando a propriedade de exclusão mútua. Isto pode ser evitado desabilitando interrupções durante a execução dos protocolos de entrada e saída da região critica. Entretanto, lembremos que somente o sistema operacional pode desabilitar interrupções. Desta forma, a única maneira segura de implementar protocolos de entrada e saída de regiões críticas é via chamadas de sistema. Existem várias propostas de protocolos de entrada e saída de regiões críticas, mas apenas um teve ampla aceitação: o protocolo de semáforos. O protocolo de semáforos foi concebido por Edsger Dijkstra, um dos maiores cientistas da computação de todos os tempos. Dijkstra chamou o protocolo de entrada na região crítica de P e o protocolo de saída de V. Um semáforo é uma estrutura de dados mantida pelo sistema operacional com um contador c e uma lista b com identicadores de processo bloqueados como ilustrado no quadro 2.3. 39 Quadro 2.3: Estrutura de dados de um semáforo. s t r u c t Semaforo { int c ; l i s t <i n t > b ; } Esta estrutura é criada por chamada de sistema especíca e identicada unicamente no sistema por meio de um identicador numérico. O contador c é inicializado com o número de processos que podem estar na região crítica ao mesmo tempo, usualmente um. A lista é inicializada como vazia. A unica situação onde o contador é inicializado com valor superior a um é quando existe mais de uma instância do recurso compartilhado que podem ser operadas concorrentemente. A chamada P, executada com interrupções desabilitadas, é denida segundo o quadro 2.4. Quadro 2.4: Operação P de um semáforo. void P( Semaforo S ) { S . c −−; i f ( S . c < 0) { S . b . push_back ( c u r r e n t ) ; block ( current ) ; } } // i n s e r e o proc . no f i n a l da f i l a // b l o q u e i a o p r o c e s s o A chamada P decrementa o contador e bloqueia o processo caso o contador seja negativo. Um valor negativo indica que não existe mais instâncias do recurso disponíveis e, portanto, o protocolo de entrada deve bloquear o processo (chamada block sobre o processo corrente, current ). Quando positivo, o valor do semáforo indica quantas instâncias do recurso estão disponíveis, e, quando negativo, seu valor absoluto indica quantos processos estão aguardando o ingresso na região crítica. A chamada V desbloqueia um processo aguardando no semáforo. Este desbloqueio faz com que a chamada P para este processo bloqueado retorne e o mesmo ingresse na região crítica. A chamada V, também executada com interrupções desabilitadas, é denida segundo o quadro 2.5. 40 Quadro 2.5: Operação V de um semáforo. void V( Semaforo S ) { i f ( b . s i z e ( ) > 0) { int p = S.b. front ( ) ; S . b . pop_front ( ) ; unblock ( p ) ; } S . c++; } // // // // exist em p r o c e s s o s bloquados a c e s s a o p r i m e i r o da f i l a remove−o da f i l a desbloqueia este processo Por sua natureza assíncrona, a comunicação por memória compartilhada é comumente utilizada em conjunto com semáforos, ou seja, as operações de leitura e escrita na memória compartilhada estão circunscritas a regiões críticas delimitadas pelo protocolo de semáforos. 2.6.3 O problema da inversão de prioridades Imagine a seguinte situação. Um processo com certa prioridade estática ingressa em uma região crítica. Um outro processo, com prioridade superior torna-se pronto e, ao assimuir a CPU tenta ingressar na mesma região crítica. O protocolo de entrada irá bloquer este processo, que dependerá do processo de menor prioridade terminar a região crítica, para que o de mais alta prioridade possa prosseguir. Note que a situação se agrava se um outro processo com prioridade intermediária entre os dois torna-se pronto. Este processo irá preemptar o de mais baixa prioridade agravando ainda mais a situação do processo de alta prioridade. Este problema denomina-se inversão de prioridades e pode comprometer severamente o atendimento das restrições de tempo real. A forma mais simples de resolver este problema é a seguinte, supondo regiões críticas controladas por semáforos. Um processo S j que detém a posse de um semáforo tem sua prioridade estática aumentada para: P = max{Pj , Pk }, k = 1, ...N Pj é a prioridade estática do onde processo j e Pk é a prioridade estática do k-ésimo processo da la de processos bloqueados no semáforo S. Este esquema é denominado herança de prioridades. O processo que herdou uma prioridade mais alta retorna à sua prioridade original ao deixar a região crítica. Desta forma, a prioridade de um processo aguardando um semáforo não ca comprometida pela prioridade do processo que está de posse do 41 semáforo. 2.7 A tabela de processos O sistema operacional mantém uma tabela para o gerenciamento de processos, a tabela de processos. Cada processo possui uma entrada nesta tabela que armazena informações referentes ao processo. Processos possuem um process identier ). identicador único, denominado PID ( Nos sistemas Unix o PID de um processo corresponde à sua entrada na tabela de processos. Assim o sistema operacional pode acessar informações de um processo dado seu PID sem ter que realizar buscas na tabela de processos. As principais informações armazenadas na tabela de processo, além do PID, são: • O estado corrente do processo. • Ponteiro para a entrada correspondente ao processo pai. • Lista de processos lhos. • Parâmetros de escalonamento (prioridades estática e dinâmica do processo, uso da CPU, etc.). • Gerenciadores de sinais denidos pelo processo. • Sinais pendentes (não tratados) enviados ao processo. • Semáforos utilizados pelo processo. • Arquivos abertos pelo processo. • Espaço de endereçamento virtual associado ao processo (áreas de texto, dados e pilha), veja gura 2.3. 2.8 Na prática Nesta seção apresentaremos uma versão de um servidor HTTP (Hypertext Transfer Protocol), o protocolo da Web. Esta versão é composta de um processo mestre que recebe requisições tipo HTTP GET (acesso à páginas) 42 via rede. Recebida a requisição o processo cria um processo lho (escravo) para tratá-la e volta a aguardar novas requisições. O servidor, apesar de simples em termos de funcionalidades, expõe os principais conceitos referentes a processos: criação, comunicação, sincronização e sinalização, conforme descrição a seguir. Ao receber uma conexão via rede o processo mestre executa a chamada fork para criar um processo lho para processá-la. O processamento da requisição consiste em acessar a página solicitada em um diretório comumente denominado Web Space e retorná-la para o cliente. Para aumentar a eciência no atendimento às requisições, servidores HTTP utilizam um cache de páginas. Este cache pode ser criado em uma região de memória compartilhada. Neste caso, esta região deve ser protegida por um semáforo dado que será acessada e alterada por múltiplos processos. Identicada a página solicitada, o processo lho entra em uma região crítica e tenta localizar a página no cache de páginas. 2 Caso a encontre, o processo retorna esta página para o cliente e sai da região crítica, encerrando sua execução. procurada no Caso a página não seja encontrada no cache, a mesma é Web Space e retornada ao cliente. encontre nem cache nem no Web Space Caso a página não se um erro é retornado ao cliente. O endereço da região de memória compartilhada onde se encontra o cache de páginas, o diretório minado Web Space e o identicador da conexão de rede (deno- socket ) são herdados pelo processo lho do processo pai. Quando o processo lho termina o sistema operacional envia um sinal SIGCHLD para o processo pai. A informação do término do processo lho pode ser útil, por exemplo, para limitar a quantidade de processos lhos ou para se determinar o tempo de vida médio do processo lho que é igual ao tempo médio de atendimento de requisições. A gura 2.12 ilustra a arquitetura da aplicação. 2.9 Exercícios 1. Comente a seguinte denição de processo: um segmento de memória que contém código executável. 2. Explique como um endereço virtual referenciado por um programa é convertido no seu correspondente endereço na memória física. Quantas 2 Escrevendo o conteúdo da página na conexão de rede. 43 Figura 2.12: Arquitetura do servidor HTTP. operações aritméticas são necessárias para tal conversão? 3. Qual a vantagem da criação de processos via chamada fork se os pro- cessos pai e lho não compartilham dados diretamente? 4. Explique as três formas de criação de processos no sistema Unix/Linux (chamadas fork /vfork e exec ) enfatizando o que ocorre com as tabelas de páginas do processo criador (que executou a chamada de sistema) e do processo recém criado. 5. Descreva as ações que o sistema operacional executa para proceder a troca de contexto desde a interrupção de relógio até o início da execução do processo que foi escalonado para ter a posse da CPU. 6. O quantum de CPU é o tempo máximo de posse da CPU por um processo. Descreva as vantagens e desvantagens deste tempo ser ex- tremamente pequeno ou extremamente grande em relação aos valores comumente adotados na prática (da ordem de 100 ms). 44 7. É possível um processo bloquear sem que o mesmo realize uma chamada de sistema? 8. Por que o esquema de escalonamento com múltiplas las é adequado para a grande maioria das aplicações atuais? 9. Como o sistema operacional evita o monopólio da CPU por parte de um único processo? 10. Cite três situações que provocam a troca de contexto entre processos. 11. Suponha que a prioridade de um processo seja calculada por uma função. Que parâmetros de entrada teria esta função? Cite aqueles que julgar mais importantes. 12. Esquematize um escalonador de processos para uma aplicação composta de múltiplos processos empregando o mecanismo de sinais. 13. Explique as diferenças do compartilhamento de memória no caso da criação de processos via chamada fork e no caso de memória compar- tilhada via IPC (Interprocess Communication). 14. No esquema de memória compartilhada o sistema operacional deve manter controle sobre quais processos estão utilizando a região compartilhada. Por que este controle é necessário? 15. Por que não é aconselhado a implementação de mecanismos de sincronização interprocesso no espaço do usuário (sem chamadas de sistema)? 16. O que caracteriza uma região crítica? 17. Na exemplicação de semáforos (Quadros 2.4 e 2.5) utilizamos as cha- block e unblock. Implemente estas chamadas com as chamadas de sistema kill e getpid. madas 18. Explique a situação onde as chamadas de sistema: shmat signal, kill, wait3 e (shared memory attach) são empregadas. 19. Seja uma aplicação que controla a operação dos geradores de uma usina hidrelétrica composta de cinco processos: regulação de rota- ção, monitoramento de parâmetros elétricos e mecânicos, desligamento emergencial, gravação de logs e interface com o operador. Atribua prioridades estáticas a cada um dos processos e justique a atribuição. 45 20. Ilustre a inversão de prioridades para o cenário descrito no exercício acima. 46 Capítulo 3 Threads No capítulo anterior introduzimos processo, um programa em execução ou uma unidade de controle, alocação e compartilhamento de recursos por parte do sistema operacional. Entretanto, existe uma unidade menor de execução e de controle, alocação e compartilhamento de recursos. denominada thread (linha) de execução ou simplesmente Esta unidade é thread 1 . 3.1 Motivações para threads Seja o exemplo do servidor HTTP apresentado no capítulo anterior. Ao processar uma requisição HTTP, caso a página solicitada não seja encontrada no cache de páginas, o processo irá bloquear no acesso ao arquivo contendo a página Web em disco. O bloqueio dos processos que atendem as requisições (processos escravos) provoca uma mudança frequente de contexto degradando o desempenho do servidor. Outra desvantagem deste servidor HTTP é a criação de um processo para o atendimento de cada requisição, aumentando a competição pela CPU e somando o overhead overhead da criação de um processo ao do processamento da requisição. Que solução seria ideal? Uma possível resposta seria utilizarmos um único processo capaz de processar cada requisição de forma independente. Esta independência signica que quando uma requisição executa uma chamada bloqueante nem o processo nem o processamento das demais requisições bloqueiam. Estas linhas de execução independentes são as threads. Em um 1 Utilizaremos o termo em inglês no feminino como é comumente utilizado pelos desenvolvedores de software. 47 sistema operacional com suporte a threads, o processo é bloqueado quando expira seu quantum de CPU ou quando todas as suas threads estiverem bloqueadas. Uma thread é um trecho de código denido por meio de uma função. Este código, se executado como thread, tem seu próprio contexto e pilha, tal como processos. Isto signica que ao assumir a CPU uma thread executa a partir do ponto onde parou, como ocorre com processos. Threads compartilham a mesma área de texto e dados do processo. Portanto, threads compartilham código (por exemplo, funções) e dados (por exemplo, variáveis globais) sem necessitar de mecanismos de comunicação interprocesso. Desta forma, a comunicação interthread é muito mais simples e natural que a comunicação interprocessos. A criação e nalização de threads são mais rápidas se comparadas com processos. A razão para tal é que threads utilizam boa parte de seus recursos do próprio processo, principalmente as áreas de texto e dados. Isto evita a alocação, desmembramento e desalocação de um grande número de páginas quando threads são criadas e nalizadas dado que a maior parte das páginas utilizadas pelas threads pertencem ao processo. A mudança de contexto de threads também é mais rápida que a de processos. Isto também tem a ver com o gerenciamento de memória. Conforme veremos no próximo capítulo, a mudança de contexto de processos implica na atualização de tabelas, invalidação de cache de páginas, dentre outras operações. No caso de threads, estas operações já foram realizadas durante a mudança de contexto do processo que abriga as threads, não sendo necessárias na mudança de contexto entre threads. 3.2 Gerenciamento de threads Quando o conceito de threads foi introduzido no início da década de 1980 os sistemas operacionais em uso na época não suportavam este conceito. Como solução inicial foram criadas bibliotecas que davam suporte a threads no espaço do usuário, ou seja, sem qualquer suporte por parte do núcleo do sistema operacional. Um exemplo desta biblioteca é a GNU Portable Threads (pth). Estas bibliotecas reimplementam, também no espaço do usuário, funções equivalentes às chamadas de sistema bloqueantes (por exemplo, a chamada read ). Isto é necessário pois uma chamada de sistema bloqueante por parte de uma thread causará o bloqueio do processo e, com ele, todas as 48 suas threads (o que invalida as vantagens do modelo de threads). Assim, um programa que faz uso de uma biblioteca de threads necessita substituir as chamadas de sistema bloqueantes por funções correspondentes da biblioteca, o que compromete a portabilidade do código. Como a biblioteca de threads não pode manipular interrupções de relógio as threads criadas por meio desta biblioteca não podem sofrer preempção. Em outras palavras, uma thread executa até que termine, que suspenda temporariamente sua execução ou que execute uma função correspondente a uma chamada de sistema bloqueante. Nestes casos, a biblioteca promoverá a troca de contexto entre as threads. Atualmente, praticamente todos os sistemas operacionais oferecem suporte a threads no nível do núcleo. Isto signica que o próprio núcleo gerencia as threads, sem a necessidade de bibliotecas no espaço do usuário. Desta forma, quando uma thread executa uma chamada de sistema bloqueante e o quantum de CPU do processo ainda não expirou, o núcleo seleciona outra thread do processo para assumir a posse da CPU. De uma maneira geral, o núcleo executa o escalonamento em dois níveis, no nível de processo (qual processo terá a posse da CPU) e no nível de theads (qual thread deste processo terá a posse da CPU). Com suporte no nível do núcleo a manipulação de threads se dá via chamadas de sistema. No Linux estas chamadas fazem parte de uma especicação denominada POSIX Threads (pthreads) que tem por meta uniformizar a manipulação de threads na linguagem C/C++ nos sistemas derivados do Unix. Threads podem ser manipuladas a partir de linguagens de programação como Java (classe Thread ) propósito geral como e Python (pacote Boost. thread ) e a partir de bibliotecas de Com o surgimento de processadores com múltiplos cores (multicore ), o uso de threads se torna extremamente vantajoso pois o sistema operacional é capaz de alocar threads para executar em cores distintos, propiciando um paralelismo real e uma utilização mais efetiva dos recursos do processador. O sistema operacional Windows, a partir da versão 7, oferece um modelo de threads onde o programador tem mais controle sobre o seu gerenciamento. Neste modelo, denominado User-Mode Scheduling, o programador pode implementar sua própria estratégia de escalonamento de threads com o objetivo de tirar proveito de especicidades da aplicação. Por exemplo, é possível alocar threads que possuem anidades (por exemplo, compartilham os mesmos recursos) em um mesmo core 49 do processador. 3.3 Utilização de threads Threads são utilizadas quando um processo executa tarefas independentes. Servidores, aplicações interativas, simuladores e sistemas de tempo real são exemplos de programas onde o uso de threads pode ser vantajoso. Para que esta vantagem seja expressiva é preciso considerar alguns pontos: • As tarefas executam operações com potencial de bloqueio. • As tarefas não são sincronizadas, isto é, não possuem uma ordem de execução interdependente, por exemplo, tarefas que dependem de resultados produzidos por outras tarefas. • As tarefas não acessam dados compartilhados com frequência, ou seja, raramente bloqueiam no acesso a regiões críticas. Bibliotecas de threads, como pthreads, oferecem funções para criação, terminação, sinalização, alteração de atributos e escalonamento de threads. O quadro 3.1 ilustra a criação de uma thread no programa principal via pthread_create. Esta chamada recebe quatro parâmetros: 1. Um ponteiro que receberá o identicador da thread criada. 2. Parâmetros de escalonamento (seção 3.5). 3. Ponteiro para a função que implementa a thread. 4. Parâmetro a ser passado à esta função. A chamada pthread_join evita o término prematuro das threads criadas pelo processo quando este termina. Esta chamada bloqueia a thread que efetuou a chamada (no caso a thread principal) até que a thread passada no primeiro parâmetro termine. O mecanismo de sinais está disponível para threads, ou seja, é possível o envio de sinais para uma thread. A chamada pthread_kill no Linux é utilizada para o envio de sinais às threads. Entretanto, nas versões correntes do Linux, os sinais SIGSTOP, SIGCONT e SIGTERM (terminação) são enviados ao processo que abriga as threads, invalidando parcilmente o uso de sinais no nível de threads. 50 Quadro 3.1: Exemplo de criação de thread. #i n c l u d e <pthread . h> // d e f i n i c a o de thread void ∗ thread1 ( void ∗ p ) { p r i n t f (" Thread r e c e b e u parametro : %s \n " , ( char ∗ ) p ) ; } // programa p r i n c i p a l main ( ) { pthread_t pth ; pthread_create (&pth , NULL, thread1 , " Alo thread " ) ; pthread_join ( pth , NULL) ; // e s p e r a thread t e r m i n a r } Em um servidor, por exemplo, é comum a criação de duas classes de threads, mestre e trabalhadora. A thread mestre aguarda requisições e, ao receber uma, cria uma thread trabalhadora para processá-la. A thread trabalhadora gera a resposta para o cliente e termina sua execução. Variantes deste esquema é o emprego de diferentes threads trabalhadoras, uma para cada tipo de requisição. Ainda, é possível utilizar um overhead pool de threads evitando o de criação e término de threads. 3.4 Comunicação e sincronização interthreads A comunicação entre threads não requer mecanismos especícos como memória compartilhada no caso de processos. Threads compartilham recursos do processo tais como estruturas de dados globais, arquivos abertos e conexões de rede. Obviamente, o acesso concorrente a estes recursos deve ser controlado por regiões críticas. As chamadas de sistema propiciam semáforos no nível de thread. Por exemplo, pthreads disponibiliza mutexes, ou semáforos binários (inicializados em um). Outros mecanismos podem ser disponibilizados por linguagens de programação. Por exemplo, a linguagem Java disponibiliza o mecanismo de monitor para sincronização de threads. Um monitor, denido em Java pela palavra reservada synchronized antes da declaração de uma função, torna esta função uma região crítica. Não há protocolos explícitos de entrada e saída como no caso de semáforos. O protocolo impícito é o seguinte: um monitor pode ser 51 invocado por múltiplas threads, mas apenas uma instância do monitor pode estar em execução por vez. Ou seja, uma função denida como synchronized é uma região crítica na linguagem Java para todas as threads no mesmo processo que utilizam tal função. 3.4.1 Variáveis de condição Variáveis de condição são também disponibilizadas via chamadas de sistema. Como o próprio nome indica, uma variável de condição está associada à ocorrência de uma condição (ou evento). Vamos ilustrar variáveis de con- dição para o caso de um servidor com uma thread mestre e várias threads trabalhadoras (gura 3.1). Figura 3.1: Threads mestre e trabalhadoras compartilhando um buer de dados. A thread mestre recebe requisições e as armazenam em uma lista de requisições pendentes. As threads trabalhadoras retiram requisições da lista para processá-las e retornam o resultado aos respectivos clientes. se, portanto, de um arranjo produtor-consumidor. Trata- Obviamente, a lista de requisições pendentes deve ser protegida por um semáforo ou mutex. As threads mestre e trabalhadoras denem regiões críticas para acesso à lista de requisições pendentes. Neste esquema, duas situações limites devem ser tratadas: 52 1. Uma thread trabalhadora vai retirar uma requisição para processar e verica que a lista de requisições pendentes está vazia. 2. A thread mestre vai armazenar uma nova requição na lista de requisições pendentes e verica que a lista está cheia (caso seja denido um tamanho máximo para a lista). Estas duas situações ocorrem quando a thread está no interior da região crítica (de posse do mutex). Uma solução trivial é a thread liberar o semáforo e tentar a operação sobre a lista novamente. Entretanto, este mecamismo de espera ocupada ( polling ) é ineciente pois requer que as threads acessem a lista de requisições pendentes com frequência, causando um alto e inútil consumo de CPU. Aqui entram as variáveis de condição. Uma variável de condição permite que threads executando uma região crítica bloqueiem até que certa condição seja satisfeita. Ao bloquear, o mutex é liberado para que uma outra thread tenha a oportunidade de acessar a mesma região crítica e eliminar a condição de bloqueio. Seja o caso da thread trabalhadora que, ao ingressar na região crítica, depara-se com a lista de requisições pendentes vazia. Ao invés de liberar o mutex, a thread executa uma chamada de sistema tipo variável de condição indicando lista não vazia. wait sobre uma Esta operação bloqueia a thread até que a condição seja satisfeita. Note que várias threads podem estar nesta situação como no caso onde não há requisições pendentes a processar. Ao executar a chamada wait o mutex é liberado, o que permite a thread mestre adicionar uma requisição pendente à lista. A adição de uma requisição satisfaz a condição (lista não vazia) associada à variável de condição. Neste momento, imediatamente antes de liberar o mutex, a thread mestre executa uma chamada de sistema tipo signal 2 sobre a variável de condição. chamada faz com que uma das threads bloqueadas na chamada wait Esta seja desbloqueada e recupere a posse do mutex. Assim, as threads trabalhadoras operam a lista de requisições pendentes da seguinte forma. Seja m um mutex e c uma variável de condição. O quadro 3.2 ilustra a utilização de variáveis de condição quando uma thread trabalhadora encontra a lista vazia. 2 Diferente da chamada signal utilizada em processos como exemplicado no quadro 2.2. 53 Quadro 3.2: Variáveis de condição na thread trabalhadora. pthread_mutex_lock(&m) ; // a d q u i r e mutex i f ( l i s t a . s i z e ( ) == 0) // l i s t a v a z i a −− aguarda c o n d i c a o pthread_cond_wait(&c , &m) ; // l i s t a nao v a z i a req = l i s t a . back ( ) ; // a c e s s a r e q u i s i c a o no f i n a l da l i s t a l i s t a . pop_back ( ) ; // r e t i r a elemento da l i s t a pthread_mutex_unlock(&m) ; // l i b e r a mutex O quadro 3.3 ilustra a utilização de variáveis de condição por parte da thread mestre quando esta opera a lista de requisições pendentes. Quadro 3.3: Variáveis de condição na thread mestre. pthread_mutex_lock(&m) ; l i s t a . push_front ( req ) ; pthread_cond_signal(&c ) ; pthread_mutex_unlock(&m) ; // // // // a d q u i r e mutex adiciona requisicao c a s o haja uma thread t r a b a l h a d o r a bloq . l i b e r a mutex Variáveis de condição evitam espera ocupada tornando a sincronização entre threads eciente. Entretanto, o uso incorreto deste recurso pode causar deadlock, como no caso de chamadas wait sem as correspontentes chamadas signal. Finalmente, variáveis de condição substituem o mecanismo de sinais que, como vimos, possui desvantagens principalmente quando utilizado para a sincronização de threads. 3.4.2 Threads e a chamada fork O que acontece quando um programa que iniciou múltiplas threads executa a chamada fork ? Em uma primeira análise seria de se esperar que o processo lho iniciasse com todas as threads em execução no processo pai. Entretanto, nos sistemas Unix o processo lho é criado apenas com a thread principal (função main ). Pior ainda, os mutexes e as variáveis de condição no processo lho assumem um estado indenido. chamada condição. fork Desta forma, é recomendado que a ocorra antes da criação de threads, mutexes e variáveis de Com este cuidado os processos pai e lho têm a oportunidade de criar suas próprias threads, mutexes e variáveis de condição de forma independente e segura. 54 3.4.3 Threads em sistemas de tempo real Um sistema de tempo real deve evitar taxas elevadas de criação e término de threads. Para estes sistemas, um os overheads pool de threads é preferível pois evita de criação e término de threads. Em sistemas de tempo real, threads são comumentes escalonadas por prioridade e sem preempção, como o escalonamento FIFO de processos. Outro ponto importante para sistemas de tempo real é a garantia que o sistema operacional trata a questão de inversão de prioridades via, por exemplo, herança de prioridades como no caso do sistema Linux. Finalmente, a possibilidade da construção de escalonadores especializados para determinada aplicação pode ser atrativo para aplicações de tempo real. O mecanismo User-Mode Scheduling do sistema Windows permite a programação de um escalonador dedicado que considere as restrições de tempo real da aplicação. No sistema Linux, escalonadores de threads podem ser implementados via chamadas de sistema que alteram dinamicamente a prioridade das threads. 3.5 Escalonamento de threads O escalonamento de threads comumente emprega um ou uma combinação de três modelos: • Escalonamento não preemptivo, onde a thread que detêm a posse da CPU executa até que a mesma bloqueie ou termine. Este modelo de escalonamento é mais comum em threads no nível do usuário. • Escalonamento round-robin (preemptivo), onde as threads prontas recebem um quantum igual de CPU. • Escalonamento por prioridades (preemptivo), onde a thread de mais alta prioridade detem a CPU até que a mesma bloqueie ou termine. As chamadas pthreads suporta o modelo de escalonamento por roundrobin e por prioridades. As threads criadas sem prioridade denida (como no exemplo do quadro 3.3 onde o segundo parâmetro de pthread_create é nulo) são escalonadas por round-robin. Estas threads recebem uma prioridade prédenida que é sempre menor que as prioridades explicitamente atribuídas. O quadro 3.4 ilustra a criação de uma thread com prioridade máxima. Vale 55 ressaltar que no caso de pthreads apenas processos com permissão de superusuário podem criar threads com prioridades explícitas. Finalmente, threads escalonadas por prioridade sofrem do problema de inversão de prioridades discutido na seção 2.6.3. Quadro 3.4: Exemplo de criação de thread. #i n c l u d e <pthread . h> // d e f i n i c a o de thread void ∗ thread1 ( void ∗ p ) { p r i n t f (" Thread r e c e b e u parametro : %s \n " , ( char ∗ ) p ) ; } main ( ) { // programa p r i n c i p a l pthread_t pth ; pthread_attr_t ta ; // a t r i b u t o s de escalonamento s t r u c t sched_param sp ; // p ar s . de escalonamento // obtem a maxima p r i o r i d a d e que uma thread pode t e r sp . s c h e d _ p r i o r i t y = sched_get_priority_max (SCHED_FIFO ) ; // d e f i n e parametros de escalonameento p t h r e a d _ a t t r _ i n i t (&sp ) ; p t h r e a d _ a t t r _ s e t i n h e r i t s c h e d (&ta , PTHREAD_EXPLICIT_SCHED) ; p t h r e a d _ a t t r _ s e t s c h e d p o l i c y (&ta , SCHED_FIFO ) ; pthread_attr_setschedparam(&ta , &sp ) ; } // c r i a thread com p r i o r i d a d e e x p l i c i t a pthread_create (&pth , &ta , thread1 , " Alo thread " ) ; pthread_join ( pth , NULL) ; // e s p e r a thread t e r m i n a r 3.6 Na prática Nesta seção o servidor da gura 2.12 será transformado em um servidor multithreaded. Nesta conguração os processos lhos são substituidos por um pool de threads trabalhadoras. O processo mestre torna-se agora uma thread mestre. O cache de páginas agora é uma área do próprio processo, sendo o semáforo substituido por um mutex. O processo manterá uma lista de requisições pendentes que é acessada pelas threads trabalhadoras. A lista é protegida por um mutex e uma 56 variável de condição associada à este mutex é empregada quando uma thread trabalhadora encontra a lista vazia. A lógica de atendimento das requisições é a mesma do caso de processos. A gura 3.2 ilustra a arquitetura do servidor multithreaded. Nesta arquitetura sinais não são utilizados. Figura 3.2: Arquitetura do servidor multithreaded. 3.7 Exercícios 1. Cite duas situações onde o emprego de threads é vantajoso e duas onde o emprego não é vantajoso. 2. O uso de threads em um sistema operacional executando em um processador com um único core ainda é vantajoso? Justique. 3. Quais as desvantagens de se utilizar threads no espaço do usuário? 57 4. Quais as condições para um processo estar no estado de bloqueado e pronto sob o ponto de vista de suas threads? 5. A linguagem Java permite a criação e sincronização de threads, mas não permite que uma thread force o término de outra thread. Pesquise o porque desta decisão. (Obs: na biblioteca pthreads existe a função pthread_kill que permite uma thread forçar o término de outra). 6. Existe inversão de prioridades entre threads? Justique. 7. Pesquise como funciona o mecanismo de User-Mode Scheduling do sistema Windows. 8. As chamadas pthreads oferece apenas semáforos binários (mutexes). Implemente semáforos não binários (inicializados com qualquer inteiro maior que zero) a partir de mutexes e variáveis de condição. 9. Qual a importância das variáveis de condição em aplicações que empregam threads? 10. Por que variáveis de condição devem sempre ser utilizadas em conjunto com mutexes? 11. Intuitivamente, por que é mais vantajoso se implementar servidores com múltiplas threads em relação a múltiplos processos? 12. Desenvolva um programa com N threads para multiplicar duas matrizes NxN onde a thread i multiplica a linha i da primeira matriz pelas colunas da segunda matriz. 13. O programa da questão anterior requer sincronização entre as threads? Justique. 14. Comente a seguinte armação: a comunicação entre threads execu- tando em processos distintos deve utilizar os mesmos mecanismos de comunicação interprocesso. 15. Forneça uma justicativa para o fato do sistema operacional não reproduzir no processo lho as threads já criadas no processo pai em uma chamada fork. Dica: Veja a chamada 58 fork é processada (Seção 2.2). Capítulo 4 Subsistema de Memória Memória é recurso fundamental em um computador. O gerenciamento eciente deste recurso, que é função do subsistema de memória, dita a própria eciência do computador. Gerenciamento de memória consiste de duas atividades: gerenciamento do espaço físico e proteção. O gerenciamento do espaço físico trata de como a memória é distribuída entre os processos. A proteção impede que processos acessem regiões de memória fora de seus respectivos espaços de endereçamento. De forma um tanto simplicada, podemos identicar três formas de gerenciamento de espaço físico. Na primeira forma, partições xas, a memória é gerenciada em grandes blocos de tamanho suciente para comportar os programas (ou partes destes), sendo que uma ou mais partições são permanentemente ocupadas pelo próprio sistema operacional. Este esquema era utilizado nos antigos mainframes e em sistemas operacionais nonotarefa como o MS- DOS. Nestes sistemas os programas são instalados integralmente em partições xas e lá permanecem até terminarem. A desvantagem deste esquema é o limitado número de processos que podem executar simultaneamente. Por exemplo, as versões iniciais do sistema MS-DOS empregava três partições (gura 4.1), uma para o sistema operacional, uma para o único processo que o sistema executa por vez e uma para os acionadores de dispositivos (BIOS - Basic Input/Output System ). swapping ), Na segunda forma, permuta ( os programas são instalados em partições de tamanho variável, mas ao bloquearem, são transferidos integralmente para disco liberando espaço para programas que estão aguardando memória para iniciar ou que se tornaram prontos enquanto em disco (gura 4.2). Para o emprego deste esquema, o sistema operacional deve 59 Figura 4.1: Partições xas de memória no MS-DOS. gerenciar as partições livres e ocupadas, por exemplo empregando uma lista ligada como ilustrado na gura 2.3. No esquema de permuta, toda vez que um processo retornar à memória o mesmo deve passar por um procedimento de carregamento a m de ter seus endereços ajustados à nova região de memória que o processo irá ocupar. A desvantagem deste esquema é o alto custo de transferir programas inteiros da memória para o disco. Este esquema foi empregado nas versões do sistema Unix pré-System V e abandonado nas versões posteriores. Figura 4.2: Permuta de processos. 60 Em termos de proteção, as técnicas de partições e permuta empregam registradores de base e limite. Quando um processo ganha a posse da CPU, o registrador de base é carregado com o endereço da primeira posição de memória que o processo ocupa e o registrador limite é carregado com o tamanho do processo. Todo o acesso à memória é confrontado com o valor destes registradores e, caso o acesso ocorra em posição inferior à base ou superior à base mais o limite, a CPU sinaliza uma exceção de proteção. Gerenciamento por partições xas e permuta empregam um único espaço de endereçamento, espaço este limitado pela quantidade de memória disponível no computador. Estes esquemas tornaram-se totalmente superados pelo fato do tamanho dos programas crescerem mais que o tamanho das memórias. O aumento do tamanho do software foi propiciado pela diversidade e capacidade dos recursos do harware (armazenamento, recursos grácos, multimídia, conectividade, etc.), bem como pelo amadurecimento da Engenharia de Software. Aplicações atuais demandam técnicas de gerenciamento de memória bem mais sosticadas que partições xas ou permuta. Esta terceira técnica, denominada memória virtual, requer parte de sua implementação em hardware e vem justamente contornar as deciências das técnicas de partições xas e permutas. O conceito de memória virtual foi introduzido na seção 2.1. A ideia é oferecer a cada processo um espaço de endereçamento amplo e independente da memória física, ou seja, um espaço virtual. Todos os endereços de memória presentes nas instruções de um programa se referem ao espaço de endereçamento virtual. Para efetivamente acessar as posições de memória a CPU deve converter estes endereços para endereços físicos (reais). Na seção 2.1 fornecemos uma visão simplicada do mapeamento por meio de uma tabela, a tabela de páginas por processo. Uma visão mais realista do mapeamento é fornecida nas seções subsequentes. 4.1 O mapeamento de endereços O mapeamento de endereços introduzido na seção 2.1 apresenta um problema de desempenho. Toda a vez que a CPU processar uma instrução contendo um endereço de memória é necessário uma consulta à tabela de páginas por processo e a realização de operações aritméticas para se chegar ao endereço físico. Isto toma muito mais tempo que a própria execução da instrução. Portanto, neste procedimento é necessário uma ajuda do hardware. Proces- 61 sadores modernos oferecem uma arquitetura de gerenciamento de memória para facilitar a implementação eciente de memória virtual. Vamos utilizar a arquitetura de gerenciamento de memória dos processadores Intel x86 como exemplo. Estes processadores são capazes de endereçar 4 Gbytes (modo normal, 32 bits) ou 64 Gbytes (modo estendido, 36 bits) de memória física. O espaço de endereçamento virtual é de 48 bits divididos em segmentos de 4 Gbytes cada. Um segmento é um bloco contínuo de memória que armazena dados com propriedades comuns em termos de proteção e permissões de acesso. Segmentos tipicamente comportam as áreas de texto, dados e pilha dos processos, além de estruturas de dados utilizadas pelo sistema operacional. Caso segmentação seja empregada, um processo possui um conjunto de segmentos e lhe é permitido apenas o acesso a estes segmentos. A razão de se utili- zar segmentos é propiciar capacidade de gerenciamento com granularidade superior a páginas. Por exemplo, a área de texto de um processo é uma área de leitura apenas. Marcando a proteção deste segmento para leitura apenas, o hardware pode identicar de pronto a violação desta restrição para qualquer acesso à area de texto, sem a necessidade de realizar esta vericação no nível de página. O sistema Linux não emprega segmentação por razões de portabilidade. Desta forma, nesta arquitetura, todo processo irá ocupar um único segmento de 4 Gbytes. O mapeamento do endereço linear no endereço físico é realizado da seguinte forma. Os processadores Intel utilizam tabelas de páginas de dois níveis. O primeiro nível é denominado diretório de páginas e o segundo nível é a tabela de páginas propriamente dita (gura 4.3). O endereço de memória onde o diretório de páginas de um processo está localizado é carregado pelo sistema operacional do registrador CR3. Os 10 bits mais signicativos do endereço apontam para uma entrada no diretório de páginas. A entrada nesta tabela contém, entre outras informações, o endereço de memória onde se encontra a tabela de páginas. Localizada a tabela de páginas, os próximos 10 bits do endereço virtual é um índice para esta tabela. Finalmente, a entrada desta tabela de páginas contem o número da página de memória física. Os 12 bits menos signicativos contém o deslocamento. A gura 4.3 ilustra este mapeamento. Cada entrada do diretório e da tabela de páginas possui 4 bytes. Desta forma o diretório de página e as tabelas de páginas ocupam, cada, 4 Kbytes de memória física. A vantagem de se empregar tabelas em dois ou mais níveis é o fato destas tabelas ocuparem memória proporcional à memória que o processo efetiva- 62 Figura 4.3: Tabela de páginas de dois níveis (Intel x86). mente utiliza. Note que tabela de páginas de um único nível ocupa memória dada pelo tamanho máximo do espaço virtual. Seja um espaço virtual de 4 Gbytes e tamanho de páginas de 4 Kbytes. Se uma entrada na tabela ocupa 4 bytes a tabela toda ocupará 4 Mbytes de memória. Considerando um processo que ocupa 1 Gbytes de memória, uma tabela de 2 níveis ocupará (considerando os mesmos 4 bytes por entrada) 1,004 Mbytes: • 1 K entradas primeiro nível, sendo que apenas 256 entradas possuem referência para o segundo nível (totalizando 4 Kbytes). • 256 tabelas no segundo nível, cada uma com 4 Kbytes, totalizando 1 Mbytes. Com tabelas em dois níveis a área oca do processo reservada ao crescimento dos dados e pilha (gura 2.1), não necessita de tabelas de páginas (segundo nível) até sua efetiva alocação. Vamos considerar páginas de 4 Kbytes e tabela de páginas de um único nível. Se um processo possui área oca de 40 Mbytes, ou 10 K páginas, teríamos 10 K entradas ocupando 40 Kbytes na tabela de páginas marcadas como não mapeadas. Com tabela de dois níveis, economizamos estes 40 Kbytes pois não precisamos alocar 63 uma tabela de páginas no segundo nível. Se considerarmos que os diretórios e tabelas de páginas são por processo e permanecem em memória durante a execução do processo, esta economia não é desprezível, principalmente em arquiteturas de 64 bits capazes de endereçar trilhões de páginas por processo. Neste caso a solução é aumentar o nível da tabela de páginas e/ou aumentar o tamanho da página. De fato, no modo estendido as arquiteturas Intel empregam tabelas de páginas de quatro níveis e tamanho de páginas de até 2 Gbytes. As entradas correspondentes ao diretório de páginas e à tabela de páginas são apresentadas na gura 4.4. Vinte bits são reservados para endereçar a entrada correspondente na tabela de páginas (no caso do diretório de páginas) ou a página física (no caso da tabela de páginas). Nove bits são utilizados em ambas as entradas, a maioria destes com o mesmo signicado nas duas entradas. Os bits principais são: • Bit P (presente): setado quando o mapeamento é válido. • Bit R (read/write): indica se a página e de leitura apenas ou de leitura e escrita. • Bit A (acessada): indica que a página foi acessada pelo processo. • Bit D (dirty): indica que a página foi escrita pelo processo. • Bit U (usuário/supervisor): indica se a página deve ser acessada apenas no modo usuário ou supervisor. Figura 4.4: Entrada do diretório e da tabela de páginas na arquitetura Intel x86. Na arquitetura Intel x86 as entradas do diretório e da tabela de páginas possuem 3 bits disponíveis para o usuário (no caso o sistema operacional). Estes bits podem ser utilizados para marcar a página como (CoW seção 2.2). copy-on-write Uma página marcada como CoW tem o bit R setado apenas para leitura. Quando o processo tenta escrever na página, a CPU gera 64 uma interrupção (violação de proteção). O manipulador desta interrupção inspeciona o bit CoW e verica que não se trata propriamente de uma violação de proteção, mas sim que a página deve ser desmembrada e o bit R ressetado para leitura/escrita na entrada da nova página. Outra alternativa para a falta do bit CoW é setar o bit U como supervisor e o bit R como leitura apenas. Esta marcação é especial pois em situação normal o supervisor teria sempre permissão de escrita na página. Desta forma, quando um processo executando em modo usuário (normal) tenta escrever em uma página, a CPU gera uma interrupção (agora de violação de permissão) que o sistema identica como sendo uma situação de copy-on-write. Cabe ao sistema operacional organizar as tabelas utilizadas pela arquitetura de hardware para que o mapeamento de endereços seja eciente. Comumente o sistema operacional dene uma estrutura própria de tabela de páginas. Esta estrutura é mapeada na estrutura de tabela de páginas da arquitetura de harware sobre a qual o sistema operacional executa. Por exemplo, o sistema Linux utiliza tabelas de páginas de três níveis. O nível Page Global Directory ), o intermediário PMD Page Middle Directory ), e o mais baixo PTE (Page Table Entry ). Portanto, mais alto é denominado PGD ( ( o endereço virtual é segmentado em quatro partes, três das quais contendo entrada para os três níveis de tabelas, mais o deslocamento na página. O esquema é similar ao da gura 4.3, com um nível intermediário a mais. Entretanto, como vimos, a arquitetura Intel x86 dene apenas dois níveis para a tabela de páginas. Quando o Linux executa nesta arquitetura, o nível intermediário (PMD) não é utilizado, ou seja, uma entrada na tabela PGD aponta diretamente para uma tabela PTE. Para facilitar a portabilidade entre as diversas arquiteturas de hardware, 1 o sistema Linux utiliza uma série de macros páginas. para operar sua tabela de O subsistema de memória enxerga uma estrutura padrão de tabelas de páginas que é traduzida por meio de macros para o formato de tabela empregada pela arquitetura de hardware. Ao portar o sistema para diferentes arquiteturas, apenas estas macros devem ser reescritas. Por exemplo, a macro pte_dirty() inspeciona se o bit D da tabela de páginas do Linux está setado. Esta macro se encarrega de localizar o bit na tabela de páginas da arquitetura de hardware. A gura 4.5 ilustra esta estratégia. 1 Uma macro é similar a uma função cujo código é replicado pelo compilador onde a macro é invocada. 65 Figura 4.5: Mapeamento entre tabelas de páginas do sistema operacional e da arquitetura de hardware por meio de macros. 4.2 Unidade de gerenciamento de memória As arquiteturas de hardware disponibilizam uma unidade de gerenciamento de memória, a MMU ( Memory Management Unit ). A MMU se interpõe entre a CPU e o barramento de acesso à memória conforme a gura 4.6, sendo parte da CPU na maioria das arquiteturas atuais. Todo endereço virtual referenciado por uma instrução é convertido pela MMU no endereço físico correspondente via tabela de páginas da arquitetura de hardware. Cada processo possui sua própria tabela de páginas. A MMU sabe qual tabela utilizar pelo valor do registrador CR3 da arquitetura x86 que é atualizado a cada troca de contexto. A tabela de páginas de um processo reside permanentemente em mémoria física enquanto este processo existir. Desta forma, o hardware dedicado da MMU é capaz de mapear um endereço virtual de forma muito eciente sem consumir ciclos da CPU. Entretanto, na 66 Figura 4.6: Posicionamento da MMU. arquitetura x86, um mapeamento requer dois acessos à memória, um para recuperar a entrada no diretório de páginas e outro para recuperar a entrada na tabela de páginas. Estes acessos diminuem a eciência do mapeamento de endereços. Para aumentar ainda mais a eciência do mapeamento de endereços, a Translation Lookaside Buer ). MMU emprega um cache denominado TLB ( Este cache é uma memória associativa que mapeia uma página virtual em sua correspondente página física. O mapeamento de endereços empregando o TLB é ilustrado na gura 4.7. As setas nas indicam o mapeamento sem o emprego do TLB e as setas espessas indicam o mapeamento por meio do TLB. Nota-se claramente nesta gura que o TLB propicia um atalho no mapeamento de endereços. Cada entrada do TLB (gura 4.8), além das páginas virtual e física, armazena um conjunto de ags, por exemplo, bits de validade (P), de modicação (D), e de permissões (R). Isto permite ao hardware vericar se a entrada é valida, se o acesso é valido, além de setar os bits de modicação de acordo com a instrução sendo executada. O TLB possui poucas entradas, na ordem de dezenas ou poucas centenas. A busca no TLB é efetuada em paralelo, ou seja, o hardware examina todas as entradas simultaneamente, o que aumenta a eciência da busca consideravelmente. Esta busca no TLB pode encontrar um mapeamento válido ou não. Em caso negativo, a MMU executa o mapeamento segundo o procedimento convencional, ou seja, via tabela de páginas, e atualiza o TLB com o novo mapeamento. 67 Figura 4.7: MMU dotada de TLB. Figura 4.8: Exemplo de entrada do TLB. A justicativa para o emprego do TLB está no princípio da localidade discutido na seção seguinte. A literatura reporta que, para aplicações convencionais, mais de 90% dos mapeamentos de endereços é executado no TLB sem a necessidade de acessar a tabela de páginas. O que acontece com as entradas do TLB quando ocorre uma troca de contexto? Deixar as entradas intactas incorremos no risco de um processo referenciar o mesmo endereço presente no TLB, mas relativo a um outro processo. Desta forma, existem duas alternativas: 1. Invalidar todas as entradas do TLB quando ocorre uma troca de con- ush texto ( do TLB). Esta é a solução mais direta e, de fato, na arqui- 68 tetura x86, quando o registrador CR3 é alterado o hardware invalida todas as entradas do TLB. 2. Adicionar o identicador do processo nas entradas do TLB e invalidar apenas as entradas referentes ao processo que perdeu a CPU. Algumas arquiteturas modernas possui esta facilidade, mas o ganho não é signicativo em relação à solução anterior, pois quando o processo retomar a CPU todas as suas entradas no TLB foram removidas para dar lugar a entradas relativas aos processos que executaram mais recentemente. Finalmente, o ush do TLB que ocorre a cada troca de contexto é mais uma motivação para o uso de threads pois quando ocorre uma troca de contexto entre threads as entradas do TLB são preservadas e, provavelmente, serão úteis para a nova thread. 4.3 Paginação Um ponto importante em gerenciamento de memória é que, sendo o espaço de endereçamento virtual maior que o físico, não é possível mapear todo o espaço virtual de um processo em espaço físico de memória. Isto requeriria que o processo todo estivesse em memória como no caso de partições xas e permuta. Portanto, para um processo, apenas um subconjunto pequeno do espaço de endereçamento virtual está mapeado em um dado instante (digamos, 10% do total). Cabe ao sistema operacional manter mapeadas as páginas que efetivamente os processos estão utilizando no momento. A atividade de mapear e desmapear (invalidar o mapeamento) páginas denominase paginação ou paginação por demanda, é realizada apenas quando necessário. enfatizando que esta atividade A seguir examinaremos a base da paginação, o princípio da localidade. 4.3.1 O princípio da localidade Se apenas um subconjunto das páginas do espaço de endereçamento virtual estão mapeadas em páginas da memória física, como determinar este subconjunto? Uma resposta imediata é: mapeie as páginas referentes aos endereços que o processo está referenciando. Bem, resolvemos um problema criando outro: quais endereços o processo está referenciando? Aqui entra o princípio da localidade. Em Computação (existe um princípio da localidade 69 na Física), o princípio da localidade estabelece que durante a execução de um programa as referências à memória apresentam localidade espacial e temporal. Localidade temporal estabelece que se um processo referenciar uma dada posição de memória, é provável que esta posição será referenciada novamente em um futuro próximo. Localidade espacial estabelece que se um processo referenciar uma dada posição de memória, é provável que posições próximas a esta serão referenciadas em um futuro próximo. Posições de memória armazenando variáveis apresenta localidade temporal, ou seja, uma váriavel e utilizada muitas vezes em determinada fase do programa (por exemplo, a variável de repetição do comando for ). Posições de memória armazenando código apresenta localidade espacial, ou seja, a próxima instrução a ser executada é comumente a instrução seguinte à instrução corrente. Generalizando, a área de dados apresenta localidade temporal e as área de texto e pilha apresentam localidade espacial. Podemos tirar proveito da localidade espacial e temporal em paginação. Ao mapearmos uma página, a localidade espacial e temporal fará com que esta página tenha seus endereços referenciados em um futuro próximo. Portanto, ao mapear uma página o sistema operacional garante que o processo possa progredir graças ao princípio da localidade. Mas, o princípio da localidade vale em termos estatísticos. Por exemplo, um comando goto viola o princípio da localidade espacial se o endereço do desvio se situar em outra página. Vamos supor inicialmente que, ao iniciar, um processo tenha todas as entradas de sua tabela de páginas de primeiro nível marcadas como ausente. Ou seja, o processo inicia com todas as páginas do espaço de endereçamento virtual não mapeadas, como no caso da chamada vfork. Esta informação está no bit P (presente) da gura 4.4 com valor zero. O sistema operacional pode iniciar a execução do processo nesta situação. Obviamente, ao tentar utilizar o primeiro endereço (endereço da função main ) resultará em falha. O hardware gera uma interrupção de falha de paginação ( page fault ). O sistema operacional interrompe o processo e providencia o mapeamento da página correspondente ao endereço referenciado. Inicialmente o sistema operacional aloca uma página da memória física para o mapeamento. Fica aqui um ponto a ser esclarecido: o sistema operacional deve ter controle sobre quais páginas da memória física estão mapeada e quais estão livres (não mapeadas). Alocada a página livre, o sistema operacional atualiza a tabela de páginas do processo mapeando a página do espaço virtual na página livre. O passo seguinte é providenciar o conteúdo 70 da página na memória física. Este conteúdo pode estar no próprio executável caso: 1. O conteúdo seja parte da área de texto do processo. 2. O conteúdo seja parte da área de dados ou pilha que não foi mapeado anteriormente ou que foi mapeado anteriormente mas nunca sofreu alteração (escrita). Nestes casos, o conteúdo da página é copiado para a memória a partir do próprio executável, via DMA caso o executável esteja em disco. Caso o conteúdo da página seja uma área de dados ou pilha que já foi mapeada e alterada no passado, então a página encontra-se na área de permuta ( swap ) e a cópia é realizada a partir desta área. A área de permuta é uma área do disco que o sistema operacional utiliza para armazenar as páginas físicas que tiveram seu conteúdo modicado e foram desmapeadas para atender a novos mapeamentos. Providenciado o mapeamento e a cópia do conteúdo da página da área de permuta para a página física, o processo torna-se pronto e, ao assumir a CPU, a instrução que gerou a falha de paginação será reiniciada. Portanto, paginação requer do hardware a capacidade de reiniciar uma instrução que falhou em uma primeira tentativa. É importante notar que uma página do espaço de endereçamento físico pode ser mapeada no espaço de endereçamento de vários processos, em instantes diferentes ou no mesmo instante no caso de compartilhamento de memória. Voltando ao princípio da localidade, uma página serve a um processo enquanto o conteúdo desta página estiver sendo acessado pelo processo. Quando a localidade deste processo muda para outra página, a página antiga é desmapeada e ca disponível para ser remapeada em outro processo quando necessário. Outra questão crucial em paginação é: Como o sistema operacional sabe que uma página se tornou antiga e, portanto, pode ser desmapeada com grandes chances de não ser necessária ao processo no futuro próximo? A resposta está no bit de acesso (bit A na Figura 4.4) da tabela de páginas. Ao acessar uma página o hardware ativa este bit na tabela de páginas. Ciclicamente, por exemplo, a cada 50 milisegundos, o sistema operacional zera estes bits para todas as páginas. Desta forma o sistema operacional pode inferir que: • Se uma página estiver com o bit de acesso ativado certamente foi utilizada no último ciclo e, portanto, não deve ser desmapeada. 71 • Se uma página estiver com o bit de acesso desativado, muito provavelmente se trata de uma página antiga, podendo ser desmapeada caso o sistema necessite de páginas livres. O termo muito provavelmente se deve ao fato da possibilidade mesmo que remota da página ter sido acessada imediatamente antes do sistema operacional zerar o bit de acesso de todas as páginas. 4.3.2 Gerenciamento do espaço virtual O sistema operacional deve gerenciar o espaço de endereçamento virtual associado à um processo, ou seja, suas áreas de texto, dados e pilha. Além de suas próprias áreas um processo comumente tem incorporado ao seu espaço de endereçamento virtual áreas compartilhadas com outros processos, por exemplo, regiões de memória compartilhada e bibliotecas compartilhadas ( shared libraries ). Estas áreas podem ser descritas por listas ligadas como na gura 2.3. No caso do Linux são utilizadas três estruturas para o gerenciamento do espaço virtual: 1. Tabela de gerenciamento de memória virtual ( mm_struct ). 2. Tabela de páginas por processo com 3 níveis (gura 4.5). vm_area_struct ). 3. Descritores de áreas ( A tabela mm_struct é referenciada na tabela de processos e armazena, principalmente, um ponteiro para o diretório global de páginas (PGD), um ponteiro para a lista de descritores de área e o caminho para o arquivo que contém o código executável do processo. Os descritores de área, organizados em uma lista duplamente ligada, armazenam o início e m da área e ags de proteção (por exemplo, a área de texto é marcada como de leitura apenas). A gura 4.9 ilustra estas estruturas. 4.3.3 Gerenciamento do espaço físico O sistema operacional deve manter controle sobre quais áreas de memória física estão livres e quais estão ocupadas. Áreas de memória são requisitadas quando um processo é carregado ou quando as áreas de pilha e dados crescem no decorrer da execução dos processos. Analogamente, áreas são liberadas 72 Figura 4.9: Estruturas para gerenciamento de memória virtual no sistema Linux. quando processos terminam ou quando mémoria alocada dinamicamente é liberada durante a execução dos processos. Para manter controle sobre as áreas livres é usual empregar-se uma lista ligada. Cada elemento da lista possui um endereço onde a área livre inicia e o tamanho desta área. O sistema Linux mantém listas ligadas para áreas de diferentes tamanhos (em número de páginas). Os tamanhos são em potência de dois assim como as requisições, conforme ilustrado na gura 4.10. Para alocação e liberação de áreas o Linux emprega um algoritmo denominado buddy (companheiro?). Seja por exemplo uma requisição de 4 páginas na situação da gura 4.10. A lista de tamanho 4 é examinada inicialmente e, se vazia, o sistema tenta as de tamanho imediatamente superior (8, 16, etc.). Neste exemplo, a área de 8 páginas é encontrada. O sistema então particiona esta área em duas de 4 páginas, retornando uma destas como resposta à requisição. A gura 4.11 ilustra este procedimento. Quando uma área é liberada o sistema examina se a mesma pode ser incorporada à uma área adjacente, formando uma área maior. Por exemplo, o retorno de uma área de uma página na situação da gura 4.11 irá gerar 73 free_area ). Figura 4.10: Lista de áreas livres ( uma área de duas páginas como ilustrado na gura 4.12. 4.3.4 Gerenciamento de páginas A tabela de páginas da arquitetura de hardware não possui informações adicionais necessárias ao sistema de paginação, por exemplo, o endereço na área de troca que a página física se encontra, os processos referenciando esta página e um contador especicando a idade da página. Desta forma, o sistema operacional mantém suas próprias tabelas com as informações necessárias para a atividade de paginação. Por exemplo, no sistema Linux a estrutura page (ou mem_map nas versões antigas), mantida pelo núcleo, armazena estas informações para cada página física. A gura 4.13 ilustra esta estrutura. Nas versões mais antigas do sistema Linux a idade da página é computada da seguinte forma. Ciclicamente o sistema zera o bit de acesso da página (bit A). Durante o ciclo as páginas acessadas têm o bit A setado pelo hardware. Ao nal do ciclo o sistema soma à idade da página um envelhecimento para páginas que não tiveram o bit A setado e um rejuvenecimento àquelas 74 Figura 4.11: Alocação de uma área livre após partição de uma área maior. que tiveram o bit A setado. mem_map Esta idade está armazenada na estrutura referente à página. No sistema Linux o envelhecimento é obtido pela subtração de uma unidade da idade da página e o rejuvenecimento pela adição de 3 unidades até um máximo de 20 (ou seja, quanto maior a idade mais nova a página). Nas versões atuais, o sistema Linux emprega apenas dois ags: página ativa e página referenciada. Adicionalmente, o sistema mantém duas listas, a lista de páginas ativas e a lista de páginas inativas. A lista de páginas ativas são aquelas que os processos estão utilizando com frequência. Ao contrário, a lista de páginas inativas são aquelas páginas que os processos deixaram de refenciar há algum tempo. O posicionamento da página em uma destas listas se dá em função dos ags de página ativa e refenciada. A heurística empregada pelo Linux é a seguinte. Ciclicamente, o sistema percorre a tabela de páginas físicas. Se a página foi referenciada em um ciclo ela é considerada ativa. Caso a página seja referenciada em dois ciclos consecutivos ela é posicionada na lista de páginas ativas. Caso a página não seja referenciada durante N ciclos a mesma é considerada inativa. Caso a pagina permaneça inativa por mais 75 N ciclos, Figura 4.12: União de áreas livres após liberação de uma área menor. Figura 4.13: Estrutura page para gerenciamento de páginas físicas. a mesma é posicionada na lista de páginas inativas. O sistema operacional deve manter um estoque de páginas livres para atender à necessidade de mapeamento de páginas virtuais quando referenciadas pelo processos. Tais páginas livres compõem a lista de áreas livres (gura 4.10). O sistema mantém um mínimo de páginas livres que, quando atingido, o sistema libera (desmapeia) um número de páginas até um limite superior ser atingido. 76 Em sistemas Unix e derivados o sistema operacional emprega um processo denominado paginador ( swapper ). Este processo, de alta prioridade, perma- nece bloqueado por um período pré-determinado, tornando pronto quando este período expira ou quando recebe um sinal do núcleo. Ao assumir a CPU, o processo paginador verica se o limite inferior de páginas livres foi atingido. Em caso negativo, o processo paginador volta ao estado de bloqueio. Em caso armativo, o processo escolhe páginas virtuais mapeadas e desfaz este mapeamento, adicionando-as à lista de páginas livres até o limite superior de páginas livres ser alcançado. As páginas liberadas retornam à lista de áreas livres gerando áreas maiores via união de áreas livres adjacentes (gura 4.12). O processo paginador utiliza um algoritmo para escolher as páginas que serão desmapeadas. Um bom algoritmo deve levar em conta a idade da página para decidir sobre o seu desmapeamento. A razão para se utilizar a idade é que princípio da localidade funciona também inversamente, ou seja, quanto mais antiga (acessada a mais tempo) a página, menor a chance dela ser acessada no futuro próximo. O algoritmo LRU ( Least Recently Used ) ordena as páginas por idade, escolhendo sempre as mais velhas para serem desmapeadas. Entretanto, manter esta lista ordenada o tempo todo é computacionalmente custoso, razão pela qual não é utilizado na prática. Uma alternativa é desmapear as páginas que atingiram uma idade limite (zero no caso das versões antigas do Linux). Algoritmos que empregam esta simplicação são denominados aging ). algoritmos de envelhecimento ( Um algoritmo passível de ser realizado na prática é o algoritmo do relógio. Neste algoritmo as páginas físicas são inspecionadas circularmente, sendo que um ponteiro indica a página corrente. Caso o sistema deseje liberar N páginas o algoritmo inspeciona a pagina indicada pelo ponteiro: 1. Se o bit A (acesso) for zero, desmapeie esta página. 2. Se o bit A for um, zere este bit. 3. Se N páginas foram desmapeadas, pare. Caso contrário avance o ponteiro para a próxima página e vá para o passo 1. Nas versões correntes do sistema Linux, o processo de desmapeamento é bem mais simples: remova páginas da lista de páginas inativas. Graças à utilização do mapeamento reverso (gura 4.13) apenas as páginas físicas são inspecionadas. Caso ocorra um desmapeamento, o sistema, a partir da 77 página física, é capaz de desfazer o mapeamento na tabela de páginas por processo. A ausência do mapeamento reverso faz com que a pesquisa em busca de páginas para desmapear deve se dar a partir das páginas virtuais de todos os processos (que são em número muito maior que as páginas físicas). Páginas desmapeadas que estejam sujas (com o bit de modicação D setado) devem ter seu conteúdo salvo na área de permuta. Uma página física tem associado a ela um endereço na área de permuta como ilustrado na gura 4.13 (campo inode da estrutura page ). Páginas virtuais das áreas de dados e pilha têm um endereço próprio na área de permuta para serem armazenadas quando forem desmapeadas na condição de suja. Páginas relativas à área de texto não possuem área de permuta pois, quando necessárias, são carregadas diretamente do arquivo executável (lembremos que as páginas da área de texto nunca são modicadas). 4.4 Paginação e sistemas de tempo real Obviamente, a atividade de paginação introduz uma grande variança no tempo médio de execução de um processo ou thread. Em sistemas ope- racionais que utilizam paginação, este tempo é função da quantidade de memória física existente e do número de processos e threads competindo pela memória. Deve-se notar que uma falha de paginação pode requerer uma operação de entrada e saída pois a página necessária pode se encontrar na área de permuta. Sistemas operacionais usualmente travam muitas páginas em memória impedindo seu desmapeamento. e estruturas de dados do núcleo. É o caso de páginas armazenando código Felizmente, sistemas operacionais como o Linux permitem que páginas de programas do usuário também sejam travadas em memória. A chamada de sistema mlock (memory lock ) permite travar as páginas de uma determinada área de memória de um processo. O endereço de início e tamanho desta área são passados como parâmetros na chamada. As páginas desta área já mapeadas ou que serão mapeadas no futuro permanecerão livres de permuta por toda a existência do processo ou munlock liberar o travamento. mlockall e munlockall travam e destravam todo o espaço endereçamento do processo. A chamada mlockall recebe um parâmetro até a chamada Analogamente, de que pode ser as constantes MCL_CURRENT ou MCL_FUTURE indicando o travavamento das páginas já mapeadas ou que serão mapeadas no futuro 78 (ou ambos os casos se as duas constantes forem passadas juntas na chamada). As chamadas de sistema acima impõe limites no número de páginas que podem ser travadas em memória, exceto para processos com privilégio de super-usuário. Em algumas implementações este limite pode ser zero, o que signica que apenas processos executando como super-usuário podem travar páginas em memória. Outra ponto que devemos atentar na implementação de sistemas de tempo real diz respeito à alocação e liberação dinâmica de memória. Esta ope- ração pode demandar atualização de estruturas do núcleo (por exemplo, vm_area_struct na gura 4.13) e, certamente, causará falhas de paginação logo a seguir quando a área de memória passar a ser utilizada. Portanto, recomenda-se alocar todas as estruturas durante a inicialização do programa e acessá-las integralmente para gerar as falhas de paginação logo no início. O quadro 4.1 ilustra um segmento de código onde esta inicialização é combinada com o travamento de páginas em memória. Quadro 4.1: Alocação dinâmica combinada com travamento de páginas em memória. #i n c l u d e <s y s /mman. h> #i n c l u d e <v e c t o r > // t r a v a p a g i n a s c o r r e n t e s e f u t u r a s em memoria i f ( m l o c k a l l (MCL_CURRENT | MCL_FUTURE) != 0) p e r r o r (" m l o c k a l l f a l h o u . " ) ; // a l o c a um v e t o r de f l o a t s v e c t o r <f l o a t > vec = new v e c t o r <f l o a t >(5000); // " t o c a " no v e t o r para c a u s a r f a l h a s de paginacao f o r ( i n t i =0; i < 5 0 0 0 ; i ++) vec [ i ] = 0 . 0 4.5 Comentários nais Gerenciamento de memória é uma atividade crítica no desempenho dos sistemas operacionais. Por este motivo sua implementação é muito otimizada e sofre mudanças constantes com a evolução do hardware e das técnicas de virtualização. 79 No texto apresentado, apesar de utilizarmos a arquitetura Intel x86 e o sistema Linux como exemplos, muitos aspectos de implementação foram omitidos ou simplicados em benefício de um entendimento mais fácil pelo leitor. Restringimos o texto aos conceitos e técnicas que são importantes para o projetista de aplicações, não para o projetista de sistemas operacionais. O entendimento destes conceitos e técnicas são fundamentais para o desenvolvimento de programas ecientes quanto à utilização de memória, conforme veremos na próxima seção. 4.6 Na prática No servidor HTTP multithreaded (gura 3.2) é importante que as threads, a tabela de requisições pendentes e o cache de páginas Web sejam travados em memória para evitar que o processamento de requisições seja prejudicado por eventuais falhas de paginação. Como estes componentes constituem praticamente todo o espaço de endereçamento do processo, podemos travar o processo inteiro em memória com a chamada mlockall. Para tanto, nosso servidor deve executar com privilégio de super-usuário. Após a chamada mlockall é importante que antes de iniciarmos o aten- dimento de requisições, todas as páginas que queremos travar em memória sejam referenciadas. Isto faz com que as todas as falhas de paginação ocorram durante a inicialização do servidor e nunca durante o processamento de requisições. Para tal, como parte da inicialização do servidor, podemos executar as seguintes ações logo após a chamada • mlockall : Alocar a tabela de requisições pendentes e o cache de páginas Web dinamicamente via chamada malloc (ou operador new em C++) e inicializar cada elemento destas estruturas (quadro 4.1). • Iniciar as threads trabalhadoras do pool de threads. Isto faz com que as páginas relativas às áreas de texto e pilha das threads sejam mapeadas e travadas em memória. Para o nosso servidor talvez o ganho de desempenho não seja perceptível, exceto em situações limites de operação. Entretanto, em sistemas de tempo real, evitar falhas de paginação deve ser um procedimento padrão na implementação de tais sistemas. 80 4.7 Exercícios 1. Por que gerenciamento de memória com partições xas ou permuta foram abandonados? Com discos rotativos sendo substituidos por memórias secundárias a estado sólido, discuta a possibilidade de permuta voltar a ser utilizada. 2. Por que o sistema de memória virtual torna impossível um processo malicioso acessar regiões de memória física ocupada por outro processo? 3. Explique, por meio de um exemplo numérico, a vantagem de se utilizar tabelas de páginas de múltiplos níveis. 4. Um computador possui um espaço de endereçamento virtual de 48 bits e um barramento de endereçamento físico (real) de apenas 32 bits. Se as páginas são de 8 KBytes, quantas entradas existem na tabela de páginas? Justique. 5. Qual o tamanho máximo de memória ocupado por uma tabela de páginas de 2 níveis sendo que 8 bits são utilizados para endereçar o primeiro nível e 12 bits para endereçar o segundo nível. O tamanho das entradas das tabelas de cada nível é de 4 bytes (32 bits). 6. Suponha um endereço virtual de 32 bits. Projete uma estrutura de tabela de páginas de 3 níveis indicando: a) as dimensões (número de entradas) das tabelas de cada nível; b) as informações armazenadas nas entradas das tabelas de cada nível; c) o tamanho da página em bytes; d) o total de memória que as tabelas ocupam quando todas as tabelas de todos os níveis estiverem presentes na memória. OBS: Esta questão não possui uma única resposta correta. 7. É possível um programa executar atribuindo a ele uma única página na memória física. Descreva como isso seria possível. 8. Descreva todas as ações executadas pelo sistema operacional para obter o endereço físico associado a um endereço virtual. Suponha que o endereço virtual não esteja mapeado em um endereço físico correspondente. 9. É possível implementar paginação em uma arquitetura de hardware que não possui MMU (Memory Management Unit). Como? 81 10. Comente a armação: Um sistema de gerência de memória baseado em paginação impõe aos processos bloqueios independentemente das instruções executadas pelos mesmos. 11. Descreva como se dá o mapeamento de endereço virtual em seu correspondente endereço físico quando este mapeamento não se encontra no TLB mas se encontra na tabela de páginas do processo. Assuma tabelas de páginas de 2 níveis. 12. O TLB (Traslation Lookaside Buer) é um cache de mapeamentos de endereços virtual para físico presente na MMU (Memory Management Unit). Indique em que situações este cache é atualizado (tem suas entradas inseridas e removidas). 13. Por que na arquitetura x86 todas as entrada do TLB são invalidadas quando ocorre uma troca de contexto? O que poderia acontecer se esta invalidação não ocorresse? 14. Suponha que uma dada instrução de um programa acesse um certo endereço virtual não mapeado. Forneça toda a sequência de eventos envolvendo o hardware e o sistema operacional que ocorrem na execução desta instrução. 15. O que é o princípio da localidade e qual seu papel no gerenciamento de memória virtual? 16. Faça um pequeno programa em C que ilustre a localidade temporal e espacial. 17. O sistema Linux gerencia memória virtual com o emprego de lista ligada. Como seria este gerenciamento com um mapa de bits? 18. Por que o algoritmo Buddy emprega blocos de páginas cujo tamanho é potência de dois? 19. Por que os algoritmos de paginação levam em conta a idade da página? 20. Cite algumas estratégias para determinar a idade das paginas. 21. Explique em que situações o sistema operacional faz uso dos bits P (presente), R (read/write), A (acessada) e D (dirty) presentes na tabela de páginas. 82 22. Descreva como o Linux procede o desmapeamento de páginas quando o estoque de páginas livres cai abaixo de um limite inferior. 23. Discuta como paginação afeta o comportamento de sistemas de tempo real e como podemos minimizar este problema. 24. Descreva como se dá o carregamento de um programa sob o ponto de vista do gerenciamento de memória. 25. O que é processo paginador, que função ele desempenha e que estratégia ele emprega para o desempenho de sua função? 83 Capítulo 5 Subsistema de Arquivos 5.1 Introdução O subsistema de arquivos é responsável por gerenciar os arquivos e diretórios os quais manipulamos corriqueiramente. Para tal, o sistema operacional deve implementar operações para a criação, atualização e remoção destes recursos. Estas operações são oferecidas na forma de chamadas de sistema. Os gerenciadores de arquivos presentes nos ambientes grácos dos sistemas operacionais provêem uma interface gráca que utiliza estas chamadas para criar, remover, mover e renomear arquivos e diretórios. O subsistema de arquivos opera principalmente sobre discos, mas não se restringe a esta forma de armazenamento. De fato, é possível a criação de arquivos em qualquer periférico capaz de armazenar dados, inclusive a memória principal do computador (apesar desta não prover persistência como discos). Para os sistemas operacionais atuais um arquivo é uma sequência de bytes, independente de seu conteúdo, nome ou formato de codicação dos dados. Um diretório é um arquivo especial que armazena referência para outros arquivos, inclusive diretórios. O subsistema de arquivos deve prover soluções para as seguintes questões: • Como a informação presente nos arquivos é armazenada no dispositivo de armazenamento? • Como o espaço físico do dispositivo de armazenamento é gerenciado? • Como arquivos e diretórios são organizados? 84 • Como aumentar a eciência e segurança de operações sobre arquivos e diretórios? Estas questões serão respondidas nas seções deste capítulo. 5.2 Dispositivos de armazenamento O dispositivo de armazenamento de alta capacidade mais comum são os discos rotativos. Um grande avanço foi conseguido em termos de capacidade de armazenamento e rapidez de acesso graças a novas tecnologias de materiais e, como sempre, à microeletrônica. A idéia básica do funcionamento do disco é simples: um prato magnético e rotativo com cabeça de leitura e gravação deslizante sobre o prato. A geometria tradicional e regular composta de cilindros, trilhas e setores foi abandonada há tempo em favor de geometrias que favorecem a velocidade de acesso. Graças ao acionador do dispositivo (capítulo 6), o disco se apresenta ao sistema operacional como uma sequência contínua de blocos de dados com tamanho entre 0,5 e 8 KBytes, tipicamente. Discos são dispositivos orientados a bloco, ou seja, a unidade atômica de leitura e escrita é o bloco. O tempo de acesso a um dado pode ser decomposto em tempo de po- seek ), sicionamento ( atraso rotacional e leitura/escrita. O tempo de posi- cionamento é o tempo necessário para o posicionamento da cabeça sobre o bloco que o dado se encontra. O atraso rotacional é o tempo necessário para a posição do início do bloco chegar até a cabeça de leitura e gravação e o tempo de leitura/escrita é o tempo necessário para ler ou escrever sobre o bloco. Por envolver movimentação mecânica, estes tempos são muitas ordens de grandeza superiores ao tempo de leitura e escrita na memória RAM. Discos rotativos estão dando lugar a unidades de armazenamento de estado sólido, já presentes em muitos dispositivos portáteis como tablets. Mesmo assim, é importante notar que a velocidade de acesso a estas unidades é ainda muito inferior à velocidade de acesso à memória RAM. Isto é devido ao custo por byte armazenado que cresce com a velocidade de acesso e a limitações do barramento de acesso como no caso do USB utilizado por dispositivos como pen drives. Em resumo, um disco, para o sistema operacional, consiste em uma sequência de blocos de tamanho xo. Análogo à memória física que consiste em uma sequência de páginas de alguns KBytes cada. 85 5.3 Gerenciamento do espaço físico O gerenciamento do espaço físico consiste em determinar quais blocos estão livres e quais blocos estão associados a um dado arquivo. Da mesma forma que o sistema operacional necessita de uma certa área da memória para gerenciar memória, é necessário uma certa área do disco para gerenciar o disco. Com o aumento da capacidade dos discos, o mesmo deve ser particionado em áreas menores para facilitar a gerência e aumentar a tolerância a falhas. O sistema Linux particiona o disco em grupos de blocos, grupos estes gerenciados independentemente como se fossem dispositivos de armazenamento separados. Cada grupo de blocos possui um arranjo ilustrado na gura 5.1. Estes grupos denem um Figura 5.1: sistema de arquivos. Layout do disco no sistema Linux. Na gura 5.1 temos os vários grupos de blocos em um dispositivo. Em cada grupo de bloco é armazenada uma cópia do superbloco. O superbloco armazena informações sobre todo o sistema de arquivos, ou seja, sobre todos os grupos de blocos. A informações presentes no superbloco incluem, para cada grupo de blocos, o tamanho do bloco, o número total de blocos, o número de blocos e inodes (seção 5.4) livres e o primeiro inode do sistema de arquivos, dentre outras. O superbloco é replicado em todos os grupos de blocos por razões de conabilidade: a partir de qualquer cópia do superbloco é possível restaurar, ao menos parcialmente, um grupo de blocos danicado por panes no dispositivo de armazenamento. O descritor de grupo armazena informações sobre o grupo, por exemplo, a localização do mapa de bits de blocos e inodes e da tabela de inodes. O mapa de bits de blocos é um vetor de bits que indica se o bloco está livre ou em uso por algum arquivo. Se o i-ésimo bit do vetor estiver ativado, o i-ésimo bloco está em uso. O mesmo ocorre para o mapa de bits do inode. 86 Portanto, o sistema Linux utiliza um mapa de bits para identicar blocos e inodes livres em um determinado grupo de blocos. 5.4 Organização de arquivos Mencionamos na seção anterior o termo Unix. inode, um termo clássico dos sistemas Um inode é um conjunto de informações que descreve os arquivos, inclusive os endereços dos blocos de dados que o compõe, como ilustra a gura 5.2. As informações sobre o arquivo são o tipo (arquivo regular, diretório, especial, etc.), tamanho do arquivo em bytes, sua proteção (leitura, escrita e execução), informações do usuário (identicador, grupo, etc.) que detém o arquivo e datas do último acesso e modicação. Os blocos que compõem o arquivo são armazenados no inode em N (da ordem de uma dezena) endereços de blocos de dados e três endereços denominados indireto simples, duplo e triplo. Os blocos indiretos não armazenam dados do arquivo, mas endereços de blocos de dados ou de blocos indiretos. Desta forma, o número de blocos que um arquivo pode ter é ilimitado para os propósitos práticos. É importante notar que o bloco indireto simples somente é utilizado quando o número de blocos do arquivo exceder a quantidade de endereços diretos do inode. Analogamente, os blocos indiretos duplo e triplo somente são utilizados, respectivamente, quando o número de blocos do arquivo propiciados pelos endereços simples e duplo não forem sucientes. O sistema Linux procura alocar blocos de arquivos no mesmo grupo de blocos por razões de eciência no acesso ao conteúdo dos arquivos. 5.5 Organização de diretórios Diretórios nada mais são que arquivos contendo o nome e o inode referentes aos arquivos que pertencem ao diretório. No Linux, o sistema de arquivos Ext2 possui a estrutura de diretórios apresentada na gura 5.3. A motivação para esta estruturação é a liberdade quanto ao tamanho de nomes de arquivos e a facilidade de remoção de arquivos. É importante notar que as informações sobre o arquivo são armazenadas no inode, e não na entrada do diretório. Por exemplo, o comando ls no Unix lista os nomes dos arquivos do diretório corrente. Esta informação é recuperada no próprio diretório. Se o comando ls for executado com a opção -la é listado, além do nome o usuário e grupo 87 Figura 5.2: Estrutura de um inode. que detém o arquivo, o tamanho do arquivo e a data da última modicação. Para obter estas informações, o comando deve acessar o inode de cada arquivo do diretório. Para se obter todas as informações presentes no inode de um arquivo pode-se utilizar a chamada de sistema stat no Unix. Figura 5.3: Entrada de diretório no sistema de arquivos Ext2. A gura 5.4 ilustra um diretório com 2 arquivos, a.tex e g.jpg. 88 Os diretórios . e .. estão sempre presentes nos diretórios e representam o diretório corrente e o diretório acima (pai), respectivamente. A entrada do diretório consiste do número do inode do arquivo, da localização (bytes adiante) da próxima entrada, do tamanho do nome e do nome do arquivo. A parte inferior da gura ilustra o rearranjo das entradas quando o arquivo a.tex é removido. Esta entrada será ocupada quando for criado neste diretório um arquivo cujo tamanho do nome for igual ou inferior a 5 caracteres. Figura 5.4: Exemplo de diretório no sistema de arquivos Ext2. 5.6 Esquemas de cache de disco Dados trocados entre memórias cujas velocidade de acesso são discrepantes deve se dar por meio de cache por razões de eciência na troca de dados. Tal ocorre entre a memória física e as memórias secundárias como o disco. Esta seção apresenta dois esquemas de cache utilizados na transferência de dados entre o disco e a memória, o cache de buers e o cache de páginas. 5.6.1 O cache de buers O cache de buers armazena cópias de blocos do disco em memória. Sua nalidade é aumentar a eciência do acesso ao disco. Suponha um programa que lê um arquivo byte a byte. Ao ler o primeiro byte o sistema operacional lê, via DMA, todo o primeiro bloco do arquivo para o cache de buers. Ao ler o segundo byte, o mesmo se encontra no cache de buers, evitando assim um 89 novo acesso ao disco. O mesmo se repete até que todos os bytes do primeiro bloco sejam lidos e, na leitura subsequente, o segundo bloco é lido do disco e armazenado no cache de buers. De forma análoga, a escrita em um bloco se dá primeiramente em sua cópia em memória e posteriomente no disco. A estrutura do buer cache no sistema Unix e versões antigas do Linux é dada na gura 5.5. O cache é basicamente uma tabela hash indexada por dois números. O primeiro identica o dispositivo de armazenamento e o segundo um bloco neste dispositivo. Estes dois números são argumentos de uma função hash que retorna um índice na tabela. Como dois pares de números podem retornar o mesmo índice na tabela hash (situação de colisão), as entradas da tabela são armazenadas em uma lista duplamente ligada. Figura 5.5: Estrutura do cache de buers. Cada elemento da lista é composto de um cabeçalho que armazena o número do dispositivo e do bloco, ponteiros para os elementos anterior e seguinte na lista, tamanho do bloco, contador de referência e ponteiro para um endereço de memória física onde o conteúdo do bloco foi copiado. Um conjunto de ags indica se o bloco está livre ou em uso, se foi modicado em memória e se está aguardando que seu conteúdo seja copiado, também via DMA, para o correspondente bloco em disco (quando modicado em 90 memória). O sistema operacional mantém uma lista duplamente ligada de elementos cujos buers em memória se tornaram livres (por exemplo, quando o arquivo ao qual o buer estava associado foi fechado). Ao necessitar inserir um novo buer no cache, o sistema operacional verica se o elemento a ser inserido encontra-se na lista de blocos livres. Em caso armativo o sistema retira o buer da lista de buers livres e utiliza o conteúdo do buer já em memória. Em caso negativo, o sistema operacional retira um buer da lista de buers livres, reposiciona este buer no cache e copia o conteúdo do buer em disco para o buer em memória. 5.6.2 O cache de páginas Mencionamos anteriormente que blocos de disco guardam certa semelhança com páginas de memória. As versões correntes do sistema Linux exploram esta semelhança, tratando acesso a disco da mesma forma que paginação. Para tal, um esquema denominado cache de páginas substitui o cache de buers. O cache de páginas mantém páginas em memória física associadas com segmentos de arquivos em disco. Neste esquema um arquivo em disco é visto como uma sequência de segmentos cujo tamanho é idêntico ao tamanho de página. Note que empregamos o termo termo bloco segmento (chunk ) para manter o com o signicado utilizado até então. O cache de páginas é também uma tabela hash, mas os parâmetros para a sua função de hash é o número do inode e o no arquivo associado. oset (número de segmentos) Como no cache de buers, cada entrada na tabela hash possui uma lista duplamente ligada com informações sobre a página. A gura 5.6 ilustra esta estrutura. O cabeçalho dos elementos da lista ligada contém o inode e oset do arquivo associados a esta página e ponteiros para os elementos anterior e seguinte na lista. Cada elemento da lista aponta para a estrutura page descrita na seção 4.3.3. física especíca. Esta estrutura descreve uma página A vantagem deste esquema de cache é utilizar o mesmo esquema de permuta ( swap ) tanto para páginas associadas a processos quanto para páginas utilizadas para ns de cache de disco. Ao ler ou escrever em um arquivo, o sistema operacional realiza esta operação primeiro na página associada ao segmento do arquivo sendo lido ou gravado. Com o cache de páginas, caso a página tenha sido alterada e sofra desmapeamento, seu conteúdo é escrito no arquivo correspondende e não na área de permuta como até então foi o caso. Obviamente, se o tamanho do 91 Figura 5.6: Estrutura do cache de páginas. bloco coincidir com o tamanho da página, a permuta de páginas pertencentes ao cache de páginas é bastante facilitado. Por empregar o mesmo esquema otimizado de paginação para acesso ao disco, o cache de páginas propicia uma eciência na manipulação de arquivos muito superior àquela propiciada pelo cache de blocos utilizado até então. 5.7 Arquivos mapeados em memória O esquema de cache de páginas tem a vantagem de integrar o cache de disco e o sistema de paginação. Entretanto, apresenta uma desvantagem: um mesmo conteúdo do disco pode estar armazenado em duas páginas físicas distintas, uma associada ao cache de páginas e outra associada ao espaço de endereçamento do processo. Uma solução para este problema são os arquivos mapeados em memória, mais precisamente, na memória virtual dos processos que os utilizam. Com arquivos mapeados em memória a replicação de dados em páginas distintas é eliminada e as operações de acesso ao arquivo são realizadas diretamente em memória e não via chamada read ou write. Deve-se observar que ao realizar o mapeamento de um arquivo em memória as páginas a ele associadas no espaço de endereçamento do processo estão inicialmente 92 desmapeadas. O acesso a estas páginas gera falhas de paginação que irão produzir o mapeamento das mesmas. A medida que estas páginas envelhecem são desmapeadas pelo sistema de paginação. O quadro 5.1 ilustra o mapeamento do arquivo database.dat em memória a partir de seu início. Quadro 5.1: Exemplo de mapeamento de arquivo em memória. #i n c l u d e #i n c l u d e #i n c l u d e #i n c l u d e <s y s /mman. h> <s y s / t y p e s . h> <s y s / s t a t . h> < f c n t l . h> int offset = 0; // i n i c i o do a r q u i v o void ∗ mapping ; // e n d e r e c o do mapeamento i n t tamanho = 5 0 0 0 ; // tamanho do a r q u i v o // pode s e r o b t i d o com a chamada s t a t // abre a r q u i v o i n t f d = open (" database . dat " , O_RDWR) ; i f ( f d > 0) { // mapeia em memoria mapping = mmap (NULL, tamanho . PROT_READ | PROT_WRITE, MAP_SHARED, fd , o f f s e t ) ; i f ( mapping != MAP_FAILED) { // a c e s s a o 100 − esimo byte do a r q u i v o char c = ∗ ( char ∗ ) ( mapping + 1 0 0 ) ; ... } } 5.8 Suporte a múltiplos sistemas de arquivos Dada a diversidade de meios de armazenamento, um sistema operacional deve suportar diferentes sistemas de arquivos, cada qual adequado às características do dispositivo. Como exemplos de sistemas de arquivos comumente empregados temos: • NTFS e ext4: sistemas empregados em discos de grande capacidade. • FAT (File Allocation Table): sistema introduzido com o MS-DOS e ainda utilizado em dispositivos de baixa capacidade como 93 pen drives. • ISO-9660: sistema utilizado em unidades de CD-ROM. • NFS (Network File System): sistema de arquivos distribuído que permite um computador acessar dispositivos remotos de armazenamento via rede. A utilização concomitante de diversos sistemas de arquivos é possível com a introdução de uma camada que abstrai as especicidades dos sistemas de arquivos suportados. de Esta camada de abstração é denominada, no Linux, Virtual File System (VFS). VFS dene um sistema de arquivos com os elementos descritos previamente neste capítulo: superbloco, inode, diretórios, etc. Cada sistema de arquivos suportado deve ser implementado como um acionador de dispositivo orientado a bloco (a ser detalhado no capítulo 6). Estes sistemas suportados são declarados em um arquivo, como /etc/mtab (no Linux). Por exemplo, a entrada neste arquivo /dev/sda2 / ext4 rw,errors=remount-ro,user_xattr,commit=0 0 0 indica o endereço do acionador de dispositivo que implementa este sistema de arquivos (/dev/sda2), o diretório raiz (/), o tipo (ext4), permissão para leitura e escrita (rw), etc. O acionador de dispositivo deve implementar rotinas que são utilizadas pelo VFS. Estas rotinas suportam as abstrações do VFS (superbloco, inode, etc.) mesmo que o sistema de arquivos que o acionador implementa não as suporte. Por exemplo, o sistema FAT não suporta inode, mas uma única tabela de alocação de blocos. Entretanto, o acionador apresenta a tabela de alocação ao VFS como inodes, um para cada arquivo no sistema. A gura 5.7 ilustra a arquitetura do VFS. Como no Linux e em outros sistemas o carregamento de acionadores de dispositivo é dinâmico (sob demanda), um sistema de arquivos pode ser adicionado ao VFS também dinamicamente. O comando mount permite incorporar um sistema de arquivos ao VFS. Por exemplo o comando mount -t iso9660 -o ro /dev/cdrom /mnt/cdrom adiciona um sistema de arquivos tipo ISO-9660 para leitura apenas (ro - read only ) cujo acionador de dispositivo que o implementa encontra-se no endereço /dev/cdrom. O sistema é mapeado em um diretório local (/mnt/cdrom), ou seja, o acesso ao conteúdo do CD-ROM se dá como se o mesmo estivesse contido neste diretório local. Deve-se notar que nos sistemas operacionais atuais, o próprio sistema se encarrega de montar um sistema de arquivos 94 Figura 5.7: Arquitetura do Virtual File System (VFS). quando necessário. Por exemplo, quando espetamos um pen drive em uma porta USB, o sistema operacional detecta o dispositivo, o identica, providencia o carregamento do acionador de dispositivo e executa a montagem, tudo sem a intervenção do usuário. 5.9 Conabilidade do sistema de arquivos O sistema de arquivos comumente armazena informações valiosas. Sua proteção é fundamental para evitar a perda face a panes em discos e outros dispositivos de armazenamento. Vamos examinar algumas soluções para aumento journaling, RAID (Redundant Array of Independent Disks ), vericadores de consistência e backups. de conabilidade do sistema de arquivos, 5.9.1 Journaling O que acontece com o sistema de arquivos se o sistema operacional interromper abruptamente sua execução devido, por exemplo, a uma perda 95 de energia? O sistema de arquivos pode car em um estado inconsistente caso o sistema operacional estiver atualizando o superbloco ou inodes, ou transferindo blocos de dados do cache de páginas para o disco. Sistemas de arquivos atuais como o ext4 do Linux emprega um conceito journaling (diário). Um sistema que emprega este conceito mantém um log das alterações no sistema de arquivos que é utilizado na sua recuperação após uma pane. É importante notar que journaling não denominado torna o sistema de arquivos imune a falhas, apenas aumenta sua robustez, propiciando uma restauração mais rápida após uma pane. Basicamente, de um arquivo. journaling log pode gravar em os metadados e/ou os dados Podemos entender metadados como as informações sobre um arquivo como aquelas presentes no seu inode. um arquivo o sistema pode gravar em log Assim, antes de alterar o conteúdo de seu inode. Caso uma pane ocorra no momento que o inode esteja sendo modicado, o sistema pode recuperar seu último estado consistente do log. Mesmo com o log de metadados, perdas de dados podem acontecer caso a pane ocorra antes da sincronização entre o cache de páginas e o disco. Esquemas mais tolerantes a falhas podem gravar em log não apenas os metadados, mas também os dados de um arquivo. Apesar de minimizar a perda de dados, este esquema degrada o desempenho do sistema e requer um extenso espaço em disco para o armazenar o log. Os sistema de arquivos NTFS (Windows) e ext4 (Linux) empregam técnicas de journaling. No caso do ext4 o administrador do sistema pode congurar se deseja ou não que dados sejam gravados em log. Nestes sistemas a inicialização após uma pane não requer a vericação integral do sistema de arquivos como ocorria anteriormente com os programas ou fsck (Linux). constam no log Com o esquema de journaling, chkdsk (Windows) apenas as alterações que são processadas durante a recuperação de panes. 5.9.2 RAID RAID utiliza mais de um disco para ns de aumento de eciência e conabilidade. Existem sete formas de utilização destes discos, denominadas RAID nível 0 até RAID nível 6, apesar de nem todas as formas serem utilizadas na prática. Eciência é obtida via paralelismo: a informação é distribuída por vários discos e acessada em paralelo. Conabilidade é obtida via redundância da informação armazenada. Alguns sistemas RAID operam mais ecientemente em hardware dedicado, por exemplo, com capacidade de 96 leitura/escrita sincronizada em vários discos. RAID 0 não provê redundância, mas apenas aumento de eciência via paralelismo. A forma mais direta de RAID é o nível 1 onde a informação é integralmente replicada em cada disco (tipicamente 2, um primário e um backup ). RAID nível 1 também é conhecido como espelhamento de discos. Uma forma mais econômica de redundância é via código de Hamming. Por exemplo, 4 bits de informação podem ser acrescidos de mais 3 bits de paridade computados via código de Hamming. A incorreção em qualquer bit pode ser recuperada a partir dos demais bits. RAID níveis 2 a 6 utilizam bits de paridade, variando na forma como estes bits de paridade são distribuídos entre os discos. Linux suporta todos os níveis de RAID, exceto o nível 3. Windows na versão Server suporta RAID níveis 0, 1 e 5. Em Linux, RAID é suportado multiple device ). por um acionador de dispositivo, o md ( Por ser uma solução em software (software RAID) Linux não necessita hardware especíco para RAID. Usualmente, emprega-se em servidores dois discos idênticos congurados como RAID nível 1 (gura 5.8). Esta conguração, em algumas versões do Linux, é especicada no arquivo /etc/raidtab. Por exemplo, para RAID nível 1 com dois discos, o conteúdo de /etc/raidtab é dado no quadro 5.2. Quadro 5.2: Conguração de RAID nível 1 no Linux. r a i d d e v / dev /md0 r a i d −l e v e l 1 nr −r a i d − d i s k s 2 nr −spare − d i s k s 0 p e r s i s t e n t −s u p e r b l o c k 1 device / dev / sdb6 r a i d −d i s k 0 device / dev / sdc5 r a i d −d i s k 1 5.9.3 Vericadores de consistência Vericadores de consistência são programas que inspecionam o sistema de arquivos em busca de inconsistências, promovendo, inclusive, a sua reparação quando possível. Estes programas varrem o superbloco, inspecionando cada bloco livre e, a partir dos inodes, cada bloco utilizado pelos arquivos. O programa mantém um vetor de blocos livres e um vetor de blocos em uso. 97 Figura 5.8: RAID nível 1 no Linux. A i-ésima posição no vetor de blocos livres tem o valor um se o bitmap de blocos indica que o bloco está livre, ou zero caso contrário. A i-ésima posição no vetor de blocos ocupados é incrementada toda a vez que o bloco estiver associado com um arquivo (inode). Ao nal, cada posição nestes vetores é confrontada. Pode haver quatro situações: 1. A entrada referente ao bloco i possui valor 1 em vetor e zero no outro (situação de consistência para o bloco). O sistema de arquivos é considerado consistente se esta condição é satisfeita para todos os blocos. 2. Um bloco possui valor zero nas duas listas (bloco perdido). A correção é marcar o bloco como livre no bitmap de blocos livres do superbloco. 3. Um bloco possui valor um nas duas listas. A correção é marcar o bloco como em uso no bitmap de blocos livres do superbloco. 4. Um bloco possui valor maior que um na lista de blocos em uso. Neste caso, dois ou mais arquivos estão utilizando o mesmo bloco. A correção é desmembrar o bloco, alocando um novo bloco para cada arquivo que 98 o utiliza. Note que os arquivos que utilizam este bloco podem estar inconsistentes pois o bloco pode ter sido sobrescrito por estes arquivos. Como vimos, a inconsistência do sistema de arquivos é provocada pela queda do sistema quando o mesmo estava atualizando o superbloco ou os inodes. O sistema Linux verica a consistência do sistema de arquivos durante a inicialização quando o último desligamento ( shutdown ) não ocorreu de forma ordenada ou após um certo número de inicializações. 5.9.4 Backups A forma mais simples de backup é copiar periodicamente, o conteúdo de um disco para outro disco ou para outros meios como tas e CD-ROMs. A segunda unidade pode estas instalada no próprio computador, ou em outra unidade acessível via rede. Em ambos os casos a cópia é efetuada em horários de baixa carga, por exemplo, durante a madrugada, e pode ser programada no Unix com o comando cron. Em caso de perda de dados por pane na unidade de armazenamento, o conteúdo do último backup é recuperado na unidade reparada ou em nova unidade. Sistemas mais sosticados de backup empregam software especíco que copiam apenas os arquivos alterados desde o último backup (backup increbackup a versão do mental). O software é capaz de localizar nas unidades de arquivo armazenada mais recentemente. 5.10 Sistemas de arquivos e sistemas de tempo real Comentamos anteriormente que é possível criar um sistema de arquivos em memória. A motivação para tal é a velocidade de acesso aos arquivos, fundamental para sistemas de tempo real que manipulam arquivos. sistemas de arquivos em memória são comumente denominados Estes RAM disks, ou seja, discos em memória RAM. No caso do sistema Linux, é possível criar um sistema de arquivos em memória física ou virtual. No primeiro caso o sistema de arquivos é denominado ramfs ( temporary le system ). caso tmpfs ( RAM le system ) e no segundo Em ambos os casos o sistema de arquivos é criado no cache de páginas. 99 O sistema de arquivos ramfs xa as páginas que utiliza em memória impedindo que as mesmas sofram permuta. O mesmo não ocorre com o sistema de arquivos tmpfs cujas páginas que utiliza não são xadas em memória. Desta forma, tmpfs não garante que um sistema de arquivos criado em memória esteja integralmente em memória o tempo todo. Entretanto, os arquivos muito acessados terão grande chance de permanecerem no cache de páginas, ou seja, em memória. Outra diferença é que, por utilizar armazenanamento secundário, tmpfs pode ter tamanho máximo muito superior a ramfs. Para criar um sistema de arquivos tipo ramfs ou tmpfs utiliza-se o comando mount: mount -t ramfs -o size=100m ramfs /mnt/ram mount -t tmpfs -o size=20m tmpfs /mnt/tmp No primeiro caso utiliza-se um sistema de arquivos de tamanho máximo de 100 Mbytes tipo ramfs. Este sistema de arquivos tem seu diretório raiz em /mnt/ram. No segundo caso um sistema tipo tmpfs é criado com 20 Mbytes em /mnt/tmp. Arquivos criados a partir de /mnt/ram ou /mnt/tmp são mapeados em páginas do cache de páginas. Deve-se notar que ambos os sistemas de arquivos são voláteis, ou seja, seus arquivos não são persistentes como os arquivos regulares em disco. 5.11 Na prática Um servidor HTTP possui um conjunto de páginas que são acessadas com grande frequência, razão pela qual estes servidores utilizam um cache de páginas Web em memória. No capítulo anterior, criamos este cache com estruturas de dados travadas em memória. Agora, podemos utilizar o sistema de arquivos tmpfs ou ramfs para tal, mantendo as páginas na forma de arquivo, muito mais conveniente para sua manipulação. Em ambos os casos o programador deve atentar para o fato que o limite estipulado para o sistema de arquivos não deve ser ultrapassado. O servidor deve controlar o tamanho do cache utilizando para tal o tamanho dos arquivos lá armazenados. Pode-se utilizar uma tabela com o nome do arquivo em cache, seu tamanho e uma marca de tempo indicando o último acesso. Esta marca de tempo é utilizada para decidir que página deixará o cache para dar lugar a uma página recém acessada (caso o cache esteja próximo ao limite). A remoção do arquivo acessado menos recentemente do cache é similar à política LRU ( Least Recently Used ) 100 de gerenciamento de páginas (seção 4.3.4). A gura 5.9 ilustra una possível estrutura para desta tabela. Figura 5.9: Tabela com as páginas em cache. 5.12 Exercícios 1. Quantos acesso a disco são necessários para se obter o endereço do primeiro bloco do arquivo /usr/include/stdio.h ? 2. Que informações são armazenadas no superbloco de um sistema de arquivos. 3. Qual o tamanho máximo de arquivo que um sistema de arquivos pode comportar no caso de blocos de 2K bytes, 4 bytes necessários para endereçar um bloco e inode com 12 endereços de bloco diretos, um endereço de bloco simples, um duplo e um triplo? 4. Quando um bloco é lido do disco, seu conteúdo é armazenado no cache de buers. Descreva a sequência de ações executadas pelo sistema operacional para efetivar este armazenamento. 5. Explique como os subsistemas de memória e de arquivos são integrados no Linux. 6. O que é e qual a nalidade de uma ligação ( ln link ) simbólica (chamada no Linux)? 7. Pesquise sobre a estrutura do sistema de arquivos FAT em suas variantes FAT16 e FAT32. 8. Quais as vantagens e desvantagens dos sistemas de arquivos mapeados em memória? 101 9. Qual a função do comando mount ? 10. Descreva uma situação de falha onde o esquema de journaling é ecaz e outra onde este esquema não é. 11. Como se dá a vericação de consistência dos sistemas de arquivos no Linux? 12. O que é Código de Hamming e qual seu emprego em RAID? 13. Quais as diferenças entre um arquivo mapeado em memória e um arquivo de um sistema de arquivos em memória (RAM disk)? 14. Quais as diferenças entre ramfs e tmpfs? 15. Descreva as chamadas de sistema referentes ao subsistema de arquivos no Linux. 102 Capítulo 6 Subsistema de Entrada e Saída O subsistema de entrada e saída é um componente extenso do sistema operacional pela diversidade de dispositivos de entrada e saída que existem nos computadores atuais. Este capítulo trata dos principais componentes deste device drivers ) subsistema: módulos, acionadores de dispositivos ( e acesso direto à memória (DMA). 6.1 Módulos Sistemas operacionais devem prover um meio para a conexão de acionadores de dispositivos. Esta conexão ocorre quando o sistema operacional detecta a presença de um dispositivo de hardware. Esta detecção pode ocorrer durante a inicialização do sistema ou no momento que o dispositivo é conectado ao computador (um disco externo, por exemplo). Analogamente, o acionador de dispositivo pode ser desativado (liberando recursos tais como memória) quando o dispositivo é desconectado. A adição de um acionador de dispositivo ao núcleo do sistema operacional pode ser vista como uma extensão do núcleo. No Linux, módulos oferecem um meio conveniente de estender as funcionalidades do núcleo, sendo usados tipicamente na implementação de: • Acionadores de dispositivos. Exemplo: e100e (Ethernet), nvidia (ví- deo), psmouse (mouse), soundcore (som). • Sistema de arquivos (lesystem drivers). Exemplo: fat, vfat, nfs. • Pilhas de protocolos de rede (network drivers). Exemplo: ipx, isdn. 103 • Inclusão de novas chamadas de sistema. Exemplo: Video for Linux (v4l). Linux provê um conjunto de chamadas de sistema para a programação de módulos e um conjunto de comandos para o gerenciamento de módulos. A gura 6.1 ilustra um módulo conectado (carregado) ao núcleo. Figura 6.1: Conexão de um módulo ao núcleo. No Linux, todo módulo deve prover duas funções de invocada pelo núcleo quando o módulo é instalado e callback : init_module cleanup_module invo- cada quando o módulo é removido. A primeira executa funções de inicialização do módulo e a segunda funções de liberação de recursos, principalmente memória alocada do núcleo. insmod A instalação de um módulo ao núcleo se dá modprobe. A remoção de módulos se dá com o comando rmmod. Os comandos lsmod e modinfo lista os módulos instalados com os comandos ou e fornece informações sobre um módulo em particular, respectivamente. Um conjunto de arquivos de conguração são empregados para: • Especicar os módulos que devem ser instalados na inicialização do sistema. black • Especicar módulos que não devem ser carregados no sistema ( • Especicar dependências entre módulos (no caso de módulos que ne- list ). cessitam serviços de outros módulos). O quadro 6.1 ilustra a programação de um módulo elementar. 104 Quadro 6.1: Código de um módulo elementar. /∗ h e l l o . c ∗ " H e l l o world " − um modulo e l e m e n t a r ∗ Compile com gcc −c h e l l o . c −Wall ∗/ / ∗ Declara o c o d i g o como modulo ( p a r t e do n u c l e o ) ∗ / #d e f i n e __KERNEL__ / ∗ uma p a r t e do n u c l e o ∗ / #d e f i n e MODULE / ∗ mas nao uma p a r t e permanente ∗ / / ∗ i n c l u s o e s padrao para modulos ∗ / #i n c l u d e <l i n u x / modversions . h> #i n c l u d e <l i n u x / module . h> / ∗ I n i c i a l i z a c a o do LKM ( l o a d a b l e k e r n e l module ) ∗ / i n t init_module ( ) { p r i n t k ("O modulo h e l l o world f o i i n s t a l a d o \n " ) ; return 0; } / ∗ Cleanup ∗ / void cleanup_module ( ) { p r i n t k ("O modulo h e l l o world f o i d e s i n s t a l a d o \n " ) ; } 6.2 Acionadores de dispositivos O sistema operacional deve prover entrada e saída da forma mais independente possível dos dispositivos que armazenam, coletam ou apresentam a informação. O subsistema de arquivos é um exemplo típico. As abstrações de arquivo e diretório são independentes do dispositivo de armazenamento. Esta independência está centrada na interconexão entre o sistema operacional e o hardware. Esta interconexão é realizada por componentes do sistema denominados acionadores de dispositivos. Um acionador de dispositivo pode ser visto como um software de duas metades. A metade de cima conecta-se ao sistema operacional por meio de uma interface denida especialmente para a conexão de acionadores de dispositivos ao restante do sistema operacional. A metade de baixo conecta-se ao 105 hardware utilizando uma interface de hardware como USB, ATA ( Technology Attachment ), RS-232, etc. Advanced No Unix e derivados os acionadores de dispositivos são de dois tipos: 1. Orientados a bloco: a transferência de dados se dá em blocos de bytes. Acionadores de disco, CD-ROM, memória USB são exemplos deste tipo. 2. Orientados a caractere: a transferência de dados se dá byte a byte. Acionadores de terminais, porta serial e porta paralela são exemplos deste tipo. Via de regra, os acionadores de dispositivos orientados a bloco interagem com cache de buers ou páginas para transferência de informação. No Unix, acionadores de dispositivos são acessados a partir do espaço do usuário da mesma forma que arquivos. Para tal, cada acionador de dispositivo é representado por um arquivo especial no diretório /dev. Para relacionar um arquivo no diretório /dev com um acionador de dispositivo são empregados dois identicadores: comando mknod número maior e número menor. O cria um arquivo especial que será associado a um acionador de dispositivo, por exemplo mknod /dev/mydriver c 60 0 cria um acionador de de dispositivo de nome mydriver do tipo caractere (c) cujos números maior e menor são 60 e 0, respectivamente. O código correspondente a acionadores de dispositivos podem fazer parte do núcleo do sistema operacional ou serem disponibilizados na forma de módulos. A segunda opção é mais atrativa pois o acionador de dispositivo pode ser carregado dinamicamente assim que o dispositivo seja conectado ou detectado pelo sistema operacional. A gura 6.2 ilustra um módulo implementado um acionador de dispositivo orientado a caractere. A chamada register_chardev associa três identicadores ao acionador: seu número maior, seu arquivo correspondente e uma tabela de operações de arquivos (FOP: File Operations Table ). FOP é uma estrutura de dados com as operações sobre arquivos denidas para o dispositivo que o acionador controla. A FOP para um acionador de dispositivo aponta para as chamada open, close, read, write, mmap e ioctl implementadas para o dispositivo controlado. A chamada open inicializa o dispositivo estabelecendo parâmetros default de operação do dispositivo. A chamada mmap permite mapear em memória as informações produzidas e consumidas pelo dispositivo de forma similar ao mapeamento de arquivos em memória (seção 5.7). 106 A chamada Figura 6.2: Acionador de dispositivo implementado como módulo. ioctl pode alterar os parâmetros atribuidos na inicialização. As chamadas read e write lê e escreve dados (bytes) no dispositivo, enquanto a chamada close libera os recursos do sistema operacional alocados ao acionador de dispositivo. Acionadores de dispositivos orientados a bloco são um pouco mais complexos pois devem interagir com o cache de páginas ou de buers como ilustrado na gura 5.7. A interação com os acionadores de dispositivo se dá pela manipulação do arquivo especial associado ao dispositivo. Vamos exemplicar com um acionador de dispositivo para câmeras web (webcam), usualmente identicado por /dev/video0 no Linux. Este acionador se comporta como se fosse um arquivo regular em disco onde as imagens da câmera são gravadas. programa que utiliza a câmera pode ler as imagens via chamada o descritor de arquivo retornado pela chamada open read O sobre para o arquivo especial /dev/video0. Alternativamente, o programa pode mapear o arquivo /dev/vi- mmap (ver quadro 5.1). Desta forma a imagem é acessada sem o uso da chamada read, simplesmente acessando o endereço de memória retornado por mmap. deo0 em seu espaço de endereçamento via chamada 107 6.3 Acesso direto à memória Acesso direto à memória (DMA: Direct Memory Access ) é um componente fundamental do subsistema de entrada e saída e presente em praticamente todos os dispositivos de entrada e saída orientados a bloco como discos, CD-ROM, etc. Estes dispositivos possuem um componente de hardware denominado controlador de DMA. A gura 6.3 ilustra o controlador de DMA em relação aos demais componentes do subsistema de entrada e saída. O controlador tem como função precípua a transferência de dados entre a memória e o controlador de dispositivo, liberando assim a CPU de copiar dados de/para a memória em operações de entrada e saída. O controlador de DMA possui registradores de controle que permite a sua programação por parte da CPU e gera interrupções para noticar a CPU sobre a conclusão de suas operações. Figura 6.3: Posicionamento do controlador de DMA. A sequência de operações do DMA é ilustrada na gura 6.4. Nesta gura ocorre a transferência de um bloco de disco para a memória, mais especicamente, para o cache de páginas ou de buers. A transferência se inicia com a programação do controlador de DMA no acionador de dispositvo 108 pela CPU que lhe fornece o número do bloco requisitado, a quantidade de bytes a ser transferida e o endereço de memória para o qual o dado deve ser copiado (gura 6.4(a)). Normalmente, este endereço é de um buer ou de uma página dos caches de buers ou de página. O controlador inicia a transferência para o endereço de memória designado (gura 6.4(b)). Ao terminar a transferência, o controlador de DMA gera uma interrupção para informar à CPU que a transferência foi completada (gura 6.4(c)). O manipulador desta interrupção executa ações apropriadas à interrupção, por exemplo, atualizando o estado do processo de bloqueado (aguardando o retorno da chamada read ) para pronto (gura 2.8). Figura 6.4: Operação de entrada e saída com DMA. Deve-se observar que apesar da transferência do controlador de DMA para a memória necessitar de acesso ao barramento de dados, o acesso à memória por parte da CPU não é prejudicado pois a mesma utiliza um barramento exclusivo e de alta velocidade para tanto. 6.4 Na prática Neste capítulo nosso servidor HTTP será estendido para retornar imagens de uma câmera como resposta de uma chamada GET para um endereço que identica a câmera, por exemplo /camera0. Para tal utilizaremos a interface de programação do Video for Linux (v4l) para capturar imagens 109 da câmera. Ao receber a requisição o servidor executa uma operação de captura de imagem sobre o acionador de dispositivo da câmera (via ioctl ), lê a imagem do buer que foi mapeado no seu espaço de endereçamento (via mmap ) e retorna a imagem para o cliente. Red Green É importante notar que a imagem é capturada no padrão RGB ( Blue ) que não é apropriado para a transmissão via rede dado o tamanho da imagem. Neste caso, é interessante converter a imagem de RGB para os formatos JPEG ou PNG, que comprimem a imagem e facilitam sua apresentação por parte do navegador. Para esta conversão pode-se utilizar as bibliotecas libjpeg ou libpng, ou ainda frameworks mais completos de processamento de imagens como o OpenCV [7]. É importante também retornar o parâmetro Content-Type do HTTP com o tipo da imagem: image/jpeg ou image/png, bem como seu tamanho no parâmetro Content-Length. 6.5 Exercícios 1. O que é um módulo e quais as diferenças deste componente para os demais componentes do sistema operacional? 2. O que um programador deve implementar para criar um módulo para o sistema Linux? 3. Qual a importância de módulos para o subsistema de entrada e saída? 4. Por que ao programar um módulo não temos acesso às bibliotecas disponíveis para a programação de aplicações? 5. Por que existe uma chamada de sistema especial para a criação de arquivos no diretório /dev ? 6. O que é um acionador de dispositivo e como este componente interage com o restante do sistema operacional e com o dispositivo controlado? 7. Que alternativa a módulos existe para a implementação de acionadores de dispositivos? 8. Cite um exemplo onde o mapeamento de arquivos em memória é empregado no subsistema de entrada e saída? 110 9. Suponha que você irá implementar um acionador de dispositivo para um braço robótico industrial. Descreva funcionalmente as chamadas presentes na FOP para este dispositivo. 10. Onde é usado DMA nos subsistemas de memória e de arquivos? 111 Capítulo 7 Virtualização 7.1 Introdução No contexto de sistemas operacionais, podemos denir virtualização como uma técnica que propicia o compartilhamento seguro e eciente de um componente, tipicamente um componente de hardware. Vimos exemplos de virtualização em processos onde uma CPU virtual é atribuída a cada processo. Esta CPU virtual possui seu próprio contexto (contador de programa, PSW, registradores, etc.) que é armazenado na tabela de processos. A CPU real é compartilhada pelas CPUs virtuais por meio da mudança de contexto. Outro exemplo de virtualização é memória virtual que permite compartilhar a memória física de maneira eciente e segura via técnicas de paginação. Finalmente, um sistema de arquivos pode ser entendido como a virtualização de uma unidade de armazenamento que possibilita seu compartilhamento seguro entre diversos processos e usuários. Portanto, em sistemas operacionais a utilização de virtualização é uma prática comum e bem estabelecida. Um sistema operacional é comumente instalado sobre uma plataforma de hardware. Se conseguirmos virtualizar toda a plataforma de hardware teremos a possibilidade de executar vários sistemas operacionais concorrentemente sobre esta plataforma. Isto traz inúmeras vantagens. Em computadores pessoais podemos executar dois ou mais sistemas operacionais, cada qual com seu conjunto próprio de aplicativos. Em servidores, podemos reduzir o número de máquinas físicas mantendo ainda os diferentes ambientes de execução necessários a cada serviço. A plataforma de hardware virtualizada é denominada 112 máquina virtual (MV). Os sistemas operacionais instalados nestas máquinas virtuais são denominados sistemas operacionais hospedados ( guest ). A máquina virtual é capaz prover aos sistemas hospedados acesso seguro ao hardware real por meio de um componente denominado monitor de máquina virtual (MMV). O MMV controla o acesso ao hardware de forma que alterações do estado do hardware por parte de um sistema operacional hospedado sejam visíveis apenas neste sistema. A gura 7.1 ilustra estes componentes. Figura 7.1: Virtualização de uma plataforma de hardware. 7.2 Requisitos para virtualização Um trabalho pioneiro para a denição dos requisitos necessários para a virtualização de plataformas de hardware foi realizado por Popek e Goldberg [6] em 1974. Neste trabalho, os autores identicaram três requisitos para um MMV: 1. Equivalência (delidade): um sistema operacional executando sobre um MMV deve exibir comportamento idêntico àquele exibido quando executado sobre o hardware real. 2. Controle de recursos: o MMV deve ter total controle sobre os recursos de hardware virtualizados. 3. Eciência: uma fração signicativa das instruções de máquina devem executar sem a intervenção do MMV. 113 Popek e Goldberg estabelecem uma condição necessária para que uma arquitetura de hardware possa ser virtualizada, isto é, para que seja possível a construção de um MMV para esta arquitetura. Para tanto, eles identicam dois tipos de instruções de hardware que impactam na construção de um MMV: 1. Instruções privilegiadas: aquelas que geram uma interrupção quando executadas em modo não supervisor (vide seção 1.3). 2. Instruções sensíveis: aquelas que mudam o estado do hardware tais como operações que afetam a MMU, os registradores especiais, etc. A condição necessária para que uma arquitetura de hardware possa ser virtualizada estabelece que, para esta arquitetura o conjunto de instruções sensíveis seja um subconjunto do conjunto de instruções privilegiadas. Esta condição permite construir um MMV que intercepte as instruções sensíveis executadas pelos sistemas operacionais hospedados. Para tal, basta o MMV executar em modo supervisor e os sistemas operacionais hospedados em modo usuário ou em um modo com privilégio intermediário. Quando um sistema operacional hospedado executa uma instrução sensível o hardware gera uma interrupção que será tratada pelo MMV. Este, identica a instrução e a executa sobre o hardware real juntamente com outras ações, caso necessário. Por exemplo, quando o operacional hospedado desabilita interrupções durante uma chamada de sistema é gerada uma exceção de permissão dado que agora o sistema operacional não possui mais a permissão para desabilitar interrupções. Esta exceção (interrupção) será tratada pelo MMV que poderá desabilitar interrupções em benefício do sistema operacional. Note que agora é o MMV que tem o controle do hardware. A arquitetura x86 não satisfaz a condição para virtualização estabelecida por Popek e Goldberg. Isto se deve ao fato que algumas instruções privilegiadas desta arquitetura simplesmente falham sem gerar interrupções quando executadas em modo usuário. Isto signica que, para esta arquitetura, um MMV não deve se basear apenas na interceptação de instruções privilegiadas. Em 2005 a Intel introduziu a tecnologia VT-x para, além de outras facilidades para virtualização, eliminar as instruções problemáticas para a virtualização. A AMD possui tecnologia similar denominada AMD-V. Apesar de não satisfazer a condição para virtualização de Popek e Goldberg, é possível virtualizar a arquitetura x86 original com um recurso denominado tradução binária. Tradução binária é uma técnica que troca as 114 instruções problemáticas por outras que geram interrupção se executadas em modo não supervisor. Nesta técnica não é o sistema operacional hospedado que executa na máquina virtual, mas sim um tradutor. O código do sistema operacional é uma entrada para o tradutor que lê suas instruções e as executa na CPU. Para determinadas instruções problemáticas, o tradutor as altera ou acrescenta outras a m de obter o comportamento desejado (gerar interrupções). Deve-se observar que apenas o núcleo do sistema operacional deve ter seu código traduzido pois somente este código utiliza instruções sensíveis. Esta técnica foi empregada pela empresa VMware na primeira virtualização comercial da arquitetura x86 em 1999. 7.3 Virtualização com suporte do hardware As arquiteturas atuais removeram as instruções problemáticas, além de outras adições importantes para virtualização. Estas adições dizem respeito, principalmente, à virtualização da memória, da CPU e da entrada e saída. Estas adições serão discutidas a seguir. 7.3.1 Virtualização da memória Memory Management Vimos no capítulo 4 que a tabela de páginas e a MMU ( Unit ) são elementos fundamentais para a conversão eciente de endereços virtuais em endereços físicos. Estes elementos foram estendidos para virtualização conforme veremos a seguir. Uma máquina virtual oferece ao sistema operacional hospedado um espaço de endereçamento que emula a memória física presente no hardware. Entretanto, este espaço de endereçamento é também virtualizado pelo MMV. A memória física real (que denominaremos agora memória de máquina) é visível apenas ao MMV. Neste cenário, a conversão de endereços se dá em dois passos. No primeiro passo o endereço virtual oferecido aos processos pelo sistema operacional é convertido para o endereço físico propiciado pela máquina virtual. Cada máquina virtual possui uma tabela de páginas em hardware. Esta tabela é alimentada pelo sistema operacional hospedado. No segundo passo o endereço físico é convertido para o endereço de máquina pelo MMV. Para tal, o hardware disponibiliza ao MMV uma tabela de páginas denominada tabela de páginas estendida. A gura 7.2 ilustra estas tabelas. Além da tabela de página estendida, a MMU das novas arquiteturas de 115 Figura 7.2: Tabela de páginas estendida. Translation Lookaside Buer ) acrescen- hardware estendem também o TLB ( tando em suas entradas um identicador de máquina virtual. Assim, em uma consulta ao TLB deve ser passado, além do endereço virtual, este identicador. Vejamos como se dá o processo de conversão de endereços (gura 7.3). Ao processar uma instrução, o hardware consulta o TLB com o identicador da máquina virtual que está de posse da CPU (1). Caso o mapeamento não seja encontrado no TLB a tabela de páginas em hardware da máquina virtual é pesquisada (2). Caso o mapeamento não seja encontrado nesta tabela, é gerada uma falha de paginação que será tratada pelo sistema hospedado (3). Este, atualizará a tabela de páginas de sua máquina virtual (4) e o processo de resolução de endereço continua. Sem virtualização, a MMU acrescentaria este mapeamento ao TLB e a conversão de endereço se encerraria. Com virtualização, o processo prossegue com uma consulta à tabela de páginas estendida (5). Nesta tabela é obtido o mapeamento do endereço físico para o endereço de máquina. Caso este mapeamento inexista na tabela estendida, é gerada uma falha de paginação, agora tratada pelo MMV (6). Providenciado o mapeamento pelo MMV, a tabela de páginas do MMV é atualizada (7) e o 116 mapeamento é inserido no TLB com o identicador da máquina virtual onde a conversão de endereços foi iniciada (8). A instrução que causou a falta de paginação é então reiniciada e o novo mapeamento poderá ser utilizado subsequentemente enquanto mantido no TLB. Figura 7.3: Mapeamento de endereços com virtualização suportada pelo hardware. Os números indicam a sequência de ocorrência dos eventos. 7.3.2 Virtualização da CPU O suporte do hardware para virtualização da CPU consiste na introdução de um novo modo do processador com privilégio superior ao de supervisor e uma exibilização na manipulação de interrupções. Modos do processador Vimos na seção 1.3 os modos do processador, supervisor e usuário. Sistemas operacionas são projetados para operar no modo supervisor, uma condição 117 necessária para exercer controle sobre o hardware. Na realidade, existem mais modos do processador, denominados níveis ou camadas de privilégio. A arquitetura x86, por exemplo, dispõe de 4 níveis, do nível 0 (modo supervisor) ao nível 3 (modo usuário). Por razões de portabilidade, apenas os níveis extremos foram utilizados, o nível 0 para o núcleo do sistema operacional e o nível 3 para os processos. Em um cenário de virtualização, é o monitor de máquina virtual que deve exercer controle sobre o hardware. Como o MMV e o sistema operacional executavam no mesmo modo supervisor, era necessário tradução binária para remover o controle do hardware do sistema operacional hospedado. Nesta solução, o sistema operacional hospedado executa no nível 1, perdendo, portanto, o controle pleno do hardware. Tradução binária gera overheads inevitáveis e, neste ponto, as novas arquiteturas de hardware deram um passo importante. Como o modo supervisor (denominado nível 0) já é utilizado pelos sistemas operacionais, as novas arquiteturas introduziram um nível de privilégio ainda maior, o nível raiz ( root ), exclusivo do MMV. Isto permite manter o nível 0 para os sistemas operacionais nativos sem a necessidade de tradução binária. A transição do nível raiz para outro nível é denominada entrada de VM entry ) enquanto a transição oposta é denominada saída de VM (VM exit ). A saída de VM transfere o controle do processador para o MMV. O VM ( hardware salva e recupera o estado do processador durante estas transições. Duas causas principais causam a saída de VM com transferência de controle para o MMV: 1. Execução de instruções exclusivas do nível raiz, por exemplo, instruções que alteram determinados registradores de estado e instruções que alteram o vertor de interrupções. 2. Ocorrência de determinadas interrupções de hardware quando o processador está em modo não raiz, por exemplo, falhas de paginação. A gura 7.4 ilustra os eventos que causam transições entre os modos do processador. Tratamento de interrupções Vimos na capítulo 1 que o tratamento de interrupções é uma condição importante para o controle do hardware. O suporte a virtualização por parte 118 Figura 7.4: Modos do processador e transições em virtualização suportada pelo hardware. do hardware deve exibilizar o tratamento de interrupções, permitindo que o MMV intervenha no tratamento de interrupções que comumente são tratadas pelo sistema operacional. Esta intervenção por parte do MMV consiste de: 1. Determinar quais interrupções de hardware devem causar a saída de VM. 2. Transferir para um sistema operacional hospedado a interrupção que causou a saída de VM imediatamente após a entrada de VM. A transferência de uma interrupção tratada inicialmente pelo MMV para o sistema operacional hospedado é denominada injeção de eventos. A - gura 7.5 ilustra este processo. Inicialmente o MMV registra junto ao hardware as interrupções que deseja interceptar (1). Quando uma destas in- terrupções é gerada (2) é causada a saída de VM (3) e o manipulador desta interrupção do MMV é invocado (4). Terminado o tratamento desta interrupção o hardware providencia a entrada de VM (5) e a injeção da interrupção para ser tratada agora pelo sistema operacional hospedado (6). 7.3.3 Virtualização de entrada e saída A virtualização do subsistema de entrada e saída requer do MMV a capacidade de emular os dispositivos de entrada e saída, recebendo requisições dos 119 Figura 7.5: Injeção de eventos. Os números indicam a sequência de ocorrência dos eventos. 1 acionadores de dispositivo utilizados pelos sistemas operacionais hospedados e repassando-as para os dispositivos de entrada e saída. Para tal o MMV utiliza seus próprios acionadores de dispositivo como ilustra a gura 7.6. As operações de entrada e saída devem causar saídas de VM, eventualmente utilizando-se o recurso de tradução binária. Isto é necessário para que o MMV possa intervir na operação. Vimos no capítulo 6 que um componente importantíssimo para o subsistema de entrada e saída é o acesso direto à memória (DMA). DMA é suportado por hardware e está presente em praticamente todos os dispositivos orientados a bloco. Desta forma, os acionadores de dispositivo interagem com os respectivos dispositivos de entrada e saída via DMA. A solução apresentada na gura 7.6 é uma solução de software. Do ponto de vista do hardware o suporte à virtualização deve incluir também mecanismos que permitem um acesso mais direto aos dispositivos de entrada e saída, minimizando a intervenção do MMV. Este mecanismo é denominado pela Intel de remapeamento de DMA. A ideia é oferecer a capacidade de transferir dados do dispositivo de entrada e saída diretamente para a memória física do sistema operacional hospedado. Note que uma operação de DMA transfere dados do dispositivo para a memória de máquina. Vimos que os endereços físico e de máquina são mapeados pelo MMV via tabela de páginas 1 Por exemplo, requisições envolvendo DMA, seção 6.3. 120 estendida. Figura 7.6: Virtualização de entrada e saída sem suporte do hardware. Ao interceptar uma requisição envolvendo DMA, o MMV verica o endereço físico suprido pelo acionador de dispositivo do sistema operacional hospedado. O MMV mapeia este endereço no correspondente endereço de máquina e repassa a operação de DMA ao hardware de remapeamento de DMA conforme ilustra a gura 7.7. Este encaminha a operação ao respectivo controlador de DMA do dispositivo. Ao completar a transferência, o controlador de DMA gera uma interrupção que será tratada inicialmente pelo MMV e transferida ao sistema operacional hospedado via injeção de eventos. 7.4 Modelos de virtualização Vimos até então as funções desempenhadas pelo MMV que podemos resumilas como funções de controle do hardware. Nesta seção apresentaremos as 121 Figura 7.7: Virtualização de entrada e saída com suporte do hardware (remapeamento de DMA). três formas comumente empregadas na contrução de MMVs. Estas formas denem os modelos de virtualização descritos a seguir. 7.4.1 Virtualização plena Na virtualização plena (ou completa) o MMV não necessita de nenhum recurso especial do hardware e nenhuma alteração nos sistemas operacionais hospedados é necessária. Virtualização plena é obtida com tradução binária, tipicamente com o MMV executando no nível 0 (supervisor) e os sistemas operacionais hospedados no nível 1 como ilustra a gura 7.8(a). A ausência de suporte a funções de virtualização por parte do hardware demanda soluções em software de alta complexidade para a implementação de MMVs, impactando negativamente na eciência. MMVs que implementam virtualização plena podem executar sobre um sistema operacional nativo (não virtualizado) denominado sistema operacional hospedeiro ( host ), como ilustra a gura 7.8(b). 122 Por ser agora uma aplicação do sistema operacional hospedeiro, o MMV deve executar no modo usuário e, portanto, depende integralmente deste sistema operacional para acessar o hardware. Neste caso, o tratamento de instruções sensíveis por parte do MMV gera chamadas de sistema no sistema operacional hospedeiro. Apesar deste esquema gerar dupla indireção no tratamento de instruções sensíveis (envolvendo o MMV e o sistema operacional hospedeiro), o mesmo é atrativo por permitir a coexistência dos sistemas operacionais hospedeiro e hospedados e de aplicações nativas que executam diretamente sobre o sistema hospedeiro. Figura 7.8: Modelo de virtualização plena. 7.4.2 Virtualização assistida pelo hardware Neste modelo o MMV tira proveito das extensões do hardware para suporte à virtualização e os sistemas operacionais hospedados continuam sem qualquer alteração. Este modelo é ilustrado na gura 7.8(a) e podemos considerá-lo como o modelo de virtualização plena com maior eciência. O MMV agora tem um nível de privilégio superior aos dos sistemas operacionais hospedados, mesmo que estes executem no modo supervisor (nível 0), como na gura 7.4. Virtualização assistida pelo hardware simplica em muito o MMV, sendo a solução ideal quando um hardware que suporta virtualização está disponível. 123 Graças ao suporte à virtualização disponível nas arquiteturas modernas de hardware, este modelo é atualmente o modelo preferido de virtualização onde os sistemas hospedados devem permanecer inalterados. 7.4.3 Paravirtualização Neste modelo os sistemas operacionais hospedados são modicados para executarem sobre máquinas virtuais. Muitas das funções desempenhadas pelo MMV são agora desempenhadas pelos próprios sistemas hospedados. Podemos, simplicadamende, considerar paravirtualização como tradução binária de instruções sensíveis incorporada previamente no núcleo dos sistemas hospedados. Esta instruções geram hypercalls para o MMV como ilustra a gura 7.9. Figura 7.9: Modelo de paravirtualização. Por requerer modicação nos sistemas operacionais hospedados paravirtualização impede que sistemas proprietários sejam hospedados em máquinas virtuais segundo este modelo (como é o caso do sistema operacional Windows). 124 7.4.4 Virtualização suportada pelo sistema operacional Neste modelo, um sistema operacional hospedeiro é capaz de criar ambientes de execução no espaço do usuário onde aplicações podem executar com total isolamento. Estes ambientes, denominados contêineres, provêem todas as funcionalidades do sistema operacional hospedado, mas sem replicar este sistema. Contêineres compartilham o núcleo do sistema operacional hospedeiro, que desempenha todas as funções de virtualização. Cada contêiner possui seu próprio sistema de arquivos, interfaces de rede, interpretador de comandos, etc. CPU e memória são alocadas aos contêineres pelo núcleo do sistema operacional. A gura 7.10 ilustra este modelo. Figura 7.10: Virtualização suportada pelo sistema operacional. Virtualização suportada pelo sistema operacional é uma solução de baixo overhead pelo fato de existir uma única instância de sistema operacional em execução sobre o hardware. Sobre esta instância, aplicações podem executar com isolamento superior àquela propiciada pelo modelo de processos. Este modelo é tipicamente empregado na virtualização de servidores onde tem-se diferentes serviços cada qual executando sobre um contêiner e com isolamento equivalente àquela propiciada pela execução destes serviços em máquinas físicas distintas. Como desvantagem, por compartilharem o mesmo núcleo, temos obrigatoriamente o mesmo sistema operacional em cada contêiner. 125 7.5 Exemplo de soluções de virtualização Nesta seção apresentaremos brevemente um exemplo de cada modelo de virtualização descrito na seção anterior. Apenas produtos de código aberto serão descritos. Entretanto, é importante notar que existem no mercado uma vasta gama de produtos de virtualização comerciais que utilizam os modelos de virtualização (ou combinações destes) descritos anteriormente. Entre as empresas fornecedoras destes produtos podemos citar VMware, Microsoft, Oracle e Citrix (versões comerciais do Xen). 7.5.1 VirtualBox VirtualBox é um produto de código aberto da empresa Oracle que utiliza virtualização plena sobre um sistema operacional hospedado (Windows, Linux, Solaris ou OS-X). Possui uma interface amigável para criar máquinas virtuais e gerenciá-las. Ações de gerenciamento incluem iniciar e terminar a execução de uma VM, criar uma imagem (cópia) de uma VM e migrar uma VM para outro sistema hospedeiro. Cada máquina virtual possui um único arquivo contendo o sistema operacional hospedado. Além da interface gráca, VirtualBox possui um conjunto de programas que permite gerenciar as máquinas virtuais a partir de utilitários de linha de comando instalados no sistema hospedeiro. VirtualBox é distribuído como software livre. 7.5.2 KVM Kernel-based Virtual Machine, ou KVM é uma solução de virtualização plena que executa sobre um sistema operacional hospedeiro, o Linux. KVM suporta sistemas hospedados sem necessidade de modicação (Windows e Linux). A solução adotada pelo KVM é estender o sistema hospedeiro com funções de virtualização assistidas pelo hardware, se tornando assim um misto de virtualização plena e assistida pelo hardware. Esta extensão, disponível em muitas distribuições do sistema Linux são propiciadas por dois módulos: kvm.ko e kvm-intel.ko ou kvm-amd-ko (estes com extensões do núcleo para as tecnologias de virtualização Intel VT-x e AMD-V). As funções propiciadas pelos módulos kvm.ko e kvm-intel/amd.ko são acessadas por um monitor de máquina virtual baseado no virtualizador QEMU. QEMU utiliza tradução binária para prover virtualização. 126 7.5.3 Xen Xen é uma solução de paravirtualização composta de um monitor de máquina virtual (ou hypervisor) que executa diretamente sobre o hardware e versões dos sistemas operacionais Linux, NetBSD, FreeBSD e OpenSolaris modicadas. Caso instalado sobre hardware com suporte à virtualização, Xen é capaz de suportar máquinas virtuais com o sistema Windows. Neste caso, Xen opera segundo o modelo de virtualização assistida pelo hardware. Xen dene uma interface de Virtual Machine Interface. hypercalls para a arquitetura x86 denominada Esta interface consiste de chamadas para o MMV que possibilitam a virtualização da CPU, da memória e de entrada e saída. A modicação (paravirtualização) de um sistema operacional para operar como hospedado no Xen requer a utilização destas chamadas em situações envolvendo a utilização da CPU, paginação e operações de entrada e saída. Por não utilizar sistema operacional hospedeiro, o processo de boot de um computador com o Xen instalado carrega somente o hypervisor (MMV) que por sua vez oferece uma interface para gerenciar as máquinas virtuais disponíveis no computador. 7.5.4 LXC LXC ( Linux Containers ) é uma solução de virtualização suportada pelo sistema operacional Linux. Como tal, LXC não provê máquinas virtuais, mas sim ambientes de execução (contêineres) sobre um mesmo núcleo do patch ) do sistema operacional. LXC é instalado por meio de uma atualização ( núcleo do Linux. Esta atualização consiste de utilitários em linha de comando de gerenciamento de contêineres (inicialização e remoção). Cada contêiner possui um arquivo de conguração contendo o nome do contêiner, parâmetros de interface de rede e sistemas de arquivos que devem ser montados no contêiner. No nível do núcleo, LXC se baseia em um mecanismo denominado control groups ). ( cgroups Este mecanismo propicia o agrupamento de processos e a alocação de recursos a estes processos (e seus descendentes) em termos de frações de CPUs, memória, disco e rede. Cgroups apresentam uma pro- priedade fundamental para virtualização: isolamento do espaço de nomes. Em outras palavras, identicadores têm seus nomes vinculados a um cgroup. Exemplo de tais identicadores incluem identicadores de processo (PID), de rede (endereços, tabelas de roteamento, etc.), identicadores de recursos 127 de comunicação inter-processo (semáforos, memória compartilhada, etc.) e identicadores (nomes) de arquivos e diretórios. LXC e outras soluções similares como OpenVZ implementam contêineres como cgroups, oferecendo basicamente uma interface de alto nível para este mecanismo. 7.6 Na prática Uma tendência atual é a chamada consolidação de servidores onde um servidor é particionado em diversos ambientes de execução, cada qual suportando um serviço. Por exemplo, uma única máquina pode abrigar um servidor de banco de dados, um servidor HTTP e um servidor de autenticação, cada qual em uma partição distinta e isolada das demais. Estas partições são realizadas com técnicas de virtualização, por exemplo, máquinas virtuais ou contêineres. Se dispomos de um computador com o sistema operacional Windows ou OS-X, podemos instalar neste sistema o nosso servidor HTTP que requer o sistema operacional Linux. Primeiramente instalamos um virtualizador sobre o sistema nativo, por exemplo, o VirtualBox. Em seguida criamos uma máquina virtual com o auxílio da interface provida pelo virtualizador. Nesta máquina virtual selecionamos o sistema Linux em uma de suas distribuições suportadas pelo virtualizador. Uma questão importante é a conguração da rede no sistema Linux hospedado. A máquina virtual oferece um ou mais adaptadores de rede virtualizados. Necessitamos congurar um adaptador com parâmetros de rede. Comumente utiliza-se duas alternativas. A primeira é alocar ao adaptador de rede da máquina virtual um endereço de rede (IP) privado e utilizar uma função de rede denominada NAT ( Network Address Translation ) para permitir que aplicações que executam no sistema operacional hospedado acessem a Internet utilizando o endereço IP público do sistema hospedeiro. Esta alternativa não é adequada para o nosso servidor HTTP pois o mesmo 2 caria inacessível aos clientes por conta do endereço privado . A segunda alternativa, esta adequada para o nosso caso, é atribuir um endereço IP público ao adaptador de rede da máquina virtual e congurar bridge ) entre as máquinas virtuais e o sistema hospedeiro. uma ponte ( 2 Com Neste NAT, o detentor do endereço privado é capaz de iniciar conexões, mas é incapaz de recebe-las como requerido no caso de servidores. 128 caso o endereço IP da máquina virtual deve pertencer à mesma subrede do sistema hospedeiro. A gura 7.11 ilustra estes esquemas de virtualização de interfaces de rede. Figura 7.11: Virtualização das interfaces de rede. Com a rede acessível a partir da máquina virtual, podemos instalar o compilador gcc/g++ e compilar o nosso servidor no sistema Linux hospedado. Solucionada a questão da rede, temos que prover um meio de acesso à máquina virtual. A máquina virtual emula o adaptador de vídeo sobre o ambiente gráco do sistema hospedeiro. Por exemplo, o VirtualBox é capaz de oferecer o ambiente gráco de qualquer sistema hospedado sobre uma janela do sistema hospedeiro. Como no nosso caso utilizamos o computador via ambiente gráco, temos igualmente pleno acesso ao ambiente gráco do sistema operacional hospedado. Acontece que muitas vezes não temos acesso à este ambiente gráco, por exemplo, quando nossa máquina virtual se encontra hospedada em um processador distante. Neste caso, temos que habilitar no sistema hospedado um protocolo de terminal remoto como o ssh ( colo de compartilhamento de Computing ). desktop Secure Shell ) ou um protoVirtual Network gráco como o VNC ( 129 7.7 Exercícios 1. O que é uma máquina virtual? 2. Qual o papel de um monitor de máquina virtual (MMV)? 3. Quais as condições necessárias para a construção de um MMV baseado na interceptação de instruções sensíveis? 4. O que é e qual a função da tradução binária? 5. Que facilidades as novas arquiteturas de hardware provêem para a execução de sistemas operacionais em ambientes virtualizados? 6. Como se dá o mapeamento de endereços virtuais para endereços de máquina em um ambiente virtualizado? 7. Como se dá o tratamento de interrupções de hardware em um ambiente virtualizado? 8. Como se dá a entrada e saída em um ambiente virtualizado? 9. Descreva os três modelos de virtualização. 10. Intuitivamente, por que a paravirtualização é mais eciente que os demais modelos de vitualização? 11. Cite as vantagens e desvantagens da virtualização no nível do sistema operacional. 12. Qual o papel da virtualização para a computação em nuvem? 130 Capítulo 8 Segurança em Sistemas Operacionais Este capítulo foi redigido com a colaboração do Prof. Marco A.A. Henriques, FEEC/Unicamp. 8.1 Introdução Com a facilidade de interconexão dos processadores às redes de comunicação, notadamente à Internet, o conceito de segurança da informação mudou radicalmente. Na época dos mainframes a segurança era garantida via controle do acesso físico do usuário às salas que abrigavam os periféricos do computador (terminais, leitoras de cartões, etc.). Com o advento dos minicomputadores, que permitem o uso simultâneo por muitos usuários, o controle através de nome e senha atendia bem o requisito de segurança, limitando o acesso apenas aos usuários previamente cadastrados. Os primeiros microcomputadores não ofereciam nenhum mecanismo de segurança posto que o acesso à máquina era restrito a um único usuário e a máquina operava totalmente desconectada das redes de comunicação. Com a popularização da Internet na década de noventa e mais intensamente a partir deste século, o esquema de controle de acesso baseado em nome e senha tornou-se insuciente para garantir a integridade dos sistemas de informação. Indivíduos, organizações e até mesmo governos nacionais passaram a explorar as vulnerabilidades dos sistemas interconectados para executar ações ilegais como roubo de informações, fraudes nanceiras, in- 131 vasões de privacidade e sabotagens. A diculdade de identicar a origem dos ataques é um fator que tem incentivado tais ações, dado que as mesmas raramente são realizadas pelo computador do próprio atacante. 8.2 Ataques à Segurança Existem diversas formas de ataque à segurança de um sistema de computação. Os mais comuns são [2]: • Violação de condencialidade: acesso à informação que apenas pessoas autorizadas poderiam dispor, por exemplo, estratégias de negócio de uma empresa. • Violação de integridade: criação, alteração ou destruição de infor- mações sensíveis, por exemplo, adição de uma pessoa ctícia a um cadastro. • Roubo de serviço: instalação de programas que utilizam CPU, armazenamento e rede em uma máquina em benefício do atacante (comumente para atacar outras máquinas). • Negação de serviço (DoS: Denial of Service ): impedir usuários legítimos de utilizar um serviço. Existem vários mecanismos de intrusão que um atacante pode utilizar por meio da Internet tais como: 1. Mascaramento: o atacante se passa por um usuário legítimo, por exemplo, via roubo de senhas. 2. Homem-no-meio ( man-in-the-middle ): o atacante intercepta e altera a comunicação entre duas partes legítimas, por exemplo, redirecionando um usuário para um site falso de comércio eletrônico. 3. Ataque de repetição ( replay attack ): uma mensagem é interceptada, por exemplo, contendo uma senha, e utilizada novamente pelo atacante quando tal informação é solicitada durante um ataque futuro. Estes ataques comumente não dependem de falhas no sistema operacional, mas sim em falhas nos aplicativos que oferecem serviços através da rede. 132 Outras formas de ataque focam em falhas (brechas) de segurança presentes nos sistemas operacionais. Tais brechas são expostas por componentes do sistema operacional (chamadas de sistema, principalmente) e por software nativo como os interpretadores de comandos, de macros e de como utilitários manipulados pelo usuário. scripts, bem As falhas de segurança comu- mente são exploradas por meio de programas de uso corriqueiro como os aplicativos de Email, mensagens, apresentarores de mídia e navegadores Web. Código malicioso (virus e suas variantes) se propagam como um código anexo a outro documento, por exemplo, anexos em mensagens de Email, scripts em páginas Web e macros em documentos editados. A execução deste código pelo aplicativo expõe a brecha de segurança que é explorada pelo atacante para o roubo de senhas, dados pessoais, dados bancários, etc. Tais informações são transmitidas via rede para uma máquina utilizada para realizar o ataque. Desta forma a proteção contra estes ataques deve se dar tanto no nível do sistema operacional quanto no nível de rede. A ferramenta básica de proteção é a criptograa. Criptograa é o processo de alterar a informação sem alterar seu conteúdo semântico de sorte que apenas o detentor de chaves criptográcas legítimas pode recuperar e/ou validar a informação original. Criptograa pode ser empregada em qualquer documento digital, por exemplo, documentos de texto, imagens e código executável. Criptograa é um tema extenso que não temos a pretensão de cobrir neste capítulo. Apenas os tópicos fundamentais serão cobertos na próxima seção para entendermos os principais conceitos de segurança da informação. Um texto introdutório sobre criptograa pode ser encontrado em [8]. 8.3 Criptograa Um algoritmo de criptograa é um procedimento transforma a informação. Esta transformação tem como propósito tornar a informação ilegível para um atacante, detectar qualquer alteração da informação original, e até mesmo atestar a origem (produtor) da informação. Para transformar a informação os algoritmos de criptograa empregam chaves criptográcas. Uma chave criptográca é uma cadeia de bits utilizada por algoritmos de criptograa para processar a transformação dos dados legíveis para ilegíveis (cifragem) ou vice-versa (decifragem). Uma informação cifrada tem seu conteúdo alterado a tal ponto que seu roubo ou interceptação não traz nenhum benefício para um atacante que não possua a chave 133 criptográca para decifrá-la. Apesar das senhas que normalmente utilizamos serem formadas por caracteres alfanuméricos (legíveis), as chaves criptográcas são tratadas como sequências de bits não mapeadas em caracteres alfanuméricos, o que diculta a sua dedução por parte de um atacante. A mesma chave criptográca pode ser empregada para cifrar e para decifrar informações. Esta forma de criptograa é denominada criptograa de chave simétrica. Alternativamente, há a criptograa de chave pública ou assimétrica na qual são empregadas duas chaves correlacionadas, sendo uma para cifrar e a outra para decifrar a informação. O emprego de criptograa de chave simétrica apresenta uma desvantagem: os elementos envolvidos na comunicação devem possuir a mesma chave. Isto apresenta um potencial risco à segurança posto que a chave deve ser compartilhada por meio de um canal inseguro, ou seja, pode ser interceptada durante sua transferência de uma parte em comunicação para a outra parte. Caso ocorra a interceptação o roubo da chave em algum lugar todo o sistema ca comprometido. Por outro lado, tem-se a vantagem de ser bem mais eciente que a criptograa de chave assimétrica no processo de cifragem e decifragem da informação. Na criptograa de chave pública emprega-se duas chaves. Uma das chaves é pública, ou seja, divulgada abertamente, e uma é privada e pertencente a uma única máquina, indivíduo ou organização. Ao cifrar um documento digital com uma das chaves, apenas e tão somente a outra chave poderá decifrá-lo. Por exemplo, se uma pessoa A desejar transmitir uma informação sigilosa para uma pessoa B, A obtém a chave pública de B, cifra a mensagem utilizando esta chave e a envia para B que utiliza sua chave privada para decifrá-la. Se, por outro lado, não se quer sigilo, mas apenas a autenticação da origem, A pode cifrar a mensagem com sua chave privada. Neste caso, B ou qualquer pessoa poderá decifrar tal mensagem usando a cheve pública de A. O sucesso na decifragem de uma mensagem com a chave pública de uma pessoa garante que só ela poderia ter cifrado tal mensagem, pois só ela possui a chave privada correspondente. Logo, cifrar com a chave privada é equivalente a assinar digitalmente um documento ou mensagem digital. Por ser mais complexa, a criptograa de chave pública, na prática, é utilizada apenas para a cifragem de mensagens curtas como, por exemplo, cifragem de uma chave simétrica utilizada em uma sessão de comunicação. Uma vez que a chave simétrica foi compartilhada de forma segura, é mais recomendável cifrar as demais mensagens (os dados) com uma algoritmo 134 criptográco que emprega chave simétrica, por questões de eciência. Esta forma de comunicação é empregada nos protocolos HTTPS ( fer Protocol Secure ) e SSH (Secure Shell ). Hypertext Trans- Estes protocolos estabelecem um canal de comunicação (conexão) seguro através do qual a informação trafega cifrada. O algoritmo mais utilizado em criptograa de chave pública é o RSA (iniciais de seus criadores: Ron Rivest, Adi Shamir e Leonard Adleman). RSA se baseia no fato de não ser conhecido nenhum algoritmo eciente de fatoração de números inteiros grandes (com centenas de dígitos). No RSA, recomenda-se que as chaves pública e privada tenham pelo menos 1024 ou 2048 bits cujos conteúdos são gerados a partir de operações aritméticas envolvendo números primos grandes. 8.4 Certicados e assinaturas digitais A idéia dos mecanismos de autenticação é associar uma chave de acesso a um detentor legítimo (máquina, software, usuário) de sorte que a apresentação desta chave identica positivamente o seu detentor. A chave de acesso pode ser uma senha textual, uma identicação biométrica, um identicador gravado em hardware, ou uma chave criptográca. Uma questão fundamental é como associar a chave ao detentor. feita pelo administrador do sistema. No caso de senhas esta associação é No caso de chaves criptográcas esta associação é feita por uma autoridade certicadora, uma entidade pública ou privada capaz de: • identicar positivamente o detentor de uma chave pública e associá-la a ele; • adotar procedimentos rígidos para a sua própria segurança; • ser acreditada pela indústria de informática. As autoridades certicadoras são comumente referidas como digitais, uma analogia aos cartórios de notas. cartórios A autoridade certicadora é a parte conável que garante que uma chave pública usada para cifrar ou decifrar um documento digital realmente está associada à pessoa que clama ser o seu legítimo detentor. O carimbo que o cartório digital emite associando a chave ao seu detentor é denominado certicado digital. O certicado digital contém informações tais como quem o emitiu (autoridade 135 certicadora), o detentor da chave (nome da empresa, por exemplo), sua chave pública, o período de validade do certicado e uma assinatura digital assegurando que o mesmo foi emitido pela autoridade certicadora indicada. International Um formato padrão de certicado digital é o X.509 da ISO ( Organization for Standardization ). A gura 8.1 ilustra o processo de criação de um certicado digital. Figura 8.1: Processo de criação de um certicado digital. Comumente o certicado é exigido apenas do servidor em operações realizadas na Internet. A gura 8.2 ilustra o uso de certicados digitais para o estabelecimento de uma conexão segura HTTPS através de uma rede insegura (Internet). No HTTPS o certicado digital é utilizado para a troca segura de uma chave simétrica entre o cliente e o servidor. A partir daí, toda a informação transmitida pela conexão é cifrada/decifrada com esta chave simétrica. Por exemplo, ao efetuar uma transação bancária, o aplicativo do cliente (navegador Web) deve estabelecer uma conexão segura com o servidor do banco. Neste processo, o cliente deve inspecionar o certicado digital do banco enviado pelo servidor durante o estabelecimento da conexão segura. Caso o certicado não seja assinado por uma autoridade certicadora con- 1 ável , um alerta de segurança é emitido. Exitem casos onde a conança baseada em certicados digitais deve ser recíproca, ou seja, ambos cliente e servidor devem apresentar seus respectivos certicados. 1 Servidores É o caso, por Web possuem uma lista de autoridades certicadores conáveis. 136 Figura 8.2: Estabelecimento de uma conexão segura no protocolo HTTPS. exemplo, da Receita Federal do Brasil, que fornece informações scais sobre um cidadão desde que o mesmo comprove sua identidade por meio de um certicado digital. Uma questão importante é como garantir a autenticidade do próprio certicado digital. Aqui entra a assinatura digital. A assinatura digital garante a origem e a integridade da informação, ou seja, permite detectar qualquer alteração indevida da mesma. etapas. A assinatura é criada em duas Na primeira, é computado um hash do documento digital a ser Secure assinado empregando algoritmos conhecidos, por exemplo, o SHA-1 ( Hash Algorithm ) que gera uma sequência de 160 bits. O hash do documento associa todo o conteúdo do documento a uma cadeia de bits de sorte que a probabilidade de dois documentos diferentes, por menor que seja a diferença, produzir o mesmo valor de hash é praticamente zero. O hash apenas não garante a integridade da informação, pois sendo o algoritmo de hash conhecido, um atacante poderia alterar tanto a informação quanto seu hash associado. Aqui entra a segunda etapa do processo. Quem gerou a informação a ser autenticada, a autoridade certicadora no caso de certicados digitais, cifra o resultado do hash utilizando sua chave privada. Assim, somente a chave pública da autoridade certicadora poderá decifrar o hash e recuperar seu valor original. A assinatura é vericada da seguinte forma: o hash do documento é recomputado pelo receptor e comparado com o hash decifrado com a chave pública de quem assinou o documento. Mesmo 137 que um atacante altere o documento e recompute seu hash, ele não possuirá a chave privada de quem assinou o hash. Caso o faça com sua própria chave privada, a chave pública do emissor do certicado irá decifrar o hash mas produzirá um valor diferente do hash originalmente cifrado, invalidando a assinatura digital. Assinaturas digitais podem ser empregadas não apenas em certicados digitais, mas em qualquer documento digital. Neste caso, ao assinar um documento com a sua chave privada, o emissor incorpora ao documento a assinatura digital (hash cifrado), o algoritmo de hash empregado, e seu próprio certicado digital. O receptor, após vericar a autenticidade do certicado digital do emissor, utiliza a chave pública nele contida para validar a assinatura digital presente documento. 8.5 Segurança do sistema operacional Vimos que a informação deve ser protegida em trânsito e no local onde é armazenada. A proteção em trânsito (por exemplo, numa transação bancária) é realizada via cifragem e mecanismos adicionais, por exemplo, nonces, números aleatórios utilizados uma única vez em uma troca de mensagens com o intúito de evitar ataques de resposta. A criptograa com o certicado e assinatura digitais pode proteger não só a informação em trânsito mas também a informação armazenada e os sistemas de software que o usuário instala em seu computador. Vimos nos capítulos precedentes várias estratégias que os sistemas operacionais adotam para oferecer certo nível de segurança aos usuários e aplicações, por exemplo: • exclusividade no tratamento de interrupções; • execução das aplicações sempre em modo usuário; • proteção de memória; • proteção individual para cada arquivo. Os sistemas operacionais, mesmo os utilizados em computadores de uso pessoal, adotam um esquema de privilégios de usuários. Normalmente, apenas dois níveis de privilégios são empregados: o nível de usuário e o nível de administrador (ou super-usuário). O sistema operacional reserva a execução 138 de certos aplicativos apenas ao nível administrador, por exemplo, a criação de novos usuários, a instalação de aplicativos, a conguração de certos serviços tais como rewalls, serviços de rede e acionadores de dispositivos. Vimos que todo sistema operacional apresenta brechas de segurança. Por exemplo, conhecendo estas brechas um programa malicioso pode realizar os mais diversos tipos de ataques. comumente é o usuário. Entretanto, o elo vulnerável do sistema O uso de senhas facilmente deduzidas, a instala- ção indiscriminada de aplicativos, notadamente em dispositivos móveis, e a desatenção no acesso à Internet (sites falsos, por exemplo) dicultam enormemente a proteção da informação. Em muitos casos, o sistema operacional deve se proteger de seu próprio usuário ! A seguir apresentaremos algumas estratégias adicionais que aumentam a segurança dos sistemas operacionais. 8.5.1 Assinatura de código Assinatura de código é o processo de atestar a procedência de um software. Neste caso, o código executável é assinado como um documento digital. A Apple e a Microsoft empregam este mecanismo nos seus sistemas operacionais. No iOS apenas softwares certicados (assinados) pela Apple podem ser executados. No Windows, o instalador de pacotes verica se o código foi assinado com uma chave emitida por uma autoridade certicadora conável, ou seja, se possui origem comprovada. Caso o software não atenda este requisito, o mesmo pode ser instalado apenas com a autorização do administrador. Infelizmente, a certicação de software, por ser onerosa para o desenvolvedor, e inviável para muitos sistemas de software gratuitos. Outro ponto é que a assinatura de código garante a procedência e não a integridade do software que pode conter falhas que expõem brechas de segurança do sistema operacional. No sistema Linux não existe um esquema formal de assinatura de código como no iOS e Windows. Neste sistema, a procedência é garantida pelo reconhecimento do fornecedor por parte da indústria e da comunidade de desenvolvedores. É o caso de entidades como a Apache Foundation e a Free Software Foundation, bem como empresas comerciais de renome que oferecem versões de seus produtos para Linux e outros sistemas de código aberto. 139 8.5.2 Cifragem do sistema de arquivos Sistemas operacionais como o Linux e iOS suportam a cifragem de partições de disco, o que protege o sistema de arquivos instalado na partição contra a violação de condencialidade. A cifragem diminui o desempenho do sis- tema de arquivos e, portanto, deve ser utilizada apenas em partições que armazenam informações vitais para o usuário ou organização. O processo de cifragem/decifragem do sistema de arquivos utiliza um chave criptográca gerada comumente a partir de uma senha suprida pelo usuário e um identicador de hardware especíco da máquina, seu hardware UID ( Unique IDentication ). No iOS a cifragem do sistema de arquivos está disponível, sendo apenas necessária a sua correta conguração. No Linux é necessário a instalação de um sistema de arquivos que suporte cifragem em uma partição exclusiva como ilustrado na gura 8.3. É o caso do ZFS e eCryptfs, por exemplo. Estes sistemas de arquivos podem coexistir com os tradicionais sistemas de arquivos (não cifrados) Ext2 e Ext4 instalados em outras partições. Figura 8.3: Uso de um sistema de arquivos cifrado no Linux. 140 8.5.3 Sandboxes Um sandbox é um ambiente de execução controlado. Este ambiente impõe restrições às aplicações que nele executam, por exemplo, impedindo aplicações: • de alterar arquivos mantidos por outras aplicações; • de alterar as congurações do sistema operacional; • de se comunicar com outras aplicações; • de acessar irrestritamente a rede. No iOS as aplicações de terceiros, apesar de assinadas pela Apple, executam em sandboxes, ou seja, o sistema operacional impede uma série de operações que poderiam interferir com outras aplicações e com o próprio sistema operacional. Tanto no iOS como no Android cada aplicação tem seu próprio diretório raiz e portanto só podem acessar arquivos de outras aplicações caso o usuário conceda permissão para tanto. No Linux, por não diferenciar aplicações via assinatura de código, sandboxes devem ser explicitamente criados. Um meio muito utilizado de criar sandboxes no Linux é via virtualização no nível do sistema operacional (seção 7.4.4), por exemplo, via LXC. Apesar de exigir mais esforço e conhecimento por parte do usuário, sandboxes baseados em virtualização são muito mais seguros e exíveis comparados com aqueles oferecidos de forma nativa pelo sistema operacional. 8.5.4 Biometria A substituição de senhas por dados biométricos, notadamente impressões digitais, está presente em muitos computadores e dispositivos móveis. Ao invés de digitar uma senha para acessar ou destravar o computador, o usuário supre seu dado biométrico. A biometria resolve o problema de utilização de senhas facilmente deduzidas, mas restringe seu emprego na própria máquina. Na atualidade, com o uso cada vez mais frequente de serviços de rede, o uso de senhas digitadas e chaves criptográcas ainda persistirá por um longo tempo. 141 8.5.5 Armazenamento de senhas e chaves criptográcas Senhas e chaves criptográcas nunca devem ser armazenadas na sua forma textual. Por exemplo, os sistemas operacionais não armazenam a senha textual, mas um hash computado a partir desta. É praticamente impossível a partir do hash derivar a senha. Ao vericar a senha o sistema computa o seu hash e compara com o hash armazenado, validando-a caso a comparação dê resultado positivo. O arquivo que contem as senhas é protegido contra acesso que não seja por parte do administrador. Em caso da senha trafegar pela rede, a mesma deve estar cifrada para evitar sua captura por sniers de rede (programas que interceptam pacotes de rede e inspecionam seu conteúdo). Chaves criptográcas privadas são armazenadas em um arquivo especial denominado keystore. No Linux este arquivo é um arquivo regular com proteção igual ao arquivo de senhas. Na plataforma Java este arquivo é cifrado por uma senha e manipulado por um aplicativo especíco, o tool. key- Esquemas mais rígidos de armazenamento de senhas utilizam hardware smartcards, memórias USB, (Radio Frequency IDentication ), ou em memória ash do especíco para armazenamento, por exemplo, etiquetas RFID próprio processador. Estes dispositivos são visíveis apenas aos sistemas que executam funções envolvendo criptograa. 8.5.6 Ações adicionais de proteção Logging Logs são arquivos contendo informações que o sistema emite. Várias destas informações dizem respeito à segurança e devem ser inspecionadas como parte do processo de proteção do sistema. Exemplo de tais informações: • tentativa de acesso local ou remoto com senha incorreta; • port scanning : tentativa de identicar serviços rede vulneráveis insta- lados na máquina; • tentativa de aquisição de privilégios de administrador por parte de um usuário ou uma aplicação; • alterações das congurações do sistema operacional, por exemplo, alteração do endereço de rede ou alteração de componentes do sistema (bibliotecas, acionadores de dispositivo, etc.). 142 A inspeção dos arquivos de log pode indicar usuários ou programas que representam riscos potenciais à segurança do sistema. Controle de acesso à rede A proteção de um sistema de informação deve transcender ao sistema operacional e ao estabelecimento de canais comunicação seguros. A proteção no nível de rede pode utilizar esquemas de NAT ( Network Address Translation ) que impede o acesso direto à uma máquina, exigindo que a interação seja iniciada pela máquina do usuário e que a informação trafegue por um roteador especíco. Este roteador é congurado com funções de segurança tais como: • detecção de ataque de DoS identicado pela abertura de conexões de rede a uma taxa elevada; • bloqueio de tráfego gerado por protocolos inseguros, por exemplo, net File Transfer Protocol ) (login remoto) e FTP ( tel- que não cifram as mensagens que transportam senhas; • bloqueio de tráfego de/para domínios com histórico de ataques e/ou fonte de código malicioso. Firewalls são camadas de proteção que impedem que programas insta- lados no computador se comuniquem via rede ou que programas externos consigam acessar o computador. A autorização para permitir que programas atravessem o rewall (acessem a rede) é dada pelo administrador. Auditoria Auditoria e o processo de assegurar (com alta probabilidade) que o sistema está protegido. Auditoria consiste em: • vericar a vulnerabilidade dos sistemas de informação, por exemplo, utilização de sistemas de software considerados inseguros; • atestar o cumprimento de normas e procedimentos de segurança da informação adotados pela organização; • coletar e examinar dados via entrevistas, testes de campo e inspeção de logs; 143 • elaborar um relatório contendo os problemas encontrados e recomendações para saná-los. Outros recursos de proteção incluem os anti-virus, atualizações automáticas do sistema operacional e funções de criptograa suportadas pelo hardware (como no iOS sobre o processor A7 da Apple). 8.6 Na prática Nosso servidor HTTP passará agora a servir páginas utilizando o protocolo HTTPS. Para tal necessitamos da capacidade de estabelecer uma conexão de Secure Socker Layer ) e utilizá-la no protocolo HTTP. transporte segura SSL ( O software aberto OpenSSL é base para as funções de segurança no Linux envolvendo criptograa. A utilização do SSL requer um certicado digital assinado. Poderíamos adquirir um de autoridades certicadoras acreditadas como Verisign, Certisign ou Thawte. Entretanto, estes certicados são pagos e têm validade tipicamente de 1 ano. Uma saída é gerarmos um par de chaves criptográcas e utilizarmos a chave privada para assinar um certicado X.509 que nós próprios criamos. Temos um certicado auto-assinado (é como se a própria pessoa atestase sua assinatura !). O OpenSSL é capaz de gerar um certicado digital auto-assinado para utilizarmos nas conexões SSL. O problema e que, com um certicado autoassinado, o navegador do cliente irá emitir um alerta do tipo tire-me daqui, indicando que o site pode não ser aquele que arma ser. Por exemplo, um site que se faz passar por um site de um banco irá utilizar um certicado autoassinado pois jamais obteria um certicado de uma autoridade certicadora acreditada em nome do banco. 8.7 Exercícios 1. Cite os tipos de ataques de segurança comumente empregados. 2. Quais as diferenças entre a criptograa de chave simétrica e de chave assimétrica. 3. O que é uma assinatura digital? 144 4. O que é uma autoridade certicadora? 5. O que é um certicado digital? 6. O que é um certicado digital auto-assinado? 7. Como é estabelecida uma conexão segura de rede? 8. Quais as medidas de segurança comumente empregadas no nível do sistema operacional? 9. Como armazenar com segurança uma chave privada? 10. Como a virtualização pode auxiliar na segurança dos sistemas operacionais e suas aplicações? 145 Capítulo 9 Desenvolvimento de Aplicações Embarcadas Este capítulo contempla os principais aspectos do desenvolvimento de aplicações embarcadas sob a ótica dos sistemas operacionais. Aplicações em- barcadas, ou sistemas embarcados, executam em processadores instalados (embarcados) em dispositivos cuja função precípua não é o processamento da informação. Exemplos de tais dispositivos incluem equipamentos eletrodomésticos, robôs, veículos e máquinas operatrizes. É importante ressaltar que há situações em que o desenvolvedor tem a liberdade de escolher o sistema operacional sobre o qual sua aplicação irá executar, e há situações em que a utilização de um sistema operacional é um requisito imposto ao desenvolvedor. Como exemplo do primeiro caso, suponha que você irá desenvolver uma aplicação para smartphone (app). Do ponto de vista comercial é interessante disponibilizar esta aplicação tanto para o sistema iOS quanto para o sistema Android, os sistemas mais populares para este tipo de dispositivo móvel. Como exemplo do segundo caso, suponha que você irá desenvolver uma aplicação para executar em uma rede de sensores sem o cujos nós sensores possuem o sistema operacional TinyOS 1 pré-instalado. Neste capítulo vamos considerar quatro arquiteturas de aplicações embarcadas e comentar o suporte necessário do sistema operacional para cada uma das arquiteturas. 1 http://www.tinyos.net/ 146 9.1 Aplicações de laço único A aplicação de laço único ( simple control loop ) consiste de uma única tarefa que nunca será interrompida ou, no máximo, será interrompida apenas para o tratamento de interrupções. Estas aplicações comumente executam uma tarefa muito especíca, por exemplo, vericar as condições operacionais de um equipamento. Em muitos casos tais aplicações embarcadas executam em processadores mais simples como microcontroladores. Um exemplo desta arquitetura de aplicações são as que executam sobre a plataforma Arduino onde a aplicação consiste de uma função setup que concentra comandos loop que concentra toda a de conguração do hardware e de uma função lógica da aplicação. A primeira é invocada uma única vez e a segunda é invocada repetidamente. Nas funções setup e loop o programador pode invocar outras funções por ele desenvolvidas ou presentes nas dezenas de bibliotecas disponíveis para a plataforma Arduino. Desta forma, a plataforma suporta um modelo de programação estruturada. A plataforma Arduino não executa um sistema operacional, possuindo apenas um bootloader responsável por receber, via linha serial, um código para ser instalado no microcontrolador. O bootloader instala o código na memória Flash do microcontrolador e inicia sua execução logo após a transferência ou logo após a plataforma ser energizada. A inexistência de um sistema operacional dá à aplicação um controle muito preciso sobre as atividades que requeiram temporização. Exemplo de tais atividades incluem geração de sinais de controle, processamento digital de sinais e geração de formas de ondas. A desvantagem da arquitetura de laço único é a execução totalmente sequencial da aplicação aliada à necessidade do programador assumir toda a responsabilidade pelo controle do hardware. 9.2 Aplicações controladas por interrupções Nesta arquitetura a aplicação consiste de uma tarefa principal e de um conjunto de tratadores de interrupção. a um laço com uma única função tipo A tarefa principal pode se reduzir wait. Neste caso, toda a lógica da aplicação está dispersa nos tratadores de interrupção. Aplicações que seguem esta arquitetura usualmente executam em hardware especializado onde as interrupções estão associadas a valores de grandezas físicas, por 147 exemplo, uma temperatura que ultrapassa um valor limite. O tratamento de interrupções pode ser preemptivo ou não preemptivo. No caso preemptivo associa-se prioridades às interrupções e um tratador de interrupção pode sofrer preempção para que outro associado a uma interrupção de prioridade mais alta possa ser invocado. No caso não preemptivo, uma interrupção é escalonada para tratamento caso outra interrupção esteja sendo tratada. A vantagem de um tratamento não preemptivo de interrupções está na inexistência de condições de corrida e, portanto, a não necessidade de se empregar mecanismos de sincronização (ver seção 2.6.2). Outra vantagem é a economia de memória de pilha pois nunca haverá invocação recursiva de tratadores de interrupção. Esta classe de aplicações comumente executam em plataformas de hardware limitadas sobre um sistema operacional compacto, por exemplo, aqueles empregados em nós de redes de sensores sem o (RSSF). RSSF empregam nós de baixo poder de processamento capazes de se comunicarem por meio de enlaces de rádio de curta distância. Via de regra, o nó está conectado a um ou mais sensores como sensores de temperatura, luminosidade, movimento, etc. Nas RSSF as interrupções são causadas por eventos periódicos como a leitura de sensores, eventos de comunicação como a recepção de uma mensagem de um nó vizinho, eventos de energia como nível de carga de bateria e eventos gerados pelos próprios sensores como a detecção de vibração ou movimento. Exemplos de sistemas operacionais para aplicações controladas por inter- 2 rupções em RSSF são o Contiki e o TinyOS. A vantagem desta arquitetura de aplicações é a facilidade de se programar 3 sistemas reativos . Como desvantagem temos a necessidade de vincular toda a lógica da aplicação ao tratamento de interrupções. 9.3 Aplicações multitarefa cooperativas Nesta arquitetura a aplicação consiste de um conjunto de tarefas executadas de forma não preemptiva e escalonadas segundo a política FIFO. Tarefas são interrompidas apenas para o tratamento de interrupções, tratamento este também não preemptivo. O sistema operacional TinyOS foi concebido para esta arquitetura de aplicações. TinyOS possui uma linguagem de programação associada, nesC, com sintaxe muito semelhante à linguagem C. nesC é 2 http://www.contiki-os.org/ 3 Que reagem a eventos sobre os quais o sistema não tem controle. 148 uma linguagem orientada a componentes. Um componente provê interfaces e declara as interfaces de outros componentes que utiliza. nesC dene tarefas, equivalentes a processos. TinyOS não suporta proteção de memória e modos do processador (ou seja, o sistema operacional e as aplicações posuem os mesmos privilégios). Isto permite o TinyOS executar sobre plataformas de hardware bastante compactas como o microcontrolador com endereçamento de 16 bits MSP430 da Texas Instruments, muito empregado em RSSF. Estes processadores possuem em torno de 50 Kbytes de memória Flash e 10 Kbytes de memória RAM. TinyOS não possui chamadas de sistema, sendo o interfaceamento com o sistema operacional provido por comandos da própria linguagem nesC. A vantagem desta arquitetura de aplicações é a inexistência de condições de corrida e a desvantagem é o escalonamento de tarefas que recai sobre o desenvolvedor. 9.4 Aplicações multitarefa preemptivas Nesta arquitetura a aplicação consiste de um conjunto de tarefas escalonadas de forma preemptiva. Para estas aplicações o sistema operacional deve ter núcleo preemptivo e oferecer mecanismos de sincronização. Tarefas podem tratar interrupções, executar ações periódicas, ou processar informação em função do estado da aplicação. escalonadas por prioridades. Tarefas são implementadas como threads Usualmente, o sistema operacional é baseado em um núcleo preemptivo como o Linux e executa em um processador com um ou mais cores. Como exemplo de sistema operacional para esta classe de 4 aplicações podemos citar o Raspian . Este sistema operacional é baseado na distribuição Debian do Linux e customizado para o hardware Raspberry Pi que segue a losoa system on a chip e emprega o processador ARM fabricado Reduced Instruction por diversas empresas. ARM é um processador RISC ( Set Computing ) com endereçamento de 32 bits que suporta diversos modos do processador, memória virtual, processamento multimídia e hardware para operações em ponto utuante. A vantagem desta arquitetura de aplicações é a exibilidade no mapeamento das funcionalidades da aplicação em tarefas correspondentes, bem como a atribuição de prioridades a estas tarefas. A desvantagem é a dicul- 4 http://www.raspbian.org/ 149 dade em se prever e evitar a ocorrência de deadlocks, estarvação de tarefas e inversões de prioridades. A gura 9.1 ilustra as quatro arquitetruras de aplicações embarcadas descritas anteriormente. Figura 9.1: Aquiteturas de aplicações embarcadas: controlada por interrupções; a) laço único; c) multitarefa cooperativa; b) d) multitarefa preemptiva. 9.5 Arquitetura em camadas Nem sempre uma aplicação embarcada emprega unicamente um dos modelos de aplicação descritos acima. Na prática, uma combinação destes modelos é comum. Para compor estes modelos uma arquitetura em camadas pode ser empregada. Em uma arquitetura em camadas as funcionalidades da aplicação são organizadas em camadas de tal forma que uma camada oferece 150 serviços por meio de interfaces bem denadas à camada superior. O próprio sistema operacional emprega esta arquitetura, vide gura 1.1. Arquiteturas em camadas são empregadas em redes de computadores, aplicações Web e design patterns ). padrões de projeto de software ( As camadas são denidas pelo projetista, ou seja, não existe uma regra rígida para estabelecê-las. A única regra é que devem ser poucas, por exemplo, cinco na arquitetura de rede TCP/IP e três na arquitetura de aplicações 3Tier. Vamos exemplicar uma arquitetura em camadas utilizada em muitas aplicações embarcadas, por exemplo, sistemas robóticos [9], e ilustrada na gura 9.2. Esta arquitetura é composta de três camadas: camada de controle de tempo real, camada executiva e camada de aplicação. Figura 9.2: Exemplo de aquitetura em camadas. 9.5.1 Camada de controle de tempo real A camada de controle de tempo real interage diretamente com o hardware, que pode ser considerado uma camada abaixo desta (mas não pertencente à arquitetura). A interação com o hardware se dá por meio de registradores, barramentos e portas, por exemplo, interface serial RS-232, barramento Inter-Integrated Circuit ) e barramento SPI (Serial Peripheral Interface Bus ). I2C ( A camada de controle de tempo real tem suas funções comumente implementadas em microcontroladores como o Arduino e executa operações com 151 período da ordem de milisegundos. Operações típicas incluem controladores (por exemplo, PID - Proporcional-Integral-Derivativo), ltragem, emissão de alarmes e geração de sinais (por exemplo, PWM - Pulse Width Modulation ). Esta camada provê uma interface para a camada executiva (via porta serial RS-232, rede Ethernet, etc.), bem como dene um protocolo de interação tipicamente baseado em passagem de mensagens. 9.5.2 Camada executiva As funções da camada executiva são comumente realizadas em processadores de pequeno e médio porte tais como o Raspberry Pi, com sistema operacional instalado (Linux, na maioria dos casos). Esta camada executa operações com período da ordem de centenas de milisegundos. Tais operações são implementadas com processos e threads (comumente em C/C++) e utilizam as funções providas pela camada de controle de tempo real. Operações típicas da camada executiva incluem sensoriamento, estimação, atuação, proteção e fusão de dados. Para a implementação desta camada as seguintes funcionaliadades do sistema operacional são utilizadas: • threads: permite a implementação de tarefas concorrentes periódicas e aperiódicas; • escalonamento por prioridades: permite que processos/threads prioritários executem primeiro e sem preempção (ex: tarefas de proteção); • bibliotecas dinâmicas: reduzem o tamanho do código e permitem incorporar funcionalidades à camada sob demanda; • sistema de arquivos em memória: aumenta velocidade de troca de informação por meio de arquivos; • programação de módulos: permite o desenvolvimento de acionadores de dispositivos para interagir com a camada inferior. A camada executiva provê uma interface de mais alto nível para a camada de aplicação baseada, tipicamente, em protocolos de comunicação cliente/servidor tais como RPC ( Transfer Protocol ). Remote Procedure Call ) e HTTP (Hypertext Protocolos de RPC são encontrados de forma nativa no Distributed sistema operacional, por exemplo, Binder no Android, DCOM ( 152 Component Object Model ) no Windows e Sun RPC no Linux. No caso de HTTP, existem diversas bibliotecas que suportam este protocolo, por exemplo, a libcurl 5 . 9.5.3 Camada de aplicação A camada de aplicação oferece funções de alto nível que são utilizadas pelas aplicações-m. Note que esta camada não implementa as aplicações-m, mas sim facilidades para a implementação conveniente e ecaz destas aplicações. As funções presentes nesta camada são dependentes do domínio de aplicação. Por exemplo, no domínio da robótica móvel, estas funções incluem algoritmos de localização, de mapeamento, de planejamento de trajetória e de locomoção. Tais funções executam em processadores mais poderosos (ou mesmo em uma nuvem) e são codicadas em linguagens de alto nível como o Matlab, Python e Java. Desta forma, a interface que esta camada oferece para as aplicações-m são baseadas em componentes, frameworks e classes de objetos, que são abstrações de software de mais alto nível comparado com protocolos cliente/servidor e passagem de mensagens. 9.6 Exercícios 1. O que caracteriza uma aplicação embarcada? 2. Cite uma aplicação prática para cada uma das arquiteturas de aplicação apresentadas neste capítulo. 3. É possível implementar uma arquitetura multitarefa cooperativa sobre um sistema operacional com núcleo preemptivo? Como? 4. Suponha uma aplicação de controle de um braço robótico industrial empregando a arquitetura da gura 9.2. Cite algumas funções desempenhadas por cada camada. 5. Para a questão acima, que processadores seriam empregados em cada camada e como se daria a comunicação entre as camadas? 5 http://curl.haxx.se/libcurl 153 Referências Bibliográcas [1] Andrew S. Tanenbaum, Sistemas Operacionais Modernos, 3a Edição, Pearson Education do Brasil, 2010. [2] Abraham Silberschatz, Peter Galvin, Greg Gagne, Operating System Concepts, 8th Edition, John Wiley & Sons, 2009. [3] Uresh Vahalia, Unix Internals - The New Frontiers, Prentice Hall, 1996 [4] http://en.wikipedia.org/wiki/Operating_system, [5] Meier, R. Professional Android Application 2012. Development, Wiley Publishing, Inc., 2009. [6] Gerald J. Popek e Robert P. Goldberg, Formal Requirements for Virtualizable Third Generation Architectures. Communications of the ACM 17 (7), 1974. [7] Gary Bradski e Adrian Kaehler, Learning OpenCV, O'Reilly, 2008. [8] Antonio C. Faleiros, Criptograa, disponível em google.com/site/professorfaleiros/cripto, https://sites. 2011 [9] Roland Sigwart e Ilah Nourbakhsh, Intoduction to autonomous mobile robotics, MIT Press, 2004. 154 Índice Remissivo Acesso direto à memória, 108 Control groups (cgroups), 127 Acionadores de dispositivos, 105 Copy-on-write, 23, 64 No Linux, 106 Diretório de páginas, 62 Tipos, 106 Diretórios no Linux, 87 Algoritmo Buddy, 73 Dispositivos de armazenamento, 85 Aplicações embarcadas, 7 Arquitetura em camadas, 150 Controladas por interrupção, 147 Escalonamento de processos, 29 Múltiplas Filas, 30 Laço único, 147 Por prioridades, 32 Multitarefas cooperativas, 148 Gerenciamento de memória Multitarefas preemptivas, 149 Arduino, 147 Arquitetura x86, 62 Arquivos Em sistemas de tempo real, 78 Backups, 99 Memória virtual, 61 Chamada mmap, 93 No Linux, 72 Chamada mount, 94 Partiços xas, 59 E sistemas de tempo real, 99 Permurta (swapping), 59 Inode, 87 Interrupção, 8 Múltiplos sistemas, 93 Inversão de priroidades, 41 Mapeados em memória, 92 No Linux, 86 Journaling, 95 Vericação de consistência, 97 Arquivos mapeados em memória, 92 KVM, 126 Cache de buers, 89 Localidade espacial, 70 Cache de páginas, 91 Localidade temporal, 70 Chamadas de sistema, 12 LXC, 127 Comunicação interprocesso, 36 Comunicação interthreads, 51 Condição de corrida, 38 Máquina virtual, 112 Módulos, 103 No Linux, 104 155 Memória virtual, 18 Sistema de arquivos em memória, 100 MMU, 66 Sistema Operacional MMV, 113 Android, 14 Modos do processador, 9 Componentes, 10 Denição, 6 Paginação Sistemas embarcados, 7 Algoritmo LRU, 77 Chamadas mlock/munlock, 78 Tabela de páginas, 19, 62, 74 Denição, 69 Tabela de páginas estendida, 115 No Linux, 74 Tabela de páginas multiníveis, 62 Princípio da localidade, 69 Tabela de processo, 42 Processo paginador, 76 Thread Paravirtualização, 124 Denição, 48 Preempção, 13 Em sistemas de tempo real, 55 Processo Escalonamento, 55 Chamadas fork/vfork, 22 Escalonamento por prioridades, 56 Criação, 22 Exemplo de código, 50 Denição, 17 Gerenciamento, 48 Em sistemas de tempo real, 31 Motivações, 47 Estados, 28 No espaço do núcleo, 49 Estrutura em memória, 18 No espaço do usuário, 48 No Linux, 22 Utilização, 50 Sinalização, 32 Time sharing, 27 Troca de contexto, 26 TinyOS, 148 TLB, 67 RAID, 96 Trap, 9 Rapian, 149 Reentrância, 13 Variáveis de condição Região crítica, 38 Denição, 52 RSSF, 148 Exemplo de código, 54 VirtualBox, 126 Segurança Certicados e assinaturas digitais, Virtualização 135 Da CPU, 117 Da entrada/saída, 119 Criptograa, 133 Da memória, 115 do sistema operacional, 138 Na arquitetura x86, 114 Tipos de ataques, 132 Requisitos, 113 Semáforos, 39 Soluções disponíveis, 126 Sincronização interprocesso, 38 156 Tradução binária, 114 Tratamento de interrupções, 118 Virtualização assistida pelo hardware, 123 Virtualização plena, 122 Virtualização suportada pelo SO, 125 Xen, 127 157