Conteúdo


Anatomia do Gerenciamento de Processos Linux

Criação, Gerenciamento, Planejamento e Destruição

Comments

Linux é um sistema muito dinâmico com constantes necessidades de alterações de computação. A representação das necessidades computacionais dos centros Linux cerca a abstração comum do processo. Os processos podem ter vida curta (um comando executado da linha de comandos) ou vida longa (um serviço de rede). Por esta razão, o gerenciamento geral dos processos e seus planejamentos são muito importantes.

No espaço do usuário, os processos podem ser representados por identificadores de processo (PIDs). Na perspectiva do usuário, um PID é um valor numérico que identifica exclusivamente o processo. Um PID não é alterado durante o ciclo de vida de um processo, mas pode ser reutilizado depois que o processo termina, portanto, nem sempre é ideal armazená-lo em cache.

No espaço do usuário, é possível criar processos de qualquer uma destas formas. É possível executar um programa (o que resulta na criação de um novo processo) ou, dentro de um programa, é possível chamar uma chamada do sistema fork ou exec. A chamada fork resulta na criação de um processo filho, enquanto uma chamada exec substitui o contexto do processo atual pelo novo programa. Será discutido aqui cada um desses métodos para entender como eles funcionam.

Para este artigo, eu crio a descrição dos processos primeiro mostrando a representação de kernel deles e como eles são gerenciados no kernel, depois revejo os vários meios pelos quais os processos são criados e planejados em um ou mais processadores e, finalmente, o que acontece se eles terminam.

Representação do Processo

Dentro do kernel Linux, um processo é representado por uma estrutura grande chamada task_struct. Essa estrutura contém todos os dados necessários para representar o processo, junto com uma pletora de outros dados para contabilidade, e para manter relacionamentos com outros processos (pais e filhos). Uma descrição completa de task_struct está além do escopo deste artigo, mas uma parte de task_struct é mostrada na Lista 1. Este código contém os elementos específicos que este artigo explora. Observe que task_struct reside em ./linux/include/linux/sched.h.

Lista 1. Uma Pequena Parte de task_struct
struct task_struct {

	volatile long state;
	void *stack;
	unsigned int flags;

	int prio, static_prio;

	struct list_head tasks;

	struct mm_struct *mm, *active_mm;

	pid_t pid;
	pid_t tgid;

	struct task_struct *real_parent;

	char comm[TASK_COMM_LEN];

	struct thread_struct thread;

	struct files_struct *files;

	...

};

Na Lista 1, é possível ver vários itens que esperava, como o estado da execução, uma pilha, um conjunto de sinalizadores, o processo pai, o encadeamento de execução (do qual pode haver muitos) e arquivos abertos. Eu os exploro posteriormente no artigo, mas apresentarei alguns aqui. A variável state é um conjunto de bits que indica o estado da tarefa. Os estados mais comuns indicam que o processo está em execução ou em uma fila de execução prestes a executar (TASK_RUNNING), em espera (TASK_INTERRUPTIBLE), em espera, mas não pode ser ativada (TASK_UNINTERRUPTIBLE), parada (TASK_STOPPED), ou alguns outros estados. Uma lista completa desses sinalizadores está disponível em ./linux/include/linux/sched.h.

A palavra flags define um grande número de indicadores, indicando tudo, se o processo está sendo criado (PF_STARTING) ou saindo (PF_EXITING), ou até mesmo se ele está atualmente alocando memória (PF_MEMALLOC). O nome do executável (excluindo o caminho) ocupa o campo comm (command).

A cada processo, também é fornecida uma prioridade (chamada static_prio), mas a prioridade real do processo é determinada dinamicamente com base no carregamento e em outros fatores. Quanto menor o valor da prioridade, maior sua prioridade real.

O campo tasks fornece o recurso de lista vinculada. Ele contém um ponteiro prev (que aponta para a tarefa anterior) e um ponteiro next (que aponta para a próxima tarefa).

O espaço de endereço do processo é representado pelos campos mm e active_mm. O mm representa os descritores de memória do processo, enquanto active_mm é o descritor de memória do processo anterior (uma otimização para melhorar os tempos do comutador de contexto).

Finalmente, o thread_struct identifica o estado armazenado do processo. Esse elemento depende da arquitetura particular na qual o Linux está executando, mas é possível ver um exemplo disso em ./linux/include/asm-i386/processor.h. Nesta estrutura, será encontrado o armazenamento para o processo quando ele é comutado do contexto de execução (registros de hardware, contador de programa etc).

Gerenciamento de Processos

Agora, vamos explorar como se gerencia processos no Linux. Na maioria dos casos, os processos são criados dinamicamente e representados por uma task_struct alocada dinamicamente. Uma exceção é o próprio processo init, que sempre existiu e é representado por uma task_struct alocada estaticamente. É possível ver um exemplo disso em ./linux/arch/i386/kernel/init_task.c.

Todos os processos no Linux são coletados de duas formas diferentes. A primeira é hashtable, que é colocada em hash pelo valor do PID; a segunda é uma lista circular vinculada duplamente. A lista circular é ideal para fazer iteração através da lista de tarefas. Como a lista é circular, não há início ou fim, mas como init_task sempre existiu, é possível usá-lo como um ponto de ancoragem para iteração futura. Observemos um exemplo disso percorrendo o conjunto de tarefas atual.

A lista de tarefas não é acessível do espaço do usuário, mas é possível resolver esse problema facilmente inserindo código no kernel na forma de um módulo. Um programa muito simples é mostrado na Lista 2, que faz iteração com a lista de tarefas e fornece uma pequena quantidade de informações sobre cada nome de tarefa (name, pid e parent). Observe aqui que o módulo utiliza printk para emitir a saída. Para visualizar a saída, é necessário visualizar o arquivo /var/log/messages com o utilitário cat (ou tail -f /var/log/messages em tempo real). A função next_task é uma macro em sched.h, que simplifica a iteração da lista de tarefas (retorna uma referência task_struct da próxima tarefa).

Lista 2. Módulo de Kernel Simples para Emitir Informações da Tarefa(procsview.c)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched.h>

int init_module( void )
{
  /* Configurar o ponto de ancoragem */
  struct task_struct *task = &init_task;

  /* Percorrer a lista de tarefas, até atingir init_task novamente */
  do {

    printk( KERN_INFO "*** %s [%d] parent %s\n",
		task->comm, task->pid, task->parent->comm );

  } while ( (task = next_task(task)) != &init_task );

  return 0;
}
void cleanup_module( void )
{
  return;
}

É possível compilar esse módulo com o Makefile mostrado na Lista 3. Quando compilado, é possível inserir o objeto de kernel com insmod procsview.ko e removê-lo com rmmod procsview.

Lista 3. Makefile para Criar o Módulo de Kernel
obj-m += procsview.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

Após a inserção, /var/log/messages exibe a saída, como mostrado abaixo. É possível ver aqui a tarefa inativa (chamada swapper) e a tarefa init (pid 1).

Nov 12 22:19:51 mtj-desktop kernel: [8503.873310] *** swapper [0] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904182] *** init [1] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904215] *** kthreadd [2] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904233] *** migration/0 [3] parent kthreadd
...

Observe que também é possível identificar a tarefa atualmente em execução. O Linux mantém um símbolo chamado current que é o processo atualmente em execução (do tipo task_struct). Se, no final de init_module for incluída a linha:

printk( KERN_INFO, "A tarefa atual é %s [%d], current->comm, current->pid );

será visto:

Nov 12 22:48:45 mtj-desktop kernel: [10233.323662] Current task is insmod [6538]

Observe que a tarefa atual é insmod, pois a função init_module é executada dentro do contexto de execução do comando insmod. O símbolo current refere-se, na verdade, a uma função (get_current) e pode ser encontrado em um cabeçalho específico de arch (por exemplo, ./linux/include/asm-i386/current.h).

Criação do Processo

Assim, vamos percorrer a criação de um processo do espaço do usuário. O mecanismo subjacente é o mesmo para as tarefas do espaço de usuário e do kernel, pois ambos eventualmente contam com uma função chamada do_fork para criar o novo processo. No caso da criação de um encadeamento de kernel, o kernel chama uma função denominada kernel_thread (consulte ./linux/arch/i386/kernel/process.c), que executa alguma inicialização, depois chama do_fork.

Uma ação semelhante ocorre para a criação do processo do espaço do usuário. No espaço do usuário, um programa chama fork, o que resulta em uma chamada do sistema para a função do kernel denominada sys_fork (consulte ./linux/arch/i386/kernel/process.c). Os relacionamentos das funções são mostrados graficamente na Figura 1.

Figura 1. Hierarquia de Função para a Criação do Processo
Hierarquia de Função para a Criação do Processo
Hierarquia de Função para a Criação do Processo

A partir da Figura 1, é possível ver que do_fork fornece a base para a criação do processo. É possível encontrar a função do_fork em ./linux/kernel/fork.c (junto com a função parceira, copy_process).

A função do_fork começa com uma chamada para alloc_pidmap, que aloca um novo PID. Em seguida, do_fork faz a verificação para ver se o depurador está rastreando o processo pai. Se estiver, o sinalizador CLONE_PTRACE será configurado em clone_flags na preparação para a bifurcação. A função do_fork continua então com uma chamada para copy_process, transmitindo os sinalizadores, a pilha, os registros, o processo pai e o PID recentemente alocado.

A função copy_process é onde o novo processo é criado como uma cópia do pai. Essa função executa todas as ações, exceto o início do processo, que é manipulado posteriormente. A primeira etapa em copy_process é a validação dos sinalizadores CLONE para garantir que eles sejam consistentes. Se não forem, um erro EINVAL será retornado. Depois, o Linux Security Module (LSM) é consultado para ver se a tarefa atual pode criar uma nova tarefa. Para saber mais sobre os LSMs no contexto do Security-Enhanced Linux (SELinux), verifique a seção Recursos.

Depois, a função dup_task_struct (encontrada em ./linux/kernel/fork.c) é chamada, o que aloca um novo task_struct e copia os descritores do processo atual nele. Depois que uma nova pilha de encadeamento é configurada, algumas informações de estado são inicializadas e o controle retorna para copy_process. De volta ao copy_process, alguma manutenção é executada, além de várias outras verificações de limite e segurança, incluindo uma variedade de inicialização em seu novo task_struct. Uma sequência de funções de cópia é então chamada, copiando os aspectos individuais do processo a partir da cópia de descritores de arquivos abertos (copy_files), cópia de informações de sinais (copy_sighand e copy_signal), cópia de memória do processo (copy_mm) e, finalmente, da cópia de encadeamento (copy_thread).

A nova tarefa é então designada a um processador, com alguma verificação adicional baseada nos processadores nos quais o processo pode executar (cpus_allowed). Depois que a prioridade do novo processo herda a prioridade do pai, uma pequena quantidade adicional de manutenção é executada e o controle retorna para do_fork. Neste ponto, seu novo processo existe, mas ainda não está em execução. A função do_fork corrige isso com uma chamada para wake_up_new_task. Essa função, que é possível encontrar em ./linux/kernel/sched.c), inicializa algumas das informações de manutenção do planejador, coloca o novo processo em uma fila de execução e depois o ativa. Finalmente, retornando para do_fork, o valor do PID é retornado ao responsável pela chamada e o processo é concluído.

Planejamento de Processo

Enquanto um processo existe no Linux, ele pode potencialmente ser planejado através do planejador Linux. Embora fora do escopo deste artigo, o planejador Linux mantém um conjunto de listas para cada nível de prioridade no qual as referências task_struct residem. As tarefas são chamadas por meio da função schedule (disponível em ./linux/kernel/sched.c), que determina o melhor processo para execução com base no carregamento e histórico de execução do processo anterior. É possível obter informações adicionais sobre o planejador Linux versão 2.6 em Recursos.

Destruição do Processo

A destruição do processo pode ser orientada por vários eventos—a partir do término do processo normal, através de um sinal ou através de uma chamada para a função exit. No entanto, a saída do processo é orientada, o processo termina através de uma chamada para a função do kernel do_exit (disponível em ./linux/kernel/exit.c). Esse processo é mostrado graficamente na Figura 2.

Figura 2. Hierarquia de Função para Destruição do Processo
Hierarquia de Função para Destruição do Processo
Hierarquia de Função para Destruição do Processo

O propósito por trás de do_exit é remover todas as referências do processo atual do sistema operacional (para todos os recursos que não são compartilhados). O processo de destruição primeiro indica que o processo está saindo, através da configuração do sinalizador PF_EXITING. Outros aspectos do kernel usam essa indicação para evitar a manipulação do processo enquanto ele está sendo removido. O ciclo de separação do processo dos vários recursos que ele acumula durante seu ciclo de vida é executado através de uma série de chamadas, incluindo exit_mm (para remover páginas da memória) para exit_keys (que dispõe de uma sessão por encadeamento e chaves de segurança de processo). A função do_exit executa várias contagens para a disposição do processo, depois uma série de notificações (por exemplo, para sinalizar o pai que o filho está saindo) é executada através de uma chamada para exit_notify. Finalmente, o estado do processo é alterado para PF_DEAD e a função schedule é chamada para selecionar um novo processo para execução. Observe que, se a sinalização for requerida para o pai (ou o processo estiver sendo rastreado), a tarefa não desaparecerá completamente. Se nenhuma sinalização for necessária, uma chamada para release_task realmente exigirá a memória que o processo usou.

Indo Além

O Linux continua a se desenvolver e uma área que terá mais inovação e otimização é o gerenciamento de processo. Embora fiel aos fundamentos de UNIX, o Linux continua ultrapassando limites. Novas arquiteturas de processadores, Multiprocessamento Simétrico (SMP) e virtualização orientarão novos avanços nesta área do kernel. Um exemplo é o novo planejador O(1) introduzido no Linux versão 2.6, que fornece escalabilidade para sistemas com grandes números de tarefas. Outro exemplo é o modelo de encadeamento atualizado que usa Native POSIX Thread Library (NPTL), que permite encadeamento eficiente além do modelo LinuxThreads anterior. É possível obter informações adicionais sobre essas inovações e inovações futuras em Recursos.


Recursos para download


Temas relacionados

  • Um dos aspectos mais inovadores do kernel 2.6 é seu planejador O(1). Ele permite que o Linux seja dimensionado para um número muito grande de processadores, sem a sobrecarga típica. É possível obter informações adicionais sobre o planejamento do kernel 2.6 em "Inside the Linux Scheduler" (developerWorks, junho de 2006).
  • Para uma consulta minuciosa ao gerenciamento de memória do Linux, consulte Understanding the Linux Virtual Memory Manager de Mel Gorman (Prentice Hall, 2004), que está disponível no formato PDF. Este livro fornece uma apresentação detalhada, mas acessível, do gerenciamento de memória no Linux, inclusive um capítulo sobre os espaços de endereços de processos.
  • Para uma ótima introdução ao gerenciamento de processo, consulte Performance Tuning for Linux: An Introduction to Kernels (Prentice Hall, 2005). Uma capítulo de exemplo está disponível na IBM Press.
  • O Linux fornece uma abordagem interessante sobre as chamadas do sistema que envolvem a transação entre o espaço de usuário e o kernel (espaços de endereços separados). É possível ler mais sobre o assunto em "Comando Kernel Utilizando Chamadas do Sistema Linux" (developerWorks, março de 2007).
  • Neste artigo, serão encontrados casos nos quais o kernel verifica os recursos de segurança do responsável pela chamada. A interface básica entre o kernel e a estrutura de segurança é chamada de Módulo de Segurança do Linux. Para explorar esse módulo no contexto do SELinux, leia "Anatomia do Security-Enhanced Linux (SELinux)" (developerWorks, abril de 2008).
  • O padrão Portable Operating System Interface (POSIX) para encadeamentos define uma interface de programação de aplicativos (API) padrão para criar e gerenciar encadeamentos. É possível localizar implementações para POSIX em Linux, Sun Solaris e até mesmo em sistemas operacionais que não são baseados em UNIX.
  • A Native POSIX Thread Library é uma implementação de encadeamento no kernel Linux para executar eficientemente encadeamentos POSIX. Essa tecnologia foi introduzida no kernel 2.6, onde a implementação anterior era chamada de LinuxThreads.
  • Leia "TASK_KILLABLE: Novo Estado de Processo no Linux" (developerWorks, setembro de 2008) para obter uma introdução a uma alternativa útil para os estados de processo TASK_UNINTERRUPTIBLE e TASK_INTERRUPTIBLE.
  • Leia mais sobre os artigos de Tim no developerWorks.
  • Na zona Linux do developerWorks, encontre mais recursos para desenvolvedores Linux (incluindo desenvolvedores que são novos para Linux) e varra nossos artigos e tutoriais mais conhecidos.
  • Consulte todas as dicas de Linux e tutoriais de Linux no developerWorks.

Comentários

Acesse ou registre-se para adicionar e acompanhar os comentários.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Linux
ArticleID=383083
ArticleTitle=Anatomia do Gerenciamento de Processos Linux
publish-date=12202008