O que são processos Unix?

por Sérgio Henrique Miranda Júnior em 24 Mar 2015

Criado em 1972, no laboratório Bell Labs da empresa AT&T, por Ken Thompson, Dennis Ritchie e outros, o sistema operacional Unix foi um dos primeiros a ser desenvolvido. O aludido sistema operacional tem como característica principal o design modular, também conhecido como a filosofia Unix. Segundo esta filosofia, um programa deve realizar uma única tarefa, que precisa ser bem executada. Sistemas que tem como base o Unix, são largamente utilizados atualmente. Muito provavelmente, os sites que você gosta de visitar são hospedados em máquinas que executam um sistema operacional baseado em Unix. Se você trabalha com desenvolvimento web e utiliza a plataforma Linux ou Mac OSx, também está utilizando um sistema operacional que tem como base o Unix. Por isso, é importante conhecermos mais sobre processo Unix, que é a base do sistema operacional. Portanto, este post tem como objetivo demonstrar o que é possível extrair de informações sobre os processos e como eles se comportam dentro do sistema operacional.


Processos Unix

Processos são os blocos que constroem um sistema Unix. Todo código - que constitui um programa - é executado dentro de um processo. Por exemplo: quando você abre seu iTunes, um novo processo é criado e, quando fecha o programa, o processo é finalizado. Isso é verdade para qualquer código executado no seu sistema! Seu gerenciador de banco de dados (MySQL, PostgreSQL), seu navegador web, entre outros, são todos executados dentro de um processo. Para interagir com o hardware e outras funcionalidades disponibilizadas pelo Sistema Operacional, os processos utilizam chamada de sistema, que é a interface de comunicação com o Kernel. Assim, vamos entender quais informações podemos obter dos processos que estão sendo executados pelo sistema operacional e o que os processos podem fazer.

Process Identifier (PID)

Todo processo que esteja rodando em sua máquina tem um identificador único, conhecido como Proccess Identifier (PID). O PID é apenas um número e não diz muito sobre o processo, mas essa é a forma como o Kernel o vê. Desta maneira, torna-se fácil de ser trabalhado em qualquer linguagem de programação, pois trata-se de um número único. Sendo assim, facilita-se a diferenciação ao visualizar escritas de logs, pois basta escrever o PID do processo para saber a qual deles a linha escrita no log pertence. Para visualizar o PID dos que estão rodando na sua máquina, basta executar o programa ps no seu console:

  
    $ ps -aux
  

Em Ruby você pode visualizar o PID do seu processo da seguinte maneira:

  
    Process.pid
  

Parent Process Identifier (PPID)

Todo processo possui o seu processo gerador, ou seja, é o processo que gerou o novo processo. A identificação do processo gerador se dá através do Parent PID (PPID). Suponha que você utilize um Mac. Ao abrir o seu Terminal.app você visualizará um promp do bash. O processo gerador do promp do bash será o seu Terminal.app. Através do programa ps é fácil visualizar o PPID dos processos que estão sendo executados:

  
    $ ps -af
  

Para visualizar o PPID do seu processo Ruby basta utilizar o seguinte código:

  
    Process.ppid
  

Descritores de arquivo

Da mesma forma que o PID representa um processo que está sendo executado, os descritores de arquivo representam os arquivos que estão abertos. Ademais, de acordo com a filosofia do Unix, tudo é considerado um arquivo. Isso significa que dispositivos, arquivos, sockets e pipes são todos tratados como arquivos. Nesse sentido, no momento em que algum recurso é aberto (arquivo, socket, pipe, etc), um número de descritor de arquivo é atribuído a esse recurso. E, ainda, os descritores de arquivo não são compartilhados entre outros processos (apenas entre pai e filho). Em Unix, todo processo é acompanhado de três recursos abertos por padrão: entrada padrão (STDIN), saída padrão (STDOUT) e saída de erro padrão (STDERR). Desse modo, foi criada uma forma padronizada de se ler entradas do teclado ou pipes (STDIN) e de escrever em monitores, arquivos e impressoras (STDOUT e STDERR). Essa padronização é uma das inovações trazidas pelo Unix, uma vez que, antes de exitir a STDIN, cada programa deveria incluir drivers para suportar teclados ou, ainda, para escrever a saída gerada em monitores ou impressoras. Em Ruby é fácil visualizar o número do descritor de arquivo associado ao recurso aberto pelo seu processo:

  
    file = File.open('/etc/hosts')
    puts file.fileno
  

Outra possibilidade é utilizar a ferramenta de linha de comando lsof. De posse do PID do processo que você deseja analisar. Basta digitar:

  
    $ lsof -p <PID a ser analisado>
  

Ambiente

Basicamente, o ambiente é definido por suas variáveis, que são armazenadas em uma estrutura de chave/valor e podem ser utilizadas pelos processos. Todo processo herda as variáveis de ambiente do seu processo gerador, que podem ser utilizadas para setar valores de configurações antes de executar seu processo. Supondo que a funcionalidade de log é ligada através da variável de ambiente LOG, vamos utilizar como exemplo o seguinte programa Ruby:

  
     if ENV['LOG'].eql? "true"
       puts "o sistema irá logar tudo"
     else
       puts "o sistema não realizará log de suas ações"
     end
  

Você pode setar a variável de ambiente antes de executar o programa anterior da seguinte maneira:

  
     $ LOG="true" ruby test_env.rb
  

Argumentos

Todo processo possui acesso a um array chamado ARGV, que é um abreviação de argument vector. Esse array contém os argumentos que foram passados para o processo através da linha de comando. Uma das utilizações mais comuns para o ARGV é a passagem de nomes de arquivos para o programa a ser executado. Um código em Ruby que imprime os valores passados pode ser feito da seguinte maneira:

  
     #!/usr/bin/env ruby
     puts ARGV.inspect
  

Altere as permissões do arquivo para tornar possível sua execução: chmod +x <nome_do_arquivo>. Após realizar o passo anterior basta executar o programa através da sua linha de comando:

  
    $ ./seu_programa --variavel1=teste --variavel2=segundoteste
  

Nomes e códigos de saída

Todo processo possui um nome, sendo possível trocá-lo em runtime. Os nomes podem ser utilizados para troca de informação com os usuários do sistema. Imagine que você possua um processo que realiza a função de deletar os arquivos antigos de backup. Esse processo pode ter o nome ?deletando backups antigos?. Assim, quando o usuário do sistema buscar todos os processos, ele identificará fácilmente qual é a tarefa que este está realizando. Por outro lado, ao terminar sua execução, um processo pode ter um código de saída. Esse código é expressado em números inteiros (0-255) e pode ser utilizado para troca de informações com outros processos. Tradicionalmente, um processo finalizado com o código zero indica que foi terminado com sucesso. Para exemplificar, vamos construir o seguinte programa em Ruby:

  
    puts "o nome do seu programa é:"
    puts $PROGRAM_NAME
    $PROGRAM_NAME = "MEU PROCESSO RUBY"
    sleep(100)
  

Execute seu programa, abra uma nova janela no seu terminal e digite: ps -af. Perceba que o nome do último processo listado será: "MEU PROCESSO RUBY".

Fork

Fork é um dos conceitos mais poderesos existentes em Unix. A chamada de sistema fork(2) permite que um processo que está sendo executado crie outro processo! Esse novo processo, chamado de processo filho, é exatamente a cópia do processo pai. O processo filho herda uma cópia da memória utilizada pelo processo pai, bem como todos os descritores de arquivo. Dessa maneira, ambos os processos podem dividir os arquivos abertos, socket, etc. Ou seja, todos os descritores de arquivos. O processo filho é considerado um novo processo e tem seu próprio PID; já o PPID possui o valor do PID do processo orginal (pai). Pelo fato de um processo filho herdar toda a memória do processo pai, é possível utilizar um processo para carregar toda uma aplicação (Rails app por exemplo) e depois realizar o fork para ter novos processos da mesma aplicação quase de forma instantânea, pois o trabalho pesado de carregar a aplicação terá sido feito pelo primeiro processo. É mais rápido que carregar a aplicação de forma separada em 3 processos. Outro ponto interessante que o fork(2) proporciona é a concorrência em vários processadores. O código abaixo demonstra o processo principal carregando a aplicação e, logo em seguida, executando 3 forks, que acontecem muito rápido, para que os processos filhos possam executar algum trabalho.


  require 'benchmark'

  path = ""

  rails_boot = Benchmark.measure {
    require "#{path}/config/application"
  }

  puts "Rails loaded after #{rails_boot}s..."

  3.times do
    pid = fork do
      puts "Rails app is already loaded"
      puts "do some work ..."
    end
    puts pid
  end

Processos orfãos

O que acontece com o processo filho quando o processo pai termina sua execução primeiro? Nada acontece! O Sistema Operacional não trata processos filho e pai de forma diferente. O processo filho continua sua execução normalmente.

Wait

Como mencionado anteriormente, a execução entre processo pai e filho ocorre separadamente, sem nenhuma dependência entre eles. Todavia, existe uma maneira de esperar o processo filho terminar sua execução. Basta utilizar a chamada de sistema wait(2) e o processo pai ficará bloqueado até que um processo filho termine sua execução. A chamada wait(2) retorna o PID do processo que está finalizando, dessa forma torna-se fácil identificar qual processo filho terminou. É possível passar um PID para especificar qual processo você deseja esperar a finalização, utilizando a chamada de sistema waitpid(2). Essa ideia de esperar/monitorar um filho está no core dos padrões de programação Unix. Esse padrão muitas vezes é chamado de ?master/worker ou preforking?. O script em Ruby abaixo demonstra a utilização da chamada de sistema wait(2).

  
     3.times do
       fork do
         puts "child process"
         sleep(0.5)
       end
     end
     3.times do
       puts Process.wait
     end
     puts "master process"
  

O código acima comporta-se da maneira esperada: imprime 3 'child process', logo em seguida imprime os 3 PID dos processos filhos e, por último, finaliza imprimindo 'master process'. Se a linha que realiza a chamada Process.wait for removida, o comportamento será totalmente diferente, pois o processo pai continuará sua execução independente dos processos filhos. Assim, a frase 'master process' aparecerá no meio das outras frases e não mais no final.

Processos Zumbis

Quando um processo filho termina sua execução, o kernel coloca seu PID em uma fila para ser entregue ao processo pai quando ele executar a chamada waitpid(2). Se o processo pai nunca executar a chamada waitpid(2) a informação do processo filho continuará lá, gastando recurso do Kernel. Processos filhos que não possuem sua informação coletada pelo processo pai são chamados de processos zumbis. Para ilustrar essa situação execute o seguinte código Ruby:

  
    fork do
      puts "child process"
    end

    puts "master process"
    sleep(100)
  

Abra outra janela no seu terminal e execute o seguinte comando: ps -j. Oberseve que um processo terá um +Z na coluna State, indicando que sua execução foi finalizada mas o processo pai ainda não realizou uma chamada wait para coletar a informação armazenada no Kernel.

Processos podem receber sinais

Outra maneira de processos se comunicarem, que não através do código de saída, é através de sinais. A chamada wait(2) mostrada anteriormente, é uma chamada bloqueante. Ou seja, um processo pai ficará bloqueado, esperando que um filho termine, sem a possibilidade de realizar nenhuma ação. Os sinais vem para resolver este problema: através do envio de sinais, é possível realizar a comunição entre dois processos. Para exemplificar o envio de sinais vamos utilizar o seguinte código feito em Ruby:

  
    Signal.trap("INT") do
      puts "Trying to terminate..."
    end

    sleep(100)
  

Execute o código acima e logo em seguida tente terminá-lo pressionando ctrl-c (envia o sinal INT para o processo). Perceba que o processo não é terminado, pois estamos interceptando o sinal INT e executando outro código. Para terminá-lo basta abrir outra aba no seu terminal e executar o comando: kill <pid_do_processo>.

Processos podem se comunicar

A comunicação entre processos pode ser feita de várias formas (conforme mostrado anteriormente), mas as duas mais comuns são através da utilização de pipes e sockets. Pipes é uma maneira unidirecional de comunicação. Um processo tem o papel de escrever dados no pipe e o outro processo de ler os dados que foram escritos. Dessa maneira, o processo que está escrevendo pode somente escrever, enquanto o processo que está lendo pode somente ler. Já o socket pode ser utilizado tanto para ler quanto para escrever. Quando a comunicação entre processos é feita na mesma máquina, pode-se utilizar o Unix Socket, que é um socket próprio para esse tipo de comunicação. O código Ruby abaixo exemplifica uma comunicação entre processos utilizando pipes.

  
     reader, writer = IO.pipe

     pid = fork do
       reader.close
       3.times do
         puts "realizando trabalho pesado"
         writer.puts `ls -la`
       end
     end

     writer.close
     puts "master process waiting on child to do the hard work"
     Process.waitpid pid
     while message = reader.gets
       $stdout.puts message
     end
  

O primeiro passo feito pelo código é criar um pipe. Logo em seguida, é feito um fork para realizar o trabalho pesado (executar o comando ls -la três vezes!). Percebam que o processo filho escreve no pipe, enquanto o processo pai espera a tarefa ser terminada para realizar a leitura. Ou seja, o pipe tornou possível a troca de informações entre o processo pai e o processo filho e, dessa forma, ambos podem seguir o seu caminho e trocar informação através do pipe.

Processos Deamons

Processos deamons são processos que rodam em segundo plano, não sendo controlados por um terminal. São exemplos comuns de processos deamons: servidores web (nginx, apache), sistema gerenciador de banco de dados (MySql, PostGreSQL), entre outros. São processos que sempre estarão executando para servir aos requests.

fork + exec

Nós já sabemos o que a chamada de sistema fork faz, mas e a chamada exec? Exec serve para transformar seu processo corrente em outro processo. Basicamente, é possível transformar um processo ruby em um processo ls, ou em um processo echo. O padrão fork + exec é utilizado pela maioria das bibliotecas que realizam a criação de novos processos. O código Ruby abaixo demonstra esse padrão.

  
    puts "ruby process"
    exec('ls -la')
    puts "ruby again" # essa linha de código nunca será executada
  

A última linha do código nunca será executada, pois a chamada ao método exec transaformará o processo corrente (processo Ruby) em um processo ls.



Saber o que é possível extrair de informação sobre os processos que estão sendo executados na sua máquina, ou até mesmo no seu servidor, é essencial para torná-lo um melhor solucionador de problemas e também um melhor profissional.