Baixe o app para aproveitar ainda mais
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.
Compartilhar