Sistemas Operacionais

IPC e Sincronização

Professor: Gabriel Soares Baptista

Material Complementar

Acesse os códigos da aula (codes_aula_5_so.zip) para acompanhar os exemplos práticos.

Introdução

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.

Implementação de Threads (Revisão)

O local de gerenciamento das threads (usuário ou núcleo) define a latência e a capacidade de resposta da aplicação.

  • Threads no espaço do usuário: Uma biblioteca local gerencia o chaveamento rapidamente, sem o núcleo saber da existência das threads (sem transições de privilégio pesadas).
  • Desvantagem: Se uma única thread realiza chamada bloqueante (ex: leitura de disco), o núcleo suspende o processo inteiro.

Threads no Espaço do Núcleo e Abordagens Híbridas

  • Threads no espaço do núcleo: São gerenciadas diretamente pelo SO. Se uma bloqueia, o núcleo escalona outra do mesmo processo. Porém, a sincronização exige chamadas de sistema completas.
  • Abordagens Híbridas (Ativações/Upcalls): O núcleo fornece processadores virtuais à aplicação. Se uma thread bloqueia, um upcall avisa a biblioteca local para redistribuir o trabalho.
Reflita
Se uma thread no espaço do usuário bloqueia, o que acontece com as outras? Por que isso seria catastrófico em produção? O que o modelo de threads no núcleo faria diferente?

Comunicação entre Processos (IPC)

O IPC precisa resolver três problemas centrais:

  1. A transferência de dados entre espaços de endereçamento.
  2. A sincronização das atividades entre os processos.
  3. A manutenção da ordem correta de execução.

Mecanismos físicos comuns incluem a Memória Compartilhada (mais rápida, sem cópias pelo núcleo) e Pipes (fluxos unidirecionais de dados).

Comunicação via Pipe em C

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.

Reflita
No código do pipe, o que aconteceria se o pai nunca escrevesse nada no pipe? Por que os close() são necessários em ambos os processos?

Condições de Corrida (Race Conditions)

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.

Ilustração do Spooler de Impressão

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.

O Bug da Condição de Corrida

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.

Reflita
Por que 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?

Regiões Críticas e Exclusão Mútua

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.

As Quatro Condições Ideais

Qualquer solução de sincronização ideal deve obedecer a quatro condições:

  1. Exclusão Mútua Rígida (nunca dois processos na CR ao mesmo tempo).
  2. Independência de Hardware, funcionando sem supor o número de CPUs ou sua velocidade.
  3. Não-Bloqueio Externo, onde um processo fora da CR não impede outros de entrarem nela.
  4. Espera Limitada, garantindo que nenhum processo espere eternamente (ausência de starvation).

Exclusão Mútua com Espera Ocupada (Busy Waiting)

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:

  • Desabilitar Interrupções (perigoso para o usuário).
  • Alternância Estrita (viola a condição 3).
  • Solução de Peterson (1981): software puro para dois processos.
  • TSL (Test and Set Lock): suporte de hardware nativo que bloqueia o barramento para garantir atomicidade.

Solução de Peterson Completa

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.

Reflita
Por que essa solução, embora elegante, não é mais confiável em arquiteturas modernas? O que compiladores e processadores fazem que "quebra" Peterson?

Sono e Acordar (Sleep and Wakeup)

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 Que Vai Errado: A Janela de Vulnerabilidade

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:

  1. Consumidor testa count == 0, mas é interrompido antes do sleep().
  2. Produtor insere item e envia wakeup(consumer).
  3. O sinal é perdido pois o consumidor ainda não está dormindo.
  4. Consumidor retoma e dorme. Produtor logo encherá o buffer e também dormirá.

Semáforos e Mutexes

Semáforos de Dijkstra (1965)

Resolvem a perda de sinais. São variáveis inteiras que armazenam "sinais de acordar" acumulados. Operam via funções atômicas:

  1. DOWN (P): Decrementa se > 0. Se for 0, o processo dorme.
  2. UP (V): Incrementa e acorda um processo, se houver algum esperando.

Mutexes (Mutual Exclusion)

O Mutex é um semáforo simplificado (0 ou 1) com o conceito de "Dono". Ideal para proteger estruturas de dados.

Mutex para Exclusão Mútua

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.

Reflita
Se você esquecer de chamar o unlock, o que acontecerá com as outras threads que tentarem acessar o contador?

Sincronização Real: Produtor-Consumidor

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.

Reflita
Por que os semáforos sozinhos não são suficientes para proteger a integridade da estrutura de dados do buffer?

Problemas Clássicos de IPC

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).

Solução para os Filósofos

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.

Reflita
Como a hierarquia de recursos (sempre pegar o garfo de menor índice primeiro) impede a formação de um ciclo de espera?

Monitores e Abstrações de Alto Nível

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:

  • WAIT: Bloqueia e libera o monitor.
  • SIGNAL: Acorda um processo esperando (não tem memória!).

Próximos passos

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.