Pular para o conteúdo principal

O Problema de Gestão de I/O

A palestra que dei no Sétimo Encontro do Grupo de Usuários de C e C++ do Brasil apresentou meu resultado atual na pesquisa de um framework baseado em uma forma de orientação a aspectos em C++ descrita no "Modern C++ Design" do Alexandrescu.

Minha palestra não apresentou, porém, o que é exatamente o problema a resolver, assumindo que fosse razoavelmente conhecido. Seu foco foi apresentar todos os mecanismos disponíveis para se resolver o problema no Linux 2.6, suas peculiaridades e seus aspectos comuns.

Uma certa razão me motivou, após esta apresentação, a rebobinar meu raciocínio e considerar o problema original. [1] O que há exatamente de tão problemático na gestão de I/O em um programa?

A imagem inicial é a de um programa cujo objetivo é interagir com um dos dispositivos ligados ao computador. A forma básica dessa interação é a cópia de dados do dispositivo para a memória e da memória para o dispositivo, sendo que trabalho útil por parte do programa ocorre enquanto os dados estão na memória.

O trabalho de um tal programa pode, portanto, se manifestar por uma recorrente execução desta sequência exata: copiar do dispositivo para a memória, processar, copiar da memória para o dispositivo, reiniciar.

Agora, observamos um fato simples: enquanto o dispositivo copia, o processador está ocioso; enquanto o processador computa, o dispositivo está ocioso.

Esse fato pode ser um problema para você por duas razões altamente relacionadas: seu computador é tão caro que apenas se justifica quando trabalha a uma taxa máxima e sua fila de tarefas é tão grande que há uma tarefa que poderia processar ou copiar enquanto o processador ou o dispositivo estão ociosos.

Com o objetivo de solucionar esta questão, sistemas operacionais surgiram com a missão de coreografar tarefas de modo a obter a máxima utilização de todas as partes da máquina; i.e. levando uma tarefa ao processador e outra ao dispositivo simultaneamente, se possível.

Este sistema operacional não está conforme a nossa imagem inicial. Ele deve continuamente colocar e tirar tarefas do processador, colocar e tirar tarefas do dispositivo, em uma sequência complexa determinada por mutações indeterminadas em uma máquina de estados. Tais mutações são indeterminadas porque o sistema operacional não pode computar previamente por quanto tempo cada tarefa ocupará cada elemento. O sistema operacional deve agir e depois observar e esperar por alguma ocorrência significativa, então agir e voltar a observar e esperar. [2]

Esta é então nossa segunda imagem: a de um programa cujo objetivo é interagir com múltiplos dispositivos ligados ao computador. A forma básica dessa interação é a alternância entre um estado de paciência, enquanto se observa todos os dispositivos que se pressupõe estarem fazendo algo de útil, e um estado de atividade, iniciado por uma ocorrência significativa ao programa.

O trabalho de um tal programa pode, portanto, se manifestar por uma sequência que intercala atividade em cada um dos dispositivos, de modo que o programa se ocupa em fazer o que puder enquanto um dispositivo ou outro não demanda atenção.

Atividade, individualmente, será copiar ou processar; tomando uma execução do programa e evidenciando o que este programa faz com apenas um dispositivo, veremos a mesma sequência da imagem inicial arranjada no tempo na mesma ordem, separada por períodos em que o programa não se ocupou deste dispositivo.

Considerando com cuidado a imagem de um dispositivo podemos obter um conceito complexo e sofisticado. Um teclado e um monitor são dispositivos simples onde apenas se obtém ou enfia dados, e portanto a imagem desses dispositivos é simples. Uma controladora de vídeo com aceleração 3D é mais complexa, e possui evidentemente processador e memória próprios. Uma controladora de rede por si só não é um dispositivo complexo, mas a interação com ela é complexa, já que o recebimento de dados é algo totalmente imprevisível; além disso, a pilha de protocolos de rede que se constrói sobre ela possibilita o estabelecimento do que são efetivamente dispositivos virtuais, cujo exemplo contemporâneo obrigatório é a sessão TCP/IP.

Podemos nos convencer que a própria máquina é um dispositivo, e que um dispositivo é realmente uma máquina com processador e memória. O programa que dirige dispositivos se transforma em um elemento de um par que interage; o programa neste dispositivo e outro programa em outro dispositivo.

Um exemplo onde esta imagem ocorre claramente é o servidor típico da Internet: sua missão é coreografar atividade entre milhares de dispositivos virtuais TCP/IP onde o dispositivo é, exatamente, um outro programa que executa em uma outra máquina.

A presença de dados passíveis de serem obtidos, do ponto de vista de um dos programas, é determinada pelo fato de o outro programa ter enfiado algum dado do outro lado; estabelecem-se assim protocolos de comunicação que tornam possível a coreografia entre ambas as partes e a realização de trabalho útil.

Se um dos programas enfia dados na ligação virtual, esses dados ficarão em suspenso enquanto o outro programa não recuperá-los. Um tal dispositivo virtual, enquanto está ocioso, pode portanto está disponível ou indisponível. Considerando novamente as nossas exigências de desempenho, é imprescindível para o programa atuar rapidamente sobre o dispositivo quando ele estiver disponível, e é permitido ignorá-lo enquanto estiver indisponível.

Nossa imagem final é portanto a imagem de um programa operando milhares de dispositivos onde cada um desses dispositivos entende-se como sendo uma outra máquina executando um outro programa, potencialmente conforme a mesma imagem e também operando milhares de dispositivos.

Sua atividade envolve coreografar uma dança entre si mesmo e um outro programa de modo que ambos possam trocar dados: um copia para fora enquanto o outro copia para dentro. Além disso, sua atividade envolve agendar diversas danças entre si e todos os dipositivos: cuidando dos disponíveis e ignorando os indisponíveis.

Para atender a essa necessidade, o sistema operacional tem à disposição controladoras especiais na máquina, e o programa tem à disposição serviços especiais do sistema operacional.



[1] este problema está afirmado em todo bom material escolar sobre Sistemas Operacionais, como os do Tanembaum.

[2] o mecanismo básico da máquina que viabiliza esse comportamento é a controladora de interrupções.

Comentários

Postagens mais visitadas deste blog

return void();

É uma pequeneza mas eu gostaria muito que as linguagens da família do C permitissem retornar void quando a função retorna void . Escrever da forma a seguir me aborrece. public class Foo  {    public void log (String msg) { }    public void bar (String x, String y) { }    public void bar (String x)    {      if (x == null)      {        log("x was null");        return;      }      bar(x, "default");    } } Eu ficaria mais feliz escrevendo assim: public class Foo  {    public void log (String msg) { }    public void bar (String x, String y) { }    public void bar (String x)    {      if (x == null) return log("x was null");      bar(x, "default");     } }

.NET COM Interop, IStream e Visual Studio

Investigando oportunidades para otimizar um componente, experimentei definir métodos que não solicitam dados (em arrays etc.) como argumentos mas sim instâncias de IStream , e me deparei com um problema interessante: interoperabilidade com .NET. Usar componentes COM em programas .NET não é particularmente difícil; no Visual Studio, é possível adicionar componentes COM como referências no projeto. Se o componente for capaz de Automation, é possível usar a ferramenta tlbimp.exe para gerar manualmente um callable wrapper para o componente, que, se eu entendi as coisas corretamente, não precisa ser distribuído com a aplicação. Porém, todos os métodos que eu aprendi tem essa peculiaridade: eles parecem assumir que um componente não usa interfaces definidas por outros componentes -- como, por exemplo, declarando parâmetros com tipo IStream , uma interface definida pelo sistema. Pelo método do callable wrapper , a ferramenta assume que as interfaces mencionadas integram o componente...

Eclipse PDE com Gradle

Na virada de 2016 para 2017, adotamos oficialmente o Gradle como ferramenta de construção de software na Prodist. A fila da migração está fluindo confortavelmente desde então. Já temos as provas de conceito necessárias para todos os projetos, inclusive os projetos nativos para compilador C++. Para fontes Java usamos Eclipse. Integramos projetos Gradle ao Eclipse usando Buildship . Acho que o Buildship ainda tem um longo caminho pela frente, mas realiza tudo o que necessitamos. Já estamos entregando com base nesse esquema. O problema mais importante que encontramos foi na integração de projetos OSGi. No Eclipse, trabalhamos projetos OSGi com o PDE. Nosso ciclo de desenvolvimento inclui rodar configurações "OSGi Framework" no Eclipse para teste. Infelizmente, o PDE assume uma estrutura de projeto própria e não funciona corretamente caso contrário. Estes problemas estão registrados em bugs como, por exemplo, o  bug 153023 . Nossos projetos OSGi com Gradle usam o layout ...