Aplicação, processos e threads
Este curso é baseado no excelente livro Operating Systems: Three Easy Pieces de Remzi H. Arpaci-Dusseau e Andrea C. Arpaci-Dusseau.
Aplicação e processo
O termo aplicação é um conceito geral que designa um programa executado por um usuário. Não é um termo técnico preciso, mas é frequentemente usado para se referir a um programa que interage com o usuário, como um editor de texto, um navegador web ou um jogo.
Um processo é uma instância de um programa em execução. Ele representa o ambiente de execução da aplicação, incluindo seus recursos e seu estado. Um processo é criado quando uma aplicação é iniciada e pode ser encerrado quando a aplicação é fechada. Um mesmo programa pode ser executado várias vezes, criando assim vários processos distintos.
De forma mais formal, inicialmente um programa é apenas um arquivo binário executável armazenado em disco. Esse arquivo binário contém:
- O código de máquina da aplicação, que é o código executável pelo processador.
- Os dados estáticos, que são variáveis globais e constantes utilizadas pela aplicação.
- Os recursos necessários para a execução da aplicação, como as bibliotecas compartilhadas e os arquivos de configuração.
Um processo é criado quando o sistema operacional carrega o arquivo binário na memória e inicializa o ambiente de execução. Portanto, de forma figurada, um processo é uma cópia do programa na memória, com seu próprio espaço de endereçamento, seus próprios recursos e seu próprio estado.
Ambiente de execução
O ambiente de execução de um processo inclui:
- Um identificador de processo (PID), que é um número único atribuído pelo sistema operacional para identificar o processo.
- O espaço de endereçamento, que é a memória alocada para o processo para armazenar seu código, seus dados e sua pilha de execução.
- Os descritores de arquivos, que são referências aos arquivos abertos pelo processo.
- As variáveis de ambiente, que são pares chave-valor usadas para configurar o comportamento do processo.
- Os sinais, que são notificações enviadas ao processo para indicar que um evento ocorreu, como uma interrupção ou um erro.
- Os threads, que são unidades leves de execução dentro de um processo. Um processo pode ter vários threads que compartilham o mesmo espaço de endereçamento e os mesmos recursos, mas que executam independentemente uns dos outros.
Espaço de endereçamento
Quando um processo é criado, o sistema operacional aloca para ele um espaço de endereçamento, que é uma faixa de endereços de memória que o processo pode usar para armazenar seu código, seus dados e sua pilha de execução. Esse espaço de endereçamento é isolado dos outros processos, o que significa que um processo não pode acessar diretamente a memória de outro processo.
Na verdade, do ponto de vista do programa, ele tem toda a memória à sua disposição, sem se preocupar com os outros processos. A memória é dividida em alguns grandes segmentos:
- O segmento de código, que contém o código executável do programa.
- O segmento de dados, que contém as variáveis globais e as estruturas de dados estáticas.
- O segmento de pilha, que é usado para armazenar as variáveis locais e os parâmetros das funções.
- O segmento de heap, que é usado para alocação dinâmica de memória
(por exemplo, com
mallocem C ounewem C++).
Compartilhamento da CPU
É muito provável que você tenha muito mais processos ativos do que núcleos de processador na sua máquina. Portanto, é necessário encontrar uma forma de compartilhar a CPU entre os diferentes processos de maneira transparente para o usuário.
A ideia principal é fazer cada processo acreditar que tem a CPU inteiramente para si. Para isso, o sistema operacional utiliza um mecanismo chamado escalonamento (ou scheduling em inglês). O escalonamento consiste em atribuir fatias de tempo (ou quanta) para cada processo, de modo que cada processo possa executar por um certo tempo antes de ser interrompido e dar lugar a outro processo.
Quando um processo é interrompido, seu estado é salvo no sistema operacional, para que ele possa retomar sua execução mais tarde a partir do mesmo ponto. O sistema operacional utiliza um algoritmo de escalonamento para decidir qual processo deve ser executado em determinado momento. Se você já ouviu o termo "context switch", é exatamente isso que significa: o sistema operacional salva o estado do processo atual e o restaura depois para retomar sua execução.
Também é o sistema operacional que gerencia qual processo executa em qual núcleo de processador. Ele pode mover um processo de um núcleo para outro para otimizar o uso dos recursos do sistema. Isso permite distribuir a carga de trabalho entre os diferentes núcleos e melhorar o desempenho do sistema.
Como usuário, você não precisa se preocupar com o escalonamento dos processos, pois o sistema operacional cuida disso automaticamente. Você pode, no entanto, influenciar o escalonamento modificando a prioridade dos processos ou sua afinidade (ou seja, o núcleo do processador no qual eles devem ser executados).
No Windows, você pode usar o Gerenciador de Tarefas (aba Detalhes) para modificar a prioridade de um processo ou sua afinidade.

Processos e threads
Seu sistema operacional trabalha muito para isolar os processos uns dos outros, mas há casos em que é desejável que um mesmo programa possa executar várias tarefas em paralelo. Também é mais simples, às vezes, que certas tarefas compartilhem recursos, como a memória, ao invés de duplicar esses recursos para cada tarefa. É aí que entram os threads.
Um thread é uma unidade leve de execução dentro de um processo. Um processo pode ter vários threads que compartilham o mesmo espaço de endereçamento (memória) com exceção da pilha, mas que executam independentemente uns dos outros. Portanto, se você tem um processo com vários threads, esses threads podem ser executados em paralelo em vários núcleos de processador, o que permite melhorar o desempenho do programa. Mesmo sem ter vários núcleos de processador, os threads permitem melhor uso da CPU, evitando tempos de espera desnecessários, por exemplo, ao aguardar uma resposta da rede ou ao realizar operações de entrada/saída.
Assim, podemos ver um thread como um processo leve que compartilha a memória do processo pai. Ele também é gerenciado pelo escalonamento do sistema operacional. Assim como os processos, geralmente é difícil prever exatamente quando um thread será executado, pois isso depende do escalonamento do sistema operacional e da carga de trabalho atual do sistema.
Kernel threads e user threads
De forma um pouco confusa, existe outro conceito de thread. De fato, podemos distinguir os threads gerenciados pelo sistema operacional (chamados de kernel threads) e os threads gerenciados pela aplicação (chamados de user threads). Os kernel threads são gerenciados pelo sistema operacional e são escalonados pelo kernel. Eles correspondem à descrição acima. Eles podem ser executados em paralelo em vários núcleos de processador e são isolados uns dos outros (exceto pela memória).
Os user threads (green threads, corrotinas em Go, fibers em Rust, etc.) são gerenciados pela aplicação e são escalonados pela própria aplicação. Eles geralmente são mais leves que os kernel threads, pois não exigem gerenciamento pelo sistema operacional. No entanto, eles não podem ser executados em paralelo em vários núcleos de processador, pois são escalonados pela aplicação e não pelo sistema operacional. É como se o processo dividisse ele mesmo seu tempo e fizesse seu próprio escalonamento. Mas tudo isso dentro de um único processo ou thread.
Algumas linguagens como Go misturam os dois conceitos. Elas permitem ter M corrotinas (user threads) que executam em N threads (kernel threads). É o ambiente de execução do Go que gerencia o escalonamento das corrotinas nos threads. Isso permite um gerenciamento refinado dos recursos ao mesmo tempo em que se aproveita o escalonamento do sistema operacional e o paralelismo multi-core.