3/14/2008

Conceitos em Linux (V)

5 - O Padrão de Hierarquia de Sistema de Arquivos

Este texto é apenas uma amostra do padrão para sistemas derivados ou compatíveis com sistemas UNIX com finalidade didática, apenas para familiarizar leitores sem conhecimento prévio com a organização dos diretórios do Linux. Para uma exposição detalhada visite:
http://www.pathname.com/fhs

Diferentemente de outros sistemas operacionais, cuja organização depende muito do usuário, sistemas Linux (na realidade sistemas UNIX em geral, mas como nossa discussão é Linux, vou omitir de agora em diante este fato, presumindo que o leitor tenha entendido) têm uma organização própria na qual cada parte tem seu lugar bem definido. Isso simplifica a localização de executáveis, bibliotecas compartilhadas, arquivos de configuração, arquivos de usuários, etc. dando ao sistema mais coerência. Na opinião deste pobre escritor, está aqui a parte mais bela de um sistema Linux, o que faz realmente com que seja mais fácil de utilizar e administrar do que outros sistemas.
O padrão classifica arquivos utilizando dois critérios, cada um dando origem a duas categorias distintas. Arquivos podem ser compartilháveis ou não e podem ser estáticos ou mutáveis (variáveis). Não entrarei em detalhes, mas é com estes critérios que toda esta divisão é feita no sistema de arquivos.
Como vimos, um sistema é composto de arquivos de dispositivos, arquivos executáveis (programas), diretórios e arquivos comuns de usuários. Entretanto, programas podem ser compilados estaticamente, formando um grande executável ou utilizando bibliotecas compartilhadas. Este último método é preferível pois apesar de colocar uma carga extra na administração do sistema, melhora o desempenho geral. Uma biblioteca compartilhada é um trecho de executável que pode estar presente em vários executáveis de forma idêntica. Isso é muito comum em programação, onde as funções são construídas para ser de uso mais geral possível, evitando desperdício de codificação (codificação leva tempo, o que significa recursos e dinheiro). Um programador pode aproveitar as funções fornecidas por outro e assim reduzir seu trabalho total, tornando a produção mais ágil e robusta, qualquer erro de codificação pode ser resolvido alterando-se a biblioteca, não sendo necessário (ou pelo menos não devendo ser necessário) alterar todos os executáveis. As bibliotecas são carregadas quando o primeiro executável que a usa a chamar (é feita uma ligação dinâmica) e a partir deste instante outros executáveis que a usarem não precisarão mais carregá-las, economia de tempo de carga, memória, enfim, economia. Obviamente outros sistemas usam bibliotecas compartilhadas, mas no Linux este uso é mais racional e organizado.
Além disso, executáveis muitas vezes necessitam de arquivos de configuração estáticos ou variáveis. O próprio núcleo é construído em módulos (bibliotecas de ligação dinâmica) e depende de arquivos de configuração, mesmo que indiretamente. Aos puristas, tudo bem, podemos construir um núcleo Linux estático, mas quem quer fazer isso num PC? Certamente tal construção é conveniente em muitas ocasiões (dispositivos embarcados, por exemplo), mas não é este o foco deste texto.
Um sistema Linux possui um diretório raiz do qual partem todas as outras hierarquias. Não há construção por dispositivos (C:, D:, por exemplo). Todos os diretórios e caminhos devem ser “montados” como subdiretórios do diretório raiz, simbolizado por “/”.
Neste ponto é interessante o leitor acompanhar o texto utilizando um sistema Linux, pois assim poderá verificar o conteúdo dos diretórios em seu sistema. Não há pretexto para não faze-lo, pois se não tiver um Linux instalado, basta utilizar um “live-CD”.
Dentro de “/” temos:
/bin: aqui devem estar os binários essenciais para a utilização do sistema pelos usuários. Exemplos: ls, mkdir, grep, login, mount.
/boot: aqui devem estar os arquivos necessários para a inicialização do sistema. Exemplos: system.map, vmlinuz-, initrd-, config-, grub/ (se estiver usando grub, obviamente).
/dev: diretório para acomodar nós de dispositivos. Exemplos: hda, sda, cdrom, console, random, urandom, ram0, null, audio.
/etc: aqui devem estar os arquivos de configuração do sistema (bem como scripts de configuração e serviços). Exemplos importantes são: bashrc, crontab, fstab, gshadow, shadow, passwd, modules, profile, rc.local, rc.modules, rc.sysinit, inittab, resolv.conf, xinetd.conf. Subdiretórios importantes: init.d/, rcn.d/ (onde n é um número de 0 a 6), X11/.
/home: diretório dos usuários (opcional).
/lib: aqui devem estar as bibliotecas compartilhadas dos executáveis em /bin e módulos do kernel. Exemplos: modules//, libc.so.6.
/media: ponto de montagem para dispositivos removíveis. Exemplos: dvd, floppy.
/mnt: ponto de montagem para sistemas de arquivos temporários.
/opt: diretório para instalação de programas que não estão presentes em sua distribuição.
/root: diretório home do superusuário (opcional).
/sbin: aqui devem estar os binários de administração do sistema. Exemplos: arp, clock, dump, fdisk, hdparm, halt, portmap, route, runlevel, shutdown.
/srv: diretório reservado para dados de serviços oferecidos pelo sistema.
/tmp: diretório reservado para arquivos temporários. É recomendável que seja limpo em cada carga do sistema.
/usr: recursos do sistema UNIX. Esta é na verdade uma hierarquia de diretórios e como tal deve ser estudada com mais detalhe.
/usr/bin: aqui devem estar os binários compartilhados por todos os usuários, não essenciais ao sistema.
/usr/include: diretório para conter os cabeçalhos de programas.
/usr/lib: reservado para as bibliotecas compartilhadas dos executáveis em /usr/bin.
/usr/local: hierarquia local.
/usr/sbin: binários para administração do sistema que não são essenciais. Exemplos: biosdecode, chroot, crond, cupsd, dmidecode.
/usr/share: dados independentes de arquitetura.
Há ainda uma outra hierarquia importante, a /var.
/var: hierarquia para acomodar dados variáveis do sistema.
/var/cache: cache das aplicações do sistema.
/var/lib: informações sobre variáveis de estado.
/var/local: variáveis dos programas em /usr/local.
/var/lock: arquivos de trava. São importantes para evitar que programas que compartilham recursos deixem uns aos outros em estado inconsistente.
/var/log: diretório para arquivos de registro de atividades e erros do sistema. Exemplos importantes: /var/log/kernel/errors, /var/log/kernel/warning, /var/log/dmesg.
/var/opt: dados variáveis dos programas em /opt.
/var/run: dados relevantes para processos em execução.
/var/spool: dados de transferência de arquivos.
/var/tmp: dados temporários que são preservados a cada carga do sistema.
Fora do padrão da hierarquia de sistemas de arquivos para UNIX, sistemas Linux exibem alguma particularidade, como os seguintes diretórios:
/proc: hierarquia para o sistema de arquivos virtual do kernel. É uma maneira de gerenciar processos e serviços, fornecendo uma comunicação direta com o núcleo.
/sys: sistema de arquivos virtual com informações sobre os subsistemas da máquina (sysfs, utilizado por um aplicativo que gerencia conexões dinâmicas de dispositivos, o udev).
/initrd: pasta geralmente vazia mas essencial para o processo de carga, caso utilize uma ramdisk (e certamente utiliza, creia em mim).

Com toda esta rigidez na padronização é nítido que algum esforço tem de ser feito pelos programadores para que seja seguido, mas a recompensa é um sistema claro, limpo e rápido, onde se evita ao máximo redundâncias. Obviamente a escolha de se manter todas as bibliotecas compartilhadas juntas requer atenção para que não haja conflito entre versões. Isto não é fácil e freqüentemente é um dos pontos fracos do Linux, pois a resolução das dependências dos executáveis e suas bibliotecas nem sempre é uma tarefa agradável e isenta de erros de programação, o que pode tornar a instalação de um programinha bobo em um verdadeiro pesadelo. É aí que entram as distribuições e seus gerenciadores de pacotes específicos. Instalar um programa a partir do código fonte pode requerer horas (ou dias) de pesquisa para resolver todas as bibliotecas compartilhadas requeridas, instalando-as uma a uma, na ordem em que dependem umas das outras. Então empresas e fundações (ou gente doida com tempo livre) se preocupam em compilar os programas e colocar informações sobre as dependências em “pacotes” de software para que um programa instalador possa resolver as dependências automaticamente, e se não houver disponibilidade local dos programas, até mesmo procurá-los em sites mantidos por eles na Internet.
Cada distribuição lida com isso de uma forma diferente. O Debian, por exemplo tem repositórios organizados e divididos em categorias de programas e um instalador esperto – o apt-get. Os pacotes têm uma extensão de identificação como .deb e um sistema de controle de versões. Isso torna fácil instalar um programa que já esteja em seus repositórios. Basta configurar de onde gostaria adquirir os pacotes (pode ser de um DVD ou pela Internet) e digitar “apt-get install nome_do_pacote” ou ainda utilizar um dos instaladores gráficos disponíveis se você não gosta de terminais em modo texto e pronto, seu programa está instalado. Se o programa que você quer não estiver nos repositórios... compre analgésicos, terá dor de cabeça.
O Mandriva possui medias (repositórios) e pacotes com extensão .rpm. O instalador é o urpmi, mas também há alternativas gráficas. Recomendo alguns sites para ver sobre a árvore de distribuições do Linux no final deste texto.
Há um projeto para unificar todos estes processos de instalação – o autopackage – mas ainda não tem muitos adeptos. Em minha opinião, é essencial que este projeto entre na base de padrões Linux pois assim muitos problemas de distribuição de pacotes seriam resolvidos. Instalar programas no Linux é ainda um de seus pontos fracos e uma melhoria neste sentido poderia ser crucial para que tenha vantagem sobre sistemas comerciais amplamente difundidos, que possuem um sistema de instalação de software simples (mas meio burro).

Para ver o padrão FHS leia:
Pathname
Para saber sobre as distribuições do Linux visite:
Br-Linux
Linux.org
Distrowatch
TI Experience
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: processo de carga do Linux

9/05/2007

Conceitos em Linux (IV)

4 - Muitos usuários:
Uma das tarefas do Linux é gerenciar vários usuários. Para garantir a estabilidade do sistema, é necessário que haja pelo menos a separação entre o que um usuário comum pode fazer e o que é possível ao administrador. É também conveniente que vários usuários possam compartilhar recursos sem que um interfira com os recursos privativos de outro. Vejamos como o Linux implementa este tipo de separação.
Cada usuário possui um uid, um número que o identifica como apto a utilizar recursos e pertence a pelo menos um grupo, identificado por um número único no sistema (gid).
Num nível fundamental, a segurança de um sistema Linux e a privacidade dos seus usuários dependem dos privilégios de acesso dos usuários e seus grupos a cada arquivo. Para tanto, cada arquivo deve ter registrado (em seu i-node):
seu dono: o uid de quem o detém
seu grupo: o gid de quem o detém
os privilégios de seu dono
os privilégios de seu grupo
os privilégios dos outros usuários

Os privilégios podem ser:
direito de leitura
direito de escrita
direito de execução
acesso especial com set-uid
acesso especial com set-gid
acesso especial com stick-bit ativo

Isso é implementado com um campo de 12 bits listado abaixo:
set-uid
set-gid
stick-bit
dono – leitura
dono – gravação
dono – execução
grupo – leitura
grupo – gravação
grupo – execução
outros – leitura
outros – gravação
outros – execução
Podemos agrupá-los em 4 campos de três bits. Assim, as permissões de acesso especial ficam com o primeiro grupo, as do dono com o segundo, as do grupo com o terceiro e as de outros com o quarto. Com três bits, temos números que vão de 0 a 7. Desta forma, as permissões ficam:
100 = 4: set-uid
010 = 2: set-gid
001 = 1: stick-bit
100 = 4: dono – leitura
010 = 2: dono – gravação
001 = 1: dono – execução
100 = 4: grupo – leitura
010 = 2: grupo – gravação
001 = 1: grupo – execução
100 = 4: outros – leitura
010 = 2: outros – gravação
001 = 1: outros – execução
Podemos combinar as permissões tomando os valores intermediários, respeitando a representação binária. Por exemplo, tomando o campo do dono, podemos dar permissão de leitura e escrita fazendo 110 = 6 ou leitura e execução 101 = 5, ou tudo 111 = 7. Seguindo esta lógica, para dar acesso total ao dono, de leitura e execução para o grupo, leitura apenas para outros, sem acesso especial, escrevemos como: 0754.
Comentarei mais sobre privilégios adiante. Agora vejamos como é identificado um processo em execução, no tocante às permissões de acesso.
Quando um programa é carregado, ele é identificado por três valores de usuário:
ruid: o uid real, uid de quem chamou o processo e só pode ser alterado pelo superusuário;
euid: o uid efetivo, uid que é usado para avaliar os privilégios de acesso do usuário e pode ser alterado pelo superusuário para qualquer valor ou pelo usuário para os valores ruid ou suid;
suid: o uid salvo, geralmente o uid do dono (ruid) e pode ser alterado caso o bit set-uid esteja ativo no arquivo que contém o programa, situação na qual este valor passa a ser o uid do dono do arquivo e não o de quem o chamou.
Um tratamento análogo é feito para o grupo.
Esta é a utilidade dos bits set-uid e set-gid. Se ativarmos o bit set-uid em um arquivo executável (programa), o chamador poderá invocar eventualmente os privilégios do dono do programa. Para entender como isso se torna importante, vamos a dois exemplos.
Digamos que eu tenha um programa que grave CDs (cdrecord). Seria muito interessante se qualquer um que o chame tenha privilégios de superusuário, pois tal programa deve obter prioridade de execução de tempo real. Entretanto não é conveniente distribuir a senha de superusuário para todos os usuários certo? Alguém discorda? Isso pode ser resolvido marcando o arquivo cdrecord com set-uid ativo, sendo seu dono o superusuário (uid = 0).
Quando um usuário carregá-lo (digamos luiz, com uid = 500), suas permissões ficarão assim:
ruid = 500
euid = 500
suid = 0
Então o processo faz a chamada ao sistema seteuid(0), que tenta acertar o uid efetivo para 0. Como o suid é zero, isso é permitido e as permissões ficam:
ruid = 500
euid = 0
suid = 0
Como o euid é zero, o processo adquire todos os privilégios do superusuário, pois é este valor que é analisado para avaliar permissões.
Um exemplo importante é o programa “passwd”. É necessário que ele tenha privilégios administrativos para poder escrever no arquivo de senhas caso seja necessário alterá-las. Mas usuários comuns devem ser capazes de alterar as próprias senhas, o que torna o esquema anterior imprescindível ao processo de autenticação dos usuários.
Agora podemos ver com calma os tipos de privilégios. Para arquivos ordinários, podemos selecionar quem terá permissão para ler, escrever (alterar ou apagar) ou executar cada um em separado, selecionando os usuários por dono, grupo ou os que não se encaixam nestas categorias. É importante que outros não tenham acesso a seus arquivos, se deseja compartilhar algo, crie um grupo de compartilhamento, marque seu arquivo como pertencente a este grupo e peça às pessoas que querem acesso a estes arquivos para entrar neste grupo. Caso seu arquivo seja marcado com set-uid ou set-gid ativos, todos terão os privilégios do dono ou do grupo, respectivamente.
É interessante notar que diretórios são tratados como executáveis. Para abrir um diretório, é necessário que o usuário tenha permissão para execução.
Para executáveis, há ainda o stick-bit (em versões mais antigas, nas mais novas ele não tem efeito). Caso seja um diretório, o efeito de o stick-bit estar ativo é que usuários que têm permissão de escrita no diretório, ou seja, podem criar ou apagar arquivos, só podem apagar os arquivos dos quais sejam donos. Isso é particularmente útil em diretórios compartilhados, onde vários usuários possuem arquivos que não devem ser apagados por outros. No caso de um arquivo executável comum, o efeito do stick-bit ativo é que ao se carregar o programa, é feita uma cópia do seu texto na área de troca (swap), assim seu carregamento será muito mais rápido em uma próxima chamada. Resumindo:
set-uid: ajusta o uid salvo do processo para o do dono do arquivo, para que este possa ajustar seu uid efetivo, mudando seus privilégios;
set-gid: ajusta o gid salvo do processo para o do grupo do arquivo, para que este possa ajustar seu gid efetivo, mudando seus privilégios;
stick-bit: em diretórios impede que usuários com permissão de escrita apaguem arquivos que não lhe pertençam, em executáveis permitem que se salve uma cópia do texto do programa na área de troca.
Disso tudo torna-se claro que a segurança do sistema depende fundamentalmente das permissões de acesso a cada arquivo, diretório ou binário, principalmente quais programas têm ou não permissões especias e também do sistema de autenticação (sua intergridade adicionada a boas senhas). É importantíssimo que os usuários tenham consciência de que a segurança do sistema todo depende de quão boas são as senhas de todos. Só com este cuidado já é possível tornar um Linux minimamente seguro.

Para mais informações sobre implementação de segurança em sistemas operacionais de forma geral, leia “Sistemas Operacionais Modernos” (2ª ed.), por Andrew Tanenbaum (editora Prentice Hall).
Leia os sites de segurança:
linux security
linux security Brasil
Leia os sites de informação sobre Linux:
IBM Developer Works
Referência Debian
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: o padrão de hierarquia de sistema de arquivos

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: , ,

7/30/2007

As Sociedades Fantásticas III

As Sociedades Telepatas Heterogêneas

Começo dizendo que, para o devido entendimento deste ensaio, é imperativo que leia o anterior (“As sociedades telepatas semi-homogêneas”). Portanto, ao prosseguir, certifique-se de já tê-lo feito.
Continuando esta série de ensaios, proponho a análise das sociedades telepatas heterogêneas. Por sociedade telepata heterogênea, entendemos um conjunto de organismos cuja organização mental permite a individualidade. De forma mais específica, há um contato telepático forte entre as entidades, mas estas podem reter pensamentos, ocultando-os da coletividade como bem entenderem.
Desta forma, um organismo pode viver experiências e ter pensamentos sem ter de se preocupar em dividi-los com a coletividade. A coletividade sequer tomaria consciência de tais atitudes. Entretanto, o indivíduo não poderia ocultar-se para sempre. Algo, algum dia, teria de ser compartilhado ou suspeitas poderiam recair sobre este. O medo agora é de mão dupla, o indivíduo teme ter de revelar algo que poderia ser desaprovado, mas a coletividade teme o indivíduo que oculta demais. Surge o medo do misterioso.
É interessante notar que agora o indivíduo tem a capacidade de ocultar, e portanto administrar, informação. Ocultar informação pode significar deter algo que ninguém mais detém. Significa saber algo que ninguém mais sabe, poder algo que ninguém mais pode. Surge a especialização e com ela, o poder.
Indivíduos são diferentes entre si e o tempo os torna especiais. Se a coletividade sentir que um indivíduo tem características que o torna próprio para administrar outros, este pode se tornar um líder. Surge a hierarquia social.
Outros indivíduos podem não concordar com sua administração e surgem sociedades diferentes. Sociedades estas que podem competir por recursos e querer a saída, ou eliminação, de outros que não concordam com seus modos.
Surge a guerra. Surgem as classes sociais e suas inerentes disputas. Em resumo, uma gama de disputas tanto internas quanto externas.
Primeira conclusão: a disputa é a marca principal da individualidade em uma sociedade.
Por haver indivíduos especiais (ou especialistas) haverá a tendência de se importar mais com a dissolução de uns do que com a de outros. Já podemos chamar tal dissolução de morte, pois agora há perda de informação com a dissolução de um organismo. Isso pode ser bastante penoso para a coletividade. Surgem os laços sociais. Surge a morte. Surge a dor da perda.
Pode ser interessante pensar que indivíduos que passem mais tempo próximos desenvolvam vínculos maiores e assim, os que crescem perto de outros se tornarão mais envolvidos com estes. Surge um conceito que podemos denominar “família”.
Simplesmente do fato de se ter a possibilidade de se ocultar algo de outros de maneira permanente, induziu-se o surgimento de hierarquia social, família e morte. Acho lícito imaginar que as famílias (ou grupos de entes próximos) podem querer perpetuar seus bens apenas entre seus iguais, da mesma forma que seu conhecimento e modo de vida. Surge a tradição e a propriedade. É claro que tanto uma quanto outra podem ter seus efeitos atenuados pelo fato de haver ainda uma comunicação muito boa entre os indivíduos, mas o importante é que estas possibilidades agora existem.
Modos próprios, tradição, hierarquia social e medo da morte abrem juntos um caminho quase certo, a religião. A necessidade de se estabelecer de modo indubitável o certo e o errado.
Podemos nos perguntar neste momento: como pode apenas a perda de parte da informação dos indivíduos perante uma sociedade causar uma diferenciação tão grande entre modelos? Se compararmos com o modelo do ensaio anterior, podemos notar que este aqui contém traços que se aproximam bastante das nossas próprias sociedades e o anterior sequer lembrava uma sociedade humana. Neste caso a telepatia pode servir apenas para atenuar problemas sociais existentes, mas os problemas em si são muito próximos aos que temos.
Podemos neste ponto esboçar culpados para nossas dificuldades. Se não conduzi mal estes ensaios, é a ocultação permanente da informação que abre caminhos para que problemas sociais apareçam. É a falta de uma comunicação melhor entre as pessoas.
Num próximo ensaio gostaria de abordar os graus de comunicação possíveis entre seres e avaliar seu impacto sobre os problemas sociais, bem como o que podemos fazer para melhorar nossa comunicação para dirimir, pelo menos em parte, nossos problemas sociais e trabalhar por um mundo melhor.

6/13/2007

Linux: primeiras impressões

Depois de um tempo como usuário Linux, resolvi falar sobre minhas impressões sobre este sistema operacional e sua relação com o resto do mundo.
Quando comecei a aprender utilizar um computador, o mundo era mais simples. Os processadores eram simples e assim também seus sistemas operacionais. Comecei operando um CP-300 quando pequeno, mas só fui entender o que realmente eu estava fazendo quando ganhei um MSX. Não havia problemas, ninguém fora da sua sala poderia invadir seu computador, ele nunca exibia uma tela azul e sempre obedecia a seus comandos, mesmo se eles não fizessem sentido. Caso algo desse errado, era só apertar a tecla “break” e tudo ficava bem novamente. E eu tinha algo que adorava, o acesso direto a hardware, montar um gráfico era, literalmente, brincadeira de criança.
O fato é que eu cresci, e assim também minhas necessidades. Ainda me lembro da primeira transformada de Fourier que tentei implementar no MSX, depois de dois dias ininterruptos de cálculo, houve um pequeno “apagão” elétrico e tive que começar tudo outra vez. Mais três dias de ansiedade por uma resposta à minha série temporal. Estava claro para mim que eu precisava de mais.
Minha sorte é que eu sempre estive anos atrás dos topo-de-linha e quando este contratempo ocorreu, já havia gente se desfazendo de seus 80486.
Agora sim, eu tinha comigo uma máquina dezenas de vezes mais veloz, com processador de 32 bits, contra os 8 bits do Z80 do MSX, eu tinha ganho o mundo em minhas mãos. Doce ilusão. O sistema operacional que acompanhava o PC prometia multitarefa, tinha interface gráfica, gerenciava até o “kit multimídia” (alguém ainda lembra desse tempo?), mas tinha um único probleminha – ele não funcionava.
Mais de meia hora com o micro ligado e “bam”, sempre vinha aquela maldita frase e eu só viria a entender seu significado sórdido anos depois – falha geral de proteção – e aí era só no botão. Foi depois de algumas experiências traumáticas com este famigerado Windows 3.11 que eu entendi o motivo dos novos computadores necessitarem de um botão de “reset” na frente do gabinete.
Indignado, resolvi atualizar a máquina e instalar o mais moderno (e pesado) Windows 95. Agora sim, sistema operacional com nome de ano, multitarefa melhorada, mais brilho em seu monitor, essas coisas. Doce ilusão. Os problemas continuaram e a máquina simplesmente não era confiável para meus trabalhos. Então eu a utilizava apenas para pequenos programas e jogos. É, eu me diverti um bocado nessa máquina, admito.
Programar era outro suplício. Se eu conseguisse achar alguém que me fornecesse um compilador razoável, a documentação era escassa e eu não saía do lugar. Toda sorte de problemas surgiam, compiladores que não seguiam um padrão, diferenças entre versões – coisas que eu não podia compreender. Antes era só ligar o computador e escrever um programa em BASIC/Assembly para o MSX, totalmente documentado. O mundo tinha mudado e eu estava chateado.
Mas eu ainda o usava para pequenos trabalhos de faculdade quando ele faleceu. Acho que o laudo foi falência múltipla das placas, não me lembro bem. Minha família, sempre presente, me conseguiu um AMD K7. Pude então instalar o moderno e mais belo Windows 98 SE, com multitarefa melhorada, mais brilho em seu monitor, mais seguro (não me lembro bem contra quê), tudo o que o outro não tinha. Doce ilusão. Pelo menos dava para ouvir música nele, com um pouco de sorte ele não travaria.
Os outros problemas só se agravaram. Eu consegui uma linha telefônica, mas nunca me arrisquei a acessar a Internet. O principal motivo eram as informações desencontradas. Achei que não era possível ter um sistema estável e eu deveria conviver com isso. Ainda bem que isso também era uma ilusão.
Após choramingar para um monte de gente, um amigo (na época cunhado) sugeriu que eu instalasse um Linux no meu K7. Depois de sofrermos bastante, éramos novatos e inexperientes, conseguimos instalar o Suse 8. Achei feio e a maioria das coisas não funcionava. Mas ele tinha uma vantagem, eu conseguia informações sobre ele se eu precisasse. Quando fiquei um pouco mais esperto, instalei o Mandrake 10.0. Confesso, nem tudo funcionava como deveria, mas era robusto, ele simplesmente não travava. Só por esse motivo já teria valido a pena, mas havia outros.
Fui então percorrer um caminho que sugiro a todos os que me perguntam – qual é a melhor distribuição Linux? - usar as principais distribuições. Pode ser chato, pode ser demorado, mas assim você consegue achar aquela que mais lhe agrada. Cada pessoa tem um conjunto específico de necessidades e cada pessoa usa seu computador de modo diferente, com objetivos diferentes. Certamente uma distribuição irá agradar mais a uns que outros e cada um se dá melhor com uma distribuição ou outra. Isso eu já achei fantástico, eu não estava mais restrito ao sistema operacional da moda (que rodasse no meu computador) eu poderia escolher.
Na escalada por informações, aprendi a deixar o sistema de um jeito em que tudo funcionava (um Debian Sarge) e ele nunca travava. Programar não era mais um suplício, os compiladores seguem um padrão e são muito bem documentados e então eu pude me concentrar em estudar programação. Programação estruturada, orientada a objetos, assembly x86 e chamadas ao sistema, eu tinha multitarefa real, mais brilho em meu monitor, mais desempenho, segurança ao acessar a Internet, essas coisas que me prometiam e nunca cumpriam. E não era ilusão.
Apanhei muito dele, nunca consegui fazer meu soft-modem funcionar nele (mesmo usando um Kurumin – distribuição muito boa por sinal – o modem só não funcionava na minha máquina, nas outras sim), acesso a rede por um hard-modem, vídeo 3D só comprando uma placa, meu computador usava um vídeo integrado SIS e algumas coisas irritantes, como instalar softwares, me davam (e ainda dão) dor de cabeça.
Mas eu podia trabalhar nele. Meus programas, desde simples “scripts” a softwares mais elaborados, têm interface gráfica – coisa que meu dinheiro não poderia comprar se eu adotasse soluções proprietárias.
Melhorei meu computador, instalei nele o Windows XP e o Mandriva 2006 e as coisas começaram a ir bem. Com o C# eu podia fazer bons programas no meu Windows e o XP é um bom sistema para trabalho. Mas comecei a achar muito chato de mexer nele. Era ele quem estava no comando do meu computador, quando na realidade deveria ser eu. Isso eu nunca senti com o Linux. Optei então, agora com um laptop, a não usar o Windows XP (isso sai meio caro pois a licença do Windows está embutida no preço do laptop). Estou usando um Mandriva 2007.0, mas penso em voltar para o meu Debian. Estou com saudades.
O mais engraçado disso tudo é que o núcleo do Linux é construído com uma técnica de programação “ultrapassada”. Ao invés de se valer das modernas técnicas de programação orientada a objetos, ainda usa a linguagem reconhecidamente procedural C. Enquanto outros sistemas usam C++ (orientada a objetos) o núcleo do Linux é feito em C. O resultado desta postura é contrário ao que os adeptos das técnicas inovadoras a todo custo poderiam esperar, o sistema é mais robusto, seu código é legível, tem maior desempenho e estabilidade e usa a orientação a objetos onde é necessária, na implementação do sistema de arquivos virtual (com uma certa dificuldade, mas com muita elegância).
Um ponto a favor do Linux é que eu sei o que está sendo feito, como está sendo feito e onde procurar informações se eu precisar fazer diferente. Isso é impossível com sistemas proprietários.
É bonito saber como o escalonador de processos gerencia prioridades de execução dinamicamente, como é feita a sincronização de processos, o gerenciamento de memória e paginação, os caches de disco e todos os contextos de execução dos códigos. Assim, eu posso tomar um núcleo feito para ter melhor desempenho em uma máquina moderna e adaptá-lo para uma máquina antiga, sem que ela “sofra”. Toda essa versatilidade não tem preço. Literalmente.
Ao invés de ter milhares de chamadas ao sistema obscuras, tem uma interface com o usuário simples com apenas algumas centenas de chamadas, fazendo o mesmo serviço, mas de forma muito mais eficiente. Se alguém da Microsoft discordar, mostre o código e eu pensarei novamente em minhas palavras.
Os únicos problemas que eu tenho ao usar o Linux se referem ao fato de que algumas grandes empresas de hardware se recusam a fornecer suporte de drivers. Foi o caso do meu soft-modem e do meu vídeo integrado. Agora meu K7 ainda tem o problema de eu não poder atualizar o driver do barramento AGP, pois sabe-se lá o que a SIS fez nele (no controlador do barramento) que faz com que apresente comportamento estranho (é... meu Linux trava). Me recuso a abrir o driver e resolver o problema. Simplesmente não compro mais desta marca e não a recomendo a ninguém. Os problemas se estendem também aos softwares proprietários que preciso instalar, que não estão com código fonte disponível ou não estão nos repositórios da minha distribuição. É realmente cansativo. É preciso que haja a adoção de um padrão de instalação de binários (o padrão já existe, é só adotá-lo) e parar de achar que todo mundo tem que mostrar o código e/ou fornecer um binário para sua distribuição específica. Nem todos querem abrir o código fonte de seus softwares, apesar de que acho que lentamente a indústria perceberá o quão vantajoso isso é. Mas isso não pode ser colocado pela força, mas sim pelas vias naturais de adaptação do mercado. Um padrão de instalação de binários só ajudaria a disseminar o Linux como sistema operacional alternativo e torna-lo-ia competitivo em um setor em que ele engatinha, o usuário completamente leigo. A exceção da instalação de softwares de terceiros, o Linux é muito mais fácil de se lidar do que qualquer outro sistema operacional que já pude experimentar.
Em outra oportunidade comentarei sobre detalhes da programação do núcleo que achei interessantes, como a noção de tempo do núcleo e sua relação com o escalonador de processos. Até mais.

5/22/2007

Coisas legais para se fazer com um par EPR

Pares EPR são coisas legais. É por isso que toda vez que eu encontrar (e entender) uma coisa legal que dá para fazer com um par EPR vou colocar aqui.
Vamos tomar duas pessoas que queiram se comunicar à distância, digamos Ana e Beatriz, doravante chamadas de A e B, respectivamente. No espírito do artigo “A Desigualdade de Bell”, deste mesmo blog, vamos tomar uma grandeza que tenha como auto-estados apenas os valores ı0> e ı1>. Vamos construir um estado “emaranhado”, tal como:



e dar o primeiro “bit quântico”, que chamaremos q-bit, para A e o segundo para B (da esquerda para a direita). Vamos supor que A queira enviar uma informação para B, codificada num estado ıψ> (que suporemos normalizado, sempre). Digamos que:
ıψ> = αı0> + βı1>.
Vamos à nossa experiência. Material necessário:



Indicaremos com subscritos 1, 2 e 3 os operadores que atuarem apenas no primeiro, segundo ou terceiro subespaço (subsistema) respectivamente. Isso pode ser entendido como um operador que atua como o identidade nos demais subespaços componentes do sistema completo, que é gerado por produtos tensoriais. Operadores que atuarem em mais de um subespaço terão como indicação subscritos justapostos.
A então resolve aplicar o operador N em seus dois q-bits. Em seguida, aplica o operador H apenas no seu primeiro q-bit. O resultado disso é o seguinte:



Se A medir seus dois primeiros q-bits, pode enviar esse resultado, que é um número clássico, por um canal de comunicação qualquer (clássico) para B.
B então pode tomar as seguintes atitudes:



Note que B é capaz de transformar seu querido q-bit que recebeu como parte do par EPR (nosso estado inicialmente “emaranhado”) no estado ıψ> possuído por A. Tudo ocorre como se o estado ıψ> se “teleportasse” de A para B.
Isso é muito interessante, pois A pode transferir informação a B de tal forma que não há como um intermediário interceptar a transmissão. Mesmo que intercepte a informação sobre as medidas de A, não pode reproduzir o estado ıψ>, pois não possui o “irmão” do estado emaranhado para tal. Não vou discutir a fragilidade dos estados emaranhados, nem se este pode se tornar um esboço de comunicação segura, já que um computador quântico poderia quebrar criptografias convencionais de maneira eficiente. Mas é interessante. Outro fator de interesse é que como a informação de ıψ> vai de um canto a outro sem passar por lugares intermediários, ela é resistente a erros. Temos então um canal de comunicação livre de erros (abstraindo o envio de informação das medidas de A). Isso é divertido!
Como um último tema, eu lhe faço a pergunta: por que o teleporte, com este esquema, não viola a teoria da relatividade restrita? Não, não viola.
Até a próxima.

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