Compreendendo a diferença entre um processo e um programa no sistema operacional com um exemplo de código

Estou estudando sistemas operacionais e tentando compreender o conceito de processos e programas. Entendo que um programa é um arquivo executável, enquanto um processo é a execução desse programa. No entanto, gostaria de uma explicação mais detalhada com um exemplo de código para ilustrar a diferença.

Alguém poderia fornecer um exemplo de código em C que demonstre a distinção entre um programa e um processo? Especificamente, gostaria de ver como um único programa pode resultar em vários processos quando executado simultaneamente e como esses processos compartilham ou não memória e recursos.

Aqui está um trecho de código C simples que calcula o fatorial de um número:

#include <stdio.h>

int factorial(int n) {
    if (n == 0) return 1;
    return n * factorial(n - 1);
}

int main() {
    int num = 5;
    printf("Factorial of %d is %d\n", num, factorial(num));
    return 0;
}

Alguém poderia modificar este código para criar vários processos que calculam o fatorial de diferentes números simultaneamente? Além disso, explique como esses processos diferem do programa original, especialmente em termos de memória e alocação de recursos.

Estou procurando obter uma compreensão profunda dessas linguagens e também procurei em diferentes sites como elas operam em cenários mais avançados, portanto, insights e exemplos de código relacionados às suas diferenças complexas seriam muito apreciados.

O máximo que podemos dizer para todos os sistemas operacionais é que dois processos não compartilham memória (caso compartilhassem, seriam threads, que devem compartilhar o mesmo programa)*.

A partir daí, várias diferenças vão aparecer entre sistemas operacionais. No Linux/BSD necessariamente todo processo começa com uma duplicata quase exata de outro (fork) compartilhando o estado do programa (a memória é copiada), os manipuladores de arquivos abertos e as variáveis de ambiente; dentro do programa deve haver uma instrução para que o processo troque de programa (exec). Já no Windows, só é permitida a criação de processos “do zero”, com um programa, da sua escolha, rodando do início (opcionalmente, você pode mandar conservar os manipuladores de arquivos e as variáveis de ambiente).

O exemplo a seguir é específico de Linux, então não vai ser tão útil para você (tem Windows no seu perfil), mas ilustra:

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

int factorial(int n) {
	if (n == 0) return 1;
	return n * factorial(n - 1);
}

int main() {
	int num = 5;
	// Pedir a criação de um novo processo
	switch (fork()) {
		case 0:
			// No novo processo, o fork() dá zero.
			// Substituir o conteúdo da variável num.
			// Os processos não compartilham memória, então a 'num' do processo original vai ficar quieta.
			num = 8;
			break;
		case -1:
			// Fork deu erro.
			// Interromper.
			perror("fork");
			return 1;
		default:
			// No processo original, fazer nada.
			break;
	}
	// Ambos os programas executarão essa linha, mas terão resultados
	// diferentes pois o conteúdo de num na memória de ambos não é compartilhado.
	// Como eles compartilham arquivos abertos (NO LINUX), os dois printf sairão
	// no mesmo terminal/arquivo.
	printf("Factorial of %d is %d\n", num, factorial(num));
	// Trocar o programa de ambos os processos.
	// Esse programa só joga "terminei" no terminal/arquivo.
	execlp("echo", "echo", "terminei", NULL);
	// O programa foi trocado. Nunca devemos chegar aqui.
	// Se chegou, a troca de programa não deu certo.
	return 1;
}

* Há alguns mecanismos que permitem processos compartilharem memória depois de criados, mas eles são muito mais lentos que threads.

1 curtida

No caso particular do Linux, eu recomendo o linux-kernel-labs para entender como estas questões foram modeladas no kernel. Algumas nuances a mais:

threads e processos são quase o mesmo em sistemas UNIX, por exemplo, no Linux, ambos usam tasks como abstração (task_struct). Ou seja, uma tarefa, ser um processo ou uma thread, é meramente uma característica de para onde os campos desta struct apontam e quais valores alguns campos armazenam.

No Windows, threads e processos são coisas totalmente diferentes em termos de como o kernel representa os mesmos. Este é um fator que torna implementar paralelismo via processos no Windows, algo ineficiente. É um dos possíveis culpados quando um port de Linux para Windows resulta em perda de desempenho. Ou seja, aqui uma thread é outra estrutura que pertence a uma lista de threads na estrutura do processo.

Da mesma forma, isto torna threads em Linux um pouco menos leves que threads em Windows.

No tocante a compartilhamento de memória, não necessariamente vai ser muito mais custoso compartilhar memória depois. mecanismos de IPC via shmat() ou similares tem praticamente o mesmo overhead se comparado a como fork() compartilha as páginas entre o processo pai e filho. Outra questão relevante é que fork() compartilha memória entre pai e filho de forma a ter a propriedade de copy on write (COW), logo, em alguns aspectos, o compartilhamento de memória via fork() pode não se comportar como algumas pessoas esperam, e dai é melhor recorrer a mecanismos tradicionais de IPC.

Entretanto, existem inúmeras formas de compartilhar memória entre processos (pipes, sockets, arquivos, etc) e muitos destes de fato tem um overhead bem maior, entretanto, apesar de chamarmos de compartilhamento de memória, muitos destes métodos, diferente de shmat() e similares, não são “compartilhamento de memória” ao pé da letra (regiões na RAM acessíveis por ambos os processos/threads).

3 curtidas