Aula 08 — PCB, Context Switch, Threads e Pthreads
:::note Ambiente sugerido
Exemplos de código testados em Linux com gcc. Compile os programas pthreads com -lpthread.
:::
Objetivos
Ao final desta aula você deve ser capaz de:
- Descrever todos os campos do PCB e explicar qual é usado durante uma mudança de contexto.
- Explicar o fluxo completo de uma mudança de contexto: o que o kernel salva, onde salva e como restaura.
- Distinguir thread de processo, indicando o que é compartilhado e o que é privado por thread.
- Comparar os modelos N:1 (muitos-para-um), 1:1 (um-para-um) e M:N (muitos-para-muitos) quanto a concorrência, bloqueio e suporte a multicore.
- Implementar programas com múltiplas threads usando a API Pthreads (
pthread_create,pthread_join,pthread_exit), passando e retornando dados. - Enunciar a Lei de Amdahl e aplicá-la para calcular o speedup máximo de uma aplicação parcialmente paralelizável.
Conteúdo
O PCB revisitado — todos os campos
O PCB (Process Control Block) — task_struct no Linux — é o repositório completo do estado de um processo. É o que permite ao SO "pausar" e "retomar" um processo como se nada tivesse acontecido.
| Campo do PCB | Conteúdo |
|---|---|
| Estado do processo | Pronto / Exec / Bloq |
| PID / PPID | IDs do processo |
| Contador de Programa (PC) | Próxima instrução (essencial para retomar) |
| Registradores CPU | Acumuladores, ponteiros, flags (essencial para retomar) |
| Informações de scheduling | Prioridade, fila |
| Informações de memória | Tabela de páginas (recarregado na MMU) |
| Lista de arquivos | FDs abertos |
| Informações de I/O | Dispositivos alocados |
| Contabilização | Tempo de CPU, limites |
Linux — task_struct (campos selecionados):
/* Somente leitura — campos da task_struct do Linux */
long state; /* TASK_RUNNING, TASK_INTERRUPTIBLE... */
struct sched_entity se; /* dados de scheduling (CFS) */
struct task_struct *parent; /* ponteiro para o pai */
struct list_head children; /* lista de filhos */
struct files_struct *files; /* tabela de descritores abertos */
struct mm_struct *mm; /* espaço de endereçamento (null=kernel)*/
pid_t pid; /* identificador único */
int prio; /* prioridade estática */
Filas de scheduling
O SO mantém várias filas de processos além da fila de prontos:
Filas de dispositivo: cada dispositivo de I/O tem sua própria fila. Um processo bloqueado aguardando leitura de disco fica na fila daquele disco, não na fila de prontos.
Mudança de Contexto (Context Switch)
Uma mudança de contexto ocorre quando o SO precisa trocar o processo em execução por outro. É puro overhead — nenhum trabalho útil é realizado durante ela.
Fluxo completo:
Custo típico: 1–10 µs por mudança de contexto (depende de arquitetura, tamanho do TLB, número de registradores). CPUs modernas otimizam com múltiplos bancos de registradores (UltraSPARC).
O que torna o context switch caro em sistemas modernos:
- Flush do TLB (Translation Lookaside Buffer) — as entradas da tabela de páginas em cache são invalidadas
- Pipeline da CPU precisa ser esvaziado (instruções "em voo")
- Dados do cache L1/L2 do processo anterior tornam-se inúteis (cache pollution)
Thread: a unidade leve de execução
Definição: um thread é uma unidade básica de utilização da CPU composta por ID de thread, contador de programa, registradores e pilha própria. Um processo pode ter múltiplos threads, todos compartilhando o mesmo espaço de endereçamento.
Processo vs. Thread
| Aspecto | Processo | Thread |
|---|---|---|
| Espaço de endereçamento | Próprio (isolado) | Compartilhado com outros threads do processo |
| Segmentos Text / Data | Próprios | Compartilhados |
| Heap | Próprio | Compartilhado |
| Pilha (Stack) | Própria | Própria (por thread) |
| Registradores / PC | Próprios | Próprios (por thread) |
| Arquivos abertos (fds) | Próprios | Compartilhados |
| Criação | ~30x mais lento que thread | Mais rápido que fork() |
| Comunicação | IPC (pipe, shm, socket) | Variável global direta |
| Isolamento de falha | Alto (segfault mata apenas o processo) | Baixo (segfault mata o processo inteiro) |
Analogia: o processo é como um escritório; os threads são os funcionários que compartilham a mesma sala, os mesmos armários (heap), mas cada um tem sua própria mesa e caderneta de notas (pilha e registradores).
Benefícios dos threads
- Responsividade: enquanto uma thread aguarda I/O, outras continuam respondendo ao usuário.
- Compartilhamento de recursos: compartilham memória naturalmente — sem IPC explícito.
- Economia: criar/destruir thread é muito mais barato que processo.
- Escalabilidade: em sistemas multicore, threads realmente executam em paralelo.
Modelos de implementação de threads
Threads de usuário (gerenciadas por biblioteca, sem suporte do kernel) precisam ser mapeadas para threads de kernel. Há três modelos:
Modelo N:1 — Muitos-para-Um
- N threads de usuário mapeiam para 1 thread de kernel.
- Biblioteca gerencia trocas de thread no espaço do usuário — eficiente.
- Problema crítico: se T1 fizer uma syscall bloqueante (ex.:
read()), todo o processo bloqueia — K1 bloqueia no kernel e T2, T3, T4 ficam paradas. - Não usa multicore — apenas K1 executa por vez.
- Exemplo histórico: Green Threads (Java antigo), GNU Portable Threads.
Modelo 1:1 — Um-para-Um
- Cada thread de usuário tem uma thread de kernel correspondente.
- Syscall bloqueante em T1 não afeta T2, T3, T4.
- Paralelismo real em multicore — K1, K2 podem executar simultaneamente em núcleos distintos.
- Custo: criar thread de usuário cria thread de kernel — overhead maior; alguns SO limitam o número total.
- Usado pelo Linux, Windows — é o modelo dominante hoje.
Modelo M:N — Muitos-para-Muitos
- M threads de usuário mapeiam para N ≤ M threads de kernel.
- Melhor equilíbrio teórico: paralelismo sem overhead de 1:1.
- Complexidade de implementação enorme.
- Raramente usado hoje — Linux/Windows adotaram 1:1, tornando M:N menos relevante.
Comparação
| Critério | N:1 | 1:1 | M:N |
|---|---|---|---|
| Syscall bloqueante | Bloqueia todo o processo | Bloqueia apenas a thread | Bloqueia apenas a thread |
| Paralelismo multicore | Não | Sim | Sim |
| Overhead de criação | Baixo | Médio | Médio |
| Complexidade | Baixa | Baixa | Alta |
| Usado hoje | Não | Linux, Windows | Raros |
Lei de Amdahl
Ao paralelizar uma aplicação, a parte serial limita o speedup máximo:
speedup <= 1 / (S + (1 - S) / N)
Onde S é a fração serial (não paralelizável) e N é o número de núcleos.
Exemplos:
| S (parte serial) | N=2 | N=4 | N=8 | N=∞ |
|---|---|---|---|---|
| 25% | 1,60× | 2,29× | 2,91× | 4× |
| 10% | 1,82× | 3,08× | 4,71× | 10× |
| 5% | 1,90× | 3,48× | 5,93× | 20× |
| 1% | 1,98× | 3,88× | 7,48× | 100× |
Conclusão: mesmo com infinitos núcleos, a parte serial limita o speedup máximo a 1/S. Uma aplicação 25% serial nunca poderá ser acelerada mais de 4×, independentemente de quantos núcleos forem adicionados.
Pthreads na prática
API essencial
| Função | Propósito |
|---|---|
pthread_create(&tid, &attr, func, arg) | Cria nova thread que executa func(arg) |
pthread_join(tid, &retval) | Aguarda a thread tid terminar; captura valor de retorno |
pthread_exit(retval) | Encerra a thread atual com valor de retorno |
pthread_self() | Retorna o pthread_t da thread atual |
pthread_attr_init(&attr) | Inicializa atributos com valores padrão |
pthread_attr_destroy(&attr) | Libera recursos de atributos |
Programa 1: passagem e retorno de dados
/* Executável — passagem de argumento e retorno via pthread */
/* Compile: gcc -o soma_par soma_par.c -lpthread */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
/* estrutura para passar múltiplos dados à thread */
typedef struct {
int inicio;
int fim;
} Intervalo;
void *soma_parcial(void *arg)
{
Intervalo *iv = (Intervalo *)arg; /* cast do argumento */
long *resultado = malloc(sizeof(long)); /* heap — sobrevive ao retorno */
*resultado = 0;
for (int i = iv->inicio; i <= iv->fim; i++)
*resultado += i;
printf("[thread] soma [%d..%d] = %ld\n",
iv->inicio, iv->fim, *resultado);
pthread_exit(resultado); /* retorna ponteiro para o heap */
}
int main(void)
{
pthread_t tid[2];
Intervalo iv[2] = {{1, 500}, {501, 1000}};
long total = 0;
/* cria 2 threads para somar metades do intervalo [1..1000] */
for (int i = 0; i < 2; i++)
pthread_create(&tid[i], NULL, soma_parcial, &iv[i]);
/* coleta resultados */
for (int i = 0; i < 2; i++) {
long *parcial;
pthread_join(tid[i], (void **)&parcial); /* captura retorno */
total += *parcial;
free(parcial); /* libera heap alocado pela thread */
}
printf("Soma total [1..1000] = %ld (esperado: 500500)\n", total);
return 0;
}
Programa 2: múltiplas threads com array de trabalho
/* Executável — N threads, cada uma com ID e tarefa */
/* Compile: gcc -o multithread multithread.c -lpthread */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define N 5
void *trabalhador(void *arg)
{
int id = *(int *)arg; /* recebe ID da thread */
printf("[thread %d] iniciando (TID=%lu)\n", id, pthread_self());
sleep(1); /* simula trabalho */
printf("[thread %d] concluída\n", id);
return NULL;
}
int main(void)
{
pthread_t tids[N];
int ids[N]; /* cada thread precisa do seu próprio inteiro */
for (int i = 0; i < N; i++) {
ids[i] = i; /* NÃO passe &i — valor muda! */
pthread_create(&tids[i], NULL, trabalhador, &ids[i]);
}
for (int i = 0; i < N; i++)
pthread_join(tids[i], NULL);
puts("Todas as threads concluídas.");
return 0;
}
Armadilha clássica: nunca passe
&ido loop diretamente para as threads. O valor deimuda antes da thread ser executada. Armazene em um array (ids[i] = i) e passe&ids[i].
Programa 3: dados compartilhados (race condition sem mutex)
/* Executável — demonstra race condition com variável global */
/* Compile: gcc -o race race.c -lpthread */
#include <pthread.h>
#include <stdio.h>
#define N_THREADS 8
#define ITER 100000
long contador = 0; /* COMPARTILHADA — sem proteção! */
void *incrementa(void *arg) {
for (int i = 0; i < ITER; i++)
contador++; /* NÃO ATÔMICO */
return NULL;
}
int main(void) {
pthread_t tids[N_THREADS];
for (int i = 0; i < N_THREADS; i++)
pthread_create(&tids[i], NULL, incrementa, NULL);
for (int i = 0; i < N_THREADS; i++)
pthread_join(tids[i], NULL);
printf("Esperado: %ld\n", (long)N_THREADS * ITER);
printf("Obtido: %ld ← provavelmente menor (race condition)\n", contador);
return 0;
}
Atividade prática rápida: execute este programa várias vezes e observe que o resultado varia. Adicione um
pthread_mutex_te veja o resultado se tornar determinístico (ver Aula 06).
Exercícios
Questões dissertativas
Liste os campos do PCB e explique quais são INDISPENSÁVEIS durante uma mudança de contexto — ou seja, quais devem ser salvos/restaurados para que o processo retome corretamente.
Explique por que o flush do TLB durante uma mudança de contexto é caro. Como arquiteturas modernas mitigam esse custo?
Quais estruturas de dados são compartilhadas entre threads do mesmo processo? O que cada thread mantém de forma privada?
Compare o modelo N:1 e o modelo 1:1 quanto ao comportamento quando uma thread faz uma chamada de sistema bloqueante. Qual o impacto em um sistema multicore?
Aplique a Lei de Amdahl: uma aplicação tem 20% de código serial (não paralelizável). Qual o speedup máximo com 4 núcleos? E com infinitos núcleos? O que isso implica para o desenvolvedor?
Por que é um erro passar &i (endereço da variável de loop) como argumento para as threads em um loop de pthread_create? Como corrigir?
O que é thread-local storage (TLS)? Dê um exemplo prático de por que errno precisa ser thread-local em programas multithread.
Qual a diferença entre pthread_join() e pthread_detach()? Quando usar cada um?
Explique os quatro benefícios dos threads listados no Silberschatz e dê um exemplo concreto de aplicação para cada um.
Analise: pthread_join() é chamado para uma thread que já terminou há 5 segundos. O que acontece? O retval é recuperado? Há algum problema?
Quiz de múltipla escolha
1. Durante uma mudança de contexto de P_A para P_B, qual campo do PCB é o MAIS crítico para garantir que P_A retome exatamente onde parou quando for re-escalonado?
- a)PID do processo
- b)Lista de arquivos abertos
- c)Contador de programa (PC) e conjunto de registradores da CPU
- d)Prioridade de escalonamento
- e)Tempo de CPU acumulado
2. No modelo 1:1, uma thread T1 faz uma syscall read() bloqueante aguardando dados do disco. O que acontece com T2, T3 (outras threads do mesmo processo)?
- a)T2 e T3 também bloqueiam — o processo inteiro fica suspenso
- b)T2 e T3 continuam executando normalmente em seus threads de kernel
- c)T2 e T3 são migradas para outro processo
- d)T2 e T3 são canceladas automaticamente
- e)O kernel cria novos threads de kernel para T2 e T3
3. Qual das seguintes estruturas de dados NÃO é compartilhada entre threads do mesmo processo?
- a)Variáveis globais
- b)Heap (memória alocada com malloc)
- c)Pilha (stack) de cada thread
- d)Descritores de arquivo abertos
- e)Código do programa (text segment)
4. Uma aplicação é 30% serial. Quantos núcleos são necessários para atingir speedup de 2×?
- a)2 núcleos
- b)3 núcleos
- c)4 núcleos
- d)5 núcleos
- e)Impossível — o speedup máximo é menor que 2×
5. A armadilha de passar '&i' do loop para pthread_create() causa que tipo de problema?
- a)Segmentation fault — acesso inválido de memória
- b)Deadlock — as threads esperam umas pelas outras
- c)Race condition no valor lido pelas threads — todas podem ver o mesmo valor de i
- d)Stack overflow — cada thread tenta usar a mesma pilha
- e)Nenhum problema — é uma prática segura
6. Qual é a principal vantagem do modelo N:1 sobre o modelo 1:1?
- a)Suporte completo a multicore
- b)Syscalls bloqueantes não travam o processo inteiro
- c)Troca de contexto entre threads no espaço do usuário — sem syscall — é mais rápida
- d)Pode criar threads ilimitadas
- e)Não há nenhuma vantagem — N:1 é obsoleto
7. O que pthread_detach() faz com uma thread?
- a)Cancela a thread imediatamente
- b)Pausa a thread até que pthread_join() seja chamado
- c)Marca a thread para liberação automática de recursos ao terminar — sem necessidade de join
- d)Separa a thread para executar em um processo diferente
- e)Remove a thread do scheduling até que seja reativada
8. Por que a mudança de contexto é chamada de 'puro overhead'?
- a)Porque consome energia elétrica sem produzir cálculos do processo
- b)Porque nenhuma instrução do programa do usuário é executada durante o salvamento/restauração de contexto
- c)Porque o scheduler sempre escolhe o processo errado
- d)Porque o hardware não auxilia no processo de salvar registradores
- e)Porque as threads têm que aguardar a conclusão do context switch
9. Qual é o modelo de threads usado pelo Linux e Windows para threads Pthreads / nativas?
- a)Modelo N:1 — muitos threads de usuário para um kernel thread
- b)Modelo 1:1 — cada thread de usuário mapeada para um kernel thread
- c)Modelo M:N — pool de kernel threads gerenciado pelo SO
- d)Modelo Híbrido — N:1 para I/O, 1:1 para CPU-bound
- e)Sem modelo definido — depende da versão do kernel
10. Um programa Pthreads cria uma thread que aloca memória com malloc() e retorna o ponteiro via pthread_exit(). O que o thread principal DEVE fazer com esse ponteiro?
- a)Ignorar — a memória é liberada automaticamente quando a thread termina
- b)Chamar pthread_join() para capturar o ponteiro e depois free() para liberar a memória
- c)Chamar pthread_cancel() para garantir que a thread não acesse mais a memória
- d)Fazer uma cópia da memória antes que a thread seja destruída
- e)A thread não pode retornar ponteiros para heap — causa undefined behavior
Referências
Principais (essenciais)
- SILBERSCHATZ, A.; GALVIN, P. B.; GAGNE, G. Fundamentos de Sistemas Operacionais. 9. ed. LTC, 2015.
- §3.1 (PCB), §3.2 (scheduling, filas, mudança de contexto), §4.1 (threads), §4.3 (modelos), §4.4 (pthreads)
Aprofundamento (opcionais)
- TANENBAUM, A. S. Sistemas Operacionais Modernos. 4. ed. Pearson, 2016. §2.2 — Threads.
- KERRISK, M. The Linux Programming Interface. No Starch Press, 2010. Cap. 29 (pthreads) e 33 (tópicos avançados).
man 7 pthreads,man 3 pthread_create,man 3 pthread_join— referência POSIX.
Recursos Complementares
Opcional — Vídeos para reforçar os conceitos desta aula.
14:35Process Management — Processes and Threads
Neso Academy
Conceito de thread, diferença de processo, benefícios. Cobre o §4.1 do Silberschatz.
25:00:00Operating Systems — Full Course (25h)
freeCodeCamp.org
Para esta aula, foque nos módulos Process Control Block, Context Switch e Introduction to Threads.