Professor: Gabriel Soares Baptista
Hoje veremos por que um sistema pode parar de progredir mesmo quando a sincronização parece correta.
Deadlock não é lentidão.
É um bloqueio estrutural em que processos ou threads ficam presos esperando uns pelos outros.
Ele surge quando recursos são adquiridos em ordens incompatíveis e ninguém consegue completar o que começou.
Imagine uma ponte estreita com um carro em cada direção.
Cada um já ocupa parte do caminho e precisa que o outro recue primeiro.
Se nenhum ceder, o sistema não fica apenas mais lento.
Ele fica travado.
Um conjunto de processos está em impasse quando cada processo do conjunto espera por um evento que apenas outro processo do mesmo conjunto pode causar.
Consequência:
Essa é a diferença entre um sistema ocupado e um sistema logicamente paralisado.
Deadlocks acontecem porque processos disputam recursos.
| Tipo | Descrição | Exemplos |
|---|---|---|
| Preemptível | Pode ser retirado e devolvido depois | CPU, páginas de memória |
| Não preemptível | Não pode ser tomado à força sem quebrar a execução | impressora, mutex, lock de banco |
O problema aparece quase sempre com recursos não preemptíveis.
Todo recurso passa por três etapas básicas:
O deadlock aparece quando diferentes processos executam esse ciclo em ordens incompatíveis.
| Fenômeno | O que acontece | Progresso? |
|---|---|---|
| Deadlock | espera circular permanente | não |
| Starvation | um processo é sempre preterido | sim, para os outros |
| Livelock | todos reagem, mas ninguém avança | atividade sem progresso útil |
Resumo:
| Estado | Significado |
|---|---|
| Seguro | existe uma sequência em que todos podem terminar |
| Inseguro | não há garantia formal de que todos terminarão |
| Deadlock | ninguém consegue prosseguir com os recursos disponíveis |
Estado inseguro não é deadlock imediato.
Ele significa que o sistema entrou numa região de risco.
Para haver deadlock, quatro condições devem existir ao mesmo tempo:
graph TD
A[Exclusao Mutua] --> D[Deadlock]
B[Posse e Espera] --> D
C[Nao Preempcao] --> D
E[Espera Circular] --> D graph TD
A[Exclusao Mutua] --> D[Deadlock]
B[Posse e Espera] --> D
C[Nao Preempcao] --> D
E[Espera Circular] --> D Deadlock não é qualquer espera longa.
Ele exige a presença simultânea das quatro condições.
Se você quebrar apenas uma delas, o ciclo não se fecha.
Na prática, quase toda estratégia de prevenção ataca exatamente esse ponto.
O erro clássico é adquirir locks em ordem oposta.
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
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);
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
return NULL;
}
void* threadB(void* arg) {
pthread_mutex_lock(&lock2);
sleep(1);
pthread_mutex_lock(&lock1);
pthread_mutex_unlock(&lock1);
pthread_mutex_unlock(&lock2);
return NULL;
}
O sleep(1) não cria o deadlock.
Ele apenas aumenta a chance de o escalonamento revelar o bug.
sequenceDiagram
participant A as Thread A
participant L1 as lock1
participant B as Thread B
participant L2 as lock2
A->>L1: lock(lock1)
B->>L2: lock(lock2)
A->>L2: espera por lock2
B->>L1: espera por lock1
Note over A,L2: Espera circular permanente sequenceDiagram
participant A as Thread A
participant L1 as lock1
participant B as Thread B
participant L2 as lock2
A->>L1: lock(lock1)
B->>L2: lock(lock2)
A->>L2: espera por lock2
B->>L1: espera por lock1
Note over A,L2: Espera circular permanente No exemplo anterior:
lock1 e lock2 são exclusivoslock1 e espera lock2lock2 e espera lock1Logo, o ciclo de espera fica completo.
No Resource Allocation Graph:
Quando focamos só em processos, removemos os nós de recurso e ligamos quem espera por quem.
Esse formato é muito usado em bancos de dados e ferramentas de diagnóstico.
Com múltiplas instâncias de um recurso, o ciclo deixa de ser prova suficiente.
Se algum processo puder terminar e devolver uma instância, o sistema ainda escapa.
| Estratégia | Ideia | Custo |
|---|---|---|
| Avestruz | ignorar | zero overhead direto |
| Detecção e recuperação | deixar acontecer e depois desfazer | monitoramento + rollback |
| Evitação | só conceder se o estado continuar seguro | alto custo analítico |
| Prevenção | quebrar uma condição de Coffman | menos flexibilidade |
Cada uma sacrifica algo diferente para reduzir risco.
Em sistemas gerais, muitas vezes o kernel não tenta resolver genericamente todo deadlock.
A lógica é pragmática:
flowchart LR
A[Requisicao de recurso] --> B{Vale monitorar formalmente?}
B -- Nao --> C[Concede e segue]
C --> D{Travou?}
D -- Nao --> E[Execucao normal]
D -- Sim --> F[Bug, reinicio, rollback ou correcao de codigo] flowchart LR
A[Requisicao de recurso] --> B{Vale monitorar formalmente?}
B -- Nao --> C[Concede e segue]
C --> D{Travou?}
D -- Nao --> E[Execucao normal]
D -- Sim --> F[Bug, reinicio, rollback ou correcao de codigo] Para múltiplos recursos, usamos quatro estruturas:
A ideia é procurar processos que ainda conseguem terminar com o que há em $A$.
Se algum termina, ele devolve recursos e o teste continua.
#include <stdio.h>
#include <stdbool.h>
#define N 3
#define M 4
void detectar_deadlock(int available[M], int current[N][M], int request[N][M]) {
int work[M];
bool finish[N] = {false};
for (int j = 0; j < M; j++) work[j] = available[j];
bool mudou = true;
while (mudou) {
mudou = false;
for (int i = 0; i < N; i++) {
if (finish[i]) continue;
bool pode_terminar = true;
for (int j = 0; j < M; j++) {
if (request[i][j] > work[j]) {
pode_terminar = false;
break;
}
}
if (pode_terminar) {
for (int j = 0; j < M; j++) work[j] += current[i][j];
finish[i] = true;
mudou = true;
}
}
}
}
Considere:
$$A = (2,1,0,0)$$
| Processo | Alocado $C$ | Requisição $R$ |
|---|---|---|
| $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)$ |
Ordem possível:
$$P_3 \rightarrow P_2 \rightarrow P_1$$
Como todos terminam, não há deadlock.
Depois de detectar um ciclo, o sistema precisa desfazê-lo.
Opções clássicas:
Critérios de escolha incluem custo de rollback, prioridade, recursos segurados e impacto para o usuário.
O Banqueiro só concede um recurso se o sistema continuar em estado seguro depois da concessão.
Ele não pergunta apenas se há recurso livre agora.
Ele pergunta se ainda existirá uma sequência de término para todos depois do empréstimo.
Modelos usados:
MaxAllocationNeed = Max - AllocationAvailableSempre que um processo pede algo novo, o sistema faz uma simulação mental:
Available, Allocation e NeedSe encontrar, concede de verdade.
Se não encontrar, adia o pedido.
| Cliente | Máximo | Já tem | Necessidade |
|---|---|---|---|
| A | 6 | 1 | 5 |
| B | 5 | 1 | 4 |
| C | 4 | 2 | 2 |
| D | 7 | 1 | 6 |
Se o caixa tem 5, ele consegue atender C.
Quando C termina, devolve recursos e abre caminho para os demais.
Portanto, o estado é seguro.
Um único tipo de recurso.
| Processo | Máximo | Já recebeu | Ainda precisa |
|---|---|---|---|
| $P_1$ | 9 | 3 | 6 |
| $P_2$ | 4 | 2 | 2 |
| $P_3$ | 7 | 2 | 5 |
$$Available = 3$$
Sequência segura:
$$P_2 \rightarrow P_3 \rightarrow P_1$$
Nem todos terminam de imediato, mas existe uma ordem segura.
| Processo | Máximo | Já recebeu | Ainda precisa |
|---|---|---|---|
| $P_1$ | 8 | 4 | 4 |
| $P_2$ | 6 | 4 | 2 |
| $P_3$ | 5 | 3 | 2 |
$$Available = 1$$
Agora ninguém consegue terminar.
Se nenhum processo conclui, ninguém devolve recursos.
Esse é um estado inseguro.
#include <stdio.h>
#include <stdbool.h>
#define N 5
#define M 3
bool estado_seguro(int available[M], int allocation[N][M], int max_demand[N][M]) {
int need[N][M], work[M], ordem[N], k = 0;
bool finish[N] = {false};
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
need[i][j] = max_demand[i][j] - allocation[i][j];
for (int j = 0; j < M; j++) work[j] = available[j];
bool progresso = true;
while (progresso) {
progresso = false;
for (int i = 0; i < N; i++) {
if (finish[i]) continue;
bool pode_terminar = true;
for (int j = 0; j < M; j++) {
if (need[i][j] > work[j]) { pode_terminar = false; break; }
}
if (pode_terminar) {
for (int j = 0; j < M; j++) work[j] += allocation[i][j];
finish[i] = true;
ordem[k++] = i;
progresso = true;
}
}
}
return k == N;
}
O algoritmo é elegante, mas pouco usado em sistemas operacionais gerais.
Motivo principal:
Na prática, isso raramente é conhecido com precisão.
Na prevenção, o sistema muda as regras para tornar o deadlock impossível.
| Condição atacada | Estratégia |
|---|---|
| Exclusão Mútua | virtualização, como spooling |
| Posse e Espera | pedir tudo no início |
| Não Preempção | liberar tudo ao falhar, quando possível |
| Espera Circular | ordem global de aquisição |
Se todos adquirirem recursos na mesma ordem, o ciclo não fecha.
#include <pthread.h>
extern pthread_mutex_t lock1;
extern pthread_mutex_t lock2;
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;
}
trylock e RetrocessoOutra estratégia prática é falhar rápido e soltar o que já foi adquirido.
#include <pthread.h>
#include <unistd.h>
extern pthread_mutex_t lock1;
extern pthread_mutex_t lock2;
void* worker(void* arg) {
while (1) {
pthread_mutex_lock(&lock1);
if (pthread_mutex_trylock(&lock2) == 0) {
pthread_mutex_unlock(&lock2);
pthread_mutex_unlock(&lock1);
break;
}
pthread_mutex_unlock(&lock1);
usleep(1000);
}
return NULL;
}
Sem o pequeno backoff, isso pode virar livelock.
Na próxima aula: 8 - memory management.
Depois de entender como processos travam ao disputar recursos, vamos estudar o espaço onde eles vivem: a memória.