Professor: Gabriel Soares Baptista
Acesse os códigos da aula (codes_aula_5_so.zip) para acompanhar os exemplos práticos.
Gerenciar concorrência é um grande desafio nos SOs modernos. Processos e threads são fluxos independentes, mas precisam colaborar para realizar tarefas complexas.
Sistemas que dividem tarefas (como um servidor web) dependem da Comunicação entre Processos (IPC).
Sem sincronização, a concorrência gera dados corrompidos e travamentos imprevisíveis. Exploraremos como garantir uma troca de dados segura e eficiente.
O local de gerenciamento das threads (usuário ou núcleo) define a latência e a capacidade de resposta da aplicação.
O IPC precisa resolver três problemas centrais:
Mecanismos físicos comuns incluem a Memória Compartilhada (mais rápida, sem cópias pelo núcleo) e Pipes (fluxos unidirecionais de dados).
Pai e filho comunicando-se de forma síncrona. O filho aguarda no read() até receber os dados.
Arquivo de referência: 1_pipe_communication.c
Observe como o fork() cria o processo filho e como os descritores de arquivo do pipe são usados para enviar e receber a mensagem.
close() são necessários em ambos os processos?
Ocorrem quando processos competem por um recurso compartilhado e o resultado final depende da ordem de escalonamento da CPU. São falhas intermitentes e perigosas.
Analogia do Spooler de Impressão: Se o Processo A lê o próximo slot livre (ex: 7) mas é interrompido, o Processo B pode ler o mesmo slot, gravar seu arquivo e avançar o ponteiro. Quando A retoma, sobrescreve o arquivo de B na posição 7, causando perda de dados.
A falha ocorre pois operações como contador++ não são atômicas (envolvem carregar, incrementar e salvar na CPU). Interrupções no meio desse ciclo perdem a atualização.
Duas threads tentam incrementar um contador até 2 milhões, mas falham sistematicamente devido à falta de atomicidade.
Arquivo de referência: 2_race_condition.c
Compile e execute várias vezes. Note que o resultado final é quase sempre diferente de 2.000.000.
contador++, que parece uma única instrução, não é atômica? Em que ponto exato o escalonador poderia interromper a thread e causar a inconsistência?
Para evitar condições de corrida, isolamos o acesso a recursos compartilhados nas Regiões Críticas (CR).
O objetivo é a Exclusão Mútua: garantir que apenas um processo esteja na sua CR por vez.
Qualquer solução de sincronização ideal deve obedecer a quatro condições:
A Espera Ocupada retém o processo em um laço testando uma variável. Isso desperdiça CPU e pode causar Inversão de Prioridade.
Métodos históricos e Hardware:
A Solução de Peterson resolve a exclusão mútua via software para dois processos, combinando o conceito de "vez" com o "interesse".
Arquivo de referência: 3_peterson_solution.c
Analise as funções enter_region e leave_region. Note como a variável vez resolve empates quando ambos processos têm interesse.
A Espera Ocupada sofre de graves problemas de desperdício de CPU e Inversão de Prioridade.
Para solucionar isso, usam-se as primitivas sleep() (bloqueia o processo e libera a CPU) e wakeup() (acorda o processo).
O Problema do Produtor-Consumidor: O produtor insere itens em um buffer de tamanho fixo (dormindo se cheio) e o consumidor retira itens (dormindo se vazio).
O uso direto de sleep/wakeup cria uma condição de corrida devido à falta de atomicidade entre testar o estado e dormir.
Cenário do Deadlock:
count == 0, mas é interrompido antes do sleep().wakeup(consumer).Resolvem a perda de sinais. São variáveis inteiras que armazenam "sinais de acordar" acumulados. Operam via funções atômicas:
O Mutex é um semáforo simplificado (0 ou 1) com o conceito de "Dono". Ideal para proteger estruturas de dados.
Protegendo o contador global de condições de corrida de forma eficiente.
Arquivo de referência: 4_mutex_counter.c
Compare com o Exemplo 2. Note como as chamadas lock e unlock garantem o resultado exato de 2.000.000.
unlock, o que acontecerá com as outras threads que tentarem acessar o contador?
A arquitetura correta combina Mutex (proteção do buffer) com Semáforos (controle de fluxo de itens e vagas).
Arquivo de referência: 5_producer_consumer.c
Analise como os semáforos empty e full impedem que o produtor e o consumidor acessem o buffer em estados inválidos, enquanto o mutex garante exclusão mútua.
Filósofos Comensais: Cinco filósofos e cinco garfos. Exigem dois para comer. Se todos agirem simetricamente, ocorre Deadlock.
Solução: Quebrar a espera circular (ex: usando Hierarquia de Recursos).
A solução hierárquica garante que pelo menos um filósofo consiga comer, quebrando o ciclo de dependência.
Arquivo de referência: 6_dining_philosophers.c
Observe a lógica para evitar que todos os filósofos fiquem bloqueados simultaneamente.
Abstrações onde o próprio compilador assume o gerenciamento da exclusão mútua.
Regra de ouro: Apenas um processo ativo no monitor por vez.
Variáveis de Condição:
Na próxima aula: 6 - escalonamento.
Descobriremos como o S.O. decide quem ganha o privilégio de usar a CPU e quais algoritmos garantem justiça e fluidez.