Modelagem OO de um Kernel

Tenho um projeto de SO, que inicialmente estava sendo escrito em linguagem C. Comecei a encontrar problemas ao tentar programar genericamente (utilizando macros em C), e também na confecção de drivers (usando struct’s e ponteiros de funções). Resolvi então tentar utilizar uma linguagem OO (C++ no caso, afinal não dá pra fazer SO em Java ou C#, principalmente pela falta de ponteiros).

Em mts situações, OO facilita bastante a vida, como no caso dos drivers (usando interfaces). O grande problema tá sendo como modelar o código (usando padrões de projetos). Em linguagens ou ambientes onde tudo está pronto e configurado (gerenciador de memória, garbage collector, sistemas E/S), usar padrões de projetos “em tese” é algo factível, o problema é fazer isso onde não há nada, nem mesmo uma função pra dar “Hello World” na tela.

Aqui está o X da questão…

Como vou imprimir dados na tela, se não há sistema de E/S? Como vou alocar memória com new, sendo q nem gerenciador de memória há?

Não estou me referindo à como implementar isso (eu tenho conhecimento de como desenvolver sistemas de E/S, o gerenciador de memória, criação e escalonamento de threads e processos, etc). Qual seria o “melhor jeito”, ou o “mais elegente” de implementar isso em OO?

Outra questão é com relação à programação genérica. Cada processador implementa um conjunto de recursos, então um código que funciona em processadores x86_64 não vai funcionar em processadores x86. O mesmo pra outras platformas, como SPARC e ARM.

Eu quero deixar bem claro, não espero q ninguém escreva código Assembly ou entenda de arquitetura de SO’s, longe disso, somente como eu poderia modelar algo do gênero. O kernel é basicamente constituído de:

Kernel
—+ Gerenciador de Boot
—+ E/S de Vídeo
—+ Gerenciador de memória
------+ Memória virtual
------+ Heap do kernel
—+ Gerenciador de Interrupções e Exceções
—+ Escalonador de processos e threads
—+ Rotinas de sincronização
—+ Gerenciador de módulos (drivers) e aplicações

NESSE caso, qual seria o melhor padrão para utilizar?

  1. Inicialmente pela ausência de um gerenciador de memória:
  • Variáveis globais somente e DI?
  • Singletons estáticos globais?
  1. Com relação à código genérico / dependente de plataforma:
  • Criar uma interface, e implementar uma classe para cada plataforma?
  • Criar uma classe base abstrata, deixar os métodos dependentes de plataforma como abstratos e usar herança?
  • Criar um container para armazenar os objetos?
  • Criar uma classe Registry para armazenar as instâncias dos objetos (lembrando q eu preciso ter acesso global aos objetos criados)?

Já pensei em algumas coisas, mas gostaria de sugestões de outros desenvolvedores.

Desde já obrigado.

Será que há algum documento parecido com esse para o Haiku OS (que é uma versão open-source do BeOS? ) Veja o site:

http://www.haiku-os.org/

e veja como é que eles fizeram a modelagem.

Em particular, veja: http://www.haiku-os.org/legacy-docs/bebook/index.html

Primeiramente, obrigado por ter respondido. Conheço o projeto Haiku, o kernel é 90% estruturado (usando programação estilo C), e somente em poucas situações foi usado OO. A parte desenvolvida em OO foram as API’s (mais conhecidos como ToolKits) do sistema, que são uma programação de um nível mais elevado (em modo usuário).

Me refiro à programação mais baixo nível de um kernel. Muito se fala aqui no GUJ sobre o padrão Singleton, que é mt mau utilizado pelos programadores, mas ninguém se compromete a dizer com clareza oq acha de seu uso. Supondo a situação que descrevi acima, somente preciso de 1 instância do Kernel, somente 1 gerenciador de memória, somente 1 gerenciador de boot, somente 1 escalonador de tarefas. Então “o mais correto” seria criar um Singleton pra cada classe. Baseado nisso, faço algumas colocações:

  1. O que falei acima tá certo ou errado afinal?
  2. Ou eu poderia concentrar tudo dentro de um container, que instancia / inicializa os objetos (Kernel, BootManager, MemoryManager, etc.) e injetar os objetos onde precisar?
  3. Outra questão importante, preciso dar um mínimo de suporte para o kernel / drivers, mas usando as classes acima. Em C++, devo sobrecarregar o operador new para ter suporte à ele. Isso se faz desse jeito:
void * operator new(size_t size)
{
    return MemoryManager::instance().malloc(size);
}

Só a partir daí eu poderia usar new pra alocar novos objetos. Usar um singleton nesse caso está correto ou não?

  1. Eu deveria implementar um Registry, por exemplo na classe Kernel para ter acesso às instâncias como em:
void printf(char * s, ...)
{
    Kernel::video->print(...);
}

Hum?

Há algum tempo li uma discussão onde o Linus Torvalds defendia que C++ não é uma boa escolha para o Kernel de um SO. Segundo ele, bem que tentaram usar C++ no Kernel do Linux, isso lá pelos idos de 1992, mas acabaram vendo que não era uma boa.
Embora ele não seja o dono da verdade, sem dúvida é uma opinião de peso. Mas infelizmente não estou encontrando o endereço mais :frowning:

Estou ciente desse fato, mas acredito que há mais prós q contras com relação ao C++. Tá certo q não será possível utilizar todos os recursos do OO, como RTTI e Exceptions (implementar toda a ABI do C++ é bem complicado, mas eu simplesmente desabiblito alguns recursos q julgo não serem tão necessários). Uma vantagem do C++ sobre o C por exemplo está na sobrecarga de funções e métodos (dada a qtia de código, fica difícil “inventar” nome pras funções, pois são mts). Outra coisa “irritante” é com relação à programação genérica, usar macros #if, #ifdef e #ifndef no meio do código é algo extremamente IMUNDO (o código fica sujo, ilegível), sem contar na programação estruturada que é coisa do século passado…

Mas a questão aqui não é se o C++ é boa ou ruim pra escrever SO, é como eu devo MODELAR em OO o sistema. O mais difícil já foi feito, implementei uma ABI básica para chamar os construtores e destrutores estáticos globais e estáticos locais, além de já ter implementado uma “mini” STL (Vector, List, String, Iterator, etc.). Mas falando mais de OO e menos de kernel, surgiu uma questão aqui. Por exemplo, supondo que eu queria criar um singleton para alguma classe, só q eu precise passar parâmetros para inicializar esse singleton, ou mesmo q não seja um singleton, e eu precise injetar algum objeto em outro, só q não pode ser no construtor. Qual seria o melhor modo de fazer isso? Usando um método q inicializa a classe depois do construtor como em:

class MemoryManager
{
    public:
        static MemoryManager & instance()
        {
            static MemoryManager mm;

            return instance;
        }

        void initialize(__PARÂMETROS__)
        {
        }

    private:
        MemoryManager()
        {
        }
};

MemoryManager::instance().initialize(...);

Ou usar um método estático, passar os parâmetros e ele injetar na instância do objeto como em:

class MemoryManager
{
    public:
        static void initialize(__PARÂMETROS__)
        {
            instance.MEMBRO_TAL = param1;
            ...
        }

        static MemoryManager & instance()
        {
            return instance;
        }

    private:
        static MemoryManager instance;

        MemoryManager()
        {
        }
};

MemoryManager MemoryManager::instance;

...

MemoryManager::initialize(tal, tal e tal);

MemoryManager::instance().FAÇA_ALGUMA_COISA(...);

Isso não tem relação nenhuma com programação de kernel, é uma dúvida q eu sempre tive, mesmo programando em Java. Qual solução seria mais indicada? Ou existe um meio melhor de fazer isso?

[quote=magnomp]Há algum tempo li uma discussão onde o Linus Torvalds defendia que C++ não é uma boa escolha para o Kernel de um SO. Segundo ele, bem que tentaram usar C++ no Kernel do Linux, isso lá pelos idos de 1992, mas acabaram vendo que não era uma boa.
Embora ele não seja o dono da verdade, sem dúvida é uma opinião de peso. Mas infelizmente não estou encontrando o endereço mais :([/quote]

Há diversos textos na internet sobre esse assunto. Talvez a mais recente seja essa:
http://www.realworldtech.com/beta/forums/index.cfm?action=detail&id=110549&threadid=110549&roomid=2

Sendo que eu destacaria essa resposta aqui:
http://www.realworldtech.com/beta/forums/index.cfm?action=detail&id=110618&threadid=110549&roomid=2

Comentando sobre essa questão do Linus, ele já demostrou ser um cara “masoquista”, nunca vi um programador gostar mais de complicar o código doq ele. Algumas colocações q eles fez eu discordo RADICALMENTE. Dizer que sobrecarga de funções deixa o código “confuso”? Confuso pra mim é ter 10 funções para criar um processo, onde cada letra significa uma coisa totalmente diferente, e ter pelo menos umas 15 funções pra conexão via socket, onde num dá pra saber qual é a melhor.

Outra questão é com relação à rapidez q o código é executado. Se eu fosse tão “obcecado” por velocidade, não escreveria nem código C nem C++, faria tudo em Assembly (se ele acha C++ lento, imagina linguagens interpretadas como PHP, Python e Java). Ainda no quesito velocidade, o kernel Linux não é um bom exemplo de software extremamente rápido, já começa na compilação (sinceramente, não entra na minha cabeça um programa ficar HORAS compilando código…).

Outra coisa q ele comentou é q não tem coisa q se faça em OO q não dê pra fazer em C, concordo com ele absolutamente, mas convenhamos q se pode ter mais produtividade…

Bom, cai naquele negócio, cada um defende o seu. Ele já demostrou q não gosta de C++, e se vc não gosta de uma coisa, com certeza não vai falar bem. Na época q ele começou a programar oq imperava eram linguagens estruturadas. Hj em dia tudo oq se fala é de programação OO e Design Patterns. Daqui há 10 anos talvez surja outra metodologia de desenvolvimento, é uma evolução natural.

Galera, não quero gerar discussão sobre esse assunto, até pq foge ao escopo do tópico e do GUJ em si. Analisem as 2 situações abaixo:

/* Programação estruturada */

/* stdio.c */

void putchar(char c)
{
#if ARCH == x86
    /* Implementação para x86 */
#elif ARCH == SPARC
    /* Implementação para SPARC */
#elif ARCH == ARM
    /* Implementação para ARM */
#endif
}

...

putchar('A');
// Programação OO (usando Java como exemplo)

/* Video.java */

public interface Video
{
    public void putchar(char);
}

/* x86Video.java */

public class x86Video implements Video
{
    public void putchar(char c)
    {
        // Implementação x86
    }

    // Completando com um singleton ou algum outro padrão...
}

/* SparcVideo.java */

public class SparcVideo implements Video
{
    public void putchar(char c)
    {
        // Implementação SPARC
    }

    // Completando com um singleton ou algum outro padrão...
}

/* ArmVideo.java */

public class ArmVideo implements Video
{
    public void putchar(char c)
    {
        // Implementação ARM
    }

    // Completando com um singleton ou algum outro padrão...
}

/* Platform.java */

public class Platform
{
    static final int X86   = 0;
    static final int SPARC = 1;
    static final int ARM   = 2;

    public Video getVideo(int pf)
    {
        switch (pf)
        {
            case X86: return x86Video::instance();
            case SPARC: return SparcVideo::instance();
            case ARM: return ArmVideo::instance();
            default: return null;
        }
    }
}

Platform::getVideo(X86).putchar('A');

Qual é a opinião de vcs com relação aos 2 códigos acima? Qual demora mais pra compilar / executar / entender?

:arrow: Singleton
Havendo absoluta necessidade, não há problema nenhum em usar. O Singleton deve ser usado em objetos que não podem, em nenhuma circunstância, ser inicializado mais de uma vez. Os seus exemplos caem nessa definição.

Porém, não pode haver uma etapa separada de inicialização. Teoricamente, o objeto deveria sempre existir. No Java, existe o bloco estático, onde é possível buscar todos os parâmetros necessários via algum registry. No C++, não sei o que poderia substituir o bloco estático.

:arrow: Vantagem do C++
Você havia dito que não pretende usar RTTI. Isso invalida qualquer tentativa de polimorfismo, e o seu último exemplo não rodaria como esperado sem o RTTI.
A sobrecarga de operadores não é uma vantagem, ela realmente torna o código confuso porque, numa declaração de método, não dá pra saber o método sendo executado apenas olhando o nome da função. Você precisa descobrir o tipo do parâmetro e talvez até verificar se não há conversão implícita para outro tipo. Se a sobrecarga for o único argumento a favor do C++, acho que você deveria codificar em C.

Elegância é coisa de veado, o importante é código legível. E o código em C está mais legível para mim.

  1. Só 1 coisa, RTTI não invalida qualquer tentativa de poliformismo, trata-se somente da parte de dinamic_cast e typeid (pra sua informação classes abstratas (virtuais) não tem nada a ver com RTTI).

  2. O exemplo de código C acima é SIMPLISTA ao extremo. O negócio começa a complicar qdo vc programa usando rotinas específicas de cada processador, como paginação e conjunto de registradores. Cada CPU usa seu próprio esquema de descritores, paginação, etc, como tb os bits q devem ser setados. No caso do processador i386, ele trabalha com task switch via hardware, e usa páginas de 4KB e 12 bits para indicar estado (acessado, R/W, user mode / supervisor) e 20 bits para indicar endereço. O processador x64 não usa hardware task switch, usa modo LONG e PAE, e outros processadores como ARM usam um sistema de paginação completamente diferente. Me diz 1 coisa, como eu vou criar uma estrutura pra gerenciar paginação, se cada processador implementa memória virtual do seu jeito (oq vc viu acima é “mamata”, tente abrir o código do kernel Linux pra vc ver a qtia de macros e gambiarras q são feitas pra funcionar nas diversas plataformas)?

  3. Cada processador trabalha com um ENDIAN, tente programar genericamente ENDIANS “pra ver quão gostoso é”…

  4. Qdo me referi SOBRECARGA eu disse de MÉTODOS, não de OPERADORES. A minha implementação de iteradores por exemplo usa um esquema parecido com o do Java (hasNext e next) ao invés do estilo da STL (begin, end e incremento e decremento (++ e --)). Mesmo assim eu posso forçar cast com const_cast, static_cast e reinterpret_cast (q não fazem uso de RTTI). No Java mesmo mt usa sobrecarga de métodos, veja a classe PrintStream, onde os métodos print e println foram sobrecarregados diversas vezes, uma vez pra cada tipo básico.

  5. Por último, já pensou em como implementar módulos de kernel em C? Se usa STRUCTS e ponteiros de funções, além de ter q usar UNIONS pra economizar memória. Se vc precisar adicionar 1 parâmetro q seja na struct, vai ter q recompilar todos os módulos (esse é o grande problema da modularização do kernel Linux). Se eu usar interfaces, não importa a forma como os dados são armazenados, se o cidadão usa Vector, List ou Listas feitas à mão. Eu tenho os métodos q retornam os dados formatados.

Eu posso enumerar aqui pelo menos umas 10 situações onde OO seria útil pra codificar, mas foge ao escopo desse tópico. E sobre essa questão do C ou C++, nada vai mudar minha decisão (não é de hj q tento escrever um kernel, já fiquei por 2 anos usando C e chegou num ponto onde tinha GAMBIARRA da GAMBIARRA). A questão aqui não é “elegância”, e sim SIMPLICIDADE. Sempre tento simplificar as coisas ao máximo. E vc diz legibilidade de código, acha isso legível:

void (* paging_alloc)(...);
void (* paging_free)(...);

#if ARCH == X86
void __x86_func_paging_alloc(...);
void __x86_func_paging_free(...);
void init()
{
    paging_alloc = __x86_func_paging_alloc;
    paging_free  = __x86_func_paging_free;
}
#elif ARCH==X86_64
void __x86_64_func_paging_alloc(...);
void __x86_64_func_paging_free(...);
void init()
{
    paging_alloc = __x86_64_func_paging_alloc;
    paging_free  = __x86_64_func_paging_free;
}
#elif ARCH==SPARC
void __sparc_func_paging_alloc(...);
void __sparc_func_paging_free(...);
void init()
{
    paging_alloc = __sparc_func_paging_alloc;
    paging_free  = __sparc_func_paging_free;
}
#elif ARCH==ARM
void __arm_func_paging_alloc(...);
void __arm_func_paging_free(...);
void init()
{
    paging_alloc = __arm_func_paging_alloc;
    paging_free  = __arm_func_paging_free;
}
#endif

O código acima é o trecho q inicializa a memória virtual do meu antigo kernel feito em C (+/- igual acima tem a parte do vídeo, descritores (globais / interrupções / tarefas), controlador de IRQ’s, system timer, fora outras coisas como VFS, MM, etc). Isso tava me deixando maluko…

VOLTANDO ao OO, vc diz implementar algo como:

class Registry
{
    public static TIPO VAR0 = VALOR0;
    public static TIPO VAR1 = VALOR1;
    public static TIPO VAR2 = VALOR2;
    public static TIPO VAR3 = VALOR3;
}

E pegar esse valor no Singleton assim:

class Singleton
{
    public static Singleton getInstance()
    {
        return instance;
    }

    private static Singleton instance = new Singleton();

    private Singleton()
    {
        // Inicialize o singleton com os valores do Registry;

        ATRIBUTO_TAL = Registry.VAR_TAL;
        ...
    }
}

Seria isso?

Tudo bem, se você disse que é possível ter polimorfismo sem RTTI, eu calo a minha boca. Já programei muito em C++ e ainda tem coisa que me surpreendo.

Quando eu disse sobregarga, era realmente a sobrecarga de métodos mesmo. Não gosto, porque torna-se difícil saber, a olho, qual método está sendo chamado. Isso é ruim mesmo em Java, cujas regras de definição de qual método correto pra chamar é contraintuitivo.

No seu exemplo de Singleton, é mais ou menos que eu faria. Eu só obteria os valores de registro via métodos. Existe um problema de uma variável global buscar outra variável global em C++ (a ordem de inicialização das duas variáveis não é portável).

Pra terminar, a decisão de usar C++ é sua, não minha. Nunca programei S.O. mas, ultimamente, ando preferindo C do que C++, porque aquele possui regras simples e um outro desenvolvedor não ficaria perdido porque esqueceu de fazer alguma coisa meio complicada que só o C++ tem.

Acredito que certas partes de um kernel, como o escalonador de processos, deve ter um mínimo de código e ser enxuto ao ponto de não degradar a performance calculando quem vai rodar em seguida e por quanto tempo. Dessa forma eu evitaria ter qualquer tipo de codigo adicional, bibliotecas, etc e provavelmente esta é uma dos motivos que o Linus não usa C++.