Professor: Gabriel Soares Baptista
Pseudoparalelismo: A CPU alterna entre processos em milissegundos, criando a ilusão de simultaneidade em uma única CPU física.
Programa: Uma entidade passiva, como um arquivo em disco contendo instruções (analogia: a receita do bolo).
Processo: Uma entidade ativa, com contador de programa, registradores e variáveis (analogia: a atividade de preparar o bolo).
Instâncias diferentes do mesmo programa são processos distintos.
O cientista (CPU) segue a receita (Programa) usando ingredientes (Dados) para realizar o processo (Atividade).
fork().fork() cria um clone exato do processo pai.execve para carregar um novo programa.Analise o retorno da função fork() para diferenciar pai e filho:
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid;
pid = fork(); // Clona o processo
if (pid < 0) {
printf("Erro na criação!\n");
} else if (pid == 0) {
// Retorno 0 indica que estamos no FILHO
printf("Eu sou o FILHO! Meu PID é %d\n", getpid());
} else {
// Retorno > 0 indica que estamos no PAI (pid é o ID do filho)
printf("Eu sou o PAI! Criei o filho com PID %d\n", pid);
}
return 0;
}
O que este código irá imprimir no final?
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int x = 100;
pid_t cid;
int status; // Variável para armazenar o status de saída do filho
cid = fork();
if (cid < 0) {
perror("fork failed");
return 1;
}
if (cid == 0) {
// FILHO
x = x + 50;
printf("Filho: x = %d (Meu PID: %d)\n", x, getpid());
_exit(0); // Boa prática: terminar o filho explicitamente
} else {
// PAI
// cid aqui é o PID do filho que acabou de ser criado
// 0 como terceiro argumento significa "bloqueie até o filho terminar"
waitpid(cid, &status, 0);
printf("Pai: x = %d (Esperou pelo filho %d)\n", x, cid);
}
return 0;
}
O valor de x alterado no filho será visto pelo pai?
O que este código irá imprimir no final?
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
int x = 100;
pid_t cid;
int status; // Variável para armazenar o status de saída do filho
cid = fork();
if (cid < 0) {
perror("fork failed");
return 1;
}
if (cid == 0) {
// FILHO
x = x + 50;
printf("Filho: x = %d (Meu PID: %d)\n", x, getpid());
_exit(0); // Boa prática: terminar o filho explicitamente
} else {
// PAI
// cid aqui é o PID do filho que acabou de ser criado
// 0 como terceiro argumento significa "bloqueie até o filho terminar"
waitpid(cid, &status, 0);
printf("Pai: x = %d (Esperou pelo filho %d)\n", x, cid);
}
return 0;
}
O valor de x alterado no filho será visto pelo pai?
Resposta: Não. Filho: 150, Pai: 100. No UNIX, pai e filho possuem espaços de endereçamento distintos.
init. O pai e seus descendentes formam um "grupo de processos".Um processo encerra por quatro motivos fundamentais:
exit no UNIX).kill (UNIX) ou TerminateProcess (Windows).Se o filho termina e o pai não lê seu estado, o filho torna-se um "Zumbi".
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t cid = fork();
if (cid < 0) exit(1);
if (cid == 0) {
// FILHO: Morre rápido
printf("[Filho] PID %d: Tchau, virei um zumbi...\n", getpid());
exit(0);
} else {
// PAI: Fica vivo por 20 segundos sem dar wait()
printf("[Pai] PID %d: Criei o filho %d, mas não vou ler o estado dele agora.\n", getpid(), cid);
printf("[Pai]: Verifique o terminal agora com: ps aux | grep 'Z'\n");
sleep(20); // Janela de tempo para você rodar o comando no terminal
printf("[Pai]: Acordei! Agora vou dar wait() e limpar o zumbi.\n");
wait(NULL);
sleep(5); // Tempo para você ver que o zumbi sumiu
}
return 0;
}
O pai deve usar a chamada wait() para recolher os restos mortais do processo filho e liberá-lo da tabela de processos.
Um processo pode estar em um de três estados:
As transições são geridas pelo escalonador:
Para gerenciar processos, o SO utiliza o Bloco de Controle de Processos (PCB).
| Gerenciamento de Processos | Gerenciamento de Memória | Gerenciamento de Arquivos |
|---|---|---|
| Contador de programa (PC) | Ponteiro para código | Diretório raiz |
| Registradores | Ponteiro para dados | Descritores de arquivos |
| Estado do processo | Limites de memória | IDs de usuário (UID) |
A utilização da CPU aumenta com o número de processos na memória ($n$).
Fórmula de utilização:
$$\text{Utilização} = 1 - p^n$$
onde $p$ é a fração de tempo que um processo passa esperando por E/S.
Quantas vezes a palavra "Olá" será impressa?
#include <stdio.h>
#include <unistd.h>
int main() {
for (int i = 0; i < 3; i++) {
fork();
}
printf("Olá! PID=%d PPID=%d\n", getpid(), getppid());
sleep(20);
}
Quantas vezes a palavra "Olá" será impressa?
#include <stdio.h>
#include <unistd.h>
int main() {
for (int i = 0; i < 3; i++) {
fork();
}
printf("Olá! PID=%d PPID=%d\n", getpid(), getppid());
sleep(20);
}
Cada fork() dobra o número de processos existentes.
Resposta: 8 vezes ($2^3$). O loop cria uma árvore de processos onde cada novo processo continua a execução do loop de onde o pai parou.
O que é compartilhado entre threads de um mesmo processo?
| Itens Compartilhados | Itens Privados (Individuais) |
|---|---|
| Espaço de endereçamento | Contador de programa (PC) |
| Variáveis Globais | Registradores |
| Arquivos abertos | Pilha (Stack) |
| Alarmes e Sinais | Estado da Thread |
Para usar threads no Linux, utilizamos a biblioteca pthread:
#include <pthread.h>
#include <stdio.h>
void* minha_tarefa(void* arg) {
printf("Olá da Thread! ID: %ld\n", pthread_self());
return NULL;
}
int main() {
pthread_t tid;
// Cria a thread
pthread_create(&tid, NULL, minha_tarefa, NULL);
// Aguarda a thread terminar (similar ao wait)
pthread_join(tid, NULL);
printf("Thread principal finalizada.\n");
return 0;
}
Qual será o valor final do saldo?
#include <pthread.h>
#include <stdio.h>
int saldo = 100;
void* deposito(void* arg) {
saldo = saldo + 50;
return NULL;
}
int main() {
pthread_t t1;
pthread_create(&t1, NULL, deposito, NULL);
pthread_join(t1, NULL);
printf("Saldo Final: %d\n", saldo);
}
Qual será o valor final do saldo?
#include <pthread.h>
#include <stdio.h>
int saldo = 100;
void* deposito(void* arg) {
saldo = saldo + 50;
return NULL;
}
int main() {
pthread_t t1;
pthread_create(&t1, NULL, deposito, NULL);
pthread_join(t1, NULL);
printf("Saldo Final: %d\n", saldo);
}
Saldo Final: 150. Diferente de processos, as threads enxergam as mesmas variáveis.
Se rodarmos 2 threads, cada uma somando 100 mil vezes, o total será 200.000?
#include <stdio.h>
#include <pthread.h>
long contador = 0;
void* incremento(void* arg) {
// Aumentamos para 1 milhão para dar tempo das threads colidirem
for (int i = 0; i < 1000000; i++) {
contador++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
// Criamos duas threads que vão atacar a mesma variável global
pthread_create(&t1, NULL, incremento, NULL);
pthread_create(&t2, NULL, incremento, NULL);
// Esperamos ambas terminarem
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// O esperado seria 2.000.000, mas o resultado será menor e aleatório
printf("Resultado esperado: 2000000\n");
printf("Resultado real: %ld\n", contador);
return 0;
}
Se rodarmos 2 threads, cada uma somando 100 mil vezes, o total será 200.000?
#include <stdio.h>
#include <pthread.h>
long contador = 0;
void* incremento(void* arg) {
// Aumentamos para 1 milhão para dar tempo das threads colidirem
for (int i = 0; i < 1000000; i++) {
contador++;
}
return NULL;
}
int main() {
pthread_t t1, t2;
// Criamos duas threads que vão atacar a mesma variável global
pthread_create(&t1, NULL, incremento, NULL);
pthread_create(&t2, NULL, incremento, NULL);
// Esperamos ambas terminarem
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// O esperado seria 2.000.000, mas o resultado será menor e aleatório
printf("Resultado esperado: 2000000\n");
printf("Resultado real: %ld\n", contador);
return 0;
}
O resultado será imprevisível (ex: 142.384).
Por quê? Como compartilham a mesma variável, uma thread pode ler o valor original antes que a outra termine de atualizar. Isso é uma Condição de Corrida.
Como ter uma variável que é global para as funções da thread, mas privada para as outras threads?
Útil para evitar conflitos em variáveis como errno (erros de sistema).
#include <pthread.h>
// __thread garante que cada thread tenha sua PRÓPRIA cópia
__thread int erro_local;
void* tarefa(void* arg) {
erro_local = 5; // Não altera o erro_local das outras threads
return NULL;
}
malloc e é interrompida no meio da atualização da lista de memória, e a thread B chama malloc logo em seguida, o sistema pode entrar em colapso.Na próxima aula, abordaremos Comunicação entre Processos (IPC).