Ir para conteúdo

Um pouco sobre Processos em UNIX


Rick Santos

Posts Recomendados

Neste tópico venho falar um pouco a respeito do criação de processos e finalização dos mesmos em ambientes UNIX. Não irei falar sobre a  a execução inicial e final propriamente dito do processo, como por exemplo as suas funções construturas e destruturas.

Inicialmente é essencial entender por base o que é um processo, na verdade o correto será frequentemente chama-lo "tarefa" pois um processo só pode ser um processo quando a tarefa em questão está em execução num determinado momento. Isto significa que quando se trabalha com a execução de tarefas elas estarão a ser chaveadas de tanto em tanto tempo. Mas por fins de facilidade durante o tópico irei-me referir a "processo" em todos os casos.

Enquanto no Windows os processos são criados a partir da função do sistema "CreateProcess()", em ambientes UNIX a coisa é diferente, nestes sistemas todos os processos surgem a partir de uma cópia do processo anteriormente criado. Isto quer dizer que o processo chamador da função vai ser "pai" do novo processo gerado (que é uma cópia do seu pai). Agora fica a questão, se todos os processos no sistema são cópias dos seus pais qual foi o primeiro de todos? Bem, é simples, este é regularmente chamado "Init Process", e é um processo que é criado logo após a inicialização do Kernel do UNIX em fase de boot a partir de diversas rotinas.

Um Processo Init é conhecido como um processo do tipo "Daemon" que significa que a sua execução irá permanecer em "background" até o sistema ser encerrado, podendo-se assim dizer que tem um tempo de vida ilimitado. O processo Init num sistema UNIX terá sempre o PID 1 (Process Identifier)(PID é definido a partir de um tipo de dado "pid_t" em "sys/types.h", normalmente este valor é um inteiro positivo). (No UNIX, por padrão, podem ser criados "32768" processos, para saber tal basta usar acessar o arquivo "/proc/sys/kernel/pid_max", alterando este arquivo será possível expandir ou diminuir o número de processos que podem existir no sistema. Em arquiteturas x86_64 pode no máximo existir "4194303" processos, em x86 só é possível no máximo a criação de "32767" processos no sistema).

No linux existe uma tabela de processos chamada "Process Table" que tem contém entradas que identificam/descrevem os processos existentes no sistema. Ela pode ser usada a partir do comando "ps" do terminal (por padrão este comando apenas irá mostrar processos que estejam "ligados" com o terminal).

OBS: Processos filhos do mesmo pai podem ser chamados entre eles siblings (irmãos).

---------------------------------------------------------------------------------------------------------------------------------------

A função que é usada para a criação de um processo em UNIX será sempre a função "fork()". Esta função tem como objetivo copiar literalmente todo o processo chamador da memória e alocar essa cópia noutra região para mais tarde ser substituida pela imagem do programa que será executado a partir de funções que iremos ver adiante.

A função fork() não recebe parâmetros e retornará valores inteiros diferentes consoante a situação:

 

- Um valor negativo será retornado para o processo chamador da função caso a criação do seu "filho" não seja realizada com sucesso (Normalmente este valor é retornado quando o valor "CHILD_MAX" não é respeitado, este dado define o número de processos filhos que um suposto processo pai pode ter).

- O valor zero será retornado ao processo filho caso este tenha sido criado com sucesso, o valor inteiro positivo também será retornado para o processo pai que terá criado o filho, este valor é o PID (Process Identifier), como descrito anteriormente este é um um "número" associado a um determinado processo servindo para o identificar num sistema UNIX. Caso este valor não seja retornado ao processo pai é possível este usar a função "getpid()" o que irá retornar o PID do processo filho.

 

OBS: Após a execução de fork() o Kernel colocará as páginas usadas pelos processos pai e filho como "Read-Only", por um determinado tempo, causando assim uma Page-Fault caso seja tentando escrita nelas. Neste caso será feito o mapeamento das páginas em questão do processo original para outras com a respetiva cópia da página original (Isto chama-se COW = Copy On Write). O Kernel também criará uma entrada na "Process Table" que permite o acesso ao processo em questão. Um dos últimos detalhes é que após o a execução de fork() ambos os processos pai e filho irão executar a próxima execução que é equivalente aos dois (São equivalente pois como já sabemos o filho é uma cópia do pai).

----------------------------------------------------------------------------------------------------------------------------------------

Já vimos como se dá a criação do processo em si, mas percebe-se também a inutilidade de ter um processo cópia de outro no sistema (Pai e Filho), assim tendo de ser executada uma outra função, chamada exec(). Esta função tem como objetivo substituir todo o contexto presente na memória relativamente ao processo filho criado por fork() pela imagem do arquivo executável presente em disco ("Programa que se quer executar desde o início").Na realidade exec() é um grupo de funções usadas para tal fim, no entanto a função deste "set" de funções será a execve(). Esta função recebe diversos argumentos, mas o principal é o "path" (caminho do arquivo) que contém a imagem binária que será usada para substituir os dados da memória do processo filhos copiados do pai por fork().

Execve() não retorna quaisquer valor, apenas faz com que os segmentos text, data, bss e a Stack do processo "forkado" sejam substituidos pelas secções presentes na imagem do arquivo passada em "path".

Após a substituição da imagem do processo criado por execve(), caso a imagem do novo programa que substituiu a anterior "contiver" shared libraries (Bibliotecas de funções dinâmicamente linkadas), terá de ser feita uma chamada ao Linker do Linux "ld" para leva-las directamente à memória e "linka-las" ao processo em run-time. 

OBS: Por fim é interessante saber que lá porque a imagem da memória foi substituida pela do programa que se quer executar não quer dizer que o processo em questão (filho) perca todos os atributos do processo pai, pois ele ainda é um "fork" deste, um exemplo é o mantenimento dos "Files Descriptors" do pai por parte do filho.

-----------------------------------------------------------------------------------------------------------------------------------

Antes de apresentar a forma de encerramento de um processo em ambientes UNIX, quero primeiro realizar uma breve introdução aos "Process Descriptors" ou "Descritores de Processos". Esta é uma estrutura residente no Kernel do Sistema Operativo que pertence a uma lista chamada "Task List". Cada elemento desta de Task List é um process descriptor que contém diversas informações sobre o processo correspondente (Uma delas é o famoso PID). Um Process Descriptor é do tipo "struct task_struct" (o que significa que é uma estrutura), que é definido em "linux/sched.h". Se não me engano esta estrutura em arquiteturas x86 tem um tamanho arredondado em 1.7 KiB, não me recordo ao certo o tamanho desta em arquiteturas x64, sem alguém souber avise :D.

Atualmente os process descriptors são criados via algo chamado "Slab Alocator" (cujo a descrição encontra-se fora do escopo), existindo assim uma estrutura chamada "struct thread_info" que se encontra para além do "Stack Pointer" da pilha de um processo. (Para os interessados esta estrutura é definida em "/asm/thread_info.h" ;)). O que é preciso saber é que em thread_info existe um elemento chamado "Task" que é um ponteiro para a task_struct (Process Descriptor) do processo.

Existem diversas campos de um process descriptor, um deles é o PID do processo. Existe um outro campo que será interessante para nós... O "state" que dita o estado do processo em questão, em sistemas UNIX existem cinco estados (pelo menos que eu conheça :P):

 

TASK_RUNNING - Este estado indica que o processo pode ser executado, ou encontra-se atualmente em execução.

TASK_INTERRUPTIBLE - O processo encontra-se em "hibernação" à espera da realização de um determinado evento relacionado com ele. O envio de um signal também pode fazer com que ele mude para o estado "TASK_RUNNING".

TASK_UNINTERRUPTIBLE - Igual ao anterior, a diferença é que o processo não "acorda" para TASK_RUNNING ao receber um signal.

TASK_ZOMBIE - O processo foi terminado, mas o process descriptor deste ainda não foi desalocado, até tal o processo permanecerá neste estado (Iremos ver adiante como o process descriptor é desalocado e falar mais sobre processos em estados "ZOMBIE" :ph34r:).

TASK_STOPPED - O processo encontra-se parado, e não consegue ser executado. Isto deve-se ao envio ao recebimento de certos signals por parte do processo.

OBS: É importante ter uma noção dos estados TASK_RUNNING e TASK_ZOMBIE para continuarmos para o estudo da finalização de um processo em ambientes UNIX.

---------------------------------------------------------------------------------------------------------------------------------------------------------------

Agora chegou a parte da finalização de um processo, um ponto a se saber é que todos os processos terminam a partir da syscall exit(), esta função simplesmente termina o processo e limpa todos os dados relacionados com ele da memória, deixando assim páginas livres para que o Kernel trate delas como quiser. A função exit() apenas recebe um parâmetro, que é o status de encerramento do programa (podem ser dois valores: 0 e 1, a linguagem C tem duas macros portáveis entre arquiteturas para tal que são EXIT_SUCESS e EXIT_FAILURE), este valor de retorno é relatado ao pai do processo "terminado". Referi-me a terminado entre aspas pelo simples motivo que neste ponto o processo ainda não foi terminado mas sim encontra-se no estado "TASK_ZOMBIE", isto significa que o processo está morto, mas que o seu Process Descriptor ainda se encontra alocado no Kernel. Isto significa que a função do processo pai neste caso é usar uma syscall chamada wait() que coloca o processo que a executou (o pai) em pausa até um filho deste ser terminado por completamente (desalocando o process descriptor) ou ele mesmo receber o signal. OBS: wait() recebe um valor inteiro como parâmetro e retorna o PID do processo filho terminado.

Já vimos que após a execução do exit(), para o processo ser totalmente encerrado é preciso que o seu pai execute um wait(), mas como é que o processo pai vai saber que o filho em questão foi "terminado" e mudou de estado, assim tendo ele de mandar ao filho um wait()? Isso é feito a partir de um signal chamado "SIGCHLD", este sinal vai informar o pai que determinado filho necessita de ter o seu process descriptor desalocado para assim poder ser totalmente encerrado. (Um processo que se encontre no estado TASK_ZOMBIE, ou seja depois de ter "recebido" um exit() e encontra-se à espera de wait(), é chamado processo Zombie).

Isto quer dizer que se um processo não receber um wait() do seu pai vai permanecer "perdido" pelo sistema, causando o risco de esgotar todos os PIDs possíveis. É pouco comum isso acontecer em computadores pessoais, mas imaginem um grande servidor que precisa de trabalhar com vários processos e threads ao mesmo tempo, os PIDs poderiam esgotar.

---------------------------------------------------------------------------------------------------------------------------------------------------------------

Neste resumo conseguimos ver de uma forma bastante resumida o processo de inicialização e encerramento de tarefas em sistemas UNIX, muita coisa ficou em "branco" aqui, recomendo leitura de documentação oficial sobre funções e processos. :D

 

:P


Ricardo Santos.

 

Link para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...