8/28/2007

Conceitos em Linux (III)

3 - Multiprogramação:
O Linux é um sistema inerentemente multiusuário e multiprogramado. Isso significa que ele deve ser capaz de gerenciar vários usuários que concorrem pelo uso do sistema e vários programas (processos) que competem por recursos ou os compartilham. Por multiprogramação, entenderemos aqui a disputa de vários processos pelo uso do sistema, ou seja, há vários processos compartilhando uma ou mais CPUs e demais recursos de hardware. Quero aqui tratar do compartilhamento do tempo de CPU. Portanto vamos admitir que há apenas uma CPU disponível, o que não trará perda de generalidade, uma vez que este mecanismo é implementado no núcleo com uma interface por CPU (e respectivos bloqueios, que não tratarei agora).
O responsável pelo gerenciamento do compartilhamento de tempo entre os processos é o escalonador do núcleo. É o escalonador que determina que processo será executado e por quanto tempo. É importante que seja o núcleo a decidir isso e não os processos, ou algum processo poderia se apoderar indefinidamente do sistema. Sistemas que permitem que os processos executem até ceder deliberadamente a CPU são ditos de multitarefa cooperativa. É fácil notar que se algum processo do usuário estiver com problemas, o sistema inteiro está comprometido. Infelizmente, sistemas operacionais assim foram muito populares por um tempo. O Linux não sofre deste problema. Quem decide a execução é o núcleo e se o processo do usuário estiver com problemas, o núcleo pode deixar de agendar sua execução se assim o administrador o desejar. Sistemas que funcionam desta forma são ditos de multitarefa preemptiva (ou multitarefa real). No núcleo 2.6 até mesmo partes do núcleo podem ser escalonadas, melhorando muito a performance e o controle sobre o sistema.
Para um escalonamento adequado, o escalonador deve estimar de forma adequada de que maneira o processo utiliza o sistema para poder lhe dar um agendamento que satisfaça os usuários. Assim, podemos classificar os processos em duas categorias, processos orientados à entrada e saída (que doravante indicarei apenas por E/S) e orientados à CPU.
Processos orientados à E/S são processos que utilizam muitos procedimentos e recursos de E/S e por isso passsam muito tempo esperando os dados serem escritos ou ficarem disponíveis. Quando o processo fica ocioso nesta espera, é melhor que seja bloqueado, ou seja, que aguarde que os recursos fiquem disponíveis enquanto outro processo é executado. Afinal, você deve ter pago uma fortuna no seu processador e não quer que ele fique parado a maior parte do tempo só porque um processo resolveu esperar por dados de seu HD.
Processos orientados à CPU são processos que utilizam a CPU de modo pesado, realizando muita computação, ou seja, bloqueando-se pouco. O escalonador do Linux não “gosta” de processos usurpadores de CPU.
Obviamente, esta classificação é bastante subjetiva, pois todos os processos fazem uso de CPU e de E/S, não existe processo puramente orientados à CPU ou à E/S, mas podemos classificá-los assim se mantivermos nossas categorias como conceitos difusos.
Como exemplo de processo orientado à E/S posso citar este editor de textos no qual escrevo estas notas. Por mais rápido que eu digite, a CPU do meu laptop é rápida o suficiente para esperar durante uma eternidade pela próxima tecla a ser digitada. Ele pode muito bem bloquear enquanto outro processo executa, por exemplo o decodificador de mp3 que está neste momento transformando um arquivo em mp3 num fluxo de áudio. Se eu perceber interrupção no fluxo de áudio, vou ficar irritado. Seria capaz de eu fechar o editor e escrever à mão (para mim, sem música, sem trabalho).
Um exemplo de processo orientado à CPU é justamente o decodificador de mp3 que comentei. É claro que ele executa muita E/S (lê arquivo, manda arquivo decodificado para a placa de som, etc.) mas passa a maior parte do tempo decodificando mp3, o que utiliza muito a CPU.
Mas como o escalonador pode me manter feliz? Se eu perceber interrupção no áudio com certeza vou me irritar, mas se os caracteres que eu digitar começarem a demorar muito a aparecer na tela, ficarei igualmente contrariado. Resolver este impasse é justamente o objetivo do escalonador.
Para tanto, cada processo recebe uma prioridade de execução e uma fatia de tempo de CPU. Em princípio, o processo recebe uma prioridade inicial, atribuída pelo sistema ou pelo usuário. Após isso, o escalonador recalcula sua prioridade e respectiva fatia de tempo de acordo com sua orientação.
A prioridade de um processo é um valor que varia de 0 a 139. Os valores de 0 a 99 são reservados a processos que devem responder o mais imediatamente possível, por isso chamados de processos de “tempo real”. Aplicativos onde a resposta deve ocorrer em tempo crítico são classificados como de tempo real. Um exemplo é o processo responsável pela queima de uma mídia de DVD. Se ele demorar muito a ser executado, o buffer do dispositivo irá se esvaziar, haverá interrupção no laser que queima a mídia, a mídia será perdida e o usuário ficará nervoso com o escalonador.
Há dois tipos de filas de execução de tarefas de tempo real, as filas FIFO (primeiro a entrar, primeiro a sair) onde o processo de mais alta prioridade (valor mais próximo de 0) é executado até bloquear e ceder sua vez ao próximo da fila. Se não houver mais ninguém na fila correspondente a este valor de prioridade, um valor de prioridade mais alto é testado até todas as tarefas bloquearem. O outro tipo é a RR (Round Robin – se me permitem, seria algo como “escravos de Jó”?). Neste tipo, cada tarefa recebe uma fatia de tempo e as tarefas da fila de mais alta prioridade (valor mais próximo de 0) são executadas uma por vez, numa fila circular, cada uma durante uma fatia de tempo até que todas bloqueiem e uma nova fila seja pesquisada.
Desta maneira, o Linux não garante a entrega do serviço no tempo exigido, mas faz de tudo para cumprir a agenda.
As tarefas que não são de tempo real, classificadas como “outras”, tem valores associados de 100 a 139. Para o usuário, elas têm seu valor inicial (ou valor “bom” - nice) mapeadas de -20 a 19. -20 corresponde ao valor de mais alta prioridade e 19 ao de mais baixa. Por isso mesmo é que a tarefa ociosa é marcada com prioridade 19. O valor padrão é 0 e usuários não privilegiados só têm autorização para aumentar este valor, nunca diminuir. É por isso que tarefas marcadas com valores negativos são associadas com o superusuário.
Dentro de cada prioridade, forma-se uma fila onde cada processo recebe uma fatia de tempo. As filas são executadas na ordem de prioridade. Dentro de cada valor de prioridade, os processos correspondentes são executados até que todos eles sejam marcadas como expirados. Um processo é marcado como expirado quando esgota sua fatia de tempo. Esgotada sua fatia de tempo, o escalonador recalcula sua prioridade com base no seu uso de CPU. Se o processo bloqueou pouco (orientado à CPU), recebe uma penalidade e tem sua prioridade reduzida. Se o processo bloqueou muito (orientado à E/S), recebe um “bônus” e tem sua prioridade aumentada.
Uma vez que não há mais tarefas ativas, ou seja, todas foram marcadas como expiradas, troca-se a lista de expiradas pela de ativas (trocam-se seus ponteiros) e o sistema varre as listas de prioridade em busca do valor mais baixo (mais alta prioridade) para agendar sua execução.
É interessante notar que meu editor de texto tem uma alta prioridade e uma vez que se desbloqueie, tem prioridade sobre meu decodificador de mp3. Assim, eu digito a tecla, ela é processada quase imediatamente, meu editor bloqueia e meu decodificador pode voltar ao trabalho. Quando num futuro remoto (minha CPU trabalha a 1800 Mhz) eu digitar outra tecla, o decodificador terá me satisfeito disponibilizando fluxo de áudio por aquele período (e, espero eu, enchendo alguns buffers), terá sua execução interrompida, pois um processo de mais alta prioridade foi marcado como ativo (meu editor), este processa minha tecla, bloqueia (é marcada como TASK_INTERRUPTIBLE e chama o escalonador, para ser mais exato) e eu posso voltar a ouvir música.
Até este ponto do texto o escalonador foi capaz de me deixar feliz.
Assim funciona o escalonador do kernel 2.6. Ele é implementado como possuindo um mapa de bits de prioridades (140 bits), cada bit correspondendo a uma lista de tarefas. O funcionamento exato não importa aqui, apenas o fato de que a pesquisa pela próxima tarefa não depende do número de tarefas agendadas pelo escalonador. Ele executa seu código com tempo independente do número de tarefas no sistema. Isto foi um grande avanço com relação ao kernel 2.4, no qual esta pesquisa (todos os procedimentos envolvidos) dependia linearmente do número de processos.
As fatias de tempo destinadas às tarefas dependem de sua prioridade correspondente. As tarefas de mais baixa prioridade recebem fatias de tempo menores até um mínimo de 10 ms. As tarefas de prioridade mais alta recebem uma fatia que pode chegar a 200 ms, e um valor padrão de fatia é 100 ms. Todos estes parâmetros são configuráveis, de acordo com a necessidade do administrador do sistema. O que se deve ter em mente entretanto, é que uma fatia de tempo muito grande pode tornar o sistema perceptivelmente lento ao responder, ou seja, pouco interativo. Os processos de maior prioridade podem deter o uso da CPU tempo demais e o usuário pode vir a perceber que seu processo de baixa prioridade está demorando a responder (no meu caso, poderia fazer com que uma tarefa administrativa qualquer interrompa meu fluxo de áudio). Porém, uma fatia de tempo muito pequena faria com que o sistema perdesse muito tempo em trocas de contexto, ou seja, descarregando um processo com sua memória cache associada, descritores de arquivos, recursos adquiridos, etc. e carregando outro. O sistema passaria mais tempo efetuando trocas de processos na CPU do que efetivamente os executando.
Ao se criar um novo processo, o filho recebe metade da fatia de tempo do pai, que perde o valor de tempo correspondente. Assim evita-se que processos monopolizem o processador simplesmente criando filhos.
Quando vários processadores estão presentes (em processamento simétrico) há ainda uma função que verifica periodicamente o tamanho das listas de execução de cada processador, fazendo um “balanço” (retirando processos de um e inserindo para o outro), caso um deles esteja sobrecarregado com relação aos outros.
Vimos como o núcleo gerencia o compartilhamento de um dos recursos mais importantes de um sistema, o tempo. A análise do compartilhamento de outros recursos de hardware será postergada até que tenhamos visto como funciona um sistema mínimo, capaz de inicializar e administrar tarefas básicas. Penso que assim poderemos parar de perder tanto tempo com “tecnicalismos” e colocar a “mão na massa” mais cedo.

Para mais informações sobre o funcionamento e implementação do escalonador de 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 escalonadores e gerenciamento de tarefas em sistemas operacionais de forma geral, leia “Sistemas Operacionais Modernos” (2ª ed.), por Andrew Tanenbaum (editora Prentice Hall).
Para informações gerais sobre hardware, software e configurações relacionadas ao Linux continuo recomendando:
www.guiadohardware.net
www.vivaolinux.com.br
Para um excelente guia de comandos e configurações recomendo:
www.guiafoca.org

Próximo tópico: muitos usuários.

Marcadores: , , ,

Conceitos em Linux (II)

2 - Arquivos:
Para o Linux, arquivos têm um significado especial. São as unidades que representam recursos do sistema e oferecem um mecanismo de comunicação do núcleo com o mundo exterior.
Podemos classificar os arquivos em ordinários e especiais. Vejamos as possibilidades:
2.1 - Arquivos especiais:
2.1.1 - Dispositivos de caracteres: representam dispositivos que tratam dados como fluxos, tais como teclados, linhas seriais, etc..
2.1.2 - Dispositivos de blocos: representam dispositivos que tratam dados como grupos de blocos, como HDs, memórias, pendrives, etc., qualquer dispositivo no qual faz sentido endereçar suas partes para um acesso aleatório.
2.1.3 - Conduítes (FIFO): são arquivos que servem como “filas” de dados, usados geralmente para troca de informações entre aplicativos. FIFO vem de “first in, first out” ou “primeiro a entrar, primeiro a sair”, um resumo de como deve ser uma fila. Podem servir como “buffers” para algumas aplicações.
2.1.4 - Ligações simbólicas: arquivos que simplesmente apontam para outros arquivos. Contém apenas instruções de como achar o arquivo “verdadeiro”.
2.2 - Arquivos ordinários:
2.2.1 - Diretórios: arquivos que listam nomes de arquivos em seu conteúdo, relacionando-os às informações contidas sobre eles, no sistema de arquivos. Ficará claro mais tarde.
2.2.2 - Arquivos comuns: os arquivos aos quais estamos acostumados, com vídeos, imagens, informações do usuário, etc..

Para lidar com todos estes tipos de arquivos de uma forma homogênea, o núcleo fornece uma interface comum a todos estes tipos: o Sistema de Arquivos Virtual (ou VFS). Assim, ele pode abstrair a implementação de cada dispositivo fornecendo uma maneira simples de implementar a comunicação. Compete ao desenvolvedor dos drivers para o dispositivo específico fornecer a implementação correta de cada função especificada nesta interface. Desta forma, podemos chamar a função read() para qualquer dispositivo que a suporte e o driver “saberá” o que fazer.
É interessante notar que o VFS é orientado a objetos e obedece a todos os requisitos de tal paradigma de programação, mesmo sendo codificado numa linguagem procedural (no caso, C). Isso é feito, na minha opinião, de forma muito elegante, com técnicas tradicionais de programação. Ler o código do núcleo é uma oportunidade única de se aprender boas práticas de programação.
O sistema de arquivos virtual possui os seguintes objetos associados:
2.3 - Objetos do VFS:
2.3.1 - Objeto do superbloco: contém informações sobre o sistema de arquivos montado (um sistema específico) tais como quais são as operações permitidas (ou disponíveis), tamanho de bloco, etc..
2.3.2 - Objeto i-node: o i-node, ou nó-índice, é um objeto que contém informações sobre o arquivo que ele representa. Será melhor tratado um pouco mais tarde.
2.3.3 - Objeto de entrada de diretórios: contém informações que relacionam i-nodes com nomes de arquivos. Servem para localizar arquivos (e organizá-los, obviamente).
2.3.4 - Objeto de arquivo: a representação do arquivo propriamente dito.

Um diretório é um arquivo que contém a relação entre i-nodes e nomes de arquivos. Assim, um diretório típico contém, por exemplo:
1 .
1 ..
4 bin
7 usr
...
e assim por diante. Este é um exemplo de conteúdo de diretório raiz. Note que as entradas “.” e “..” apontam para o mesmo i-node. Isso é válido no diretório raiz pois “.” é uma referência ao próprio diretório e “..” é uma referência ao diretório pai, ambos aqui relativos ao i-node 1, o raiz. Todo diretório deve ter pelo menos estas duas entradas, uma referência ao próprio diretório e uma a seu pai.

Um i-node é um objeto que contém as informações sobre o arquivo. Entre elas, posso citar:
Permissões de acesso: todo arquivo tem um campo de 12 bits que indica quem poderá acessá-lo. Mais detalhes, futuramente.
Dono: identificador do usuário que o criou.
Grupo: identificador do grupo ao qual pertence.
Tipo de dispositivo representado: indica se é um soquete de rede, um dispositivo de blocos, um conduíte, etc..
Atributos: como data de criação, última modificação e último acesso.
Operações possíveis: registro de operações possíveis sobre o próprio i-node e sobre o arquivo que relata.
Trava: indica se o arquivo está sendo usado por outro programa (sendo escrito) para que não haja conflito entre programas que o compartilhem.
Contador de referências: indica quantos diretórios o referenciam, este é um outro mecanismo para ligações (ligações verdadeiras) e será examinado adiante.
Lista de blocos: contém a lista de endereços dos blocos do arquivo que relata.
Vejamos um exemplo de como uma busca é feita num sistema de arquivos como um HD, por exemplo:



A figura 1 mostra os conteúdos parciais (e fictícios) de diretórios e i-nodes de interesse num disco. Para localizar o arquivo (no caso o executável) /usr/bin/ls o sistema executa o seguinte procedimento:
Procura o diretório raiz;
Neste, procura a entrada referente a “usr”;
Localiza o i-node correspondente, no caso 2;
Lê as informações contidas em 2, como permissões de acesso e caso seja possível executar o arquivo, retorna o endereço do bloco onde está armazenado, no caso 350;
Em 350, executa o arquivo (no caso o diretório “usr”) e procura a entrada referente a “bin”;
Localiza o i-node correspondente, no caso 12;
Lê as informações contidas em 12, como permissões de acesso e caso seja possível executar o arquivo, retorna o endereço do bloco onde está armazenado, no caso 1050;
Em 1050, executa o arquivo (no caso o diretório “bin”) e procura a entrada referente a “ls”;
Localiza o i-node correspondente, no caso 1000;
Lê as informações contidas em 1000, como permissões de acesso e caso seja possível executar o arquivo, retorna o endereço do bloco onde está armazenado, no caso 35400;
Finalmente carrega “ls”.

Mas e se o usuário luiz quiser um atalho para /usr/bin/ls em seu diretório pessoal /home/luiz/ chamado “list”?
Luiz tem duas formas de fazer isso. Uma é criar uma ligação simbólica (symlink), um arquivo especial chamado /home/luiz/list contendo o caminho “/usr/bin/ls”. Fazendo isso, seu arquivo “luiz” da figura 1 se modificaria para o da figura 2, que também mostra o novo i-node e arquivo correspondente:



A outra é criar uma ligação verdadeira (hard link), criando uma entrada em seu diretório luiz, chamada “list” que aponta para o i-node 1000, que corresponde ao arquivo “ls”. Isso incrementaria seu contador de referências. As modificações são mostradas na figura 3:



Cada uma destas maneiras tem prós e contras. No caso de se optar por uma ligação simbólica, ao se remover o arquivo original, a ligação será quebrada e o atalho apontará para um valor inválido. No caso de se optar por uma ligação verdadeira, ao se apagar uma entrada que referencie o arquivo (qualquer uma), não se apaga o arquivo, apenas é reduzido seu contador de referências. O arquivo (ou melhor, o i-node) só é liberado quando este contador chega a zero. Porém é necessário que estejam no mesmo sistema de arquivos, uma vez que referenciam o mesmo i-node. Optar por uma forma ou por outra também influencia nas estratégias de cópias de segurança (backups) adotadas.

Como esta é uma interface necessária para o núcleo, sistemas de arquivos que não são estruturados desta maneira necessitam de módulos que os façam parecer assim para o núcleo. Este é o caso de sistemas como FAT32 ou NTFS. Tais sistemas não se estruturam como descrito acima e precisam ter os objetos do VFS criados dinamicamente. Isso explica (ao menos em parte) porque é relativamente fácil elaborar interfaces “somente-leitura” para novos sistemas de arquivos, mas pode ser muito complicado elaborar uma “leitura-escrita”, principalmente se as especificações do sistema de arquivos não são totalmente conhecidas, como ocorre em sistemas de arquivos proprietários.

Para mais informações sobre o funcionamento e implementação do Sistema de Arquivos Virtual no Linux, leia “Desenvolvimento do Kernel do Linux”, escrito por Robert Love (editora Ciência Moderna).
Para mais informações sobre implementação de sistemas de arquivos em sistemas operacionais de forma geral, leia “Sistemas Operacionais Modernos” (2ª ed.), por Andrew Tanenbaum (editora Prentice Hall).
Para informações sobre como manipular diretamente arquivos, diretórios, atributos e permissões 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 continuo recomendando:
www.guiadohardware.net
www.vivaolinux.com.br
Para um excelente guia de comandos e configurações recomendo:
www.guiafoca.org

Próximo tópico: multiprogramação.

Marcadores: , ,

8/23/2007

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.

Marcadores: , ,

Creative Commons License
This work is licensed under a Creative Commons Attribution-NoDerivs 2.5 License.