Conceitos em Linux (I)
Este texto tem a intenção de ser introdutório ao Linux (mais especificamente o núcleo 2.6), colocando o básico de seu funcionamento, para que os leitores possam administrar seu sistema com entendimento dos seus princípios e fundamentos. Não tenho o interesse de esgotar o assunto, tampouco fornecer dados sobre o funcionamento exato do código em que foi escrito ou adentrar em méritos que competiriam a um curso de sistemas operacionais. Tais assuntos são brilhantemente cobertos por outros autores e, sempre que possível, procurarei indicar leituras que ache convenientes com o intuito de complementar esta explanação.Fundamentos:
Os dois principais conceitos que o Linux possui é o de processo e o de arquivo. Suas principais tarefas consistem em gerenciar estes dois tipos de recursos, então é interessante dar uma olhada mais de perto em cada um deles. Suponho que você tenha uma idéia do que é um arquivo, pois se está lendo este texto é porque tem uma certa base de informática. Não definirei arquivo. Sejamos informais.
1 - Processos:
Um processo é um “programa ativo”, um texto executável (um código binário), que reside em uma área de memória e é potencialmente executável. Um processo detém recursos e propriedades que cabe ao núcleo do sistema (podemos chamá-lo de kernel) gerenciar.
Eis alguns deles:
1.1 - Espaço de endereço próprio: cada processo “vê” apenas a memória que o núcleo permite, com um espaço de endereçamento próprio. Assim, cada processo “acha” que está com a máquina toda para ele. Estes endereços são virtuais, ou seja, não correspondem necessariamente a endereços físicos da memória RAM, cabendo ao núcleo traduzi-los.
1.2 - Identificador de processo (ou pid): um processo é constituído de vários “filamentos”, partes de programas que compartilham recursos entre si, tais como arquivos abertos, áreas de memória, dispositivos, etc. (mas não o processador). Estas “partes” de programas são chamadas “threads” e organizadas em grupos. Cada thread possui um identificador (ou tid) e um identificador de grupo (tgid) e pode compartilhar recursos com threads do mesmo grupo, mas não com threads de um grupo diferente. Desta forma, um grupo de threads representa um processo. O pid de um processo é o tgid dos seus threads. Isso é interessante, pois não há uma diferença muito grande de implementação entre processos e threads no Linux, a diferença é quem compartilha recursos com quem e se um thread não compartilha nada com ninguém, ele próprio é um processo.
1.3 - Área de texto: a região de memória do código executável do processo.
1.4 - Área de dados: a região de memória das variáveis do processo.
1.5 - Pilha: a região de memória que serve de “temporária”, para guardar valores temporários ou variáveis de escopo local em programas (em funções, por exemplo).
1.6 - Descritores de arquivos: números que representam arquivos abertos pelo processo, em memória (como os buffers para arquivos, que permitem acessos a seus conteúdos, tais como leitura ou escrita). É interessante notar que há descritores de arquivos padrão: 0 representa a entrada padrão de dados (pode ser o teclado), 1 representa a saída padrão de dados (pode ser o monitor) e 2 representa a saída padrão de erros (pode ser o monitor também).
1.7 - Pai (ou ppid – parent pid): todo processo é filho de alguém. Isso ficará mais claro quando discutirmos o processo de criação de um processo.
1.8 - Filhos: todo processo tem uma lista de seus processos filhos, caso existam.
1.9 - Dono: cada processo tem registrado “quem” o chamou (seu uid – user identificator).
1.10 - Grupo: cada processo tem registrado a que grupo pertence “quem” o chamou (seu gid – group identificator).
1.11 - Estado: cada processo se encontra em dos seguintes estados:
1.11.1 - TASK_RUNNING: o processo está sendo executado ou está na fila para ser executado.
1.11.2 - TASK_INTERRUPTIBLE: o processo teve sua execução interrompida (pode estar esperando por um recurso a ser disponibilizado) e espera ser “acordado” por um sinal.
1.11.3 - TASK_UNINTERRUPTIBLE: o processo teve sua execução interrompida, mas não poderá ser “acordado” por um sinal. Pode ocorrer quando um processo precisa esperar por uma interrupção. Por não permitir o processo responder a sinais, este estado não é muito utilizado.
1.11.4 - TASK_ZOMBIE: o processo terminou e está apenas aguardando que seu pai o chame, para desalocar seu descritor de processo e recolher seu código de saída.
1.11.5 - TASK_STOPPED: a execução parou. O processo não está sendo executado, nem deverá ser mais.
Estas são algumas das propriedades que o núcleo mantém sobre seus processos. Examinaremos agora como um processo é criado a partir de outro já existente.
Dado um processo existente, podemos criar outro fazendo com que o processo execute uma chamada ao sistema (uma syscall, uma solicitação ao núcleo) clone(). Tal chamada irá criar uma cópia do processo atribuindo a seu ppid (o pid do pai) o pid do processo que fez a chamada e colocando na lista dos filhos de quem fez a chamada, o pid do novo processo criado. Então o novo processo pode saber quem é seu pai e o antigo quem são seus filhos. Após uma “bifurcação” destas, pode ser interessante (e geralmente o é) que o novo processo execute um outro programa que não o de seu pai, então faz uma chamada ao sistema exec() (uma das chamadas desta família) para ter seu texto substituído por um novo. Quando termina sua execução, faz a chamada ao sistema exit(), para liberar seus recursos. Então, aguarda apenas seu pai realizar uma chamada ao sistema wait4(), com a qual ele pode monitorar o estado de seu filho, descobrir que ele morreu e permitir que ele descanse em paz (afinal seu filho agora deixa de ser um zumbi).
Para manter a compatibilidade com as chamadas ao sistema tradicionais (leia-se UNIX), o Linux fornece um mecanismo para criar processos convencionais através da chamada ao sistema fork(). Esta chamada é implementada no Linux como uma chamada clone() na qual seus parâmetros (argumentos) foram definidos para não se compartilhar o espaço de endereçamento virtual. Assim, uma chamada fork() dá origem a um novo processo, ou se preferir, a um novo grupo de threads. Esta é a maneira tradicional de se lidar com processos, utilizada por sistemas derivados do UNIX que ainda não tinham “suporte a threads”.
Mais adiante este tópico sobre criação de processos será importante para se entender o processo de inicialização do Linux. Por hora só vou adiantar que um Linux deve possuir (ao ser executado) pelo menos dois processos, o init, cujo pid é 1, e a tarefa ociosa, cujo pid é 0. A tarefa ociosa é executada quando todas as outras tarefas estão aguardando por recursos a serem disponibilizados e o init, é o pai de todos os processos.
Para mais informações sobre o funcionamento e implementação dos processos no Linux, leia “Desenvolvimento do Kernel do Linux”, escrito por Robert Love (editora Ciência Moderna).
Para mais informações sobre implementação de processos em sistemas operacionais de forma geral, leia “Sistemas Operacionais Modernos” (2ª ed.), por Andrew Tanenbaum (editora Prentice Hall).
Para informações sobre como criar threads no Linux usando a linguagem C e as bibliotecas do kernel, leia “Sistemas Distribuídos”, escrito por Uirá Ribeiro (editora Axcel).
Para informações gerais sobre hardware, software e configurações relacionadas ao Linux recomendo:
www.guiadohardware.net
www.vivaolinux.com.br
Para um excelente guia de comandos e configurações recomendo:
www.guiafoca.org
Próximo tópico: arquivos.


2 Comments:
Olá Luiz!
Tenho duas dúvidas:
-Qual é a diferença entre as variáveis que estão na pilha, e as que estão na área de dados?
-Não sei se entendi bem o que são os descritores de arquivos... minha outra pergunta é reflexo dessa dúvida: quando você diz que um desses descritores representa a saída padrão de erros, isso significa que o arquivo descrito contém um código de correção de erros?
Grande abraço!
Se você me permite, mais uma dúvida, hehe:
Quando você falou do estado TASK_UNINTERRUPTIBLE, não ficou claro para mim a diferença entre sinal e interrupção...
Postar um comentário
<< Home