C Processos (fork e exec)

Quando você digita ls no terminal, o Bash não "vira" o ls: ele cria um processo novo, põe o ls para rodar nele, espera terminar e mostra o prompt de novo. Todo programa que roda no seu computador é um processo, e qualquer programa em C pode criar outros. Nesta aula você vai usar as três chamadas que fazem isso no Linux e no macOS: fork (criar uma cópia do processo), exec (trocar o programa que o processo roda) e wait (esperar um processo terminar). No fim, você terá escrito o coração de um shell.

Você vai aprender
  • O que é um processo, o PID e o PPID (getpid e getppid).
  • Como o fork cria um processo filho, e como saber se você é o pai ou o filho.
  • Por que pai e filho não dividem as variáveis.
  • Como esperar o filho com wait/waitpid e ler o código de saída com WIFEXITED e WEXITSTATUS.
  • Como rodar outro programa com execvp, e o padrão fork + exec + wait.
  • Três armadilhas: processos zumbis, o printf que sai duas vezes e o exit no filho.

Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/c/processos, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Os números de processo (PID) mudam a cada execução; no seu computador eles serão bem maiores (algo como 48213).

fork, exec e wait não são do C padrão: são do POSIX, o padrão do Linux, do macOS e dos outros Unix. No Windows, use o WSL (aula Instalar no Windows e no Mac). O editor do guia também não tem processos: lá, fork dá error: use of undeclared identifier 'fork'. Por isso esta aula usa o terminal, e as caixas "Experimente" praticam as partes que rodam em qualquer lugar.

A ideia: a receita e a cozinheira

Um programa é uma receita escrita num caderno: o arquivo ./loja no disco. Um processo é alguém cozinhando aquela receita agora, com os ingredientes na bancada (a memória) e o dedo apontando em que linha está. Pode haver três pessoas fazendo o mesmo bolo ao mesmo tempo: um programa, três processos.

  • fork ("garfo", ou bifurcação: o caminho que se divide em dois) é a cozinheira se clonar. A cópia tem a mesma receita, os mesmos ingredientes e está na mesma linha. A partir daí, cada uma segue sozinha.
  • exec ("execute", executar) é a cozinheira jogar a receita fora e pegar outra: continua sendo a mesma pessoa, fazendo outra coisa.
  • wait ("esperar") é a cozinheira original esperar a cópia terminar para saber como foi.

1. PID: o número de cada processo

O sistema dá a cada processo um número único, o PID ("process ID", identidade do processo). Todo processo também tem um pai, o processo que o criou; o número do pai é o PPID ("parent process ID").

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("Meu PID: %d\n", (int) getpid());
    printf("PID de quem me chamou: %d\n", (int) getppid());
    return 0;
}
#define _POSIX_C_SOURCE 200809L
Pede aos cabeçalhos as funções e os tipos do POSIX (versão de 2008). Com -std=c23, sem esta linha, o tipo pid_t não existe (o cenário "Quando dá errado" mostra). Todos os programas desta aula começam com ela.
#include <unistd.h>
"Unix standard", o cabeçalho do POSIX com getpid, fork, exec… e sleep.
getpid()
"Get process ID": o PID deste processo, num tipo pid_t. O (int) é para o %d do printf.
getppid()
"Get parent process ID": o PID do pai.
$ gcc -std=c23 -Wall -Wextra pid.c -o pid
$ echo $$                       « o PID do próprio Bash
3
$ ./pid
Meu PID: 54
PID de quem me chamou: 3

O pai do ./pid é o Bash (o $$ do Bash é o PID dele): foi ele que criou o processo para rodar o programa. Rode de novo e o PID do programa muda, porque é outro processo; o do Bash continua o mesmo.


2. fork: um processo vira dois

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("Antes do fork (PID %d)\n", (int) getpid());
    fork();
    printf("Depois do fork (PID %d)\n", (int) getpid());
    return 0;
}
$ gcc -std=c23 -Wall -Wextra fork1.c -o fork1
$ ./fork1
Antes do fork (PID 55)
Depois do fork (PID 56)
Depois do fork (PID 55)

Uma linha "Antes" e duas "Depois". O fork criou uma cópia do processo 55, o processo 56, e as duas cópias continuaram da linha seguinte ao fork. Cada uma rodou o seu printf. Quem aparece primeiro depende de qual o sistema deixou rodar antes: pode mudar de uma execução para outra.

Quem é o pai e quem é o filho?

Para cada processo fazer uma coisa diferente, use o valor que o fork devolve. Ele "volta duas vezes", uma em cada processo, com valores diferentes:

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();

    if (pid == -1) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        printf("Sou o filho. Meu PID é %d; meu pai é o %d.\n", (int) getpid(), (int) getppid());
    } else {
        printf("Sou o pai (PID %d). Meu filho nasceu com o PID %d.\n", (int) getpid(), (int) pid);
    }
    return 0;
}
fork() devolveEm qual processoQuer dizer
0no filho"Você é a cópia." (O filho descobre o próprio PID com getpid.)
um número maior que 0no pai"Deu certo; este é o PID do seu filho."
-1no pai (não houve filho)Falhou: sistema sem memória ou limite de processos. O motivo fica em errno (aula errno e perror).
$ gcc -std=c23 -Wall -Wextra fork2.c -o fork2
$ ./fork2
Sou o pai (PID 57). Meu filho nasceu com o PID 58.
Sou o filho. Meu PID é 58; meu pai é o 57.

O mesmo if, rodando em dois processos, foi por dois caminhos: no pai, pid valia 58; no filho, 0.


3. Cada um com a sua memória

A cópia é completa: o filho recebe uma cópia de todas as variáveis do pai. Mas são cópias. Depois do fork, o que um muda o outro não vê:

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    int moedas = 10;
    pid_t pid = fork();

    if (pid == 0) {
        moedas = moedas + 90;
        printf("Filho: tenho %d moedas.\n", moedas);
        return 0;
    }

    wait(NULL);
    printf("Pai: tenho %d moedas.\n", moedas);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra memoria.c -o memoria
$ ./memoria
Filho: tenho 100 moedas.
Pai: tenho 10 moedas.

O filho ganhou 90 moedas, mas só na bancada dele. O wait(NULL) (próxima seção) fez o pai esperar o filho terminar antes de mostrar as suas: 10, como antes. Para dois processos trocarem dados, é preciso um canal de comunicação, como um arquivo, um pipe (o | do terminal) ou o código de saída. Se você quer partes do programa rodando ao mesmo tempo dividindo as variáveis, o que você procura são as threads.


4. wait e waitpid: esperar o filho e saber como foi

O pai pode esperar o filho terminar e receber o código de saída dele, o mesmo número que o echo $? mostra no terminal (aula exit e atexit):

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        printf("Filho: conferindo o estoque...\n");
        sleep(1);
        printf("Filho: faltam 3 produtos.\n");
        exit(3);
    }

    int status;
    printf("Pai: esperando o filho %d terminar.\n", (int) pid);
    waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
        printf("Pai: o filho terminou com o código %d.\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("Pai: o filho foi morto pelo sinal %d.\n", WTERMSIG(status));
    }
    return 0;
}
#include <sys/wait.h>
O cabeçalho do wait, do waitpid e das macros W….
exit(3);
O filho termina com o código 3 (aqui, "faltam 3 produtos").
waitpid(pid, &status, 0)
"Wait for PID": espera aquele filho terminar e guarda em status como foi. O 0 são as opções (nenhuma). O wait(&status) espera qualquer filho; wait(NULL) espera sem querer saber o resultado.
WIFEXITED(status)
"Wait if exited": verdadeiro se o filho terminou normalmente (return ou exit).
WEXITSTATUS(status)
"Wait exit status": o código de saída, de 0 a 255.
WIFSIGNALED(status)
Verdadeiro se o filho foi morto por um sinal (aula Sinais); WTERMSIG diz qual.
$ gcc -std=c23 -Wall -Wextra esperar.c -o esperar
$ ./esperar
Pai: esperando o filho 62 terminar.
Filho: conferindo o estoque...
Filho: faltam 3 produtos.
Pai: o filho terminou com o código 3.

O pai ficou parado no waitpid durante o segundo em que o filho dormiu, e só continuou quando o filho terminou. Por que macros, e não o status direto? Porque o status guarda várias informações num número só: o código de saída fica nos bits 8 a 15, e o número do sinal, nos bits 0 a 6. As macros tiram cada pedaço com operações de bits (aula Operadores de Bits). A missão abaixo faz isso à mão.

Missão: o editor do guia não tem fork, mas dá para entender o status aqui mesmo. Num status do Linux, o código de saída é o byte que começa no bit 8, e o sinal são os 7 bits de baixo. Complete as duas funções: codigo_de_saida deve devolver (status >> 8) & 0xFF e sinal deve devolver status & 0x7F. (O 32512 é o que o system do editor devolve: você vai ver na seção 6.)

#include <stdio.h>

int codigo_de_saida(int status) {
    return status;
}

int sinal(int status) {
    return status;
}

void explicar(int status) {
    if (sinal(status) == 0) {
        printf("%5d: terminou com o código %d\n", status, codigo_de_saida(status));
    } else {
        printf("%5d: morto pelo sinal %d\n", status, sinal(status));
    }
}

int main(void) {
    int exemplos[] = {768, 11, 0, 32512, 15};
    for (int i = 0; i < 5; i++) {
        explicar(exemplos[i]);
    }
    return 0;
}
#include <stdio.h>

int codigo_de_saida(int status) {
    return (status >> 8) & 0xFF;
}

int sinal(int status) {
    return status & 0x7F;
}

void explicar(int status) {
    if (sinal(status) == 0) {
        printf("%5d: terminou com o código %d\n", status, codigo_de_saida(status));
    } else {
        printf("%5d: morto pelo sinal %d\n", status, sinal(status));
    }
}

int main(void) {
    int exemplos[] = {768, 11, 0, 32512, 15};
    for (int i = 0; i < 5; i++) {
        explicar(exemplos[i]);
    }
    return 0;
}

768 é 3 × 256: o filho do esperar.c (código 3). 11 é um filho morto por SIGSEGV, e 15, por SIGTERM. Num programa de verdade, use sempre as macros: o padrão POSIX não promete que os bits fiquem nesses lugares em todo sistema.


5. exec: trocar de programa

A família exec põe outro programa para rodar no processo atual. O PID continua o mesmo, mas o código, as variáveis e tudo o mais são trocados pelos do programa novo. Se der certo, o exec nunca volta: não sobrou nada do programa antigo para onde voltar.

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void) {
    printf("Vou virar o wc...\n");
    fflush(stdout);
    char *args[] = {"wc", "-l", "compras.txt", NULL};
    execvp("wc", args);
    perror("execvp");
    return 1;
}
fflush(stdout);
Esvazia o buffer do printf antes da troca. O exec joga fora a memória do programa antigo, inclusive o texto que ainda estivesse esperando no buffer (aula stdin, stdout e stderr).
char *args[] = {"wc", "-l", "compras.txt", NULL};
Os argumentos do programa novo: viram o argv dele (aula Argumentos da Linha de Comando). O primeiro é o nome do programa (o argv[0]), e a lista tem de terminar com NULL: é assim que o exec sabe onde ela acaba.
execvp("wc", args)
Troca este programa pelo wc ("word count", contar palavras; com -l, conta linhas). O v quer dizer que os argumentos vêm num vetor (array); o p, que o programa é procurado nas pastas do PATH, como o terminal faz (aula Variáveis de Ambiente).
perror("execvp"); return 1;
Só roda se o exec falhou (o programa não existe, por exemplo).
$ printf "arroz\nfeijão\n" > compras.txt
$ gcc -std=c23 -Wall -Wextra exec1.c -o exec1
$ ./exec1
Vou virar o wc...
2 compras.txt

A segunda linha foi escrita pelo wc, rodando no mesmo processo que antes era o exec1. O perror e o return 1 nunca rodaram.

FunçãoArgumentosProcura no PATH?
execvp("wc", args)Num array terminado em NULLSim
execlp("wc", "wc", "-l", "compras.txt", (char *) NULL)Um por um ("l" de lista), terminando em NULLSim
execv("/usr/bin/wc", args)ArrayNão: caminho completo
execl("/usr/bin/wc", "wc", "-l", …, (char *) NULL)ListaNão
execve, execleComo execv/execl, mais as variáveis de ambiente ("e")Não

6. fork + exec + wait: o coração de um shell

Sozinho, o exec acaba com o seu programa. A combinação que o Bash usa para rodar cada comando é: fork (criar um filho), exec no filho (o filho vira o comando) e wait no pai (esperar e pegar o código de saída). Esta função faz exatamente isso:

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int rodar(char *args[]) {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }
    if (pid == 0) {
        execvp(args[0], args);
        perror(args[0]);
        _exit(127);
    }

    int status;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
        return WEXITSTATUS(status);
    }
    return 128 + WTERMSIG(status);
}

int main(void) {
    char *eco[] = {"echo", "Olá da lojinha!", NULL};
    char *lista[] = {"ls", "compras.txt", NULL};
    char *falta[] = {"ls", "nao-existe.txt", NULL};
    char *errado[] = {"lss", NULL};

    printf("[código %d]\n", rodar(eco));
    printf("[código %d]\n", rodar(lista));
    printf("[código %d]\n", rodar(falta));
    printf("[código %d]\n", rodar(errado));
    return 0;
}
LinhaO que faz
if (pid == 0) { execvp(…); … }Só o filho entra aqui e vira o comando.
perror(args[0]); _exit(127);Se o exec falhou, o filho avisa e termina com 127, o mesmo código que o Bash usa para "comando não encontrado". O _exit (com sublinhado) termina sem esvaziar os buffers: o porquê está nos cenários do fim.
waitpid(pid, &status, 0)Só o pai chega aqui: espera o comando terminar.
return 128 + WTERMSIG(status);Se o comando morreu por um sinal, devolve 128 + o sinal, a mesma conta do Bash.
$ gcc -std=c23 -Wall -Wextra rodar.c -o rodar
$ ./rodar
Olá da lojinha!
[código 0]
compras.txt
[código 0]
ls: não foi possível acessar 'nao-existe.txt': Arquivo ou diretório inexistente
[código 2]
lss: No such file or directory
[código 127]

Quatro comandos, quatro processos filhos, quatro códigos. O ls devolveu 2 ("arquivo não encontrado", um código do próprio ls), e o lss, que não existe, fez o execvp falhar: o perror mostrou o motivo e o filho saiu com 127. Junte isso a um laço que lê uma linha com fgets e a separa em palavras, e você tem um shell de verdade.

system: o atalho

O C padrão tem uma função que faz fork + exec + wait num passo só: system, de <stdlib.h>. Ela entrega o texto inteiro para o shell (/bin/sh) rodar:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int r1 = system("ls compras.txt");
    printf("system devolveu %d\n", r1);
    int r2 = system("lss");
    printf("system devolveu %d\n", r2);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra sistema.c -o sistema
$ ./sistema
compras.txt
system devolveu 0
sh: 1: lss: not found
system devolveu 32512

O system devolve o status cru, o mesmo número do waitpid: 32512 é 127 × 256, "terminou com o código 127" (o da missão da seção 4). É prático, mas tem dois problemas: roda um shell inteiro a mais, e, se o texto tiver algo que o usuário digitou, ele pode "injetar" comandos seus (um nome de arquivo como x; rm -rf ~). Com execvp, cada argumento vai separado e nada é interpretado.

Experimente: no editor do guia o system compila, mas não existe shell: os dois comandos dão not found e devolvem 32512. Use a ideia da missão da seção 4 e mostre também o código de saída: (r1 >> 8) & 0xFF.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int r1 = system("ls compras.txt");
    printf("system devolveu %d\n", r1);
    int r2 = system("lss");
    printf("system devolveu %d\n", r2);
    return 0;
}

7. Processos zumbis

Quando um filho termina, o sistema guarda o código de saída dele até o pai pedir com wait. Enquanto o pai não pede, o filho já morreu mas continua na tabela de processos: é um zumbi.

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();
    if (pid == 0) {
        return 0;                 // o filho termina na hora
    }
    printf("Filho %d terminou, mas não vou esperar por ele.\n", (int) pid);
    sleep(30);                    // o pai fica 30 s sem chamar wait
    return 0;
}

A Ana roda o programa em segundo plano (o &) e, um segundo depois, pede ao ps ("process status", situação dos processos) a lista do pai (--pid $!) e dos filhos dele (--ppid $!):

$ gcc -std=c23 -Wall -Wextra zumbi.c -o zumbi
$ ./zumbi & sleep 1; ps -o pid,ppid,stat,args --pid $! --ppid $!
[1] 4
Filho 6 terminou, mas não vou esperar por ele.
    PID    PPID STAT COMMAND
      4       3 S    ./zumbi
      6       4 Z    [zumbi] <defunct>
$ kill $!; wait $!; ps -o pid,ppid,stat,args --pid $! --ppid $!
[1]+  Terminado                  ./zumbi
    PID    PPID STAT COMMAND

Na coluna STAT, o pai está S ("sleeping", dormindo) e o filho, Z ("zombie"), com a palavra <defunct> ("defunto"). Um zumbi não gasta memória nem processador, só uma vaga na tabela; mas um programa que cria milhares de filhos sem esperar esgota as vagas. Quando o pai morreu (o kill), o sistema recolheu o zumbi, e a lista ficou vazia. A regra: todo fork tem o seu wait, como todo malloc tem o seu free.


Qual usar?

Você quer…Use
Rodar um comando simples, com texto fixo escrito por vocêsystem("…") (C padrão; funciona até no Windows)
Rodar um programa com argumentos vindos do usuário, e saber o código de saídafork + execvp + waitpid
Trocar o seu programa por outro, sem voltarexecvp sozinho
Fazer duas coisas ao mesmo tempo, cada uma com a sua memória (se uma quebrar, a outra continua)fork
Fazer duas coisas ao mesmo tempo dividindo as variáveisThreads

Quando dá errado

error: unknown type name ‘pid_t’

O programa usa pid_t e só tem <stdio.h> e <unistd.h>, sem o #define _POSIX_C_SOURCE:

$ gcc -std=c23 -Wall -Wextra semtipo.c -o semtipo
semtipo.c: In function ‘main’:
semtipo.c:5:5: error: unknown type name ‘pid_t’
    5 |     pid_t pid = fork();
      |     ^~~~~

O que significa: "nome de tipo desconhecido". Com -std=c23, o gcc pede à glibc só o que é do C padrão, e o pid_t é do POSIX. (Sem o -std, o gcc usa o modo GNU, que liga o POSIX sozinho; por isso muitos exemplos da internet funcionam sem a linha.)

Como consertar: #define _POSIX_C_SOURCE 200809L na primeira linha, antes de qualquer #include.

error: implicit declaration of function ‘wait’
$ gcc -std=c23 -Wall -Wextra semwait.c -o semwait
semwait.c: In function ‘main’:
semwait.c:10:5: error: implicit declaration of function ‘wait’ [-Wimplicit-function-declaration]
   10 |     wait(NULL);
      |     ^~~~

O que significa: o wait não está no <unistd.h>. No C23, chamar uma função sem declaração é erro (aula Protótipos).

Como consertar: #include <sys/wait.h>, que traz wait, waitpid e as macros W….

O texto do printf sai duas vezes
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    printf("Recibo da lojinha: ");
    pid_t pid = fork();
    if (pid == 0) {
        printf("(filho)\n");
        return 0;
    }
    wait(NULL);
    printf("(pai)\n");
    return 0;
}
$ gcc -std=c23 -Wall -Wextra dobrado.c -o dobrado
$ ./dobrado
Recibo da lojinha: (filho)
Recibo da lojinha: (pai)

O que significa: o printf sem \n deixou "Recibo da lojinha: " esperando no buffer (a memória do programa), e o fork copiou a memória inteira, buffer incluído. Cada processo esvaziou a sua cópia no fim.

Como consertar: fflush(stdout); logo antes de todo fork.

Com a saída num pipe ou arquivo, as linhas aparecem repetidas

É o rodar.c da seção 6, com uma única diferença: o filho termina com exit(127) em vez de _exit(127). No terminal, sai tudo certo. Mas mande a saída para outro programa (| cat) e compare:

$ ./rodar | cat                « o rodar.c, com _exit
Olá da lojinha!
compras.txt
ls: não foi possível acessar 'nao-existe.txt': Arquivo ou diretório inexistente
lss: No such file or directory
[código 0]
[código 0]
[código 2]
[código 127]
$ ./rodar2 | cat               « a mesma coisa, com exit
Olá da lojinha!
compras.txt
ls: não foi possível acessar 'nao-existe.txt': Arquivo ou diretório inexistente
lss: No such file or directory
[código 0]
[código 0]
[código 2]
[código 0]
[código 0]
[código 2]
[código 127]

O que significa: num pipe, o stdout não esvazia a cada \n: os "[código …]" do pai ficam juntos no buffer até o fim (por isso, nas duas versões, eles aparecem depois). Quando o pai criou o filho do lss, o buffer tinha três linhas, e o filho ganhou uma cópia. O exec falhou, e o exit do filho esvaziou a cópia: três linhas repetidas. O _exit termina sem esvaziar nada.

Como consertar: no filho, depois de um exec que falhou, use _exit. E faça fflush(stdout) antes do fork.

O computador fica lento e aparece fork: Resource temporarily unavailable

O que significa: um fork dentro de um laço, sem o if (pid == 0) para o filho sair do laço: cada filho também cria filhos, que criam filhos… O número de processos dobra a cada volta (2, 4, 8, 16…) até o sistema recusar (EAGAIN). Isso tem até nome: fork bomb.

Como consertar: o filho faz o seu trabalho e termina (_exit ou return dentro do if (pid == 0)); só o pai continua o laço. Teste programas com fork com cuidado.


Referência rápida

EscritaO que faz
#define _POSIX_C_SOURCE 200809LLiga o POSIX com -std=c23 (primeira linha do arquivo)
getpid(), getppid()O PID deste processo e o do pai (<unistd.h>)
pid_t pid = fork();Cria um filho: 0 no filho, o PID do filho no pai, -1 se falhou
execvp(nome, args)Troca o programa; args termina em NULL; só volta se falhar
wait(&status) / waitpid(pid, &status, 0)Espera um filho / aquele filho (<sys/wait.h>)
WIFEXITED(status), WEXITSTATUS(status)Terminou normalmente? Com que código?
WIFSIGNALED(status), WTERMSIG(status)Morreu por sinal? Qual?
_exit(código)Termina o filho sem esvaziar os buffers (<unistd.h>)
fflush(stdout)Esvazia o buffer antes de fork e exec
system("comando")Roda pelo shell e devolve o status (C padrão)
ps -o pid,ppid,stat,argsNo terminal: lista processos (Z = zumbi)

Exercícios

1. O valor no filho

Complete o teste que só é verdadeiro no processo filho:

if (pid == ) { … }

2. O fim da lista

Complete o array de argumentos do execvp:

char *args[] = {"ls", "-l", };

3. O código do filho

Complete a macro que tira o código de saída do status:

printf("%d\n", (status));

4. Um programa faz int x = 5;, depois fork(), e o filho faz x = 50;. Quanto vale o x do pai depois que o filho termina?

5. Quando a linha depois de um execvp roda?

6. O que é um processo zumbi?

Missão final: montar o comando

O editor não roda execvp, mas a parte mais delicada de chamá-lo é montar o array de argumentos, e isso dá para praticar aqui.

Missão: a função mostrar imprime um comando como o shell veria, andando pelo array até achar o NULL do fim, do mesmo jeito que o execvp faz. Os dois arrays estão incompletos: o primeiro deve ser o comando wc -l compras.txt (falta o "-l"), e o segundo, ls -a. Acrescente cada opção no lugar certo, antes do NULL, que tem de continuar sendo o último (sem ele, o laço passaria do fim do array: aula Comportamento Indefinido). A saída deve ser exatamente:

$ wc -l compras.txt
$ ls -a
#include <stdio.h>

void mostrar(char *args[]) {
    printf("$");
    for (int i = 0; args[i] != NULL; i++) {
        printf(" %s", args[i]);
    }
    printf("\n");
}

int main(void) {
    char *contar[] = {"wc", "compras.txt", NULL};
    char *listar[] = {"ls", NULL};
    mostrar(contar);
    mostrar(listar);
    return 0;
}
#include <stdio.h>

void mostrar(char *args[]) {
    printf("$");
    for (int i = 0; args[i] != NULL; i++) {
        printf(" %s", args[i]);
    }
    printf("\n");
}

int main(void) {
    char *contar[] = {"wc", "-l", "compras.txt", NULL};
    char *listar[] = {"ls", "-a", NULL};
    mostrar(contar);
    mostrar(listar);
    return 0;
}

No seu computador, troque mostrar(contar) por rodar(contar), a função da seção 6, e o comando roda de verdade.


Resumo

  • Um processo é um programa rodando, com o seu PID; o processo que o criou é o pai (PPID).
  • fork() cria uma cópia do processo. Devolve 0 no filho, o PID do filho no pai e -1 se falhar. Cada um tem a sua cópia das variáveis.
  • waitpid(pid, &status, 0) espera o filho; WIFEXITED/WEXITSTATUS e WIFSIGNALED/WTERMSIG leem o resultado. Todo fork tem o seu wait, ou sobram zumbis.
  • execvp(nome, args) troca o programa do processo; args termina em NULL, e o exec só volta se falhar.
  • fork + exec no filho + wait no pai é como o shell roda cada comando. system faz o mesmo pelo shell, com risco de injeção.
  • fflush(stdout) antes de fork e exec; _exit no filho quando o exec falha.
  • Tudo isso é POSIX (Linux, macOS, WSL), não C padrão: com -std=c23, comece com #define _POSIX_C_SOURCE 200809L. O editor do guia não tem processos.