Buscar

02-Processos-Part1

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 17 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 17 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 17 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

1 SO II 1 SO II 
2 Processo e Threads 
2.1 Processos 
 
Computadores podem fazer várias coisas ao mesmo tempo. 
Num sistema multiprogramado, a CPU troca de programa para 
programa, cada um executando por alguns milisegundos. 
Em um certo instante, a CPU executa apenas um programa por vez. 
Num período, vários programas são executados dando aos usuários a 
ilusão de paralelismo. 
Fala-se em pseudoparalelismo, em contraste com máquinas que 
tem 2 ou + processadores. 
 
2.1.1 O Modelo de Processos 
 
Neste modelo, todos o software que pode executar num certo 
momento num computador (incluindo o SO) é organizado em um 
certo número de processos sequenciais, ou só processos. 
 
Um processo é um programa em execução. 
Nesse conceito, inclui-se contador de programa, registradores e 
variáveis. 
Conceitualmente, cada processo possui sua própria CPU. 
Na prática, a CPU é chaveada rapidamente entre todos os processos 
(multiprogramação). 
Assim, usando apenas um contador de programa, é possível 
manter em execução uma quantidade considerável de processos. 
Pela velocidade da CPU e pelo pouco tempo que um processo fica 
esperando a CPU, os usuários têm a ilusão de que trabalham em 
paralelo (pseudoparalelismo). Acreditam que a máquina tem um 
contador de programa para cada processo. 
 
 
 
Por causa desse funcionamento, não é possível reproduzir a 
execução de um mesmo processo. Portanto, os processos não podem 
fazer suposições sobre o tempo em seu processamento. 
Ex.: ler um setor de disco leva 100 msegs. Vou ler 1000 setores, 
portanto vou gastar 100 segundos. 
 
Se houver necessidade de atender restrições críticas de tempo, outras 
medidas devem ser tomadas. 
 
2.1.2 Criação de processos 
 
4 eventos principais que causam a criação de processos: 
 Inicialização do sistema; 
 Um processo em execução chama uma system call de criação de 
processos; 
 Um usuário pede para criar um novo processo; 
 Início de um job batch. 
 
2 SO II 2 SO II 
Durante o boot de um sistema, vários processos são criados. Alguns 
deles são responsáveis por alguns serviços. 
Ex. email, impressão, etc. 
No Unix, esses processos são chamados de deamons. 
No Unix, o programa ps mostra todos os processos em execução. 
No Windows, CTRL-ALT-DEL uma vez tem a mesma função. 
No Windows 2000, é chamado um programa chamado task 
manager. 
No Unix, a única chamada de criação de processos é o fork. 
 
2.1.3 Término de processos 
 
Após a realização de seu trabalho, um processo deve terminar. 
Há 4 condições para encerramento de um processo: 
 Exit normal (voluntário) – o processo encerrou sua tarefa e 
termina; 
 Exit por erro (voluntário) – o processo descobre que não tem 
condições para continuar executando; 
 Erro fatal (involuntário) – em geral devido a um bug, o processo 
comete um erro de execução e é encerrado; 
o Em alguns casos, é possível informar que se quer tratar 
alguns desses erros. Nesse caso, o processo é 
interrompido e desviado para uma rotina de tratamento. 
 Morto por outro processo (involuntário) – quem mata deve ter 
autoridade para fazer isso (no Unix, função kill). 
 
2.1.4 Hierarquias de processos 
 
Em alguns sistemas, um processo pode gerar outros. Nesse caso, o 
1
o
. é o PAI e os demais são FILHOS. 
No Unix, um pai e todos os seus filhos formam um grupo de 
processos. 
Processo de inicialização do Unix: 
 No boot, um processo especial chamado init é disparado; 
 Quando ele inicia, ele lê um arquivo que diz quantos terminais 
existem; 
 Para cada um, é feito um fork de um novo processo (login); 
 Se alguém fizer um login OK, o programa login executa um 
shell naquele terminal; 
 O usuário pode disparar novos processos através do shell. 
No Windows, não há o conceito de hierarquia. 
Cada processo disparado é independente de seu pai. 
 
2.1.5 Estados de processos 
 
Há 3 estados possíveis para um processo: 
 Executando: usando a CPU neste instante; 
 Pronto: pode executar, mas não há CPU disponível; 
 Bloqueado: incapaz de executar pela espera de algum evento 
externo. 
 
 
 
 
3 SO II 3 SO II 
2.2 Threads 
 
2.2.1 O Modelo de Threads 
 
Cada processo possui seu contador de instruções. O caminho dentro 
do programa que o contador faz é chamado de thread of execution, 
ou simplesmente thread. 
Associado com uma thread, temos registradores (mantém os itens de 
trabalho atuais) e pilha (mantém o histórico de chamadas a 
procedimentos). 
Diferença entre processo e thread: 
 Processos são conjuntos de recursos agrupados para melhor 
gerenciamento; 
 Threads são unidades de execução organizadas para usar a CPU. 
 
 
 
Na figura, vemos em a) 3 processos, cada um com uma thread, e b) 
um processo com 3 threads. 
O conceito de threads acrescenta a capacidade de múltiplas linhas de 
execução para o mesmo processo. Até certo grau, essas linhas são 
independentes uma das outras. 
Até certo grau, um processo com múltiplas threads é similar a um 
computador com múltiplos processos. 
As threads de um processo compartilham seu espaço de 
endereçamento, arquivos abertos e outros recursos. 
Os processos de uma máquina compartilham memória, discos, 
impressoras, etc. 
Threads possuem algumas caracterísitcas dos processos, por isso são 
chamadas de lightweight processes. 
 
 
 
Processos diferentes são independentes (exceto pela hierarquia). 
Threads do mesmo processo, porém, não são. 
Como as threads compartilham o mesmo espaço de endereçamento 
do processo, possuem as mesmas variáveis globais, podem acessar 
itens de outras threads (incluindo stack). Assim, uma thread pode 
gravar sobre partes vitais de outras. 
 
Não há proteção entre threads porque: 
 É impossível! 
 Não deveria ser necessário! 
 
4 SO II 4 SO II 
Processos diferentes podem pertencer a usuários diferentes. Assim, 
são naturalmente concorrentes, quando não hostis. 
Já várias threads do mesmo processo pertencem ao mesmo usuário, 
que criou-as justamente para que cooperem, e não briguem entre si! 
 
Uma thread pode estar em um de vários estados possíveis: 
Executando, bloqueada, pronta, ou terminada. 
 
O mecanismo de funcionamento das threads é baseado em uma série 
de system calls: 
 Thread_create: permite criar novas threads indicando uma 
procedure qualquer; 
o A hierarquia nem sempre está presente, dependendo do 
sistema; 
o Pelo funcionamento das threads, quase não se faz 
necessário. 
 Thread_exit: a thread encerra e desaparece do sistema – não 
pode + ser escalonada; 
 Thread_wait: permite que uma thread espere pelo resultado de 
outra; 
 Thread_yield: permite a uma thread liberar a CPU para as 
demais. Isso é interessante porque não há timesharing entre 
threads. Portanto, se uma delas verifica que está gastando muita 
CPU é “polido” permitir que as demais também usem o 
processador. 
 
2.2.2 Uso de threads 
 
Múltiplas atividades podem ser realizadas dentro de uma aplicação. 
Algumas delas podem exigir o bloqueio do processo. 
Esse pode ser o argumento para se ter múltiplos processos para uma 
aplicação. 
Entretanto, pode-se necessitar de compartilhamento do mesmo 
espaço de endereçamento. 
Isso explica porque múltiplos processos podem não funcionar. 
 
Threads são fáceis de criar e destruir, já que não possuem recursos 
associados. 
Em alguns sistemas, criar uma thread é 100 vezes + rápido que criar 
um processo. 
 
Threads permitem ganhos de desempenho q uando realizam I/O. 
Uma thread pode bloquear enquanto aguarda um I/O. Nesse tempo, a 
CPU pode ser usada por outra. 
 
Threads podem ser muito úteis em sistemas com múltiplas CPUs, 
desde que o SO permite paralelismo entre as threads de um mesmo 
processo. 
 
P. ex. considere uma pessoa que está escrevendo um livro de cerca 
de 1000 páginas. Se ela disparar uma salva do livro, com uma única 
thread o processo ficaria bloqueado para comandos de teclado e 
mouse enquanto realiza o I/O necessário parasalvar o que foi feito 
até o momento. 
Com múltiplas threads, é possível realizar a salva e ainda aceitar 
novos comandos. 
Nesse caso, múltiplos processos não funcionam porque é necessário 
operar sobre o mesmo documento. 
 
Assim como esse, há inúmeros exemplos. 
5 SO II 5 SO II 
2.2.3 Implementando threads no espaço do usuário 
 
Há 2 modos de implementar threads: 
 No espaço do usuário; 
 No kernel. 
Também é possível um sistema híbrido. 
 
No espaço do usuário, o kernel não sabe nada sobre threads. Do 
ponto de vista do kernel, ele manipula processos com uma só thread. 
A 1
a
. vantagem desse método é que um pacote de threads no espaço 
do usuário pode ser implementado em SOs que não reconhece 
threads. 
 
 
 
Cada processo mentém um sistema run-time para suas threads e sua 
própria tabela de threads. 
Quando uma thread faz algo que causa o seu bloqueio, os 
registradores devem ser salvos na tabela de threads. O 
processamento é entregue a outra thread. 
Esse procedimento é + rápido que fazer um trap para o kernel. O 
escalonamento de threads é muito + rápido. 
Cada processo pode ter seu próprio algoritmo de escalonamento. 
 
Mas não há apenas vantagens. 
Se as system calls são bloqueantes, chamar uma delas numa thread 
significa bloquear todas. 
 
Uma possibilidade é a existência de um método que diga 
previamente se uma chamada qualquer irá bloquear. Assim, o teste é 
feito antes da chamada, tornando o procedimento + seguro. 
Porém, caso não haja I/O assíncrono uma chamada de I/O causa 
bloqueio até seu tratamento completo. 
Isso também ocorre no caso de page faults. 
 
Outro problema é que outras threads só podem usar a CPU se a atual 
liberar. 
Uma solução possível é o run-time solicitar uma interrupção de 
tempo do kernel. O problema com essa solução é que em geral o 
período é grande (1 seg.), isso é difícil de programar e o overhead é 
considerável. Uma thread também pode solicitar uma interrupção de 
tempo, o que pode atrapalhar a solicitação do run-time. 
 
Mas o argumento fundamental contra threads no espaço de usuário é 
que os programadores querem threads em geral para fazer chamadas 
bloqueantes. Nesse caso, não há vantagens nesse método. 
Não é interessante usar threads em aplicações CPU bound (p. ex. 
cálculos científicos, jogar xadrez, etc.). nesse caso, o overhead para 
manter o sistema não compensa. 
 
6 SO II 6 SO II 
2.2.4 Implementando threads no kernel 
 
Se o kernel sabe da existência das threads, não é necessário run-time 
nos processos. Não é preciso tabelas de threads por processo; o 
kernel mantém uma única para todas as threads do sistema. 
Todas as chamadas bloqueantes são implementadas como system 
calls, com um custo bastante alto. Porém, o bloqueio de uma thread 
permite passar o processador para outra thread do mesmo processo, 
ou de outro processo. 
 
Devido ao custo + alto de criação e destruição de threads, alguns 
sitemas usam um sistema de reciclagem: 
 As threads destruídas são apenas marcadas como não runnable; 
 Se uma nova thread precisa ser criada, uma das velhas é 
reativada; 
 Reciclagem pode ser feita em threads a nível de usuário, mas 
como o custo de criação é bem menor o ganho não é tão 
interessante. 
 
2.2.5 Implementações híbridas 
 
Nesse caso, o kernel pode criar threads num mesmo processo e 
escalonar cada uma individualmente. 
No topo de cada uma, um run-time pode criar e manter várias 
threads a nível de usuário. 
Esse sistema pode ter vantagens de ambas abordagens, mas é preciso 
ter cuidado para que não tenha as desvantagens de ambas! 
 
 
7 SO II 7 SO II 
2.3 IPC 
 
A comunicação entre processos é uma necessidade: 
 Num pipeline entre processos, a saída do 1o. deve ser passada 
para o 2
o
, etc. 
 Preferencialmente, o sistema deve ser estruturado e não baseado 
em interrupções. 
 
Há 3 questões principais: 
 A passagem de informações de um processo para outro; 
 Evitar que um processo fique no caminho do outro, 
atrapalhando-se mutuamente; 
 Respeito a dependências. 
 
Interessante que esses 3 itens se aplicam a threads também: 
 A passagem de informações é + simples, já que as threads do 
mesmo processo compartilham o mesmo espaço de 
endereçamento; 
o Se estiverem em processos diferentes, o problema cai na 
comunicação entre processos. 
 Evitar que as threads se atrapalhem mutuamente é o mesmo 
problema e tem as mesmas soluções; 
 A sequência apropriada também pode ser exigida em threads. 
 
2.3.1 Condições de corrida 
 
Processos que trabalham em conjunto podem compartilhar itens, que 
permitem leitura e/ou atualização. 
Esses itens podem ser mantidos em memória ou podem ser arquivos, 
etc. 
O uso compartilhado pode levar a interferências perigosas. P. ex.: 
 
 
 
 Considere um sistema onde se aplica a lei de Murphy; 
 Um sistema de impressão com spool possui um daemon e uma 
tabela com vários slots; 
 Há 2 variáveis, in que aponta para o próximo slot vago, e out 
que possui o nome do próximo arquivo a ser impresso; 
 Suponha que 2 processos decidem imprimir ao mesmo tempo; 
 O algoritmo de cada um é o seguinte: 
8 SO II 8 SO II 
Next_slot_vago = in; 
Tabela[next_slot_vago] = <nome do arq. A ser impresso>; 
Next_slot_vago++; 
In = next_slot_vago; 
 
 Suponha que A comece a processar e executa o 1o. comando; 
 Neste momento, o escalonador decide entregar o processador 
para B; 
 B executa o algoritmo todo, deixando um nome de arquivo na 
entrada 7 e atualizando in para 8; 
 Nesse ponto, A retoma sua execução e completa seu algoritmo, 
deixando outro nome de arquivo na entrada 7 (o arq. de B é 
perdido); 
 In é atualizado de novo para o valor certo – a consistência do 
sistema está OK, mas B teve seu processamento prejudicado. 
 
2.3.2 Regiões críticas 
 
Como se evitam condições de corrida? 
O que deve ser feito é proibir + de um processo de acessar recursos 
compartilhados ao mesmo tempo. 
Isso se chama exclusão mútua. 
O problema acima ocorreu porque o processo B usou in junto com o 
proc. A. 
 
O problema pode ser formulado de outro modo, também. 
Um processo executa certos trechos de código para realizar cálculos, 
ou outras atividades. Isso não representa risco de produzir corridas. 
Mas em certas situações, ele desvia para o código que trata dos 
recursos compartilhados. 
Esse código é chamado de região crítica ou seção crítica. 
Assim, o que precisamos é garantir que 2 ou + processos não entrem 
simultaneamente em suas regiões críticas. 
Mesmo essa garantia não é suficiente para evitar condições de 
corrida. É preciso satisfazer 4 condições para ter uma boa solução: 
1. 2 processos não podem estar simultaneamente dentro de suas 
regiões críticas; 
2. Não se pode presumir condições sobre a velocidade ou número 
de CPUs; 
3. Nenhum processo rodando fora da região crítica pode bloquear 
outro processo; 
4. nenhum processo deve esperar indefinidamente para entrar em 
sua região crítica. 
 
 
 
 
 
 
 
9 SO II 9 SO II 
2.3.3 Exclusão mútua com Busy Waiting (Espera Ocupada) 
 
Desabilitar interrupções 
 
Solução mais simples. O processo desabilita as interrupções antes de 
entrar na seção crítica e habilita de novo depois de sair. 
Não há interrupções por tempo. O processo não será interrompido, 
nem mesmo por I/O completado. 
Há 2 problemas principais: 
 O processo pode não habilitar as interrupções novamente – nesse 
caso, o sistema “morre”; 
 Num sistema com múltiplas CPUs, apenas a CPU onde o 
processo roda é afetada – as demais continuam com interrupções 
habilitadas e podem acessar o recurso compartilhado. 
 
Por sua vez, o kernel faz muito uso desse recurso, uma vez que ele 
possui muitas estruturas cuja manutenção é crítica. 
 
Variáveis de bloqueio (lock) 
 
2
a
. tentativa, uma solução por software. 
Um recurso compartilhado é controlado por uma variável única, 
também compartilhada, começando em ZERO.Se um processo quer acessar o recurso, testa a variável. Se ZERO, 
passa seu valor para UM e acessa o recurso. Se não, espera até se 
tornar ZERO. 
 
Este método tem o mesmo problema do exemplo do spool. A 
diferença é que, em vez de acessar a variável in diretamente, antes o 
acesso é na variável de lock. 
Alternação estrita 
 
 
 
Este método funciona (!). Quem não consegue entrar na região 
crítica fica testando continuamente até conseguir entrar. Este 
procedimento chama-se espera ocupada (busy waiting). 
Neste método, um processo entrega a região crítica para o próximo, 
independente de ele querer ou não. 
 
Problemas: suponha que (a) tem uma região não crítica muito 
pequena, enquanto a de (b) é grande. 
 Supondo que turn = 0 no início, (a) é o 1o. a usar a região crítica; 
 (a) passa turn para 1, executa sua região não crítica e quer entrar 
na região crítica novamente; 
 Como agora turn é 1, (a) entra no loop de espera; 
 Com turn 1, (b) entra na região crítica, passa turn para 0 e entra 
na região não crítica, que é demorada; 
 (a) consegue entrar novamente na região crítica, mas agora 
entrará em loop até que (b) saia de sua região não crítica. 
 
Esta situação viola a regra 3: o processo (a) está sendo bloqueado 
por outro fora de sua região crítica. 
 
10 SO II 10 SO II 
Solução de Peterson 
 
Em 1981, G. L. Peterson descobriu uma solução simples de obter 
exclusão mútua. 
 
 
 
Instrução TSL 
 
Este método faz uso de um recurso de hardware. 
Alguns processadores possuem uma instrução especial TSL (Test 
and Set Lock): 
 
TSL RX, LOCK 
Esta instrução lê o conteúdo da posição de memória LOCK para o 
registrador RX e armazena um valor não zero em LOCK. 
A operação toda é indivisível. O processo não pode ser interrompido 
e LOCK não pode ser acessado até a execução da instrução. 
O sistema de exclusão mútua desse método pode ser visto abaixo: 
 
 
 
2.3.4 Sleep e Wakeup 
 
A solução de Peterson e TSL são corretas, mas fazem uso de espera 
ocupada. 
Além de gastar CPU, há efeitos indesejados que podem ocorrer. 
Considere 2 processos, H com alta prioridade e L com baixa 
prioridade. As regras de escalonamento dizem que H ganha o 
processador sempre que estiver pronto. 
Suponha que L está na região crítica e H passa para pronto e quer 
entrar em sua região crítica. H ganhará o processador e entrará em 
loop. L não será mais escalonado. 
 
Para resolver este problema, foram criadas soluções que bloqueiam 
processos em vez de permitir que eles gastem CPU. 
 
11 SO II 11 SO II 
A mais simples é o par de funções sleep e wakeup. Sleep é uma 
função que o processo chama quando deseja bloquear. Wakeup é 
uma função que um processo chama para acordar um outro 
bloqueado. 
 
O problema produtor-consumidor 
 
 
 
Dois processos compartilham um buffer de tamanho fixo. Um deles 
é o produtor, que coloca informações no buffer. Outro é o 
consumidor, que retira as informações. 
É possível montar o modelo com m produtores e n consumidores, 
mas aqui será considerado apenas um de cada. 
Há duas situações problemáticas: 
 Quando o produtor tenta colocar um item no buffer e ele está 
cheio; 
 Quando o consumidor tenta retirar um item e o buffer está vazio. 
 
Associado ao buffer há uma variável count que mantém o número de 
elementos do buffer. Toda vez que um processo quer inserir ou 
retirar um item do buffer, deve testar o valor de count. 
Se o produtor quer inserir e o buffer está cheio, ele deve dormir. 
Quando o consumidor retira um item e verifica que o buffer estava 
cheio antes, ele infere que o produtor está dormindo e acorda-o. 
 
Este método não gasta CPU, mas apresenta problemas. Isso é devido 
à variável count, que é compartilhada e cujo acesso não é 
controlado. 
 
Considere a seguinte sequência de eventos: 
 Count é zero; 
 O consumidor vai testar seu valor, mas executa apenas a 
condição do IF; 
 Nesse momento, o escalonador decide entregar o processador ao 
produtor; 
 Ele produz um item, insere-o e incrmenta count; 
 Verifica que count era ZERO e acorda o consumidor; 
 Porém, ele não estava logicamente dormindo; 
12 SO II 12 SO II 
 Recuperando o processador, o consumidor chama sleep e dorme; 
 Voltando para o produtor, este produzirá item após item, 
enchendo o buffer e indo dormir também! 
 
Uma solução para este problema é adicionar um bit de wakeup 
waiting. 
Se um processo envia um wakeup para outro que ainda não está 
dormindo, o bit é setado. Posteriormente, quando o processo for 
dormir, com o bit setado ele é imediatamente acordado. 
Apesar de ser uma solução, para um sistema com + de 2 processos é 
preciso mais bits. Um por processo não chega a ser suficiente porque 
um processo pode ser acordado por + de um processo. 
 
2.3.5 Semáforos 
 
Em 1965, Dijkstra apresentou uma solução final para o problema. 
Ele sugeriu usar uma variável que seria um contador de wakeups, o 
semáforo. O valor ZERO indica que não há wakeups pendentes. Um 
valor positivo indica que 1 ou + wakeups estão pendentes. 
Junto com a variável, são propostas duas operações: down e up, 
generalizações de sleep e wakeup. 
 
A operação down testa o valor do semáforo. Se ele for maior que 
ZERO, ele é decrementado (um dos wakeups pendentes é usado). 
Se for ZERO, o processo é colocado para dormir. 
 
Tudo isso é feito num procedimento atômico, sem interrupções. 
Durante a operação, o semáforo não pode + ser acessado. 
 
A operação up incrementa o valor do semáforo. Se 1 ou + processos 
estavam dormindo neste semáforo, eles são acordados um a um pelo 
escalonador. A operação down interrompida é realizada, com o 
semáforo voltando a ZERO. Agora, um processo a menos está 
dormindo. 
 
O problema produtor-consumidor usando semáforos 
 
Primeiro, a implementação de up e down deve ser baseada em 
system calls. O SO desabilita interrupções para manipular o 
semáforo. Como isso precisa de apenas umas poucas instruções, não 
há problema. 
Se o sistema possui múltiplas CPUs, cada semáforo deve ser 
protegido por uma variável de lock, e uma instrução TSL deve ser 
usada para garantir que apenas uma CPU manipula o semáforo por 
vez. 
Notar que aqui o uso da TSL é bastante diferente da solução com 
espera ocupada. Tratar um semáforo requer poucas instruções, 
enquanto a atividade de um processo pode ser bem longa. 
 
Na solução abaixo, MUTEX é usado para exclusão mútua, enquanto 
os outros 2 semáforos controlam o número de slots vazios e 
ocupados do buffer. 
 
 
 
13 SO II 13 SO II 
 
 
 
 
 
2.3.6 Mutexes 
 
Se não há a necessidade de usar um semáforo como contador, uma 
versão simplificada chamada mutex pode ser usada. 
Um mutex é um semáforo binário (só pode assumir 1 ou 0). 
Duas funções são usadas com um mutex: 
 Mutex_lock: chamada quando um processo pretende entrar em 
sua região crítica; 
 Mutex_unlock: chamada quando o processo sai da região crítica. 
 
 
 
A vantagem desse código é que ele é executado inteiro no espaço do 
usuário, sem necessidade de interferência do kernel. 
Também não há espera ocupada. 
 
2.3.7 Monitores 
 
O uso de semáforos parece simples, mas há problemas técnicos 
interessantes. 
Considere a solução do produtor-consumidor. Se em qualquer um 
dos processos a ordem dos downs for invertida, o processo bloqueia 
14 SO II 14 SO II 
e retém o controle da região crítica, bloqueando também o outro 
(deadlock). 
Para evitar esse problema, Hoare e Brinch Hansen (1974 e 1975) 
fizeram a proposta de um sistema de sincronismo de alto nível, o 
monitor. 
Um monitor é um conjunto especial de procedimentos, variáveis e 
estruturas de dados, agrupados em um módulo especial ou pacote. 
Os processos podem chamar qualquer procedimento do monitor, 
mas não podem acessar suas variáveis internas diretamente. 
O compilador garante que apenas um processo estará ativo no 
monitor em um certomomento. Qualquer outro que chame um 
procedimento do monitor neste ponto será bloqueado. 
 
 
O monitor introduz um tipo especial de variável chamado condition. 
Também cria duas funções especiais: 
 Wait: usada quando um processo descobre que não pode 
continuar sua execução; 
 Signal: permite a um processo disparar a condição que outro está 
esperando. 
 
Como o signal é feito sobre uma variável condição interna ao 
monitor, ele só pode ser feito como última ação do processo antes de 
sair do monitor. Isso permite que o monitor seja liberado para o 
processo que é acordado. 
 
Condições não acumulam signals. Isso significa que, se não houver 
um processo esperando pelo wait ele é perdido. 
Mas o monitor oferece variáveis de status para que se teste a 
situação de um processo. Assim, o processo que pretende fazer um 
signal deve verificar se existe um processo correspondente em wait. 
 
As operações wait e signal parecem similares a sleep e wakeup. 
Na solução com sleep e wakeup, o wakeup podia ser perdido se o 
processo não estivesse esperando por ele. 
Com monitores, isso não acontece porque só um processo pode estar 
ativo num determinado momento. 
Assim, o produtor pode verificar que o buffer está cheio e entrar em 
wait sem ser interrompido. 
 
Uma linguagem atual que implementa monitores é o Java. A adição 
da palavra chave synchronized a um método garante que só uma 
thread pode chamar esse método em um determinado instante. 
15 SO II 15 SO II 
 
16 SO II 16 SO II 
Na figura anterior, podemos ver uma solução do problema produtor-
consumidor usando Java. 
Métodos sincronizados em Java diferem de monitores porque o Java 
não tem variáveis de condição. 
Há 2 procedimentos especiais, wait e notify, que são semelhantes a 
sleep e wakeup. A diferença é que wait pode ser interrompido, o que 
exige que seja protegido com uma cláusula try. 
Para evitar problemas, sua chamada pode ser colocada num método 
sincronizado. 
 
Como monitores são construções da linguagem, não são todas que 
apresentam esse recurso. P. ex. Pascal e C (C++) não possuem 
monitores. 
 
2.3.8 Passagem de mensagens 
 
Semáforos só funcionam porque há memória compartilhada entre os 
processos. Mesmo em sistemas com múltiplas CPUs, uma instrução 
TSL pode proteger o semáforo. 
Monitores são restritos porque nem todas as linguagens 
implementam esse recurso. 
Como fazer em SDs, onde a memória dos sistemas é distribuída 
(cada um possui a sua, sem comunicação entre elas)? 
 
A solução é o uso de passagem de mensagens. 
Esse método usa duas funções, send e receive. 
Essas funções podem ser implementadas em bibliotecas, portanto 
podem ser acrescentadas a qualquer linguagem sem problemas. 
 
Send(destino, $msg) envia uma msg para um destino qualquer. 
Receive(fonte, &msg) recebe uma mensagem de fonte (pode ser 
ANY). 
 
Há alguns problemas aqui que não existem nos outros métodos. 
Como as msgs são enviadas via rede, podem ser extraviadas. Isso 
exige confirmações (acknowledgements), estabelecimentos de 
relógios para timeouts e retransmissões. 
 
Como um ack pode ser extraviado, uma msgs pode ser enviada duas 
vezes. Isso exige também o uso de sequências para evitar 
duplicidade. 
 
Esse método exige um sistema de identificação dos processos, de 
forma que send e receive sejam não ambíguos. 
 
Dependendo do motivo da comunicação, pode-se exigir 
autenticação das partes envolvidas 
 
O receive pode ser bloqueante (se não houver msgs prontas para o 
processo, ele bloqueia (comunicação síncrona). 
Também é possível devolver um código de erro. Assim, o processo 
não fica bloqueado (comunicação assíncrona). 
 
Problema produtor-consumidor usando passagem de mensagens 
 
Nessa solução, o consumidor envia ao produtor N msgs vazias. 
Sempre que o produtor produzir um item, usa uma das msgs vazias 
para enviá-lo ao consumidor. 
 
 
17 SO II 17 SO II 
 
 
Uma solução para o sistema de passagem de msgs é o uso de caixas 
postais (mailbox). 
Uma caixa postal é um buffer que pode receber um certo número de 
msgs. As funções de send e receive endereçam as caixas postais (não 
os processos). 
Um processo precisa ter direito para acessar uma caixa postal. 
Se a caixa postal estiver cheia, o send de um processo bloqueia. Da 
mesma forma, se estiver vazia o receive de um processo bloqueia. 
 
Outra forma de implementar send e receive é sem buffer. Nesse 
caso, a transferência entre as duas funções é direta. Assim, para que 
um processo chame send o outro deve estar em receive. Se não, o 
processo bloqueia até que o seu par feche a comunicação. Também 
chamado de rendezvous. 
 
2.3.9 Barrier 
 
Uma barreira é um sistema de sincronização para grupos de 
processos. É voltada para aplicações que funcionam em fases.

Outros materiais