Buscar

Linux

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 22 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 22 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 22 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

Processos no Linux 
Nas seções anteriores, começamos examinando o Linux como visto do teclado, isto é, o que o usuário vê em uma janela xterm. Demos exemplos de comandos shell e programas utilitários que são usados frequentemente. Terminamos com uma breve visão geral da estrutura do sistema. Agora chegou o momento de nos aprofundarmos no núcleo e olhar mais de perto os conceitos básicos a que o Linux dá suporte, a saber, processos, memória, o sistema de arquivos e entrada/saída. Essas noções são importantes porque as chamadas de sistema — a interface do próprio sistema operacional — as manipulam. Por exemplo, chamadas de sistema existem para criar processos e threads, alocar memória, abrir arquivos e realizar E/S. Infelizmente, com tantas versões do Linux, há algumas diferenças entre elas. Neste capítulo, enfatizaremos as características comuns a todas elas em vez de nos concentrarmos em qualquer versão específica. Assim, em determinadas seções (especialmente as de implementação), a discussão pode não se aplicar igualmente a todas as versões. 10.3.1 Conceitos fundamentais As principais entidades ativas em um sistema Linux são os processos. Os processos Linux são muito similares aos processos sequenciais clássicos que estudamos no Capítulo 2. Cada processo executa um único programa e inicialmente tem um único thread de controle. Em outras palavras, ele tem um contador de programa, que controla a próxima instrução a ser executada. O Linux permite que um processo crie threads adicionais uma vez inicializado. O Linux é um sistema multiprogramado, de maneira que múltiplos processos interdependentes podem estar executando ao mesmo tempo. Além disso, cada usuário pode ter vários processos ativos ao mesmo tempo, de maneira que em um grande sistema pode haver centenas ou mesmo milhares de processos executando. Na realidade, na maioria das estações de trabalho de usuário único, mesmo quando o usuário está ausente, dúzias de processos de segundo plano, chamados daemons, estão executando. Esses processos são iniciados por um script de shell quando o sistema é inicializado. (“Daemon” é uma variação ortográfica de “demon”, um espírito do mal que age por conta própria.) Um daemon típico é o cron daemon. Ele desperta uma vez por minuto para conferir se há algum trabalho para fazer. Se houver, ele realiza o trabalho. Então TANENBAUM_BOOK.indb 506 20/01/16 14:30 SISTEMAS OPERACIONAIS MODERNOS Capítulo 10 Estudo de caso 1: Unix, Linux e Android 507 ele volta a dormir até chegar o momento da próxima verificação. Esse daemon é necessário porque no Linux é possível agendar atividades a serem realizadas minutos, horas, dias ou mesmo meses depois. Por exemplo, suponha que um usuário tenha uma consulta no dentista às 15 h da próxima terça-feira. Ele pode fazer uma entrada no banco de dados do cron daemon dizendo para o daemon acionar um alarme às 14h30min. Quando o dia e a hora agendados chegam, o cron daemon vê que tem trabalho a fazer e inicia o programa de alarme como um novo processo. O cron daemon também é usado para iniciar atividades periódicas, como fazer backups de disco diários às 4h00, ou lembrar a usuários esquecidos todos os anos em 31 de outubro para fazer um estoque de balas e bombons para o Halloween. Outros daemons cuidam do correio eletrônico que chega e sai, gerenciam a fila na impressora, conferem se há páginas livres suficientes na memória e assim por diante. Daemons são diretos para implementar no Linux, pois cada um é um processo separado, independente de todos os outros processos. Processos são criados no Linux de uma maneira especialmente simples. A chamada de sistema fork cria uma cópia exata do processo original. O processo criador é chamado de processo pai. O novo processo é chamado de processo filho. Cada um tem suas próprias imagens de memória privadas. Se o pai subsequentemente mudar qualquer uma de suas variáveis, as mudanças não serão visíveis para o filho e vice-versa. Arquivos abertos são compartilhados entre pai e filho. Isto é, se um determinado arquivo foi aberto no processo pai antes de fork, ele continuará aberto tanto no pai quanto no filho depois. Mudanças feitas no arquivo por qualquer um serão visíveis para o outro. Esse comportamento é apenas razoável, pois essas mudanças também são visíveis para qualquer processo não relacionado que abrir o arquivo. O fato de as imagens de memória, variáveis, registradores e tudo mais serem idênticos no processo pai e no filho leva a uma pequena dificuldade: como os processos sabem quem deve execuutar o código pai e quem deve executar o código filho? O segredo é que a chamada de sistema fork retorna um 0 para o filho e um valor diferente de zero, o PID (Process Identifier — Identificador de processo) do filho, para o pai. Ambos os processos em geral conferem o valor de retorno e agem conforme mostrado na Figura 10.4. Processos são chamados por seus PIDs. Quando um processo é criado, o pai recebe o PID do filho, como já mencionado. Se o filho quiser saber seu próprio PID, há uma chamada de sistema, getpid, que o fornece. PIDs são usados de uma série de maneiras. Por exemplo, quando um filho termina, o pai recebe o PID desse processo-filho. Isso pode ser importante, porque um pai pode ter muitos filhos. Como filhos também podem ter filhos, um processo original pode construir uma árvore inteira de filhos, netos e mais descendentes. Processos no Linux podem comunicar-se uns com os outros usando uma forma de troca de mensagens. É possível criar um canal entre dois processos no qual um processo pode escrever um fluxo de bytes para o outro ler. Esses canais são chamados pipes. A sincronização é possível porque, quando um processo tenta ler a partir de um pipe vazio, ele é bloqueado até que os dados estejam disponíveis. Os pipelines do shell são implementados com pipes. Quando o shell vê uma linha como
Sort<f I head
ele cria dois processos, sort e head, e estabelece um pipe entre eles de tal maneira que a saída padrão de sort esteja conectada com a entrada padrão de head. Desse modo, todos os dados que sort escreve vão diretamente para head, em vez de ir para um arquivo. Se o pipe encher, o sistema para de executar sort até que head tenha removido alguns dados dele. Processos também podem comunicar-se de outra maneira além dos pipes: interrupções de software. Um processo pode enviar o que é chamado de um sinal para outro processo. Processos podem dizer ao sistema o que eles querem que aconteça quando um sinal for recebido.
As escolhas disponíveis são ignorá-lo, pegá-lo ou deixar que o sinal elimine o processo. Terminar o processo é o padrão para a maioria dos sinais. Se um processo elege pegar sinais enviados para si, ele deve especificar uma rotina de tratamento de sinais. Quando um sinal chega, o controle vai passar abruptamente para o tratador. Quando o tratador tiver terminado e retornar, o controle volta para seu lugar de origem, análogo às interrupções de E/S de hardware. Um processo pode enviar sinais apenas para membros do seu grupo de processos, que consiste em seu pai (e ancestrais mais distantes), irmãos e filhos (e descendentes mais distantes). Um processo pode também enviar um sinal para todos os membros do seu grupo de processos com uma única chamada de sistema. Sinais também são usados para outros fins. Por exemplo, se um processo está realizando aritmética de ponto flutuante e inadvertidamente faz uma divisão por 0 (algo que não agrada nem um pouco aos matemáticos), resulta em um sinal SIGFPE (exceção de ponto flutuante). Alguns dos sinais que são exigidos pelo POSIX estão listados na Figura 10.5. Muitos sistemas Linux têm sinais adicionais também, mas os programas que os utilizam podem não ser portáteis para outras versões do Linux e UNIX em geral.
Gerenciamento de memória no Linux 
O modelo de memória do Linux é direto, a fim de permitir a portabilidade dos programas e tornar possível implementar o Linux em máquinas com unidades de gerenciamento de memória amplamente diferentes, desde as mais simples (por exemplo, o PC originalda IBM) a hardwares de paginação sofisticados. Essa é uma área de projeto que mudou muito pouco em décadas. Ele funcionou bem, então não precisou de muita revisão. Examinaremos agora o modelo e como ele foi implementado. 10.4.1 Conceitos fundamentais Todo processo do Linux tem um espaço de endereçamento que logicamente consiste em três segmentos: texto, dados e pilha. Um exemplo de espaço de endereçamento de processo está ilustrado na Figura 10.12(a) como processo A. O segmento de texto contém as instruções de máquina que formam o código executável do programa. Ele é produzido pelo compilador e montador traduzindo o C, C++, ou outro programa em código de máquina. O segmento de texto em geral é somente de leitura. Programas que se automodificavam deixaram de interessar em 1950 mais ou menos, pois eles eram muito difíceis de compreender e depurar. Assim, o segmento de texto não cresce, encolhe ou muda de qualquer outra maneira. O segmento de dados contém armazenamento para todas as variáveis, cadeias de caracteres e vetores do programa, assim outros dados. Ele tem duas partes, os dados inicializados e os não inicializados. Por razões históricas, os últimos são conhecidos como BSS (historicamente chamado Block Started by Symbol). A parte inicializada do segmento de dados contém variáveis e constantes do compilador que precisam de um valor inicial quando o programa é inicializado. Todas as variáveis na parte BSS são inicializadas para zero após o carregamento. Por exemplo, em C é possível declarar uma cadeia de caracteres e inicializá-la ao mesmo tempo. Quando o programa é inicializado, ele espera que a cadeia tenha o seu valor inicial. Para implementar essa construção, o compilador designa à cadeia um local no espaço de endereçamento e assegura que, quando o programa é inicializado, esse local contenha a cadeia de caracteres adequada. Do ponto de vista do sistema operacional, dados inicializados não são tão diferentes do texto do programa — ambos contêm padrões de bits produzidos pelo compilador que precisam ser carregados na memória quando o programa inicializa. A existência dos dados não inicializados é na realidade apenas uma otimização. Quando uma variável global não é explicitamente inicializada, a semântica da linguagem C diz que o seu valor inicial é 0. Na prática, a maioria das variáveis globais não é inicializada explicitamente e são, portanto, 0. Isso poderia ser implementado simplesmente tendo uma seção do arquivo binário executável idêntica ao número de bytes de dados, e inicializando todos eles, incluindo aqueles com o valor padrão definido como 0. No entanto, a fim de poupar espaço no arquivo executável, isso não é feito. Em vez disso, o arquivo contém todas as variáveis inicializadas explicitamente seguindo o texto de programa. As variáveis não inicializadas são reunidas após as inicializadas, de maneira que tudo o que o compilador precisa fazer é colocar uma palavra no cabeçalho dizendo quantos bytes alocar. Para deixar esse ponto mais claro, considere a Figura 10.12(a) novamente. Aqui o texto de programa tem 8 KB e os dados inicializados também 8 KB. Os dados não inicializados (BSS) têm 4 KB. O arquivo executável tem apenas 16 KB (texto + dados inicializados), mais um cabeçalho curto que diz ao sistema para alocar outros 4 KB após os dados inicializados e zerá-los antes de iniciar o programa. Esse truque evita armazenar 4 KB de zeros no arquivo executável. A fim de evitar alocar uma estrutura de página física cheia de zeros, durante a inicialização, o Linux aloca uma página zero estática, uma página protegida de escrita cheia de zeros. Quando um processo é carregado, a sua região de dados não inicializada é configurada para apontar para a página zero. Sempre que um processo realmente tenta escrever nessa área, o mecanismo copiar- -na-escrita (copy-on-write) é acionado, e uma estrutura de página real é alocada para o processo.
Ao contrário do segmento de texto, que não pode mudar, o segmento de dados pode. Programas modificam suas variáveis o tempo inteiro. Além disso, muitos programas precisam alocar o espaço dinamicamente durante a execução. O Linux lida com isso permitindo que o segmento de dados cresça e encolha à medida que a memória é alocada e liberada. Uma chamada de sistema, brk, está disponível para permitir que um programa estabeleça o tamanho do seu segmento de dados. Assim, para alocar mais memória, um programa pode aumentar o tamanho do seu segmento de dados. O procedimento de biblioteca C malloc, comumente usado para alocar memória, faz um uso intensivo dele. O descritor de espaço de endereçamento de processo contém informações sobre o alcance das áreas de memória alocadas dinamicamente no processo, chamada de heap. O terceiro segmento é o de pilha. Na maioria das máquinas, ele começa no próximo do topo do espaço de endereçamento virtual e cresce para baixo na direção de 0. Por exemplo, em plataformas de 32bits x86, a pilha começa no endereço 0xC0000000, que é o limite do endereçamento virtual de 3 GB visível ao processo no modo usuário. Se a pilha cresce abaixo da parte de baixo do segmento de pilha, uma falta de hardware ocorre e o sistema operacional baixa a parte de baixo do segmento de pilha por uma página. Programas não gerenciam explicitamente o tamanho do segmento de pilha. Quando um programa inicializa, a sua pilha não está vazia. Em vez disso, ela contém todas as variáveis (shell) do ambiente, assim como a linha de comando digitada para o shell para invocá-lo. Dessa maneira, um programa pode descobrir seus argumentos. Por exemplo, quando cp src dest é digitado, o programa cp é executado com a cadeia de caracteres “cp src dest” na pilha, de maneira que ele pode encontrar os nomes dos arquivos fonte e de destino. A cadeia de caracteres é representada como um vetor de ponteiros para os símbolos na cadeia, a fim de facilitar sua análise sintática. Quando dois usuários estão executando o mesmo programa, como o editor, seria possível, mas ineficiente, manter duas cópias do texto de programa do editor na memória ao mesmo tempo. Em vez disso, os sistemas Linux dão suporte a segmentos de texto compartilhados. Na Figura 10.12(a) e Figura 10.12(c), vemos dois processos, A e B, que têm o mesmo segmento de texto. Na Figura 10.12(b) vemos um layout possível de memória física, no qual ambos os processos compartilham o mesmo fragmento de texto. O mapeamento é feito pelo hardware de memória virtual.
Segmentos de dados e pilhas jamais são compartilhados exceto após um fork e, então, somente aquelas páginas que não são modificadas. Se qualquer um deles precisar crescer e não houver espaço adjacente para isso, não há problema, já que as páginas virtuais adjacentes não precisam ser mapeadas em páginas físicas adjacentes. Em alguns computadores, o hardware dá suporte a espaços de endereçamento separados para instruções e dados. Quando esta característica está disponível, o Linux pode usá-la. Por exemplo, em um computador com endereços de 32 bits, se essa característica estiver disponível, haverá 232 bits de espaço de endereçamento para instruções e 232 bits adicionais de espaço de endereçamento para os segmentos de dados e pilha compartilharem. Um salto (jump) ou ramificação para 0 vai para o endereço 0 do espaço de texto, enquanto mover o conteúdo de 0 usa o endereço 0 no espaço de dados. Essa característica dobra o espaço de endereçamento possível. Além de alocar dinamicamente mais memória, os processos no Linux podem acessar dados de arquivos através de arquivos mapeados na memória. Essa característica torna possível mapear um arquivo para uma porção do espaço de endereçamento do processo, de maneira que o arquivo pode ser lido e escrito como se ele fosse um vetor de bytes na memória. Mapear um arquivo torna o acesso aleatório para ele muito mais fácil do que usar chamadas de sistema de E/S como read e write. Bibliotecas compartilhadas são acessadas mapeando-as usando esse mecanismo. Na Figura 10.13, vemos um arquivo que está mapeado em dois processos ao mesmo tempo, em diferentesendereços virtuais. Uma vantagem adicional de mapear um arquivo é que dois ou mais processos podem mapear o mesmo arquivo ao mesmo tempo. Escritas para o arquivo por qualquer um deles são então instantaneamente visíveis para os outros. Na realidade, ao mapear um arquivo de rascunho (que será descartado após todos os processos saírem), esse mecanismo proporciona um caminho de alta largura de banda para múltiplos processos compartilharem memória. No caso mais extremo, dois (ou mais) processos poderiam mapear um arquivo que cobre todo o espaço de endereçamento, proporcionando uma forma de compartilhamento que é um meio caminho entre processos separados e threads. Aqui o espaço de endereçamento é compartilhado (como threads), mas cada processo mantém seus próprios arquivos abertos e sinais, por exemplo, diferentemente dos threads. Na prática, no entanto, nunca são criados dois espaços de endereçamento exatamente correspondentes.
Entrada/saída no Linux
 O sistema de E/S no Linux é relativamente simples e o mesmo que em outros UNICES. Basicamente, todos os dispositivos de E/S são feitos para parecer arquivos e são acessados como tais com as mesmas chamadas de sistema read e write usadas para acessar todos os arquivos comuns. Em alguns casos, parâmetros de dispositivos precisam ser configurados, e isso é feito com o uso de uma chamada de sistema especial. Estudaremos essas questões nas seções a seguir. 10.5.1 Conceitos fundamentais Assim como todos os computadores, aqueles executando com Linux têm dispositivos de E/S como discos, impressoras e redes conectados a eles. Alguma maneira é necessária para permitir que esses programas acessem esses dispositivos. Embora várias soluções sejam possíveis, a solução do Linux é integrar os dispositivos em um sistema de arquivos nos chamados arquivos especiais. Cada dispositivo de E/S é associado a um nome de caminho, normalmente em /dev. Por exemplo, um disco pode ser /dev/hd1, uma impressora pode ser /dev/lp, e a rede pode ser /dev/net. Esses arquivos especiais podem ser acessados da mesma maneira que quaisquer outros. Nenhum comando especial ou chamada de sistema é necessário. As chamadas de sistema usuais open, read e write funcionarão bem. Por exemplo, o comando cp file /dev/lp copia file para a impressora, fazendo que ele seja impresso (presumindo que o usuário tenha permissão para acessar /dev/lp). Programas podem abrir, ler e escrever arquivos especiais exatamente da mesma maneira que eles fazem com arquivos regulares. Na realidade, cp do exemplo não tem nem consciência de que está imprimindo. Dessa maneira, nenhum mecanismo especial é necessário para fazer E/S. Arquivos especiais são divididos em duas categorias, bloco e caractere. Um arquivo especial de bloco é aquele que consiste em uma sequência de blocos numerados. A propriedade fundamental do arquivo especial de bloco é que cada em pode ser individualmente endereçado e acessado. Em outras palavras, um programa pode abrir um arquivo especial de bloco e ler, digamos, o bloco 124 sem primeiro ter de ler os blocos 0 a 123. Arquivos de blocos especiais são tipicamente usados para discos. Arquivos especiais de caracteres são normalmente usados para dispositivos que realizam a entrada ou saída de um fluxo de caracteres. Teclados, impressoras, redes, mouses, plotters e a maioria dos outros dispositivos de E/S que aceitam ou produzem dados para as pessoas usam arquivos especiais de caracteres. Não é possível (ou mesmo significativo) buscar o bloco 124 em um mouse. Associado com cada arquivo especial há um driver do dispositivo que lida com o dispositivo correspondente. Cada driver tem o que é chamado de um número de dispositivo principal, que serve para identificá-lo. Se um driver suporta múltiplos dispositivos, digamos, dois discos do mesmo tipo, cada disco tem um número de dispositivo secundário que o identifica. Juntos, os números de dispositivos principal e secundário especificam unicamente cada dispositivo de E/S. Em alguns casos, um único driver lida com dois dispositivos relacionados de perto. Por exemplo, o driver correspondendo aos controles /dev/tty controlam tanto o teclado quanto a tela, muitas vezes pensados como um único dispositivo, o terminal. Embora a maioria dos arquivos especiais não possa ser acessada aleatoriamente, eles muitas vezes precisam ser controlados de uma maneira que os arquivos especiais de blocos não podem. Considere, por exemplo, uma entrada digitada no teclado e exibida na tela. Quando um usuário comete um erro de digitação e quer apagar o último caractere digitado, ele pressiona alguma tecla. Algumas pessoas preferem usar a tecla de backspace, e outras a DEL. Similarmente, para apagar a linha inteira recém-digitada, existem muitas convenções. Tradicionalmente @ era usado, mas com a disseminação do e-mail (que usa @ dentro do endereço de e-mail), muitos sistemas adotaram CTRL-U ou algum outro caractere. Da mesma maneira, a fim de interromper o programa em execução, alguma tecla especial precisa ser pressionada. Aqui, também, pessoas diferentes têm preferências diferentes. CTRL-C é uma escolha comum, mas não é universal. Em vez de fazer uma escolha e forçar a todos usá- -la, o Linux permite que todas essas funções especiais e muitas outras sejam customizadas pelo usuário. Uma chamada de sistema especial geralmente é fornecida para configurar essas opções. A chamada de sistema também lida com a expansão da tecla tab, habilitação e desabilitação do eco de caracteres, conversão entre retorno de carro e avanço de linha, e itens similares. A chamada de sistema não é permitida em arquivos regulares ou arquivos especiais de bloco.
O sistema de arquivos Linux 
A parte mais visível de qualquer sistema operacional, incluindo Linux, é o sistema de arquivos. Nas seções a seguir, examinaremos as ideias básicas por trás do sistema de arquivos Linux, as chamadas de sistema, e como o sistema de arquivos é implementado. Algumas dessas ideias são derivadas do MULTICS, e muitas delas foram copiadas pelo MS-DOS, Windows e outros sistemas, mas outras são únicas para sistemas baseados no UNIX. O projeto do Linux é especialmente interessante porque ele claramente ilustra o princípio de O pequeno é belo. Com um mecanismo mínimo e um número muito limitado de chamadas de sistema, o Linux mesmo assim proporciona um sistema de arquivos elegante e poderoso. 10.6.1 Conceitos fundamentais O sistema de arquivos Linux inicial foi o sistema de arquivos do MINIX 1. No entanto, como ele limitava os nomes de arquivos a 14 caracteres (a fim de ser compatível com a Versão 7 do UNIX) e seu tamanho de arquivos máximo era 64 MB (o que era um exagero nos discos rígidos de 10 MB da sua época), havia um interesse em melhores sistemas de arquivos desde o início do desenvolvimento do Linux, que começou aproximadamente 5 anos após o lançamento do MINIX 1. A primeira melhoria foi o sistema de arquivos ext, que permitiu nomes de arquivos de 255 caracteres e arquivos de 2 GB, mas era mais lento que o sistema de arquivos MINIX 1, de maneira que a busca continuou por um tempo. Finalmente, o sistema de arquivos ext2 foi inventado, com nomes longos de arquivos, arquivos longos e melhor desempenho, e ele tornou-se o principal sistema de arquivos. No entanto, o Linux suporta várias dúzias de sistemas de arquivos usando a camada do Virtual File System (VFS — Sistema de arquivos virtual), descrito na próxima seção. Quando o Linux é ligado, uma escolha de quais sistemas de arquivos devem ser compilados no núcleo é oferecida. Outros podem ser carregados dinamicamente como módulos durante a execução, se necessário. Um arquivo Linux é uma sequência de 0 ou mais bytes contendo informações arbitrárias. Nenhuma
distinção é feita entre os arquivos ASCII, arquivos binários, ou qualquer outro tipo de arquivos. O significado dos bits em um arquivo fica a cargo inteiramente do proprietário do arquivo. O sistema não se importa com isso. Nomes de arquivos são limitados a 255 caracteres e todos os caracteres ASCII exceto NUL são permitidos nosnomes dos arquivos, então um nome de arquivo consistindo de três retornos de carros é um nome de arquivo legal (mas não especialmente conveniente). Por convenção, muitos programas esperam que os nomes de arquivos consistam de um nome base e uma extensão, separados por um ponto (que conta como um caractere). Desse modo, prog.c é tipicamente um programa C, prog.py é tipicamente um programa Python e prog.o é normalmente um arquivo objeto (saída de compilador). Essas convenções não são exigidas pelo sistema operacional, mas alguns compiladores e outros programas as esperam. As extensões podem ser de qualquer comprimento, e os arquivos podem ter múltiplas extensões, como em prog.java.gz, que é provavelmente um programa Java comprimido gzip. Arquivos podem ser agrupados em diretórios por conveniência. Diretórios são armazenados como arquivos e até certo ponto tratados como tal. Diretórios podem conter subdiretórios, levando a um sistema de arquivos hierárquico. O diretório raiz é chamado / e sempre contém diversos subdiretórios. O caractere / também é usado para separar nomes de diretórios, de maneira que o nome /usr/ast/x denota o arquivo x localizado no diretório ast, que em si está no diretório /usr. Alguns dos principais diretórios próximos do topo das árvores são mostrados na Figura 10.23. Há duas maneiras de se especificar nomes de arquivos no Linux, tanto para o shell e quando abrindo um arquivo de dentro de um programa. A primeira maneira é através de um caminho absoluto, que significa dizer como chegar ao arquivo começando no diretório raiz. Um exemplo de um caminho absoluto é /usr/ast/ books/mos4/chap-10. Isso diz para o sistema procurar
no diretório raiz por um diretório chamado usr, então procurar por outro diretório, ast. Por sua vez, esse diretório contém um diretório books, que contém o diretório mos4, que contém o arquivo chap-10. Nomes de caminhos absolutos são muitas vezes longos e inconvenientes. Por essa razão, o Linux permite aos usuários e processos que designem o diretório no qual eles estão trabalhando atualmente como o diretório de trabalho. Nomes de caminhos podem ser especificados em relação ao diretório de trabalho. Um nome de caminho especificado em relação ao diretório de trabalho é um caminho relativo. Por exemplo, se /usr/ast/ books/mos4 é o diretório de trabalho, então o comando shell cp chap-10 backup-10 tem exatamente o mesmo efeito que o comando mais longo. cp /usr/ast/books/mos4/chap-10 /usr/ast/books/ mos4/backup-10 Ocorre frequentemente que um usuário precise referir-se a um arquivo que pertence a outro usuário, ou pelo menos está localizado em outra parte na árvore de arquivos. Por exemplo, se dois usuários estão compartilhando um arquivo, ele estará localizado em um diretório pertencente a um deles, de maneira que o outro terá de usar um nome de caminho absoluto para referir- -se a ele (ou mudar o diretório de trabalho). Se isso for longo o suficiente, pode tornar-se irritante ter de seguir digitando-o. O Linux fornece uma solução ao permitir que os usuários façam uma nova entrada de diretório que aponta para um arquivo existente. Essa entrada é chamada de ligação (link). Como um exemplo, considere a situação da Figura 10.24(a). Fred e Lisa estão trabalhando juntos em um projeto, e cada um deles precisa acessar os arquivos do outro. Se Fred tem /usr/fred como seu diretório de trabalho, ele pode referir-se ao arquivo x no diretório de Lisa como /usr/lisa/x. Alternativamente, Fred pode criar uma nova entrada no seu diretório, como mostrado na Figura 10.24(b), após a qual ele pode usar x para significar /usr/lisa/x. No exemplo discutido há pouco, sugerimos que antes de realizar a ligação, a única maneira para Fred referir-se ao arquivo x de Lisa era usando o seu caminho absoluto. Na realidade, isso não é de fato verdadeiro. Quando um diretório é criado, duas entradas, . e .., são automaticamente feitas nele. A primeira refere-se ao diretório de trabalho em si. A segunda refere-se ao pai do diretório, isto é, o diretório no qual ele mesmo está listado. Desse modo, a partir de /usr/fred, outro caminho para o arquivo x de Lisa é ../lisa/x. Além dos arquivos regulares, o Linux também suporta arquivos especiais de caracteres e arquivos especiais de blocos. Arquivos especiais de caracteres são usados para modelar dispositivos de E/S seriais, como teclados e impressoras. A abertura e leitura de /dev/tty lê a partir do teclado; a abertura e leitura de /dev/lp escreve para a impressora. Arquivos especiais em bloco, muitas vezes com nomes como /dev/hd1, podem ser usados para ler e escrever partições de discos brutos sem levar em consideração o sistema de arquivos. Desse modo, uma busca para o byte k seguido por uma leitura começará lendo do k-ésimo byte na partição correspondente, ignorando completamente o i-nodo e estrutura de arquivos. Dispositivos em bloco brutos são usados para paginação e troca por programas que criam sistemas de arquivos (por exemplo, mkfs) e por programas que consertam sistemas de arquivos doentes (como fsck), por exemplo. Muitos computadores têm dois ou mais discos. Em computadores de grande porte em bancos, por exemplo, frequentemente é necessário ter 100 ou mais discos em uma única máquina, a fim de conter os enormes bancos de dados necessários. Mesmo computadores pessoais muitas vezes têm pelo menos dois discos — um disco rígido e uma unidade ótica (por exemplo, DVD). Quando há múltiplas unidades de disco, surge a questão de como tratá-las. Uma solução é colocar um sistema de arquivos autocontido em cada uma e apenas mantê-las separadas. Considere, por exemplo, a situação mostrada na Figura 10.25(a). Aqui temos um disco rígido, que chamaremos de C:, e um DVD, que chamaremos de D:. Cada um tem seu próprio diretório raiz e arquivos. Com essa solução, o usuário tem de especificar tanto o dispositivo quanto o arquivo quando qualquer outra coisa além do padrão for necessária. Por exemplo, para copiar um arquivo x para um diretório d (presumindo que C: seja o padrão), você digitaria cp D:/x /a/d/x Essa é a abordagem adotada por uma série de sistemas, incluindo Windows 8, que ele herdou do MS-DOS muito tempo atrás. A solução Linux é permitir que um disco seja montado sobre a árvore de arquivos de outro disco. Em nosso exemplo, poderíamos montar o DVD no diretório /b, resultando no sistema de arquivos da Figura 10.25(b). O usuário agora vê uma única árvore de arquivos, e não precisa mais estar ciente de qual arquivo reside em qual dispositivo. O comando de cópia acima agora torna-se cp /b/x /a/d/x exatamente o mesmo que ele seria se tudo estivesse no disco rígido em primeiro lugar. Outra propriedade interessante do sistema de arquivos Linux é o travamento (locking). Em algumas aplicações, dois ou mais processos podem estar usando o mesmo arquivo ao mesmo tempo, o que pode levar a condições de corrida. Uma solução é programar a aplicação com regiões críticas. No entanto, se os processos pertencem a usuários independentes que nem conhecem uns aos outros, esse tipo de coordenação geralmente é inconveniente. Considere, por exemplo, um banco de dados consistindo em muitos arquivos em um ou mais diretórios que são acessados por usuários não relacionados. Decerto, é possível associar um semáforo a cada diretório ou arquivo e conseguir a exclusão mútua fazendo que os processos realizem uma operação down no semáforo apropriado antes de acessar os dados. A desvantagem, no entanto, é que um diretório inteiro ou arquivo torna- -se então inacessível, mesmo que apenas um registro seja necessário. Por essa razão, POSIX fornece um mecanismo flexível e de granularidade fina para os processos travarem tão pouco quanto um único byte e tanto quanto um arquivo inteiro em uma operação indivisível. O mecanismo de travamento exige que o chamador especifique o arquivo a ser travado, o byte iniciador e o número de bytes. Se a operação for bem-sucedida, o sistema faz uma entrada de tabela observando que os bytes em questão (por exemplo, um registro de banco de dados) estão travados.Dois tipos de travas são fornecidos: travas compartilhadas e travas exclusivas. Se uma porção de um arquivo já contém uma trava compartilhada, uma segunda tentativa para colocar uma trava compartilhada nele é permitida, mas uma tentativa de colocar uma trava exclusiva fracassará. Se uma porção de um arquivo contém uma trava exclusiva, todas as tentativas de travar qualquer parte daquela porção fracassarão até que a trava tenha sido liberada. A fim de colocar com sucesso uma trava, cada byte na região a ser travada tem de estar disponível. Quando coloca uma trava, um processo precisa especificar se ele quer ser bloqueado ou não caso a trava não possa ser colocada. Se ele escolher ser bloqueado, quando a trava existente tiver sido removida, o processo é desbloqueado e a trava é colocada. Se o processo escolher não ser bloqueado quando ele não puder colocar uma trava, a chamada de sistema retorna imediatamente, com o código de status dizendo se a trava foi bem-sucedida ou não. Se ela não foi bem-sucedida, o chamador tem de decidir o que fazer em seguida (por exemplo, esperar e tentar de novo).
Regiões travadas podem sobrepor-se. Na Figura 10.26(a) vemos que o processo A colocou uma trava compartilhada nos bytes 4 até o 7 de algum arquivo. Mais tarde, o processo B coloca uma trava compartilhada nos bytes 6 até o 9, como mostrado na Figura 10.26(b). Por fim, o C trava os bytes 2 até o 11. Enquanto todas essas travas forem compartilhadas, elas podem coexistir. Agora considere o que acontece se um processo tenta adquirir uma trava exclusiva para o byte 9 do arquivo da Figura 10.26(c), com uma solicitação para ser bloqueado se a trava falhar. Tendo em vista que duas travas anteriores cobrem esse bloco, o chamador será bloqueado e permanecerá assim até que ambos, B e C, liberem suas travas.
Threads no Linux 
Discutimos threads de maneira geral no Capítulo 2. Aqui focaremos nos threads de núcleo no Linux, particularmente nas diferenças entre o modelo de thread do Linux e outros sistemas UNIX. A fim de compreender melhor as capacidades únicas fornecidas pelo modelo Linux, começamos com uma discussão de algumas decisões desafiadoras presentes em sistemas com múltiplos threads. A principal questão ao se introduzir threads é manter a semântica UNIX tradicional correta. Primeiro considere fork. Suponha que um processo com múltiplos threads (de núcleo) realiza uma chamada de sistema fork. Todos os outros threads devem ser criados no novo processo? Por ora, vamos responder a essa questão com um sim. Suponha que um desses threads tenha sido bloqueado lendo a partir do teclado. O thread correspondente no novo processo também deve ser bloqueado lendo do teclado? Se a resposta for sim, qual deles tem a próxima linha digitada? Se não, o que esse thread deveria estar fazendo no novo processo? O mesmo problema se mantém para muitas outras coisas que os threads podem fazer. Em um processo com um único thread, o problema não surge, pois o único thread não pode ser bloqueado quando chamando fork. Agora considere o caso em que os outros threads não são criados no processo filho. Suponha que um dos threads não criados seja detentor de um mutex que o único thread do novo processo tenta obter após a chamada a fork. O mutex jamais será liberado e o único thread ficará pendurado para sempre. Há inúmeros outros problemas também. Não há solução simples. E/S de arquivos é outra área que apresenta problemas. Suponha que um thread esteja bloqueado lendo de um arquivo e outro thread fecha o arquivo ou faz uma Iseek para mudar o ponteiro de arquivo atual. O que acontece em seguida? Quem sabe? O tratamento de sinais é outra questão complicada. Os sinais devem ser direcionados a um thread específico ou apenas ao processo? Um SIGFPE (exceção de ponto flutuante) deve provavelmente ser pego pelo thread que o causou. E se ele não o pegar? Só esse thread deve ser eliminado, ou todos os threads? Agora considere o sinal SIGINT, gerado pelo usuário no teclado. Qual thread deve capturar isso? Todos os threads devem compartilhar um conjunto comum de máscaras de sinais? Todas as soluções para esses e outros problemas normalmente fazem que algo quebre em alguma parte.
Acertar a semântica dos threads (sem mencionar o código) é algo sério. O Linux dá suporte a threads de núcleo de uma maneira interessante que vale a pena ser analisada. A implementação é baseada em ideias do 4.4BSD, mas threads de núcleo não foram capacitados naquela distribuição porque Berkeley ficou sem dinheiro antes que a biblioteca C pudesse ser reescrita para solucionar os problemas que acabamos de discutir. Historicamente, processos eram contêineres de recursos e threads eram as unidades de execução. Um processo continha um ou mais threads que compartilhavam o espaço de endereçamento, arquivos abertos, tratadores de sinais, alarmes e tudo mais. Tudo estava claro e simples como descrito. Em 2000, o Linux introduziu uma nova chamada de sistema poderosa, clone, que dificultou a distinção entre processos e threads e possivelmente chegou a inverter a primazia dos dois conceitos. Clone não está presente em nenhuma outra versão de UNIX. Classicamente, quando um novo thread era criado, o(s) thread(s) original(ais) e o novo compartilhavam tudo, exceto seus registradores. Em particular, descritores de arquivos para arquivos abertos, tratadores de sinais, alarmes e outras propriedades globais eram por processo, não por thread. O que a chamada clone fez foi tornar possível para cada um desses aspectos estar relacionado a um processo específico ou thread específico. Ela é chamada como a seguir: pid = clone(function, stack_ptr, sharing_flags, arg); A chamada cria um novo thread, seja no processo atual ou em um novo, dependendo do sharing_flags. Se o novo thread está no processo atual, ele compartilha o espaço de endereçamento com os threads existentes, e toda escrita subsequente para qualquer byte no espaço de endereçamento por qualquer thread é imediatamente visível para todos os outros threads no processo. Por outro lado, se o espaço de endereçamento não for compartilhado,
então o novo thread recebe uma cópia exata do espaço de endereçamento, mas escritas subsequentes pelo novo thread não são visíveis para os antigos. Essas semânticas são as mesmas de fork do POSIX. Em ambos os casos, o novo thread começa executando em function, que é chamado com arg como seu único parâmetro. Também em ambos os casos, o novo thread obtém sua própria pilha privada, com o ponteiro da pilha inicializado para stack_ptr. O parâmetro sharing_flags é um mapa de bits que permite uma granularidade mais fina de compartilhamento do que os sistemas UNIX tradicionais. Cada um dos bits pode ser configurado independentemente dos outros, e cada um deles determina se o novo thread copia alguma estrutura de dados ou a compartilha com o thread chamador. A Figura 10.9 mostra alguns dos itens que podem ser compartilhados ou copiados de acordo com os bits em sharing_flags. O bit CLONE_VM determina se a memória virtual (isto é, espaço de endereçamento) é compartilhada pelos antigos threads ou copiada. Se o bit for marcado, o thread novo simplesmente é inserido com os threads existentes, de maneira que a chamada clone efetivamente cria um thread novo em um processo existente. Se o bit for limpo, o thread novo recebe seu próprio espaço de endereçamento privado. Ter o seu próprio espaço de endereçamento significa que o efeito das suas instruções STORE não é visível para os threads existentes. Esse comportamento é similar a fork, exceto como observado a seguir. Criar um novo espaço de endereçamento é efetivamente a definição de um novo processo. O bit CLONE_FS controla o compartilhamento dos diretórios raiz e de trabalho, assim como da flag umask. Mesmo que o novo thread tenha seu próprio espaço de endereçamento, se esse bit for marcado, os threads novos e antigos compartilham os diretórios de trabalho. Isso significa que uma chamada para chdir por um thread muda o diretório de trabalho do outro thread, mesmo que este outro possater seu próprio espaço de endereçamento. Em UNIX, uma chamada para chdir por um thread sempre muda o diretório de trabalho para outros threads no seu processo, mas nunca para threads em outro processo. Desse modo, esse bit capacita um tipo de compartilhamento que não é possível em versões do UNIX tradicionais. O bit CLONE_FILES é análogo ao bit CLONE_FS. Se marcado, o novo thread compartilha seus descritores de arquivos com os antigos, então chamadas para Iseek por um thread são visíveis para os outros, novamente como em geral funciona para threads dentro do mesmo processo, mas não para threads em processos diferentes. De modo similar, CLONE_SIGHAND habilita ou desabilita o compartilhamento da tabela de tratadores de sinais entre os threads novos e antigos. Se a tabela for compartilhada, mesmo entre threads em diferentes espaços de endereçamento, então mudar um tratador em um thread afeta os tratadores em outros. Por fim, cada processo tem um pai. O bit CLONE_PARENT controla quem é o pai do novo thread. Ele pode ser o mesmo que o thread chamador (caso em que o novo thread é um irmão do chamador) ou pode ser o próprio chamador. Há alguns outros bits que controlam outros itens, mas eles são menos importantes. Esse compartilhamento de granularidade fina é possível porque o Linux mantém estruturas de dados separadas para os vários itens listados na Seção 10.3.3 (parâmetros de escalonamento, imagem de memória e assim por diante). A estrutura de tarefa apenas aponta para essas estruturas de dados, de maneira que é fácil fazer uma nova estrutura de tarefas para cada thread clonado e fazê-la apontar para as estruturas de dados de escalonamento, memória e outras do antigo thread, ou para cópias delas. O fato de que tal compartilhamento de granularidade fina é possível não significa que ele seja útil, no entanto, especialmente tendo em vista que as versões do UNIX tradicionais não oferecem essa funcionalidade. Um programa Linux que tira vantagem disso não pode mais ser levado para o UNIX. O modelo de threads do Linux apresenta outra dificuldade. Sistemas UNIX associam um único PID com um processo, independente se ele tem um único ou múltiplos threads. A fim de ser compatível com outros sistemas UNIX, o Linux distingue entre um identificador de processo (PID) e um identificador de tarefas (TID). Ambos os campos são armazenados na estrutura de tarefas. Quando clone é usado para criar um novo processo que não compartilha nada com o seu criador, PID é configurado para um novo valor; de outra maneira,
a tarefa recebe um novo TID, mas herda o PID. Dessa maneira, todos os threads em um processo receberão o mesmo PID que o primeiro thread no processo.
10.8.4 Arquitetura Android
 O Android é construído sobre o núcleo do Linux padrão, com apenas algumas extensões significativas para o núcleo em si, que serão discutidas mais tarde. Uma vez no espaço usuário, no entanto, sua implementação é bastante diferente da distribuição do Linux tradicional e usa muitas de suas características que você já compreende de maneiras muito diferentes. Como em um sistema Linux tradicional, o primeiro processo do espaço usuário do Android é init, que é a raiz de todos os processos. Os daemons que o processo init do Android inicializa são diferentes, no entanto, focados mais em detalhes de baixo nível (gerenciamento de sistemas de arquivos e acesso ao hardware) em vez de mecanismos do usuário de nível mais alto
como escalonamento de tarefas cron. O Android também tem uma camada adicional de processos, aqueles executando o ambiente de linguagem Java Dalvik, que são responsáveis por executar todas as partes do sistema implementadas em Java. A Figura 10.39 ilustra a estrutura de processo básica do Android. Primeiro é o processo init, que gera uma série de processos de daemon de baixo nível. Um deles é zygote, que é a raiz dos processos de linguagem Java de nível mais alto. O init do Android não executa um shell da maneira tradicional, já que um dispositivo de Android típico não tem um console local para o acesso do shell. Em vez disso, o processo daemon adbd executa por conexões remotas (como sobre o USB) que solicitam acesso ao shell, criando processos do shell para elas conforme a necessidade. Tendo em vista que a maior parte do Android é escrita na linguagem Java, o daemon zygote e os processos que ele inicializa são centrais para o sistema. O primeiro processo zygote sempre inicia e é chamado de system_server (serviço de sistema), que contém todos os serviços de base do sistema operacional. Partes fundamentais dele são o gerenciador de energia, gerenciador de pacotes, gerenciador de janelas e gerenciador de atividades. Outros processos serão criados a partir de zygote conforme a necessidade. Alguns deles são processos “persistentes” que fazem parte do sistema operacional básico, como a pilha de telefonia no processo do telefone, que deve permanecer sempre executando. Processos de aplicativos adicionais serão criados e parados 
conforme a necessidade enquanto o sistema estiver executando. Os aplicativos interagem com o sistema operacional através de chamadas para as bibliotecas fornecidas por ele, que juntas compõem o arcabouço do Android. Algumas dessas bibliotecas podem desempenhar seu trabalho dentro daquele processo, mas muitas precisarão desempenhar uma comunicação interprocesso com outros processos, muitas vezes serviços no processo system_server. A Figura 10.40 mostra o projeto típico para APIs do arcabouço Android que interagem com os serviços de sistema, nesse caso o gerenciador de pacotes (package manager). O gerenciador de pacotes fornece uma API do arcabouço para os aplicativos chamarem em seu processo local, neste caso a classe PackageManager. Internamente, a classe deve receber uma conexão para o serviço correspondente no system_server. Para conseguir isso, no momento da inicialização o system_server publica cada serviço sob um nome bem definido no gerenciador de serviços (service manager), um daemon inicializado por init. O PackageManager no processo aplicativo recupera uma conexão do gerenciador de serviços para o seu serviço de sistema usando aquele mesmo nome. Uma vez que o PackageManager tenha se conectado com o seu serviço de sistema, ele pode fazer chamadas nele. A maioria das chamadas de aplicativos para PackageManager é implementada como comunicação interprocesso usando o mecanismo IPC do Binder do Android, nesse caso fazendo chamadas para a implementação PackageManagerService no system_server. A implementação do PackageManagerService arbitra interações através de todas as aplicações de clientes e mantém o estado que será necessário para múltiplas aplicações.
10.8.5 Extensões do Linux Na maioria das vezes, o Android inclui um núcleo Linux comum fornecendo características padrões do Linux. A maioria dos aspectos interessantes do Android como um sistema operacional está em como as características existentes do Linux são usadas. Há também, no entanto, diversas extensões significativas relativas ao Linux sobre as quais o sistema Android se apoia.
Impasses
s sistemas computacionais estão cheios de recursos que podem ser usados somente por um processo de cada vez. Exemplos comuns incluem impressoras, unidades de fita para backup de dados da empresa e entradas nas tabelas internas do sistema. Ter dois processos escrevendo simultaneamente para a impressora gera uma saída ininteligível. Ter dois processos usando a mesma entrada da tabela do sistema de arquivos invariavelmente levará a um sistema de arquivos corrompido. Em consequência, todos os sistemas operacionais têm a capacidade de conceder (temporariamente) acesso exclusivo a um processo a determinados recursos. Para muitas aplicações, um processo precisa de acesso exclusivo a não somente um recurso, mas a vários. Suponha, por exemplo, que dois processos queiram cada um gravar um documento escaneado em um disco Blu- -ray. O processo A solicita permissão para usar o scanner e ela lhe é concedida. O processo B é programado diferentemente e solicita o gravador Blu-rayprimeiro e ele também lhe é concedido. Agora A pede pelo gravador Blu-ray, mas a solicitação é suspensa até que B o libere. Infelizmente, em vez de liberar o gravador Blu-ray, B pede pelo scanner. A essa altura ambos os processos estão bloqueados e assim permanecerão para sempre. Essa situação é chamada de impasse (deadlock). Impasses também podem ocorrer entre máquinas. Por exemplo, muitos escritórios têm uma rede de área local com muitos computadores conectados a ela. Muitas vezes dispositivos como scanners, gravadores Blu-ray/DVDs, impressoras e unidades de fitas estão conectados à rede como recursos compartilhados, disponíveis para qualquer usuário em qualquer máquina. Se esses dispositivos puderem ser reservados remotamente (isto é, da máquina da casa do usuário), impasses
do mesmo tipo podem ocorrer como descrito. Situações mais complicadas podem provocar impasses envolvendo três, quatro ou mais dispositivos e usuários. Impasses também podem ocorrer em uma série de outras situações. Em um sistema de banco de dados, por exemplo, um programa pode ter de bloquear vários registros que ele está usando a fim de evitar condições de corrida. Se o processo A bloqueia o registro R1 e o processo B bloqueia o registro R2, e então cada processo tenta bloquear o registro do outro, também teremos um impasse. Portanto, impasses podem ocorrer em recursos de hardware ou em recursos de software. Neste capítulo, examinaremos vários tipos de impasses, ver como eles surgem, e estudar algumas maneiras de preveni-los ou evitá-los. Embora esses impasses surjam no contexto de sistemas operacionais, eles também ocorrem em sistemas de bancos de dados e em muitos outros contextos na ciência da computação; então, este material é aplicável, na realidade, a uma ampla gama de sistemas concorrentes. Muito já foi escrito sobre impasses. Duas bibliografias sobre o assunto apareceram na Operating Systems Review e devem ser consultadas para referências (NEWTON, 1979; e ZOBEL, 1983). Embora essas bibliografias sejam muito antigas, a maior parte dos trabalhos sobre impasses foi feita bem antes de 1980, de maneira que eles ainda são úteis.
Impassses
6.9 Resumo O impasse é um problema potencial em qualquer sistema operacional. Ele ocorre quando todos os membros de um conjunto de processos são bloqueados esperando por um evento que apenas outros membros do mesmo conjunto podem causar. Essa situação faz que todos os processos esperem para sempre. Comumente, o evento pelo qual os processos estão esperando é a liberação de algum recurso nas mãos de outro membro do conjunto. Outra situação na qual o impasse é possível ocorre quando todos os processos de um conjunto de processos de comunicação estão esperando por uma mensagem e o canal de comunicação está vazio e não há timeouts pendentes. O impasse de recursos pode ser evitado controlando quais estados são seguros e quais são inseguros. Um estado seguro é aquele no qual existe uma sequência de eventos garantindo que todos os processos possam ser concluídos. Um estado inseguro não tem essa garantia. O algoritmo do banqueiro evita o impasse ao não conceder uma solicitação se ela colocar o sistema em um estado inseguro. O impasse de recursos pode ser evitado estruturalmente projetando o sistema de tal maneira que ele jamais possa ocorrer. Por exemplo, ao permitir que um processo possua somente um recurso a qualquer instante, a condição da espera circular necessária para um impasse é derrubada. O impasse de recursos também pode ser evitado numerando todos os recursos e obrigando os processos a requisitá-los somente na ordem crescente. O impasse de recursos não é o único tipo existente. O impasse de comunicação também é um problema em potencial em alguns sistemas, embora ele possa muitas vezes ser resolvido via estabelecimento de timeouts apropriados. O livelock é similar ao impasse no sentido de que ele pode parar todo o progresso, mas ele é tecnicamente diferente, pois envolve processos que não estão realmente bloqueados. A inanição pode ser evitada mediante uma política de alocação “primeiro a chegar, primeiro a ser servido”.
Tanenbaum, Andrews S., Sistemas Operacionais Modernos, 4ª Edição.

Continue navegando