Introdução aos Sistemas Operacionais

Propaganda
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
Download