Forking de processos

Vamos começar com um exemplo simples que imprime "Hello" na saída padrão:

forking.c
#include <stdio.h>

int main() {
	printf("Hello\n");
	return 0;
}

Primeiro passo é compilar o arquivo para um executável do SO:

$ gcc forking.c -o forking

Após isso, rodamos o executável e:

$ ./forking
Hello

Dica: se quiser executar tudo em uma linha apenas, pode-se utilizar "gcc forking.c -o forking && ./forking"

So far, so good.

O primeiro fork

Para fazer forking de processos em C, precisamos usar a syscall fork através do cabeçalho unistd.h (UNIX Standard), que fornece acesso a syscalls do Kernel, permitindo interagir diretamente com o sistema operacional:

forking.c
#include <stdio.h>
#include <unistd.h>

int main() {
	fork();
	printf("Hello\n");
	return 0;
}
Hello
Hello

E por quê vemos agora a mensagem sendo impressa duas vezes? O quê acontece quando chamamos fork? Vamos detalhar o fluxo do programa:

  • o processo pai é iniciado no começo do programa; apenas o pai está em execução

  • quando o fork é chamado, o sistema cria um novo processo filho, que é uma cópia exata do processo pai, incluindo o estado do programa naquele exato momento

  • qualquer código depois do fork é executado tanto no processo pai quanto no processo filho; como ambos chamam a função "printf" com "Hello", então a mensagem é impressa 2 vezes

Um ponto a destacar aqui é que, no momento do fork, todos os file descriptors do pai (incluindo o STDOUT) foram herdados pelo filho, e é por isso que vemos a mensagem na mesma saída padrão (tela do terminal) 2 vezes.

É possível fazer com que o filho tenha outra saída padrão diferente do pai, mas não vamos entrar nestes detalhes aqui, não importam muito para o assunto principal que é concorrência

Se quisermos que cada um imprima uma mensagem diferente, teríamos que saber , de alguma forma após a chamada do fork, se estamos dentro do processo pai ou do filho.

Manual, o nosso melhor amigo

De acordo com o manual, a syscall fork tem o seguinte retorno:

On success, the PID of the child process is returned in the
       parent, and 0 is returned in the child.  On failure, -1 is
       returned in the parent, no child process is created, and  is
       set to indicate the error.

Ou seja, dentro do filho, o retorno da função é 0 . E dentro do pai, o retorno é o PID do filho. Com isto, podemos ter o seguinte código:

forking.c
#include <stdio.h>
#include <unistd.h>

int main() {
	pid_t pid = fork();
	if (pid == 0) {
		printf("Hello from child\n");
	} else {
		printf("Hello from parent\n");
	}
	return 0;
}

Que se executado, tem a seguinte saída:

Hello from parent
Hello from child

Yay!

Mas repare que, devido à natureza preemptiva do escalonador, não temos controle sobre qual será executado primeiro. Poderia ser o pai, ou mesmo o filho.

O que temos que entender aqui é que, no momento do fork, é criado um novo processo no sistema operacional, que irá competir por recursos junto a outros processos, ou seja, todo mundo junto no mesmo balaio de concorrência.

Um exemplo mais robusto

Agora, vamos a um exemplo um pouco mais robusto com o uso de fork de processos, onde um processo pai dispara 3 processos filhos que irão executar uma tarefa que demora 2 segundos cada:

forking-complex.c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

void perform() {
	printf("Processo filho (PID: %d) executando tarefa...\n", getpid());
	sleep(2); // Simula uma tarefa que leva 2 segundos
	printf("Processo filho (PID: %d) completou a tarefa!\n", getpid());
}

int main() {
	int num_children = 3;
	pid_t pid;

	for (int i = 0; i < num_children; i++) {
		pid = fork(); // Cria um novo processo

		if (pid == 0) {
			// Processo filho
			perform();
			return 0;
		}
	}

	for (int i = 0; i < num_children; i++) {
		pid_t child_pid = wait(NULL); // Aguarda qualquer filho terminar
		printf("Pai: Processo filho com PID %d terminou.\n", child_pid);
	}

	printf("Pai: Todos os filhos terminaram. Finalizando.\n");
	return 0;
}
Processo filho (PID: 271052) executando tarefa...
Processo filho (PID: 271053) executando tarefa...
Processo filho (PID: 271054) executando tarefa...
Processo filho (PID: 271052) completou a tarefa!
Processo filho (PID: 271053) completou a tarefa!
Pai: Processo filho com PID 271052 terminou.
Pai: Processo filho com PID 271053 terminou.
Processo filho (PID: 271054) completou a tarefa!
Pai: Processo filho com PID 271054 terminou.
Pai: Todos os filhos terminaram. Finalizando.

Interessante notar aqui:

  • os 3 processos filhos foram criados e iniciaram sua execução (271052, 271053, 271054)

  • 2 processos terminaram um pouco antes, sendo que o processo pai notificou que ambos tinham concluído execução

  • um último processo (271054) terminou depois que o pai tinha notificado sobre os outros dois que concluíram primeiro

  • o pai informa que todos os filhos foram concluídos e o programa finaliza

Isto, senhoras e senhores, é a maravilha da concorrência. Não temos controle algum sobre a ordem e execução das tarefas!

Sim, vou repetir isso inúmeras vezes neste guia kk

Comunicação entre processos (IPC)

Como processos não compartilham memória (a priori), precisamos de um mecanismo de comunicação entre processos, também chamado de IPC (Inter-process communication), pois um processo sem comunicação é bastante inútil.

Uma das formas de IPC é através de pipes, que são uma comunicação uni-direcional entre processos.

Se você quer entender mais sobre IPC e UNIX pipes, escrevi um artigo sobre o tema, onde é possível aprofundar nos conceitos de forma prática, e você só precisa de um terminal com shell/bash ou outra shell de preferência

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipe_fd[2];
    pid_t pid;

    pid = fork();

    if (pid == 0) {
        // Processo filho
        close(pipe_fd[0]); // Fecha a extremidade de leitura
        char mensagem[] = "Message from child!";
        // Escreve no pipe
        write(pipe_fd[1], mensagem, strlen(mensagem) + 1);
        close(pipe_fd[1]); // Fecha a extremidade de escrita
    } else {
        // Processo pai
        close(pipe_fd[1]); // Fecha a extremidade de escrita
        char buffer[100];
        // Lê a partir do pipe
        read(pipe_fd[0], buffer, sizeof(buffer));
        printf("Parent received message: %s\n", buffer);
        close(pipe_fd[0]); // Fecha a extremidade de leitura
    }

    return 0;
}

Este é um exemplo bastante simples de como 2 processos distintos podem conversar entre si, através da utilização de UNIX pipes.

Tá gostando do guia?

Se está gostando deste trabalho e considera fortalecer, o QRCode de PIX abaixo tá no jeito hein?

Ou copia a cola:

00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C

Não vamos entrar em mais detalhes sobre uso de forking em C, isto já é o suficiente para o objetivo deste guia. No próximo tópico, iremos abordar o uso de threads em C.

Last updated