Pular para o conteúdo principal

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 PCBConteúdo
Estado do processoPronto / Exec / Bloq
PID / PPIDIDs do processo
Contador de Programa (PC)Próxima instrução (essencial para retomar)
Registradores CPUAcumuladores, ponteiros, flags (essencial para retomar)
Informações de schedulingPrioridade, fila
Informações de memóriaTabela de páginas (recarregado na MMU)
Lista de arquivosFDs abertos
Informações de I/ODispositivos alocados
ContabilizaçãoTempo 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

AspectoProcessoThread
Espaço de endereçamentoPróprio (isolado)Compartilhado com outros threads do processo
Segmentos Text / DataPrópriosCompartilhados
HeapPróprioCompartilhado
Pilha (Stack)PrópriaPrópria (por thread)
Registradores / PCPrópriosPróprios (por thread)
Arquivos abertos (fds)PrópriosCompartilhados
Criação~30x mais lento que threadMais rápido que fork()
ComunicaçãoIPC (pipe, shm, socket)Variável global direta
Isolamento de falhaAlto (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

  1. Responsividade: enquanto uma thread aguarda I/O, outras continuam respondendo ao usuário.
  2. Compartilhamento de recursos: compartilham memória naturalmente — sem IPC explícito.
  3. Economia: criar/destruir thread é muito mais barato que processo.
  4. 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érioN:11:1M:N
Syscall bloqueanteBloqueia todo o processoBloqueia apenas a threadBloqueia apenas a thread
Paralelismo multicoreNãoSimSim
Overhead de criaçãoBaixoMédioMédio
ComplexidadeBaixaBaixaAlta
Usado hojeNãoLinux, WindowsRaros

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=2N=4N=8N=∞
25%1,60×2,29×2,91×
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çãoPropó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 &i do loop diretamente para as threads. O valor de i muda 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_t e veja o resultado se tornar determinístico (ver Aula 06).


Exercícios

Questões dissertativas

Q1

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.

Q2

Explique por que o flush do TLB durante uma mudança de contexto é caro. Como arquiteturas modernas mitigam esse custo?

Q3

Quais estruturas de dados são compartilhadas entre threads do mesmo processo? O que cada thread mantém de forma privada?

Q4

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?

Q5

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?

Q6

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?

Q7

O que é thread-local storage (TLS)? Dê um exemplo prático de por que errno precisa ser thread-local em programas multithread.

Q8

Qual a diferença entre pthread_join() e pthread_detach()? Quando usar cada um?

Q9

Explique os quatro benefícios dos threads listados no Silberschatz e dê um exemplo concreto de aplicação para cada um.

Q10Difícil

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

Quiz10 questões

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.

Thumbnail do vídeo: Process Management — Processes and Threads14:35
ENOpcional

Process Management — Processes and Threads

Neso Academy

Conceito de thread, diferença de processo, benefícios. Cobre o §4.1 do Silberschatz.

Thumbnail do vídeo: Operating Systems — Full Course (25h)25:00:00
ENAprofundamento

Operating Systems — Full Course (25h)

freeCodeCamp.org

Para esta aula, foque nos módulos Process Control Block, Context Switch e Introduction to Threads.