_________________________________________________________________
76
Capítulo 6
Sistema de entrada/saída
O sistema de entrada e saída de um sistema operacional é a parte responsável
por gerenciar a comunicação com quaisquer dispositivos que estabeleçam uma forma
de comunicação entre o sistema e os aplicativos através de dispositivos de diferentes
funcionalidades que sejam classificados como de E/S.
Neste capítulo são apresentadas as formas de comunicação entre o sistema
operacional e os dispositivos de entrada e saída e as formas de armazenamento e acesso
a arquivos.
_________________________________________________________________
77
6.1. Introdução
O sistema operacional se comunica com os dispositivos de entrada e saída
através da emissão de sinais de comando para estes elementos. Estes sinais de comando
podem ser classificados segundo três funcionalidades básicas:
•
•
•
Controle dos dispositivos;
Interceptar interrupções;
Tratamento de erros;
Para entendermos melhor as formas de comunicação existentes, os dispositivos
de E/S podem ser divididos em duas classes, segundo a forma com que manipulam
dados:
Dispositivos de bloco: armazenam informações em blocos de tamanho fixo, cada um
com seu próprio endereço. Variam de 512 a 32.768 bytes, e cada bloco é lido ou escrito
independentemente dos outros. Este é o modelo mais comum de ser utilizado.
Dispositivos de caractere: envia ou recebe um fluxo de caracteres, sem considerar
qualquer tipo de estrutura, como os blocos do modelo anterior. Este modelo não é
endereçável e não possui operações de posicionamento. Exemplos de dispositivos que
empregam este modelo são impressoras, mouse, interfaces de rede, etc.
6.2. Controle de acesso a dispositivos de E/S
O sistema operacional necessita de uma forma de gerenciar a comunicação entre
os dispositivos de entrada e saída com o restante do sistema, e os seguintes métodos
podem ser empregados nesta tarefa:
• E/S programada: quando um processo inicia uma operação de E/S, a UCP faz a
verificação se o dispositivo já se encontra pronto para a transmissão de outro
dado (estado chamado de espera ociosa ou pooling). Este é um modelo simples,
mas pouco eficaz, pois a UCP fica alocada ao processo até que a operação de
E/S seja concluída, ficando ociosa durante este tempo.
• E/S orientada a interrupção: utiliza interrupções para liberar a UCP para
outros processos enquanto uma operação de E/S é realizada. Assim que um
dispositivo de E/S começa uma operação, a UCP chama o escalonador e outro
processo é iniciado e uma interrupção salva o contexto do processo atual.
Quando a operação de E/S é concluída, o dispositivo gera uma nova interrupção
que detém o processo atual e salva o seu estado.
A desvantagem deste método é que a ocorrência de interrupções
constantes gera uma sequência de trocas de contexto, o que gera um desperdício
de UCP.
• E/S utilizando DMA (Direct Memory Access – Acesso Direto a Memória): O
DMA é um módulo responsável por controlar a comunicação entre a memória
_________________________________________________________________
78
principal e o dispositivo de E/S; o sistema computacional tem que possuir um
controlador de DMA implementado.
O DMA faz uma E/S programada, mas onde o controlador faz todo o
trabalho, ao invés da UCP, que fica liberada para exercer outras tarefas. A única
desvantagem deste método é que o uso de DMA é mais lento do que o controle
feito pela UCP, o que em alguns casos pode ser desvantajoso.
6.3. Drivers de dispositivos
Dependendo do dispositivo de E/S a ser utilizado, pode existir uma grande
variação de características, como o número de registradores utilizados e a natureza dos
comandos. Cada dispositivo de E/S ligado ao computador precisa de um código
específico para controlá-lo, chamado driver do dispositivo.
O driver fornece um conjunto de instruções que, através delas, o sistema
operacional é capaz de acessar o dispositivo de E/S. A figura 6.1 ilustra o modelo de
comunicação entre os drivers de dispositivos e o sistema operacional.
Figura 6.1. Posicionamento lógico dos drivers de dispositivos
_________________________________________________________________
79
Normalmente um driver de dispositivo trata de um tipo ou, no máximo, de uma
classe de dispositivos. Não existem restrições em se ter um driver que faça o controle de
vários dispositivos, mas não é uma boa idéia. Alguns exemplos de cada tipo são
apresentados a seguir:
Drivers de classes de dispositivos: um driver de disco SCSI pode tratar vários discos
SCSI de diferentes tamanhos e velocidades.
Drivers de dispositivos específicos: para o funcionamento de dispositivos com
recursos muito específicos, por exemplo, joystick com sensor de movimento, teclado
virtual, etc.
O ideal é que drivers executassem no espaço de memória do usuário e que
realizassem chamadas ao sistema para realizar operações de leitura e escrita aos
registradores do dispositivo. Este modelo tornaria o núcleo do sistema operacional
isolado dos drivers e os drivers isolados entre si. Desta forma, o maior fator de erros no
sistema operacional, que são drivers defeituosos que interferem no núcleo do sistema,
seria eliminado.
6.4. Software de E/S independente de dispositivo
A questão de existir uma interface uniforme para os drivers de dispositivos é de
extrema importância para o sistema operacional, pois faz com que todos os dispositivos
de E/S e drivers se pareçam os mesmos. Caso contrário, o sistema operacional teria que
ser modificado para cada novo dispositivo que surgisse.
A solução para esta questão é feita através da padronização de interfaces. Desta
forma, é definido um conjunto de funções que são disponibilizadas pelo núcleo do
sistema operacional que os escritores de drivers devem saber que estão disponíveis e
quais respostas podem esperar de cada uma delas. Torna-se mais fácil acoplar um novo
driver se ele estiver em conformidade com a interface provida pelo sistema operacional.
Na prática, nem todos os dispositivos são idênticos, mas podem apresentar muitas
funções em comum, o que torna possível criar modelos “genéricos”, como ilustrado na
figura 6.2. Na esquerda está ilustrado um modelo de projeto sem o uso de uma
interface-padrão para o driver, e na direita, um modelo com uma interface-padrão para o
driver.
Figura 6.2. Projeto de interface de drivers
_________________________________________________________________
80
Alguns exemplos de interfaces uniformes para drivers de dispositivos:
• Alocação e liberação de dispositivos;
• Armazenamento em buffer;
• Relatórios de erros;
• Fornecimento de tamanho de bloco independente do dispositivo (camadas
superiores lidam com dispositivos abstratos que usam o mesmo tamanho de
bloco lógico).
6.5. Software de E/S do espaço do usuário
A maior parte do software de E/S está dentro do sistema operacional, mas parte
dele é formada por bibliotecas ligadas aos programas dos usuários e até mesmo
programas completos que executam fora do módulo. Por exemplos, os procedimentos
de bibliotecas, como funções write, print, open, etc.
Outra categoria importante de software de E/S é o sistema de spooling, que
representa uma forma de lidar com dispositivos dedicados de E/S em sistemas de
multiprogramação. O método usa um processo especial (daemon) e um diretório
especial (diretório de spool)
O uso deste sistema busca respondar a seguinte pergunta: como impedir que um
usuário crie um processo que ocupe um dispositivo de E/S durante longos períodos, mas
que não faça nada?
Por exemplo, no serviço de impressão, inicialmente um processo gera todo o
arquivo a ser impresso e o coloca no diretório de spool. É função do daemon, que é o
único processo com permissão de usar os arquivos do diretório de spool, de imprimir os
arquivos, impedindo o acesso direto pelos usuários e, consequentemente, evitando de
manter o processo desnecessariamente aberto. Além de impressoras, outros sistemas
utilizam o mecanismo de spool, por exemplo, a transferência de arquivos através de
uma rede.
6.6. Escalonamento de Disco
O escalonador de disco influencia no desempenho dos sistemas, uma vez que são
nos discos que ficam armazenados até o momento do processamento os programas e os
dados, tais como compiladores, montadores entre outros. Portanto, o gerenciamento
apropriado do disco é importante para um sistema de computador. Note que este item
trata de estratégias para os discos rígidos “tradicionais”, isto é, composto por partes
móveis3, e não dos compostos por memórias sólidas4, ainda muito recentes e pouco
empregados, mas que certamente serão levados em consideração no projeto de novos
sistemas operacionais.
3
Faça uma revisão sobre a estrutura e funcionamento de discos rígidos, matéria abordada em livros de
Arquitetura de Computadores
4
Aproveite e faça uma pesquisa sobre o funcionamento de memórias SSD – solid state drive
_________________________________________________________________
81
6.6.1. Características físicas relevantes ao desempenho
Fisicamente os discos rígidos possuem duas superfícies, sendo cada uma delas
divididas em trilhas e setores. Um setor é a menor unidade onde pode ser feito um
acesso em um disco, através da cabeça de leitura e escrita. Uma solicitação de acesso
deve especificar superfície, trilha e setor desejados.
A velocidade de acesso ao disco pode ser composta por três partes. Para realizar
o acesso ao setor do disco, primeiro devemos mover a cabeça de leitura e escrita para a
trilha apropriada. O tempo gasto neste movimento é denominado seek time (tempo de
busca). Uma vez atingida a trilha correta, é necessário esperar que o setor desejado seja
rotacionado sob a cabeça de leitura e escrita. Este tempo é denominado latency time
(tempo de latência). Finalmente, há a transferência de dados entre o disco e a memória
principal. O tempo gasto na transferência dos dados é denominado transfer time (tempo
de transferência). O tempo total do serviço de um pedido ao disco é dado pela soma
destas três fatias de tempo.
É importante que os acessos ao disco sejam feitos tão rapidamente quanto
possível. Sempre que existe um pedido de entrada e saída emitido por um processo, é
enviada uma chamada ao sistema operacional. Este pedido deve especificar informações
tais como:
•
•
•
•
Entrada ou saída;
Endereço do disco (disco, trilha, setor);
Endereço de memória;
Quantidade de informação a ser transferida.
Se o disco estiver disponível, o pedido pode ser atendido imediatamente. Caso
contrário é necessário colocar os pedidos dos processos na fila de espera do dispositivo.
6.6.2 Algoritmos de Escalonamento de Disco
Os algoritmos de escalonamento de disco escolhem um processo da fila de
pedidos pendentes para ser atendido. Os algoritmos de escalonamento de disco podem
ser FCFS, SSTF, SCAN, C-SCAN, LOOK e C-LOOK A seguir descreveremos cada
um destes algoritmos.
First-Come-First-Served (FCFS)
Esta é a forma mais simples de escalonamento. Os pedidos são atendidos na
mesma ordem em que chegam à fila do dispositivo. Entretanto, o algoritmo FCFS pode
não fornecer o melhor tempo médio de serviço. Considere, por exemplo, uma fila de
pedidos por trilhas no disco ordenada da seguinte forma:
98, 183, 37, 122, 14, 124, 65 e 67,
Sendo 98 a primeira trilha pedida e 67 a última. Se a cabeça de leitura e escrita está
inicialmente na trilha 53, ela primeiro se moverá para a trilha 98, depois para a 183,
para a 37 e assim por diante. O movimento total da cabeça de leitura e escrita é igual a
_________________________________________________________________
82
640 trilhas e pode ser visto na figura 6.3. Se pudesse ser feita uma troca na ordem de
atendimento dos pedidos, o tempo para atender aos pedidos poderia decrescer
substancialmente. Por exemplo, trocando os pedidos pela trilha 122 e 14. Assim, após
atender ao pedido 37, atenderia ao pedido 14, depois o 122 e 124. Este fato melhoraria
muito o desempenho, pois reduziria o movimento total da cabeça de leitura e escrita
para 425 trilhas.
Figura 6.3. Escalonamento de disco FCFS.
Shortest-Seek- Time- First (SSTF)
Este algoritmo seleciona o pedido com o menor seek time a partir da posição
corrente da cabeça de leitura e escrita. Uma vez que o seek time é proporcional a
diferença de trilhas entre os pedidos, esta abordagem é implementada movendo a cabeça
de leitura e escrita para a trilha mais próxima na fila de pedidos. Para a mesma fila de
pedidos, temos na figura 6.4, a ilustração de como é o procedimento do algoritmo SSTF.
O problema deste algoritmo é starvation. Supondo que sempre chegue a fila de
pedidos trilhas mais próximas a cabeça de leitura e escrita que uma trilha mais distante,
esta vai esperar indefinidamente.
Figura 6.4. Escalonamento de disco SSTF
_________________________________________________________________
83
SCAN
Neste algoritmo a cabeça de leitura e escrita inicia em um dos lados do disco e
se movimenta para o outro extremo, servindo os pedidos quando a cabeça atinge cada
trilha até chegar ao outro extremo. Ao chegar ao outro extremo, o movimento da cabeça
de leitura e escrita é revertido e os pedidos são novamente atendidos. Supondo
novamente a mesma fila de pedidos das figuras anteriores, a figura 6.5 mostra o
procedimento deste algoritmo.
Figura 6.5. Escalonamento de disco SCAN
Uma variação deste algoritmo é denominada C-SCAN ou circular SCAN. A
diferença entre estes algoritmos é que ao atingir o extremo oposto, a cabeça de leitura e
escrita volta imediatamente ao início e recomeça a atender os pedidos.
Existem ainda duas outras versões semelhantes aos algoritmos SCAN e
C-SCAN, denominadas LOOK e C-LOOK. Nestas duas últimas versões, a cabeça de
leitura e escrita não chega a atingir os extremos (só no caso de haver um pedido
explícito por eles), nelas os movimentos são revertidos quando o último pedido por
trilha em cada direção é atingido.
_________________________________________________________________
84
6.7 Resumo
O sistema Operacional tem um importante papel no funcionamento de uma
infinidade de dispositivos responsáveis pela entrada e saída de dados do sistema. Deve
ainda gerenciar as formas de transferência de dados entre tais dispositivos e a memória
do sistema computacional, além de saber se comunicar corretamente com eles, o que faz
necessário o uso de drivers de dispositivos.
Os sistemas de discos são essenciais em muitos computadores. Os pedidos por
entrada e saída de dados armazenados em disco são gerados tanto pelo sistema de
arquivos quanto pelo sistema de memória virtual. Cada pedido especifica o endereço no
disco que é para ser referenciado. Este endereço inclui a superfície, a trilha e o setor. A
idéia dos algoritmos de escalonamento de disco é tentar diminuir o movimento total da
cabeça de leitura e escrita, tais como FCFS, SSTF, SCAN, C-SCAN, LOOK e
C-LOOK.
Leitura Obrigatória: Capítulo 5 do livro “Sistemas Operacionais Modernos”, 2ª
edição, TANEMBAUM, A., até o item 5.3 (inclusive), e o subitem 5.4.3.
_________________________________________________________________
85
Capítulo 7
Comunicação e sincronismo entre processos
A existência de múltiplos processos ou threads, sendo escalonados ou
executados paralelamente em sistemas multiprocessados é uma situação bastante
comum em sistemas operacionais modernos, porém, podemos nos deparar com
situações onde dois ou mais destes processos necessitam fazer acesso exclusivo a
recursos do sistema, originando situações de disputa que, se não forem devidamente
tratadas, podem resultar em resultados incorretos e até mesmo no travamento do sistema
operacional. Este capítulo descreve o problema conhecido como condições de disputa
entre processos e as formas mais empregadas em sua resolução.
_________________________________________________________________
86
7.1. Condições de disputa
Em sistemas operacionais modernos, podem ocorrer diversas situações nas quais
processos em atividade simultaneamente podem compartilhar algum recurso do sistema,
como um dispositivo de E/S, memória principal, arquivos, etc. Diante desta
possibilidade, surgem possíveis condições de disputa (race conditions), caracterizadas
por dois ou mais processos lendo ou escrevendo em dados ou recursos compartilhados,
cujo resultado final depende das informações manipuladas pelos processos e pela ordem
de execução dos mesmos.
Como exemplo, imaginem duas threads, T1 e T2 que compartilham uma
variável x, de seu processo pai P, cujo valor inicial é igual a 5. Ambas leem o valor de x,
e enquanto T1 realiza uma operação de subtrair 1 de x, T2 realiza a adição de 1 ao valor
de x. Se as operações foram realizadas na ordem descrita, x começou com o valor igual
a 5, T1 fez uma subtração, diminuindo este valor para 4, e grava este resultado na
memória. Porém T2 já havia lido o valor de x quando este era igual a 5, e adicionou 1,
em seguida atualizando o valor na memória para 6. Se a thread T1 for utilizar
novamente a variável x, lerá o valor 6, porém, esperava ler o valor 4, o último valor que
ela havia gravado na memória. Situações como esta durante a execução de programas
podem levar a situações de erros inesperados, e difíceis de ser depurados.
Devido a natureza multiprogramada da maioria dos sistemas operacionais
modernos, e o extenso uso de threads, torna-se necessário uma técnica para implementar
a exclusão mútua, ou seja, impedir que processos utilizem um recurso compartilhado
que já se encontre em uso naquele exato momento por algum outro processo do sistema.
No exemplo anterior, se T1 foi a primeira thread a acessar a variável x, ela deveria
“trancar” o acesso a x, impedindo que outros processos a utilizassem5, até o momento
que ela resolvesse “liberar” x.
Uma forma de solucionar condições de disputa seria identificar em um programa
as partes que fazem acesso a recursos compartilhados que podem gerar situações de
disputa. Estes trechos são chamados de regiões (ou seções) críticas, e serão
implementados mecanismos para que dois processos não estejam em suas seções
críticas ao mesmo tempo, evitando situações de disputa. A figura 7.1 ilustra esta
situação.
5
Em alguns casos é possível que outros processos acessem um recurso compartilhado, como a variável
citada, porém, com permissão somente para leitura do valor, incapazes de alterá-la, enquanto não estiver
liberada.
_________________________________________________________________
87
Figura 7.1. Exclusão mútua usando regiões crítica
Apesar de satisfazer as condições de disputa, a utilização de seções críticas não
garante que os processos paralelos funcionem cooperativamente de forma correta e
eficiente. Uma solução considerada boa deve atender as quatro condições apresentadas a
seguir:
•
•
•
•
Nunca dois processos podem estar simultaneamente em suas seções críticas;
Nada pode ser afirmado sobre a velocidade ou sobre o número de CPUs;
Nenhum processo executando for a de sua seção crítica pode bloquear outros
processos;
Nenhum processo deve esperar eternamente para entrar em sua seção crítica.
Algumas técnicas podem ser utilizadas para realizar a exclusão mútua,
impedindo que outros processos acessem áreas da memória compartilhada em sua seção
crítica, são apresentados a seguir.
7.2. Técnicas de exclusão mútua com espera ociosa
7.2.1. Desabilitar interrupções
Consiste em deixar um processo desabilitar todas as interrupções logo após
entrar em sua seção crítica, e reabilitá-las logo após deixar esta seção. Enquanto as
interrupções estão desabilitadas, a CPU não pode ser alternada entre processos. Na
prática, não se mostra uma solução muito interessante, pois dá aos processos do usuário
o poder de desabilitar interrupções, e não garante que seja reabilitada após a saída da
seção crítica. A situação é ainda mais complicada em sistemas multiprocessados, pois
apenas a CPU que desabilitou o uso de interrupções será afetada, outras CPUs existentes
poderão continuar executando outros processos e acessando áreas críticas da memória
compartilhada.
7.2.2. Variáveis de bloqueio
Modelo de bloqueio por software, utilizando uma variável compartilhada (lock),
inicialmente com o valor 0. Quando um processo quer entrar em sua seção crítica, ele
_________________________________________________________________
88
verifica o valor desta variável. Se for igual a 0, ele pode entrar na sua seção crítica e
altera o valor da variável para 1. Se, ao testar a variável, o valor já for igual a 1, o
processo deve aguardar até que o valor passe para 0, indicando que algum outro
processo saiu da seção crítica, liberando os recursos do sistema.
Este método também pode estar sujeito a falhas: suponha que um processo
verificou o valor de lock, que é igual a 0. Existe a possibilidade que, antes que este
primeiro processo altere o valor de lock para 1, um segundo processo seja escalonado,
execute e altere a variável de lock para 1. Ao executar novamente, o primeiro processo
também grava o valor 1 na variável de lock, e então existirão dois processos em suas
seções críticas ao mesmo tempo.
7.2.3. Alternância obrigatória
A primeira vista, este método guarda semelhanças com o uso de variáveis de
bloqueio. Utilizando uma variável de controle turn, um processo A verifica o valor
desta variável, que inicialmente é igual a 0, e entra em sua seção crítica. Neste
momento, um processo B verifica o valor da variável turn, que continua igual a 0, e fica
aguardando em uma espera ociosa6, até que este valor seja alterado para 1.
Quando o processo A deixa sua seção crítica, muda o valor de turn para 1, o que
permite ao processo B entrar em sua região crítica. Apesar de ser uma solução que
obrigue que os processos alternem suas entradas nas regiões críticas, não garante o
impedimento de uma situação de parada do sistema causada por erro na alternância7,
violando a terceira condição da lista já apresentada.
7.2.4. Solução de Peterson
Em 1981, o matemático G. L. Peterson apresentou um modo simples e eficiente
de implementar a exclusão mútua. Antes de entrar em sua seção crítica, cada processo e
faz uma chamada a uma função enter_region, passando como parâmetro seu número de
processo, 0 ou 1, o que servirá para verificar se ele deve esperar até o momento seguro
para entrar em sua seção crítica, caso necessário e manifesta seu interesse em entrar na
seção crítica, marcando uma variável de controle, no vetor interested. Após terminar o
uso das variáveis críticas, o processo chama o procedimento leave_region, para indicar
seu término e liberar as variáveis compartilhadas para uso por outros processos.
No código para a solução de Peterson, exibida a seguir, suponha que nenhum
processo está em sua seção crítica. Se o processo 0 chama a função enter_region, seu
interesse é marcado na sua respectiva posição do vetor interested e a variável turn é
marcada com 0. Como neste exemplo o processo 1 ainda não manifestou nenhum tipo
de interesse, a função enter_region retorna imediatamente, garantindo o acesso a seção
crítica ao processo 0.
Caso o processo 1 se manifeste, chamando a função enter_region, ele ficará
suspenso até que o valor da posição 0 no vetor interested seja modificada para FALSE,
6
Espera ociosa (busy waiting) deve ser evitada, já que gasta tempo de CPU. Só é aceitável quando há
uma expectativa razoável que o tempo de espera seja breve.
7
Como isto pode acontecer? Pesquise a respeito e apresente os resultados.
_________________________________________________________________
89
o que só acontecerá quando o processo 0 chamar a função leave_region para sair de sua
seção crítica. Nesta solução, caso os processos chamem a função enter_region quase
simultaneamente, ambos armazenarão seus valores na variável turn, porém, prevalecerá
o que armazenou por último, sobrescrevendo o anterior.
7.2.5. Instrução TSL
Trata-se de uma solução para o problema da seção crítica que trabalha em um
nível mais baixo do sistema, utilizando a seguinte instrução de montagem (presente
principalmente em sistemas multiprocessados):
TSL RX, LOCK
A Instrução TLS é um mnemônico para test and set lock, isto é, lê o conteúdo da
palavra de memória (neste caso, o lock, no registrador RX), e armazena um valor
diferente de zero no endereço de memória lock; as operações de leitura e
armazenamento de uma palavra são indivisíveis, o que garante que nenhum outro
processador acesse a palavra de memória até o término da instrução, pois bloqueia o
acesso ao barramento de memória para outras CPUs.
Desta forma, a variável lock controla o acesso a memória compartilhada: se seu
valor for igual a 0, qualquer processo pode utilizar a instrução TLS para mudar seu
valor para 1 e ter controle sobre os dados na memória compartilhada; quando terminar,
basta alterar novamente seu valor para 0 (através de uma instrução move).
7.3. Dormir e Acordar
Os métodos de Peterson e o TSL, apesar de funcionais, apresentam a ocorrência
de espera ociosa, o que gasta tempo de CPU desnecessariamente e, em alguns casos,
pode causar o problema da inversão de prioridade8.
Uma alternativa é utilizar algumas funções primitivas de comunicação
interprocessos, que fazem o bloqueio dos processos, ao invés da espera ociosa. As mais
utilizadas são as de sleep, que faz com que um processo ”durma”, isto é, entre em
espera, até que seja “acordado” por um outro processo, através de um wakeup.
O exemplo clássico da utilização destas duas primitivas é o problema do
produtor-consumidor, com buffer limitado (bounded buffer). Trata-se de um buffer
compartilhado, de capacidade limitada, entre dois processos. Um destes processos
assume a função de produtor, criando e armazenando itens neste buffer compartilhado, e
o outro, o consumidor, retira estes itens do buffer, um por vez, esvaziando-o. O mesmo
problema pode ser adaptado para múltiplos produtores e consumidores.
Neste problema, as situações que devem ser consideradas para a comunicação
interprocessos são:
8
Pesquise a respeito, parece uma daquelas perguntas que o professor gosta de colocar na prova.
_________________________________________________________________
•
•
90
Quando o produtor quer colocar um novo item produzido no buffer, mas este
encontra-se cheio. Neste caso, o produtor deve ser colocado para dormir, e ser
acordado quando ao menos um processo consumidor retirar um ou mais itens do
buffer.
Quando o consumidor quer retirar um item do buffer, mas este estiver vazio; de
forma análoga ao processo produtor, o consumidor deve ser colocado para dormir e
ser acordado quando o produtor tiver colocado algum novo item no buffer.
O número de itens no buffer compartilhado é mantido através de uma variável
count. Os processos produtor e consumidor são implementados com procedimentos para
inserir itens e remover itens, respectivamente, conforme ilustrados a seguir:
7.4. Semáforos
A questão sobre a comunicação interprocessos foi abordada em 1965 pelo
conceituado cientista da computação, E. W. Dijkstra9, que propôs a utilização de uma
variável inteira, o semáforo, utilizada para contar o número de sinais de acordar salvos
para uso posterior. Duas operações podem ser feitas sobre o sinal, up e down (apesar de
ser muito comum ver implementações que utilizam as letras P e V para implementar as
operações. Como Dijkstra era holandês, originalmente as operações eram Proberen
(testar), e Verhogen (incrementar)).
O resultado da operação down sobre um semáforo depende do valor desta
variável, o que pode resultar nas seguintes situações:
• Se o valor for maior do que zero, este é decrescido de 1, gastando um sinal de
acordar armazenado, e o processo segue adiante;
• Se o valor for igual a zero, o processo é colocado para dormir, sem terminar a
operação de down, ao menos por enquanto.
Observe que as operações de verificar o valor da variável, alterá-lo e, se
necessário, dormir, são executadas como uma operação atômica e indivisível.
A operação up incrementa o valor de um semáforo em uma unidade. Caso
existam um ou mais processos dormindo no semáforo incrementado (o que quer dizer
que seu valor era igual a zero antes do up), devido a uma operação de down prévia, um
deles será escolhido pelo S.O. para ser acordado. A operação de incrementar o semáforo
e acordar um processo também é indivisível.
A seguir é apresentada uma solução com semáforos para o problema do
produtor-consumidor. As variáveis usadas como semáforo nesta solução são:
• full: conta o número de lugares preenchidos no buffer; inicializada com o valor
0;
• empty: conta o número de lugares vazios do buffer; inicializada com o número
de lugares do buffer;
9
Pesquise sobre a obra de Dijkstra, o site http://en.wikipedia.org/wiki/Edsger_W._Dijkstra contém um
resumo de sua vida e obra
_________________________________________________________________
91
• mutex: usada para garantir que os processos produtor e consumidor não acessem
simultaneamente o buffer. Inicializada com o valor 1, pois é um semáforo
binário. Observe no trecho de código a seguir que em ambos os processos, a
primeira operação feita em um mutex é sempre um down, e ao sair da seção
crítica, um up, o que garante a exclusão mútua.
Semáforos também podem ser empregados na sincronização de processos,
garantindo que certas sequências de eventos ocorram, como o produtor parando de
executar quando o buffer estiver cheio, e parando o consumidor quando o buffer estiver
vazio.
7.5. Mutexes
Um mutex funciona como um semáforo simplificado, pois não possui a
capacidade de contar, e, como o nome diz10, é utilizado para tratar a questão da exclusão
10
Mutex é a abreviação de mutual exclusion – exclusão mútua.
_________________________________________________________________
92
mútua. Pode ser representado com somente 1 bit, já que só pode representar dois
estados: impedido ou desimpedido.
Sobre um mutex operam as operações de lock, para ter acesso a uma seção
crítica, e unlock, para liberá-la. Caso o mutex já esteja em uso e outro processo tente
utilizar o lock, este permanecerá bloqueado até que o processo que esteja na seção
crítica utilize o unlock. Se múltiplos processos estiverem bloqueados sobre um mesmo
mutex, quando este for liberado, o S.O escolherá um deles aleatoriamente para adquirir
o mutex em seguida.
7.6. Monitores
Apesar das funcionalidades, alguns dos métodos anteriores, como os semáforos,
possuem vulnerabilidades contra falhas críticas, podendo enfrentar até situações que
terminem em uma situação de impasse11, o que motivou vários pesquisadores a estudar
métodos alternativos. Na década de 70, dois pesquisadores, Hoare e Brinch Hansen,
desenvolveram pesquisas separadamente, e chegaram a soluções similares, que
consistiam em uma unidade básica de sincronização de alto nível, chamada de monitor.
Monitores são coleções de procedimentos, variáveis e estruturas de dados,
agrupadas em módulos ou pacotes. Um processo pode chamar procedimentos que fazem
parte de um monitor a qualquer momento, mas não tem acesso às estruturas internas de
dados ao monitor a partir de procedimentos declarados fora do monitor.
Para garantir a exclusão mútua, somente um processo pode estar ativo em um
monitor em certo momento, o que pode ser feito simplesmente através de mutexes ou
semáforos binários. Também são utilizadas variáveis condicionais para o bloqueio de
processos que não possam continuar no momento. Sobre estas variáveis podem ser
aplicadas duas operações:
• Wait: faz o bloqueio do processo que está chamando um procedimento do
monitor, por exemplo, quando um produtor descobre que o buffer está cheio.
• Signal: pode acordar processos, alterando o valor de uma variável condicional
que um processo parceiro está esperando, por exemplo, um consumidor que
retira um item do buffer e acorda o produtor.
Através destas operações torna-se permitido a um processo acordar o outro, o
que levanta uma questão: como evitar que dois processos pertençam simultaneamente
ao monitor? Neste ponto, as propostas de Hoare e Hansen se diferenciam:
• Hoare propõe simplesmente manter o processo recém-acordado e suspender o
outro;
• Hansen apresenta uma proposta mais simples conceitualmente e em sua
implementação: o comando signal, que desperta o outro processo só pode
aparecer como o último comando de um procedimento do monitor, o que garante
que após acordar o outro processo, o que estava em execução saia
11
Deadlock, assunto que será tratado no próximo capítulo. Por enquanto, basta saber que deadlocks são
muito, muito ruins. Você não vai querer que um aconteça em seu sistema.
_________________________________________________________________
93
imediatamente. Caso vários processos estiverem esperando por um signal,
somente um deles, a ser escolhido pelo escalonador do sistema, é despertado.
Tanto monitores quanto semáforos apresentam uma deficiência em sistemas
modernos: ambos foram projetados para uso em sistemas dotados de uma ou mais
UCP’s que utilizam uma memória comum a todas. Seus respectivos modelos de
funcionamento não são aplicáveis a um ambiente de processamento distribuído,
formado por múltiplas UCP’s, cada uma com sua memória de acesso local nãocompartilhada, conectadas através de uma rede local.
7.7. Troca de mensagens entre processos
A comunicação entre processos através de troca de mensagens é feita através de
chamadas ao sistema, de maneira similar a utilizada em semáforos, permitindo a criação
de procedimentos de biblioteca representados por chamadas com a seguinte estrutura:
send(destination, &message) → envia uma mensagem para um destino especificado;
receive(source, &message) → recebe uma mensagem, cuja origem pode ser
especificada; caso não existam mensagens disponíveis, o receptor pode ficar bloqueado
até que alguma mensagem chegue.
A estruturação de um sistema de comunicação através de troca de mensagens é
mais complexa do que os casos apresentados anteriormente, pois deve lidar com
questões inerentes da troca de mensagens entre processos em diferentes equipamentos,
como a possibilidade de perda de mensagens pela rede, o que gera a necessidade de um
mecanismo de confirmação de recebimento (acknowledgement, ou simplesmente ack)
para garantir que a mensagem foi entregue ou gerar uma retransmissão. E o quê
acontece se a confirmação for perdida? Outra questão importante é sobre a autenticação:
como saber que a mensagem recebida é realmente do remetente especificado12?
Como exemplo da utilização da troca de mensagens no problema do
produtor/consumidor, considere o exemplo a seguir. Sua execução começa com o
consumidor enviando 100 mensagens vazias (sendo 100 o tamanho estabelecido para o
buffer) ao produtor que, se tiver algum item a ser fornecido ao consumidor, envia de
volta uma das mensagens recebidas, mas agora contendo o item a ser consumido. Caso
o produtor trabalhe mais rápido do que o consumidor e fique sem mensagens vazias, ele
será bloqueado até que uma mensagem vazia retorne do consumidor.
12
Estes são problemas clássicos de transmissão de dados. Maiores detalhes serão discutidos nas
disciplinas de Redes de Computadores
_________________________________________________________________
94
Sistemas de troca de mensagens podem empregar caixas postais, para o
armazenamento temporário de mensagens de forma similar a buffers. Desta forma, os
parâmetros de send e receive são as caixas, e não processos.
7.8. Barreiras
Barreiras são mecanismos de sincronização para grupos de processos, ao invés
de pares que atuam no modelo produtor/consumidor. Certas aplicações utilizam o
conceito de fases, de forma que nenhum processo que tenha concluído pode passar para
a fase seguinte até que todos os outros processos também já estejam em condição de
fazê-lo. Como exemplificado na figura 7.2, ao alcançar uma barreira estabelecida, um
processo ficará no estado de bloqueado (através de uma chamada a um procedimento de
biblioteca barrier) até que todos os demais processos também alcancem a barreira, para
ultrapassá-la juntos.
Figura 7.2. Três momentos do uso de barreira com processos
_________________________________________________________________
95
Como exemplo de sua aplicabilidade, suponha um ambiente de processamento
paralelo heterogêneo onde um programa realize uma operação aritmética que requer um
grande número de cálculos, como a multiplicação de duas matrizes de grandes
dimensões, deva ser realizada. Uma alternativa é que vários processos distintos
trabalhem paralelamente, cada um realizando parte do trabalho (geralmente, a
multiplicação de um subconjunto das linhas da matriz). É esperado que tenham
processos terminando em instantes de tempo distintos, e para que o resultado final seja
apresentado como resultado de um único processo13 é necessário que os processos que
terminaram antes aguardem pelos que ainda estão em execução.
7.9. Problemas clássicos de comunicação interprocessos
Existem diversos problemas na literatura que ilustram situações de impasse
relativamente comuns na comunicação interprocessos. A seguir são apresentados alguns
problemas clássicos
O problema do Jantar dos Filósofos
Problema proposto em 1965 por Dijkstra, desde então é utilizado para analisar o
funcionamento de novas técnicas de sincronização entre processos que venham a surgir.
O problema é caracterizado por cinco filósofos sentados em torno de uma mesa circular,
cada um com seu prato de espaguete. Cada filósofo só faz duas coisas na vida: comer e
filosofar. Quando vai comer, ele deve pegar os garfos à sua direita e à sua esquerda,
pois o espaguete está escorregadio, portando, precisa dos dois, 14 porém, só existem
cinco garfos na mesa. Quando termina de comer, ele coloca os garfos de volta na mesa e
começa a filosofar, até que sinta fome novamente, quando tornará a pegar os garfos para
comer mais. A figura 7.3 ilustra o que seria o ambiente real do problema.
Figura 7.3. A mesa de almoço dos filósofos
13
Geralmente o processo paralelo cria a ilusão que um único sistema computacional de grande porte
tenha feito o processamento, e não que o resultado foi gerado por um conjunto de recursos
computacionais atuando cooperativamente.
14
Em publicações mais recentes, o enunciado foi ‘atualizado’ para comida japonesa e cada filósofo usa
hashis, os ‘pauzinhos’ para comer, o que torna mais plausível a necessidade de pegar os dois talheres
adjacentes, ao invés dos garfos.
_________________________________________________________________
96
Ao observar a figura, é elementar perceber que, quando um filósofo começa a
comer, seus vizinhos não conseguem fazer o mesmo, já que pelo menos um dos talheres
necessários não encontra-se disponível. A grande questão do problema é buscar por uma
solução na qual cada filósofo faça o que precisa fazer, sem nunca ficar travado. Em uma
situação extrema, imagine que cada um dos filósofos pegue o garfo à sua direita (ou
esquerda, o importante é que todos peguem o do mesmo lado) simultaneamente:
nenhum deles irá comer e ficará esperando que o filósofo do lado solte o garfo, o que
nunca irá acontecer, caracterizando uma situação de deadlock, que será discutida com
detalhes no capítulo seguinte.
Existem várias propostas para a solução deste problema, porém, algumas levam
a situações de processos atingirem um estado de starvation, ou que o correto
funcionamento dependerá de uma aleatoriedade em algum fator, ou ainda, limitam a
somente um filósofo estar comendo a cada instante. Existe uma solução que evita
deadlocks, e permite o máximo paralelismo a um número arbitrário de filósofos, através
de arranjos de semáforos, um por filósofo, e um mecanismo de controle para determinar
se o filósofo está comendo, pensando ou faminto (tentando pegar os garfos).
O problema do Barbeiro Sonolento
Neste problema, em uma barbearia há um barbeiro, uma cadeira de barbeiro e n
cadeiras usada para eventuais clientes esperarem a vez de serem atendidos. Quando não
existem clientes a serem atendidos, o barbeiro usa sua cadeira para dormir. Quando um
cliente chega, ele deve acordar o barbeiro para ser atendido, e se outros clientes
chegarem enquanto o barbeiro estiver ocupado, eles se sentarão nas cadeiras (se houver
cadeiras vazias) ou sairão da barbearia. Trata-se de um problema de sincronização de
processos, para que barbeiro e cliente não entrem em condições de disputa. Este é um
problema que representa uma situação de fila, onde o recurso (o barbeiro) acessa uma
fila limitada (as cadeiras de espera) e os processos (clientes) entram nessa fila, desde
que essa não esteja cheia.
Figura 7.3. A mesa de almoço dos filósofos
_________________________________________________________________
97
7.10. Resumo
Em sistemas operacionais modernos, é comum ter vários processos que precisam
se comunicar entre si. Em diversas situações estes processos podem compartilhar
posições de memória, a partir dos quais realizam operações de leitura e escrita de dados.
Eventualmente surgem situações de disputa, onde dois ou mais processos querem fazer
uso simultâneo de um recurso compartilhado, o que pode levá-los a condições de
conflito.
São implementados mecanismos para garantir que não existam conflitos de
acesso a regiões críticas, que são trechos de seus algoritmos onde são feitos acessos a
áreas compartilhadas de memória, através de mecanismos de controle como semáforos,
mutexes e monitores, dentre outros.
Leitura Obrigatória: Capítulo 2 do livro “Sistemas Operacionais Modernos”, 2ª
edição, TANEMBAUM, A., até o item 2.4 (inclusive)
Leitura recomendada: Capítulo 7 do livro “Sistemas Operacionais com Java”, 6ª
edição, SILBERSCHATZ, A.
_________________________________________________________________
98
Capítulo 8
Deadlocks
Em ambientes multiprogramados, diversos processos podem competir por
recursos do sistema. Quando um processo pede por recursos que não estão disponíveis,
o processo entre em estado de espera. Pode ocorrer que o processo nunca mude de
estado, porque os recursos pedidos por ele estão sempre alocados a outros processos.
Esta situação é chamada de deadlock. Neste capítulo serão descritos alguns métodos
utilizados em um sistema operacional para tratar o problema de deadlock.
_________________________________________________________________
99
8.1. Modelo do Sistema
Um sistema computacional consiste de um número finito de recursos, que
podem ser distribuídos entre diversos processos. Os recursos podem ser classificados
em vários tipos, cada um contendo uma ou mais instâncias. Como exemplos de tipos de
recursos, temos os ciclos de CPU, espaço de memória, arquivos, dispositivos de entrada
e saída de dados, dentre outros. Se um sistema possui dois discos, então o tipo de
recurso tem duas instâncias.
Recursos computacionais podem ser classificados como preemptivos, caso ele
possa ser retirado do processo proprietário sem causar nenhum tipo de prejuízo, como o
travamento do processo em si ou até do S.O.; já um recurso não-preemptivo não pode
ser retirado do atual processo proprietário sem que ocorra algum tipo de falha, por
exemplo, uma impressora imprimindo um documento de um processo PA, se for alocada
para um processo PB antes do término da impressão corrente, o processo de impressão
não será corretamente completado, deixando um documento impresso apenas
parcialmente.
Um processo utiliza um recurso através da seguinte sequência de eventos:
pedido, uso e liberação. Todos estes eventos são realizados através de chamadas ao
sistema. Desta forma, o sistema operacional é o responsável por manter uma tabela com
informações sobre quais recursos estão livres, quais estão alocados, e, se alocados, para
qual processo. Se um processo pede por um recurso que no momento se encontra
alocado a outro processo, ele pode ser adicionado a uma fila de espera por aquele
recurso.
A aquisição de certos recursos deve ser feita e gerenciada pelos próprios
processos dos usuários. Uma técnica comum de ser utilizada é através do uso de
semáforos, discutidos anteriormente, um para cada recurso. No caso de um processo
precisar de dois ou mais recursos, eles devem ser adquiridos sequencialmente. A partir
do momento em que existem dois ou mais processos competindo pelos mesmos
recursos do sistema, estamos diante de uma situação de um possível deadlock.
Para compreender melhor os métodos utilizados para tratar o problema de
deadlock, é necessário defini-lo e caracterizá-lo.
Definição: um conjunto de processos está em um estado de deadlock quando todos os
processo do conjunto estão esperando por um evento que só pode ser causado por outro
processo do conjunto. A figura 8.1(a) ilustra um cruzamento de veículos automotivos,
em uma situação onde pode ocorrer um deadlock no sistema, enquanto a figura 8.1(b)
ilustra a ocorrência do deadlock propriamente dito.
_________________________________________________________________
100
Figura 8.1. Um possível deadlock (a) e a ocorrência de um deadlock (b).
Caracterização: uma situação de deadlock ocorre se e somente se as quatro condições
seguintes são mantidas no sistema simultaneamente: mutual exclusion (exclusão
mútua), hold and wait (espera por recursos), no preemption (ausência de preempção) e
circular wait (espera circular). A seguir estão definidas estas quatro condições:
• Exclusão mútua: em um determinado instante, cada recurso está em uma de
duas situações: ou associado a um único processo ou disponível.
• Espera por recursos: processos que, em um determinado instante, retêm
recursos concedidos anteriormente podem requisitar novos recursos.
• Sem preempção: recursos concedidos previamente a um processo não podem
ser forçosamente tomados deste processo – eles devem ser explicitamente
liberados pelo processo que os retém.
• Espera Circular: Deve existir um conjunto de processos {p0, p1, ...pn} tal que
p0 espera por um recurso que está sendo usado por p1, p1 espera por um recurso
que está sendo usado por p2 e assim por diante, até que pn espera por um
recurso que esta sendo usado por p0.
Existem quatro estratégias possíveis para tratar o problema do deadlock, que
podem ser utilizados com diferentes graus de sucesso e complexidade de tratamento,
são elas:
•
15
Ignorar o problema. Se você ignorar a possibilidade de deadlock, talvez ele
ignore você; este método também é conhecido como ‘Algoritmo do Avestruz15’
(enterre a cabeça na areia e finja que nada está acontecendo); método desprezado
pelos matemáticos, mas aceito por engenheiros, caso a ocorrência de deadlocks
no sistema seja baixa; métodos de prevenção podem diminuir a performance do
sistema.
É sério, este é o nome do método, que é adotado por diversas versões de Windows e Unix.
_________________________________________________________________
•
•
•
101
Detecção e recuperação. Neste caso, os deadlocks poderão ocorrer, e neste caso,
deve haver um método para detectá-los e tomar alguma ação;
Anulação dinâmica, por meio de uma alocação cuidadosa de recursos;
Prevenção, negando estruturalmente uma das quatro condições necessárias para
gerar um deadlock.
Em 1972, Richard Holt provou que as quatro condições necessárias para a
ocorrência de deadlock podem ser modeladas utilizando grafos dirigidos, com dois tipos
de nodos: processos, que são representados através de círculos, e recursos, simbolizados
como quadrados. Um arco de um recurso ( ) para um processo (ο) indica que o recurso
foi previamente requisitado, alocado e está sendo usado pelo processo que o solicitou.
Já um arco de um processo (ο) para um recurso ( ) indica que o processo está no estado
de bloqueado, esperando pelo recurso relacionado.
A figura 8.2(a) ilustra um recurso R alocado a um processo A, a figura 8.2(b)
ilustra um processo B, aguardando por um recurso S, e em 8.2(c) ilustra um deadlock,
onde o processo D já tem alocado o recurso T e necessita do recurso U para continuar,
ao mesmo tempo que o processo C tem o recurso U alocado, mas não pode continuar
sua execução (e eventualmente, liberar este recurso), pois também precisa do recurso T.
A presença de um deadlock em uma representação de alocação de recursos no formato
de um grafo pode ser identificada através da presença de ciclos.
Figura 8.2. (a) processo de posse de um recurso; (b) processo aguardando por um recurso; (c)
deadlock.
8.2. Prevenção de Deadlock
Podemos prevenir uma situação de deadlock garantindo que pelo menos uma das
quatro condições necessárias para a ocorrência de deadlock não aconteça. Assim,
examinaremos cada uma delas separadamente.
Exclusão mútua
A condição de exclusão mútua é mantida para tipos de recursos que não podem
ser compartilhados (por exemplo, uma impressora). Já os recursos que podem ser
compartilhados não necessitam de acesso mutuamente exclusivo, e, portanto, não
_________________________________________________________________
102
causam deadlock (arquivo somente de leitura). Um processo nunca espera por um
recurso que pode ser compartilhado. Em geral não é possível prevenir deadlock negando
a condição de exclusão mútua.
Espera por recursos
Para que a condição hold and wait nunca se mantenha no sistema, devemos
garantir que sempre que um processo peça por um recurso, ele não mantenha qualquer
outro recurso. Um protocolo que pode ser utilizado requer que cada processo peça e
receba todos os recursos necessários antes que inicie a execução. Este fato provoca a
subutilização dos recursos.
Sem preempção
Para que a ausência de preempção não ocorra podemos utilizar vários
protocolos. Se um processo está mantendo um recurso e está pedindo por outro recurso
que não pode ser dado a ele imediatamente, isto é, o processo deve esperar então todos
os recursos que ele está mantendo devem ser liberados. O processo deve então
posteriormente pedir pelo recurso novo e por todos aqueles que foram liberados. Num
outro protocolo, se um processo pede alguns recursos, ele primeiro deve verificar se os
recursos estão disponíveis. Se estiverem, alocá-os. Caso contrário, ele deve verificar se
eles estão alocados a outros processos que estão esperando por recursos adicionais. Se
estiverem, retira os recursos desejados do processo que está esperando e alocá-os ao
processo requisitante. Se os recursos não estão mantidos por outros processos em estado
de espera e nem estão disponíveis, então o processo deve esperar.
Um processo só é reiniciado se ele ganha o recurso que está sendo pedido e recupera
qualquer outro recurso que venha a ter perdido.
Espera circular
Para que a espera circular não ocorra é imposta uma ordenação linear de todos os
tipos de recursos. É atribuído a cada recurso um número inteiro único, que permite a
comparação entre os recursos e determinar qual recurso precede outro na ordenação.
Num protocolo para prevenir deadIock cada processo pode pedir somente recursos
numa ordem crescente de numeração. Isto é, um processo pode pedir inicialmente um
tipo de recurso ri, após isto, o processo pode pedir um novo recurso rj se e somente se
F(rj) >F(ri).
8.3 Evitar DeadIock
Para evitar deadlock, em um sistema onde se permita que possam ocorrer as
quatro condições que podem levar a uma situação de deadlock, é necessário ter alguma
informação adicional sobre como cada processo pedirá os recursos. Existem vários
modelos que diferem quanto à quantidade de informações fornecidas. O modelo mais
útil e simples necessita que cada processo declare a priori o número máximo de
recursos de cada tipo que ele poderá necessitar. Dada a informação a priori é possível
construir um algoritmo que evite a situação de deadlock. Este algoritmo examina
dinamicamente o estado da alocação de recursos para garantir que nunca haverá espera
circular. O estado da alocação de recursos é definido pelo número de recursos alocados
_________________________________________________________________
103
e disponíveis, e a demanda máxima dos processos. Um estado é seguro se o sistema
pode alocar recursos para cada processo (até o seu máximo) em alguma ordem e ainda
evitar deadlock
Formalmente, um sistema está em um estado seguro somente se existe uma
sequência segura. Uma sequência de processadores p0, p1, ..., pn é uma sequência
segura para cada pi se os recursos que pi ainda pode pedir podem ser satisfeitos pelos
recursos disponíveis mais os recursos mantidos por todos os pj, com j < i. Quando um
processo pede um recurso que está disponível, o sistema deve decidir se o recurso pode
ser imediatamente alocado ou se o processo deve esperar. O pedido é garantido somente
se leva o sistema a um estado seguro. Um algoritmo clássico utilizado para evitar
deadlock foi estabelecido por Dijkstra e Habemann em 1965, e é comumente conhecido
como ‘algoritmo do banqueiro’.
O algoritmo possui esse nome por tratar do problema através da seguinte
analogia: um banqueiro (o S.O.) de uma pequena cidade pode negociar com um grupo
de clientes (os processos), aos quais ele libera linhas de crédito (que são recursos do
sistema). A função do algoritmo é determinar se a liberação de uma requisição é capaz
de levar o sistema a um estado inseguro; se for este o caso, o banqueiro nega a
requisição.
No caso do algoritmo tratar de um único tipo de recurso, para saber se um estado
é seguro ou não, o banqueiro leva em consideração cada solicitação de empréstimo. Ele
verifica se dispõe de recursos suficientes para atender algum dos clientes. Caso positivo,
ele assume que os empréstimos feitos a este cliente serão pagos (devolvidos); em
seguida, é considerado o cliente mais próximo do limite de recursos e assim por diante.
Se todos os empréstimos puderem ser pagos (devolvidos), o estado é considerado
seguro e a requisição inicial pode ser atendida.
A seguir são ilustradas três situações da alocação de recursos em um sistema.
Em (a), nenhum dos 10 recursos estão alocados a nenhum processo, porém, esta
quantidade é suficiente para atender individualmente a qualquer um deles, portanto, é
um estado seguro; em (b), houve uma distribuição de 8 dos 10 recursos inicialmente
disponíveis; por enquanto, nenhum processo tem os recursos necessários, porém, caso
os dois recursos disponíveis sejam alocados para C, ele irá executar, e no seu término
irá liberar os recursos, permitindo que B ou D entrem em execução logo em seguida,
portanto, também é um estado seguro.
Finalmente, em (C) está representada uma situação em que a alocação de
recursos empregada levou a um estado inseguro. Com apenas mais um recurso livre,
não importa a qual processo ele será alocado, pois nenhum terá a quantidade de recursos
necessários para a execução e posteriormente, a liberação dos recursos.
Apesar de este exemplo ilustrar uma situação de estado seguro que levou a um
deadlock, não podemos afirmar isto para qualquer situação, pois é considerada que a
diferença entre um estado seguro e um inseguro é que o primeiro pode garantir que
todos os processos terminarão; já no estado inseguro, esta garantia não pode ser dada.
Por melhores que sejam as intenções, evitar completamente deadlocks em um sistema é
praticamente impossível, por isso a maior parte dos esforços concentram-se na
prevenção e recuperação do sistema.
_________________________________________________________________
104
No caso de múltiplos recursos, o algoritmo do banqueiro também pode ser
utilizado, o exemplo a seguir ilustra as matrizes C e R e os vetores de recursos
existentes (E), alocados (P) e disponíveis (A).
Para determinar se um estado é seguro, o algoritmo do banqueiro executa os
seguintes passos:
1. Procure uma linha, R, cujas necessidades de recursos sejam todas inferiores ou
iguais a A. Caso não exista uma linha com estas características, o sistema
eventualmente entrará em deadlock, pois nenhum processo pode ser executado
até a sua conclusão;
2. Considere que o processo da linha escolhida requisita todos os recursos de que
precisa (o que garante ser possível) e termina. Marque esse processo como
terminado e adicione ao vetor A todos os recursos que lhe pertenciam;
3. Repita os passos 1 e 2 até que todos os processos sejam marcados como
terminados, que é o caso seguro, ou até ocorrer um deadlock, que é o caso
inseguro.
Apesar de o algoritmo ser considerado ótimo em teoria, dificilmente é utilizado
por sistemas operacionais na prática, pois raramente um processo sabe antecipadamente
prever todos os recursos que irá necessitar durante a sua execução, e que podem variar
dinamicamente à medida que novos processos de usuários sejam criados e terminados.
_________________________________________________________________
105
8.4. Detecção de Deadlock
Se o sistema não possuir algum protocolo que garanta a não ocorrência de
deadlock então deve ser implementado algum esquema que detecte e recupere o
sistema.
8.4.1. Detecção
Um algoritmo que examina o estado do sistema deve ser invocado
periodicamente para determinar se ocorreu deadlock. Para isso, o sistema deve:
• Manter informações sobre a alocação corrente de recursos pelos processadores e
quaisquer pedidos de alocação de recursos pendentes.
• Fornecer um algoritmo que utilize estas informações para determinar se o
sistema entrou num estado de deadlock.
Note que o esquema de detecção e recuperação possui overheads que incluem os
custos de tempo de execução para manter as informações necessárias a execução do
algoritmo de detecção e as perdas potenciais na recuperação do deadlock.
Como dito anteriormente, no caso de existir apenas um recurso de cada tipo, a
detecção de deadlocks pode ser feita através da presença de ciclos no grafo de alocação
de recursos. Caso existam vários elementos no sistema que forneçam o mesmo recurso
(por exemplo, duas unidades de disco; caso uma esteja ocupada, a outra pode atender as
solicitações de um processo da mesma maneira que a outra unidade atenderia), será
necessário outro método para a detecção de deadlocks. Serão utilizados os seguintes
elementos:
• O vetor E, que representa o número total de recursos existentes de uma
determinada classe i, (1 ≤ i ≤ m);
• O vetor A, que representa o número total de recursos disponíveis (isto é, não
alocadas no momento a nenhum processo) de recursos de uma determinada
classe neste instante;
• A matriz de alocação atual, C, onde a i-ésima linha da matriz informa quantas
instâncias de cada classe de recurso o processo Pi possui no momento;
• A matriz de requisições R, que indica quantas instâncias de um recurso j que Pi
precisa.
O exemplo a seguir ilustra o uso de vetores e matrizes na detecção de deadlocks.
Considere que o sistema possui quatro unidades de fita (UF), dois plotters (P), três
scanners (S) e uma unidade de CD-ROM:
_________________________________________________________________
106
Inicialmente, os processos P1 e P2 não podem ser executados, pois eles
demandam de uma unidade de CD-ROM e um scanner, respectivamente, que no
momento não estão disponíveis. Porém, o processo P3 pode ser satisfeito com os
recursos disponíveis, entra em execução e ao terminar, libera todos os recursos
alocados, o que permitirá ao processo P2 entrar em execução e, ao terminar, irá liberar
os recursos necessários para que P1 entre em execução, portanto, não existe deadlock no
sistema.
Usando o mesmo raciocínio, suponha agora que além das duas unidades de fita e
do plotter, o processo P3 necessite de um CD-ROM, o que acontecerá?
8.4.2. Recuperação
A recuperação de uma situação de deadlock deve ocorrer sempre que ela for
detectada. Existem três operações que recuperam o sistema, baseado nas quatro
condições que levam ao deadlock A primeira seria violar a exclusão mútua, o que não
pode ser feito. A segunda é abortar um ou mais processos para quebrar a espera circular
e trazê-los de volta ao sistema em um tempo posterior. A terceira é retirar alguns
recursos de um ou mais processos em deadlock. Assim, se utilizarmos a segunda ou a
terceira operação, devem ser considerados três pontos:
• Seleção de uma vítima. Quais recursos de quais processos devem ser retirados?
• Rollback: Se retirarmos um recurso de um processo, o que deveríamos fazer com
ele' Devemos retornar a execução até atingir um estado seguro?
• Starvation: Como garantir que o starvation não ocorrerá? Isto é, como garantir
que um mesmo processo não será sempre o escolhido para ser retirado do
sistema?
Seleção de um processo como vítima
Dado um conjunto de processos, a escolha de uma vítima é de ordem econômica.
Devemos retornar até um estado seguro os processos, com um custo mínimo. Os fatores
que podem determinar este custo são:
• Prioridade do processo;
_________________________________________________________________
107
• Quanto tempo o processo já completou e o quanto ainda falta para terminar a sua
execução.
• Quantos e quais tipos de recursos o processo usou até o momento?
• Qual a quantidade de processos envolvidos no rollback?
Rollback
Uma vez decidido qual o processo que sofrerá o rollback, devemos determinar o
quanto este processo deve retornar na sua execução. A solução mais simples é retornar
tudo, isto é, parar o processo e reiniciá-lo, porém esta solução é a mais custosa. Outra
solução é retorná-lo o suficiente para quebrar o deadlock, porém isto necessita que o
sistema mantenha mais informações sobre o estado de todos os processos que estão
executando.
Starvation (inanição)
Alguns sistemas podem evitar o deadlock através de uma política que defina
quais processos devem ganhar um recurso e em qual momento, porém, este processo
pode ter suas solicitações negadas indefinidamente, causando a starvation. Deve-se
garantir que um processo só possa ser escolhido como vítima um número limitado de
vezes. Desta forma, impedimos que ocorra starvation.
_________________________________________________________________
108
8.5. Resumo
Deadlocks representam um potencial problema para sistemas operacionais, pois
estão relacionados com a alocação de recursos do sistema aos processos do usuário. Na
ocorrência de uma situação onde um processo obtém acesso exclusivo a alguns recursos
e necessita de recursos alocados a outros processos e assim por diante, gerando uma
situação de impasse, está caracterizado um deadlock.
Deadlocks são difíceis de ser evitados por questões de sua própria natureza,
porém, existem métodos que podem ser utilizados para previnir a sua ocorrência e caso
aconteçam, corrigir o sistema.
Leitura Obrigatória: Capítulo 3 do livro “Sistemas Operacionais Modernos”, 2ª
edição, TANEMBAUM, A.
Leitura recomendada: Capítulo 8 do livro “Sistemas Operacionais com Java”, 6ª
edição, SILBERSCHATZ, A.
Download

UFF Sistemas Operacionais