Sistemas Operacionais

Revisao para a Segunda Avaliacao

Professor: Gabriel Soares Baptista

Objetivo da Revisao

Esta aula revisa os tres blocos principais da segunda avaliacao:

  1. IPC, condicoes de corrida e exclusao mutua
  2. Gerencia do processador e escalonamento
  3. Impasses: deteccao, prevencao e algoritmo do banqueiro

A ideia e sair daqui sabendo reconhecer os problemas, calcular exemplos pequenos e justificar respostas no estilo da prova.

Como Estudar Para Esta Prova

O conteudo exige tres tipos de habilidade:

Tipo Exemplo
Conceitual explicar por que contador++ nao e atomico
Interpretacao de codigo identificar uma condicao de corrida ou deadlock
Calculo montar linha do tempo de escalonamento ou sequencia segura

Nao basta decorar nomes de algoritmos. A prova tende a cobrar o comportamento do sistema em uma situacao concreta.

Parte 1: IPC

Comunicacao entre Processos

Processos possuem espacos de enderecamento separados. Para cooperarem, precisam de mecanismos de IPC.

Exemplos comuns:

  • pipes: fluxo unidirecional de bytes
  • memoria compartilhada: varios processos acessam a mesma regiao
  • sockets: comunicacao local ou em rede
  • filas de mensagens: troca estruturada de mensagens

O IPC resolve a troca de dados, mas tambem cria o problema da sincronizacao.

Pipe: Ideia Central

Um pipe conecta uma ponta de escrita a uma ponta de leitura.

int fd[2];
pipe(fd);

if (fork() == 0) {
    close(fd[1]);
    read(fd[0], buffer, sizeof(buffer));
} else {
    close(fd[0]);
    write(fd[1], "mensagem", 9);
}

Perguntas importantes:

  • quem escreve?
  • quem le?
  • o que acontece se ninguem escrever?
  • por que fechar descritores nao usados?

Memoria Compartilhada

Memoria compartilhada costuma ser mais rapida que pipes porque evita copiar dados pelo kernel a cada mensagem.

Mas ela traz um risco:

Se dois processos ou threads modificam a mesma estrutura ao mesmo tempo, o resultado pode depender da ordem de escalonamento.

Isso e uma condicao de corrida.

Condicao de Corrida

Uma condicao de corrida ocorre quando:

  1. existem dados compartilhados
  2. mais de um fluxo acessa esses dados
  3. pelo menos um acesso modifica os dados
  4. o resultado depende da ordem de execucao

Exemplo classico:

contador++;

Parece uma operacao unica, mas normalmente envolve carregar, incrementar e salvar.

Exemplo de Race Condition

#include <pthread.h>
#include <stdio.h>

long contador = 0;

void* incremento(void* arg) {
    for (int i = 0; i < 500000; i++) {
        contador++;
    }
    return NULL;
}

Se duas threads executam isso, o valor esperado seria 1000000.

Mas o valor final pode ser menor, porque atualizacoes se perdem.

Onde a Atualizacao se Perde?

Imagine contador = 10.

Passo Thread A Thread B
1 le 10
2 le 10
3 calcula 11 calcula 11
4 salva 11
5 salva 11

Duas incrementacoes ocorreram, mas o contador aumentou apenas uma unidade.

Regiao Critica

Regiao critica e o trecho de codigo que acessa recurso compartilhado e precisa ser protegido.

// regiao critica
contador++;

O objetivo da exclusao mutua e garantir que apenas uma thread ou processo execute a regiao critica por vez.

Mutex

Mutex e uma trava para exclusao mutua.

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void* incremento(void* arg) {
    for (int i = 0; i < 500000; i++) {
        pthread_mutex_lock(&lock);
        contador++;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

Com o mutex, a operacao composta fica protegida.

Semaforos

Semaforos generalizam a ideia de controle de acesso.

Operacoes classicas:

  • down, P ou wait: tenta consumir uma permissao
  • up, V ou signal: devolve uma permissao

Usos comuns:

Caso Mecanismo
proteger estrutura compartilhada mutex
contar vagas em buffer semaforo
contar itens produzidos semaforo

Produtor-Consumidor

O produtor-consumidor combina mutex e semaforos.

sem_t empty; // vagas livres
sem_t full;  // itens disponiveis
pthread_mutex_t mutex;

// produtor
sem_wait(&empty);
pthread_mutex_lock(&mutex);
insere_item();
pthread_mutex_unlock(&mutex);
sem_post(&full);

O semaforo controla o estado do buffer. O mutex protege a estrutura interna do buffer.

Exercicio Rapido 1

No codigo abaixo, marque a regiao critica e diga qual problema pode ocorrer.

int saldo = 100;

void saque(int valor) {
    if (saldo >= valor) {
        saldo = saldo - valor;
    }
}
Reflita

O que acontece se duas threads chamarem saque(80) ao mesmo tempo?

Resposta do Exercicio 1

A regiao critica e o acesso conjunto a saldo:

if (saldo >= valor) {
    saldo = saldo - valor;
}

Problema possivel:

  • as duas threads podem ler saldo = 100
  • ambas concluem que o saque e permitido
  • o sistema permite dois saques de 80
  • o resultado viola a regra de negocio

Parte 2: Escalonamento

Escalonar e decidir quem usa a CPU, quando e por quanto tempo.

O escalonador tenta equilibrar:

  • responsividade
  • justica
  • vazao
  • tempo medio de espera
  • tempo medio de retorno
  • custo de chaveamento de contexto

Metricas de Escalonamento

Metrica Significado
Tempo de espera tempo parado na fila de prontos
Tempo de retorno termino menos chegada
Tempo de resposta primeira execucao menos chegada
Vazao processos finalizados por unidade de tempo
Utilizacao da CPU tempo fazendo trabalho util

$$T_{retorno} = T_{termino} - T_{chegada}$$

FCFS

First Come, First Served

Executa na ordem de chegada.

Vantagens:

  • simples
  • previsivel
  • pouca sobrecarga

Problema:

  • se um processo longo chega antes dos curtos, ocorre efeito comboio

FCFS: Conta Classica

Todos chegam em t=0 na ordem P1, P2, P3.

Processo Burst
$P_1$ 24 ms
$P_2$ 3 ms
$P_3$ 3 ms

Tempos de espera:

  • $P_1 = 0$
  • $P_2 = 24$
  • $P_3 = 27$

$$T_{espera}^{medio}=\frac{0+24+27}{3}=17\text{ ms}$$

SJF

Shortest Job First

Escolhe primeiro o processo com menor burst de CPU.

Quando todos chegam juntos, minimiza o tempo medio de espera.

Limites:

  • precisa conhecer ou estimar o burst
  • pode causar starvation de processos longos

SJF: Mesmos Processos

Processo Burst
$P_1$ 24 ms
$P_2$ 3 ms
$P_3$ 3 ms

Ordem SJF: $P_2, P_3, P_1$

Tempos de espera:

  • $P_2 = 0$
  • $P_3 = 3$
  • $P_1 = 6$

$$T_{espera}^{medio}=\frac{0+3+6}{3}=3\text{ ms}$$

SRTN

Shortest Remaining Time Next

E a versao preemptiva do SJF.

Se chega um processo novo com tempo restante menor que o processo atual, o atual e interrompido.

Bom para:

  • melhorar resposta de processos curtos

Custo:

  • mais chaveamentos de contexto
  • exige estimativa de tempo restante

Round-Robin

Cada processo recebe um quantum.

Se nao terminar dentro do quantum, volta para o fim da fila.

E muito usado como base de sistemas interativos.

Quantum curto Quantum longo
melhor resposta menos overhead
mais trocas de contexto mais parecido com FCFS
pode desperdicar CPU pode piorar interatividade

Round-Robin: Exemplo

Todos chegam em t=0, quantum 4 ms.

Processo Burst
$P_1$ 10 ms
$P_2$ 4 ms
$P_3$ 5 ms

Linha do tempo:

0   4   8   12  16  18  19
| P1 | P2 | P3 | P1 | P3 | P1 |

Terminos:

  • $P_2 = 8$
  • $P_3 = 18$
  • $P_1 = 19$

Prioridades e Aging

No escalonamento por prioridades, o processo mais prioritario roda primeiro.

Risco:

  • processos de baixa prioridade podem nunca executar
  • isso e starvation

Solucao comum:

  • aging: aumentar gradualmente a prioridade de quem espera demais

Exercicio Rapido 2

Considere os processos abaixo, todos chegando em t=0.

Processo Burst
$P_1$ 6 ms
$P_2$ 8 ms
$P_3$ 7 ms
$P_4$ 3 ms

Calcule o tempo medio de espera usando SJF.

Resposta do Exercicio 2

Ordem SJF:

$$P_4 \rightarrow P_1 \rightarrow P_3 \rightarrow P_2$$

Tempos de espera:

  • $P_4 = 0$
  • $P_1 = 3$
  • $P_3 = 3 + 6 = 9$
  • $P_2 = 3 + 6 + 7 = 16$

$$T_{espera}^{medio}=\frac{0+3+9+16}{4}=7\text{ ms}$$

Parte 3: Deadlocks

Deadlock ocorre quando um conjunto de processos fica bloqueado esperando eventos que apenas processos do mesmo conjunto podem causar.

Nao e apenas lentidao.

E ausencia estrutural de progresso.

Condicoes de Coffman

Para haver deadlock, quatro condicoes precisam ocorrer ao mesmo tempo:

  1. Exclusao mutua
  2. Posse e espera
  3. Nao preempcao
  4. Espera circular

Se quebrarmos qualquer uma delas, impedimos o deadlock.

Codigo com Deadlock

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void* threadA(void* arg) {
    pthread_mutex_lock(&lock1);
    sleep(1);
    pthread_mutex_lock(&lock2);
    return NULL;
}

void* threadB(void* arg) {
    pthread_mutex_lock(&lock2);
    sleep(1);
    pthread_mutex_lock(&lock1);
    return NULL;
}

O sleep(1) nao causa o deadlock sozinho. Ele apenas aumenta a chance de expor o problema.

Onde Estao as Condicoes?

No exemplo anterior:

Condicao Onde aparece
Exclusao mutua mutex so pode ter um dono
Posse e espera A segura lock1 e espera lock2
Nao preempcao o sistema nao toma mutex a forca
Espera circular A espera B, B espera A

Deteccao de Deadlock

Na deteccao, o sistema permite que o deadlock aconteca e depois procura ciclos ou estados sem progresso.

Estrategias comuns:

  • grafo de alocacao de recursos
  • wait-for graph
  • algoritmo matricial com recursos disponiveis, alocados e requisitados

Depois de detectar, o sistema precisa recuperar.

Deteccao Matricial

Quando existem varios tipos de recursos, o grafo pode ficar grande. Uma forma pratica de detectar deadlock e usar matrizes.

Estruturas principais:

  • Available: recursos livres agora
  • Allocation: recursos que cada processo ja possui
  • Request: recursos que cada processo ainda esta esperando

A pergunta da deteccao e:

Com os recursos livres agora, existe algum processo que consiga terminar e devolver o que possui?

Deteccao Matricial: Algoritmo

Passos:

  1. comece com Work = Available
  2. procure um processo ainda nao finalizado com Request <= Work
  3. se encontrar, finja que ele termina
  4. some Allocation desse processo em Work
  5. repita ate todos terminarem ou ate nao haver mais progresso

Se algum processo ficar sem conseguir terminar, ele esta envolvido em deadlock ou preso por falta de recursos que ninguem conseguira liberar.

Exemplo Passo a Passo da Deteccao

Considere quatro tipos de recurso: R1, R2, R3, R4.

$$Available = (2,1,0,0)$$

Processo Allocation Request
$P_1$ $(0,0,1,0)$ $(2,0,0,1)$
$P_2$ $(2,0,0,1)$ $(1,0,1,0)$
$P_3$ $(0,1,2,0)$ $(2,1,0,0)$

Request significa: o que o processo ainda esta esperando para conseguir prosseguir.

Passo 1: Procurar Quem Pode Terminar

Comecamos com:

$$Work = Available = (2,1,0,0)$$

Testamos cada processo:

Processo Request Cabe em Work?
$P_1$ $(2,0,0,1)$ nao, falta R4
$P_2$ $(1,0,1,0)$ nao, falta R3
$P_3$ $(2,1,0,0)$ sim

Entao simulamos $P_3$ terminando.

Passo 2: P3 Termina e Devolve Recursos

$P_3$ possui:

$$Allocation(P_3) = (0,1,2,0)$$

Ao terminar, ele devolve esses recursos:

$$Work = (2,1,0,0) + (0,1,2,0) = (2,2,2,0)$$

Agora marcamos $P_3$ como finalizado e repetimos o teste.

Passo 3: Procurar o Proximo

Agora:

$$Work = (2,2,2,0)$$

Testamos os restantes:

Processo Request Cabe em Work?
$P_1$ $(2,0,0,1)$ nao, falta R4
$P_2$ $(1,0,1,0)$ sim

Entao $P_2$ pode terminar.

Passo 4: P2 Termina e Devolve Recursos

$P_2$ possui:

$$Allocation(P_2) = (2,0,0,1)$$

Ao terminar:

$$Work = (2,2,2,0) + (2,0,0,1) = (4,2,2,1)$$

Agora $P_1$ tambem consegue terminar, pois:

$$Request(P_1) = (2,0,0,1) \leq (4,2,2,1)$$

Resultado da Deteccao

A ordem encontrada foi:

$$P_3 \rightarrow P_2 \rightarrow P_1$$

Como todos conseguem terminar nessa simulacao, o sistema nao esta em deadlock.

Ideia importante:

  • nao importa se essa sera a ordem real de execucao
  • importa que existe uma ordem possivel de termino
  • se nenhuma ordem completa existisse, os processos restantes seriam suspeitos de deadlock

E Se Ninguem Couber em Work?

Exemplo com um unico tipo de recurso:

Processo Allocation Request
$P_1$ 1 1
$P_2$ 1 1

$$Available = 0$$

Nenhum processo consegue receber o que pediu. Como ninguem termina, ninguem devolve recurso.

Nesse caso, a deteccao conclui que existe deadlock entre $P_1$ e $P_2$.

Recuperacao

Opcoes apos detectar um deadlock:

  1. preemptar algum recurso, se for possivel
  2. fazer rollback para um estado anterior
  3. abortar um ou mais processos

Na pratica, a escolha depende de custo, prioridade, risco e impacto para o usuario.

Prevencao

Prevencao muda as regras para tornar deadlock impossivel.

Condicao atacada Estrategia
Exclusao mutua transformar recurso em compartilhavel, quando possivel
Posse e espera pedir todos os recursos de uma vez
Nao preempcao liberar o que tem quando nao conseguir mais
Espera circular impor ordem global de aquisicao

Ordem Global de Locks

Se todas as threads pegam locks na mesma ordem, a espera circular nao fecha.

void* threadA(void* arg) {
    pthread_mutex_lock(&lock1);
    pthread_mutex_lock(&lock2);
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    return NULL;
}

void* threadB(void* arg) {
    pthread_mutex_lock(&lock1);
    pthread_mutex_lock(&lock2);
    pthread_mutex_unlock(&lock2);
    pthread_mutex_unlock(&lock1);
    return NULL;
}

Evitacao: Algoritmo do Banqueiro

O banqueiro evita entrar em estados inseguros.

Ele concede um pedido somente se, depois da concessao, ainda existir uma sequencia em que todos possam terminar.

Estruturas usadas:

  • Available: recursos disponiveis
  • Max: demanda maxima declarada
  • Allocation: recursos ja alocados
  • Need = Max - Allocation

Deteccao Matricial vs Banqueiro

Os dois algoritmos parecem parecidos porque ambos simulam processos terminando e devolvendo recursos.

A diferenca esta na pergunta feita:

Algoritmo Pergunta central Quando roda?
Deteccao matricial Ja existe deadlock agora? depois que recursos ja foram concedidos
Banqueiro Se eu conceder este pedido, o estado continuara seguro? antes de conceder um novo pedido

Deteccao e diagnostico. Banqueiro e controle preventivo por simulacao.

Diferenca nos Dados Usados

Na deteccao matricial, o sistema usa:

  • Available: o que esta livre agora
  • Allocation: o que cada processo ja possui
  • Request: o que cada processo esta pedindo agora

No banqueiro, o sistema tambem precisa conhecer a demanda maxima:

  • Max: maximo que cada processo pode pedir
  • Need = Max - Allocation: quanto ainda pode vir a pedir

Por isso o banqueiro e mais exigente: ele precisa que os processos declarem seu consumo maximo antecipadamente.

Diferenca no Resultado

Deteccao matricial:

  • se todos conseguem terminar, nao ha deadlock
  • se alguns nao conseguem terminar, esses processos estao em deadlock

Banqueiro:

  • se existe sequencia segura, o pedido pode ser concedido
  • se nao existe sequencia segura, o pedido deve esperar

Importante: estado inseguro no banqueiro nao significa deadlock imediato. Significa que, se o sistema continuar concedendo recursos daquele jeito, pode chegar a um deadlock.

Banqueiro: Como Verificar Seguranca

Passos:

  1. comece com Work = Available
  2. procure um processo com Need <= Work
  3. finja que ele termina
  4. some sua alocacao de volta em Work
  5. repita ate todos terminarem ou ate nao haver progresso

Se todos terminam, o estado e seguro.

Se nao ha sequencia completa, o estado e inseguro.

Exemplo de Banqueiro

Um unico tipo de recurso.

Processo Maximo Alocado Necessidade
$P_1$ 9 3 6
$P_2$ 4 2 2
$P_3$ 7 2 5

$$Available = 3$$

Sequencia segura:

$$P_2 \rightarrow P_3 \rightarrow P_1$$

Proximos Passos

Na proxima semana teremos o feriado e, em seguida, a Avaliacao C2. Vou disponibilizar no AVA uma atividade avaliativa semelhante ao esquema da prova, com questoes conceituais, interpretacao de codigo e exercicios de escalonamento e deadlocks para orientar a preparacao final.