2. Processos


[Home] [Dissertação] [Biba] [Linux] [Conjugue] [br.ispell] [axw3] [uplink]

Neste capítulo apresentaremos o conceito de processo e muitos outros que lhe são aparentados. Percorreremos numerosos exemplos de uso de comandos unix diretamente ou indiretamente relacionados ao tema.

Conceitos:

Ferramentas:

2.1. [conceito de processo] Em sistemas multitarefa (como o unix), múltiplas tarefas podem estar sendo realizadas simultâneamente, por exemplo a formatação de um disquete, a impressão de um arquivo e a edição de um texto. A cada uma delas corresponde um programa que a executa. Um programa em execução é um processo.

Num computador que possui apenas uma cpu, na verdade apenas um processo pode estar sendo executado em cada instante. O que se faz é executar um processo durante uma fração fixa de segundo, congelá-lo e passar a executar um outro durante essa mesma fração de segundo, e assim por diante. Isso cria a ilusão de simultaneidade.

2.2. [o comando ps] Através do comando ps pode-se examinar os processos correntes. Por exemplo:

  hal:~$ ps
    PID TTY STAT  TIME COMMAND
     45 v02 S     0:00 -bash
    105 v02 R     0:00 ps

Vamos entender o que está acontecendo: há um processo (o shell) com o qual você dialoga a fim de executar comandos. Esse é o processo de número 45. O comando solicitado consiste na execução do programa ps. Esse programa, ao ser executado, descobre que neste momento há dois processos: ele próprio (de número 105) e o shell.

Na verdade além desses dois pode haver muitos outros que o ps não exibe para não poluir a saída. Através das opções a e x entretanto pode-se exibir todos os processos correntes:

  hal:~$ ps ax
    PID TTY STAT  TIME COMMAND
      1  ?  S     0:00 init [5]             
      6  ?  S     0:00 bdflush (daemon)
      7  ?  S     0:00 update (bdflush)
     25  ?  S     0:00 /usr/sbin/crond -l10
     36  ?  S     0:00 /usr/sbin/syslogd
     38  ?  S     0:00 /usr/sbin/klogd
     40  ?  S     0:00 /usr/sbin/inetd
    101 v01 S     0:00 /sbin/agetty 38400 tty1 linux
    106  ?  Z     0:00 (atrun) 
     45 v02 S     0:00 -bash
    107 v02 R     0:00 ps ax

Neste caso, todos os processos além do shell e o ps dizem respeito apenas à administração do sistema. O agetty (por exemplo) é o processo que está controlando o login no console virtual 1. É a ele que você informa o seu username e password ao logar nesse console.

O número de um processo é usado para identificá-lo dentre os outros, por exemplo quando é necessário interromper prematuramente a sua execução. O unix vai numerando os processos em ordem crescente, à medida em que vão sendo criados.

Exercício 2.3: reproduza os exemplos acima de uso do ps, e experimente também usá-lo com a opção "u" (por exemplo "ps u" ou "ps aux".

Exercício 2.4: invoque a man page do ps com "man ps" e leia nela o que significa o campo "STAT". Lembre-se que você pode localizar palavras ao ler uma man page com o comando "/" (para uma lista de comandos pressione "h").

Exercício 2.5: Em geral há um limite superior pequeno para o número que um processo pode ter (por exemplo 32767). O que você acha que acontece quando esse limite é atingido?

Exercício 2.6: Em unix, mesmo algumas operações extremamente simples como listar os arquivos de um diretório envolvem o disparo de pelo menos um novo processo. No entanto a troca do diretório corrente (comando "cd") não provoca o disparo de processo algum. Você saberia explicar porquê?

2.5. [execução em background] Por vezes um processo pode ser de execução demorada. Enquanto ele não terminar, o shell permanecerá aguardando, e você também. Para evitar esse problema, pode-se disparar o processo em "background". Vejamos um exemplo. O comando abaixo irá comprimir todos os arquivos do diretório corrente. Compressão de arquivos é uma típica operação exigente em termos de cpu, por isso um comando assim pode ser demorado:

  hal:~$ gzip -9 *

Enquanto a compressão não terminar, você não poderá usar o shell para disparar novos comandos. Se, por outro lado, essa compressão for colocada em background, o shell permanecerá livre para novos comandos:

  hal:~$ gzip -9 * &

É a ocorrência do caracter "&" no final do comando instrui o shell para dispará-lo em "background".

Exercício 2.6: crie um subdiretório, povoe ele com arquivos e use-o para disparar a compressão em background como indicado acima. Por exemplo:

  hal:~$ cd
  hal:~$ mkdir lixo
  hal:~$ cd lixo
  hal:~$ cp /bin/* .
  hal:~$ gzip -9 * &
  hal:~$ ps u
  (... outros comandos ...)
  hal:~$ cd ; rm -rf lixo

2.7. [Prioridade] Num sistema operacional onde vários processos podem estar simultâneamente em execução, surge às vezes o problema de ser necessário privilegiar ou desprivilegiar a execução de um processo ou um grupo de processos.

Para agilizar a execução de um processo urgente ou para evitar que um processo demorado atrapalhe a execução de outros, o sistema operacional atribui a cada processo uma prioridade, que pode ser alterada quando necessário.

Neste curso iremos limitar-nos ao comando "nice". Com ele, pode-se disparar um processo com baixa prioridade, o que significa que o sistema operacional irá privilegiar a execução dos outros processos em detrimento a ele.

O uso típico do "nice" é impedir que um processo demorado atrapalhe o uso interativo do sistema pelo(s) usuário(s) logados nele. No exemplo que demos antes da compressão, o disparo através do nice seria assim:

  hal:~$ nice gzip -9 * &

Exercício 2.8: Leia a man page do comando renice para saber como alterar a prioridade de um processo após o seu disparo.

2.9. [O que é Environment?] Environment é uma coleção de variáveis que servem de parâmetros para os programas que o sistema executa. Uma das mais conhecidas delas, comum ao unix e ao msdos é a variável PATH. Quando se solicita na linha de comandos do shell a execução de um programa o shell irá procurá-lo nos diretórios relacionados na variável PATH.

Frequentemente torna-se necessário para o usuário lidar com essas variáveis para customizar os aplicativos que utiliza, por isso convém ter algumas noções precisas nesse campo.

Vejamos um exemplo simples. O comando "man" via de regra consulta a variável "PAGER" para saber qual paginador é o preferido do usuário. O paginador padrão costuma ser o "more", mas há outros, como o "less" da Free Software Foundation. Ao invocar uma man page (por exemplo "man ps") você poderá identificar o paginador em uso através do prompt de comandos ":" típico do less no canto inferior esquerdo ou pela string "more" típica do more no mesmo canto.

Uma pessoa que prefira usar o more irá atribuir "more" à variável PAGER, enquanto uma pessoa que prefira o less irá atribuir "less".

Uma diferença importante entre as variáveis PATH e PAGER citadas é que PATH é consultada principalmente pelo shell, enquanto que PAGER é consultada por uma aplicação (man) disparada através do shell. No msdos isso seria irrelevante porque as variáveis de environment são "do sistema", pouco importando quem as está consultando. No unix, o environment é "per-process", portanto o shell precisa ser "avisado" de que aquela variável deve ser exportada para as aplicações:

  hal:~$ set PAGER=more
  hal:~$ export PAGER

Um outro detalhe é que nesse ponto a sintaxe depende do shell em uso. O unix conta com basicamente duas famílias de shells, uma baseada no Bourne Shell e outra no C Shell. Num shell com sintaxe estilo C-shell os comandos anteriores teriam que ser substitídos pelo "setenv":

  hal:~$ setenv PAGER=more

Exercício 2.10: examine o valor da variável PAGER. Um modo de fazer isso é executar o comando "echo $PAGER". O comando "set" executado sem argumentos exibe todas as variáveis atualmente definidas.

Exercício 2.11: atribua repetidamente "more" e "less" para a variável PAGER conforme os exemplos acima executando também uma leitura de man page (por exemplo "man ps") para confirmar a troca do paginador.

Exercício 2.12: examine o(s) arquivo(s) de inicialização do shell que você usa para saber quais variáveis são criadas ou inicializadas neles. Esses arquivos são citados em geral no final da man page (seção "FILES").

2.13 [O que é swap?] A memória é um dos componentes mais caros de um computador. Ao mesmo tempo, a situação habitual é que possuimos menos memória do que necessitamos para fazer tudo o que queremos.

Se pudéssemos visualizar os acessos à memória do computador, perceberíamos que algumas regiões dela permanecem sem serem acessadas durante períodos de tempo relativamente longos. Por exemplo: se você estiver com várias aplicações disparadas mas usando apenas uma delas, as áreas de memória ocupadas pelas outras não estarão sendo acessadas.

Por causa disso sistemas operacionais como o unix usam o conceito de "memória virtual". Cria-se um espaço de memória fictício bem maior do que o que realmente existe. Por exemplo: se o seu computador possui 32 megabytes de memória, ele passará a ter, digamos, 64 megabytes de "memória virtual". Isso significa que a soma de toda a memória requisitada por todos os processos em execução pode ser 64 megabytes ao invés de 32.

Bem... e os outros 32? serão alocados 32 megabytes do winchester para eles. Assim, num determinado momento, um processo pode estar apenas parcialmente residindo na memória. O sistema operacional determina uma estratégia para escolher o que manter na memória física e o que manter no disco. Havendo necessidade, pode-se subitamente enviar ao disco parte do que está na memória ou vice-versa. Esse processo é chamado "swap", e a região do disco alocada é chamada "área de swap".

Assim, se você permanecer algum tempo sem usar uma aplicação, poderá acontecer que ao tentar usá-la novamente haverá um delay significativo para a sua resposta. É o sistema operacional trazendo de volta à memória partes desse processo que estavam residindo em disco.

Exercício 2.14: No Linux há um comando que apresenta dados de ocupação da memória e da área de swap ("free"). Dê uma olhada na man page dele e experimente usá-lo.

2.14 [o comando kill] Uma máquina rodando unix dificilmente trava, mas por vezes uma aplicação particular pode travar. Nesse caso, não é necessário reinicializar todo o sistema, mas apenas matar a aplicação particular que travou, e para isso usa-se o comando "kill". Processos não interativos (que não estão amarrados a um terminal para serem encerrados com um comando "quit" ou equivalente) necessitam também do kill para terem sua execução abortada.

Um modo simples de testar isso é o seguinte:

  hal:~$ sh
  hal:~$ ps
    PID TTY STAT  TIME COMMAND
     45 v02 S     0:00 -bash
    308 v02 R     0:00 sh
    309 v02 R     0:00 ps

Neste momento o prompt que você ganhou não é o do shell que estava em uso (pid=45), mas o do shell recém-disparado (pid=308). Dele, você pode matar o shell recém-disparado voltando ao primeiro:

  hal:~$ kill -9 308

A finalidade do "-9" é assegurar que o processo será morto, pois, como veremos no próximo parágrafo, nem sempre o "kill" mata o processo atingido.

Exercício 2.15: tente reproduzir o exemplo que acabamos de dar.

2.15 [O que é hangup?] Suponha que você disparou um processo cuja execução ainda não encerrou, mas você precisa fechar a sessão e ir embora para casa. Uma situação típica em que isso ocorre é quando se faz download de um arquivo grande pela Internet. O "logout" irá abortar a execução daquele processo, exceto no caso em que ele foi protegido contra o "hangup".

O que vem a ser isso?

Quando a sessão é encerrada, o processo não terminado não é abortado explicitamente. O que ocorre é que ele é submetido a um "hangup".

O unix pode enviar a qualquer processo um dentre vários "sinais". O sinal 9 por exemplo é o sinal "SIGKILL", e o efeito dele é invariavelmente abortar imediatamente a execução do processo. O sinal 15 costuma ser o "SIGTERM". A intenção dele é "avisar" o processo que dentro de alguns segundos ele receberá um sinal "SIGKILL". Um editor de textos nesse momento salvará todos os buffers para evitar a perda do trabalho do usuário. Você pode enviar qualquer um dos sinais a qualquer um dos seus processos através do comando "kill". De fato, a finalidade do comando "kill" não é matar um processo, mas enviar a ele um sinal.

O sinal 1 é o SIGHUP. O logout de que falávamos no início faz com que os processos disparados naquela sessão e ainda não encerrados recebam o sinal 1.

O sinal 1 é usado para fazer com que alguns processos importantes como o init ou o inetd releiam o seu arquivo de configuração, com isso reconfigura-se um servidor sem interrupção do serviço. Fora esses casos, há somente dois modos de um processo reagir ao sinal 1. O primeiro e mais comum é abortar. O segundo é ignorar o sinal.

A fim de que um processo possa ignorar o hangup é necessário que ele tenha sido preparado para isso. O modo de fazê-lo é usar o comando "nohup".

2.17 [Shell scripts] Todo processo foi um dia um arquivo executável. Se falássemos de processos sem criar um desde o início, nossa exposição estaria incompleta. O arquivo executável mais simples que podemos criar é um "shell script", que corresponde no msdos aos arquivos ".BAT".

Vejamos um exemplo elementar:

  hal:~$ cat >teste
  #!/bin/sh
  ls -l|wc -l

Digitando essas três linhas e um Control-D ao final, você criará um arquivo chamado "teste" no diretório corrente cujo conteúdo são as duas linhas, aquela que começa com "#" e a com o "ls".

A primeira informa ao sistema operacional que o interpretador a ser utilizado para executar o script é o shell /bin/sh, que costuma ser o interpretador default. A segunda vai executar um ls e redirecionar a linha para o wc, que vai contar o número de linhas (opção -l). Como o ls foi informado para colocar a saída em modo "longo" (opção -l), o efeito do script é contar o número de entradas do diretório corrente.

Só falta uma coisa para esse script funcionar. É necessário setar o atributo de executabilidade do arquivo teste. Maiores detalhes sobre isso serão dados na aula sobre filesystem. Por ora, basta sabermos que é necessário dar o seguinte comando:

  hal:~$ chmod a+x teste

A partir de agora o script pode ser executado como qualquer outro programa:

  hal:~$ ./teste

Shell scripts podem ser bastante complexos, incluindo comandos para interação com o usuário, controle de loops, varredura das entradas de um diretório, e muito mais.

Exercício 2.18: siga os passos acima para criar o script teste e execute-o em vários diretórios como /etc, /tmp, etc.

Exercício 2.20: você sabe porque colocamos o "./" antecedendo o nome do script teste acima?

2.20 [Scripts em awk] Além dos shells, o unix conta com vários outros interpretadores que podem ser usados para a criação de scripts. Talvez o mais tradicional seja o awk. Outros que surgiram mais recentemente são o perl e o tcl. Os três possuem versões para sistemas não-unix, inclusive msdos. O perl em particular é bastante poderoso e eficiente, sendo muito utilizado atualmente em servidores web.

A título ilustrativo daremos dois exemplos simples de scripts awk, que contém alguns dos conceitos fundamentais dessa linguagem.

Vamos ao primeiro. Note que ele é composto por três blocos. O primeiro, com o label "BEGIN", é executado antes de qualquer leitura da entrada. O segundo é executado uma vez para cada linha lida da entrada, e o terceiro, com o label "END", é executado ao término da leitura da entrada.

  #!/usr/bin/awk -f
  BEGIN {
    t = 0;
  }
  {
    if ($5 != "SIZE") t += $5;
  }
  END {
    print "total: t";
  }

O programa acumula na variável t todos os números lidos nas quintas colunas de cada linha da entrada, exceto quando essa quinta coluna contém a string "SIZE". Esse script foi feito para trabalhar em conjunto com o ps. Se você chamar o script de "s5", o modo de usar é assim:

  hal:~$ ps aux|s5

Nosso segundo exemplo introduz um conceito importantíssimo em linguagens como awk e perl, a saber, o de "vetor associativo". Qualquer programador conhece o conceito de vetor, mas alguns nunca se depararam com vetores associativos. Neles, ao invés de índices numéricos, têm-se strings como índices. Isso é de extrema utilidade em processamento de texto de forma geral.

  #!/usr/bin/awk -f
  {
    if ($5 > 10000) s[$9] = $5;
  }
  END {
    for (i in s) print i ": " s[i];
  }

Se chamarmos o script de "grandes", então a forma de usá-lo será "ls -l|grandes". A saída dele é composta pelos arquivos no diretório corrente com tamanho superior a 10000 bytes.

Exercício 2.21: Teste os scripts dados neste parágrafo. Você pode seguir os passos descritos no parágrafo anterior para criar o arquivo e torná-lo executável. Se você tiver experiência com algum editor de textos unix use-o.

2.21 [at e cron] É frequente programar-se a execução de processos para horários de baixo uso da cpu ou de algum outro recurso, tipicamente de madrugada ou fins de semana. No unix isso é feito através do comando "at". Processos que necessitam ser executados de forma periódica, por exemplo uma limpeza diária em diretórios de arquivos temporários como /tmp, são disparados pelo "cron".

Processos disparados dessa forma não estão amarrados a um terminal, e por isso não têm para onde enviar a sua saída ou mensagens de erro, que via de regra são retornadas ao usuário por mail. Vejamos um exemplo.

  hal:~$ at now + 1 minute
  ls

O "at" executará o script que ler da entrada padrão (algumas implementações do "at" ao invés de ler um script da entrada padrão esperam um nome de script na linha de comandos). No caso, a entrada padrão é digitada manualmente e consiste apenas do comando "ls" (após digitar o "ls" seguido do "enter", pressione control-d). Dentro de um minuto você receberá um mail com o conteúdo do diretório corrente, e poderá lê-lo com o comando "Mail" (por exemplo).

Exercício 2.22: Leia na man page do at de que formas você pode especificar um horário para ele. Cheque também nessa man page como fazer para listar os jobs aguardando o momento da execução.

2.22 [o que são threads?] O advento da linguagem Java tem ajudado a popularizar o termo "thread". Do ponto de vista do usuário, é difícil distinguir "thread" de "processo", e na verdade há livros que confundem os dois conceitos.

Se você já acessou uma página web com duas figuras se movimentando, ao mesmo tempo em que se pode usar o mouse e o teclado para preencher um formulário, então você talvez tenha tido a impressão de que havia vários processos em execução simultânea, mas na verdade o que havia eram vários "threads".

A diferença entre processo e thread é que a área de dados de um processo é própria, enquanto a de um thread não. Isso significa que cada processo tem a sua própria área de dados, em geral inacessível aos outros, enquanto os threads compartilham uma mesma área de dados. É difícil visualizar exatamente o que significa isso sem conhecer aspectos internos de sistemas operacionais e sem ter noções relativamente avançadas de programação, mas como regra geral convém recordar que se tratam de conceitos distintos, e é por isso mesmo que possuem nomes distintos.