- Padrões de design orientados a objetos também permitem implementar polimorfismo e modularidade em kernels escritos em C, possibilitando um design de sistema mais flexível
- Usando vtable (tabela de funções virtuais), é possível padronizar interfaces de dispositivos e serviços e oferecer vários comportamentos por meio de mudanças dinâmicas em tempo de execução
- Serviços do kernel e o escalonador fornecem interfaces consistentes para ações como iniciar, parar e reiniciar via vtable, encapsulando os detalhes de implementação
- Em conjunto com módulos de kernel, isso permite carregamento dinâmico de drivers e expansão do sistema sem recompilação
- Essa abordagem oferece flexibilidade e liberdade para experimentação, mas tem como desvantagem a verbosidade causada por sintaxe complexa e passagem explícita de objetos
Liberdade e padrões orientados a objetos no desenvolvimento de OS
- Desenvolver seu próprio OS permite experimentação livre, sem as restrições de colaboração ou de aplicações reais
- Liberdade em relação a vulnerabilidades de segurança, manutenção de código e pressão de release
- Esse é um dos atrativos do desenvolvimento de OS, pois permite explorar padrões de programação não convencionais
- O artigo da LWN “Object-oriented design patterns in the kernel” apresenta casos em que o kernel Linux implementa princípios orientados a objetos em C
- O polimorfismo é implementado com structs que incluem ponteiros de função
- Encapsulamento, modularidade e extensibilidade permitem aproveitar benefícios de orientação a objetos mesmo em kernels de baixo nível
Conceito básico de vtable
- vtable é uma struct com ponteiros de função que define a interface de um objeto
- Exemplo: struct para operações de dispositivo
struct device_ops { void (*start)(void); void (*stop)(void); }; struct device { const char *name; const struct device_ops *ops; };
- Exemplo: struct para operações de dispositivo
- Dispositivos diferentes (por exemplo,
netdevedisk) usam a mesma API, mas com implementações distintasnetdev.ops->start()chama o comportamento do dispositivo de rede, enquantodisk.ops->start()chama o do dispositivo de disco
- Mudança em tempo de execução: é possível trocar a vtable dinamicamente para alterar o comportamento sem mudar o código chamador
- Com sincronização adequada, isso permite uma evolução dinâmica de comportamento de forma limpa
Casos de uso no OS
Gerenciamento de serviços
- Serviços do kernel (gerenciador de rede, pool de workers, servidor de janelas etc.) podem ser gerenciados com uma interface consistente
- Struct de serviço:
struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; };
- Struct de serviço:
- Cada serviço implementa seu comportamento específico, mas pode executar iniciar/parar/reiniciar de forma padronizada a partir do terminal
- Reduz o acoplamento entre código e serviços, simplificando a administração
Escalonador
- O escalonador pode suportar várias estratégias, como round-robin, shortest-job-first, FIFO e escalonamento por prioridade
- A interface é simplificada em
yield,block,addenext - Definida por vtable, permitindo trocar a política de escalonamento em tempo de execução
- É possível mudar toda a política sem modificar o restante do kernel
- A interface é simplificada em
Abstração de arquivos
- A struct file_operations do Linux implementa a filosofia de que “tudo é arquivo”
- Exemplo: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
struct file_operations { struct module *owner; loff_t (*llseek)(struct file *, loff_t, int); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); ... };
- Exemplo: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- Sockets, dispositivos e arquivos de texto fornecem a mesma interface de read/write
- O código em espaço de usuário pode operar de maneira consistente sem precisar conhecer os detalhes de implementação
Integração com módulos de kernel
- Módulos de kernel permitem carregar dinamicamente drivers ou hooks por meio da troca de vtable
- Como nos módulos do Linux, o kernel pode ser expandido sem recompilação ou reboot
- Ao adicionar novas funcionalidades, basta atualizar a vtable da struct existente
Desvantagens
- Complexidade sintática:
- É preciso passar explicitamente o objeto, como em
object->ops->start(object) - Isso é mais verboso em comparação com a passagem implícita do C++
- As assinaturas de função também ficam longas:
static void object_start(struct object* this) { this->id = ... }
- É preciso passar explicitamente o objeto, como em
- Vantagem: a passagem explícita deixa claras as dependências da função, tornando transparente o acoplamento entre objeto e comportamento
- Um tradeoff adequado entre complexidade e clareza no código de kernel
Implicações
- vtable oferece uma forma simples de reduzir complexidade enquanto preserva a flexibilidade
- Facilita trocar comportamentos em tempo de execução, manter interfaces consistentes e adicionar novas funcionalidades
- Também apresenta uma nova forma de implementar design orientado a objetos em C, destacando o lado experimental e divertido do desenvolvimento de OS
- Material adicional: o projeto xine (https://xine.sourceforge.net/hackersguide#id324430) mostra como gerenciar variáveis privadas com vtable
- O desenvolvimento de OS é um campo de experimentação criativa, e padrões orientados a objetos provam ser ferramentas poderosas até mesmo em sistemas de baixo nível
1 comentários
Comentários do Hacker News
NULLdefault) ou stubs na base. Em OOP mais moderno ou em linguagens orientadas a conceitos, também há a forma de fazer cast para interfaces que usam apenas subconjuntos da API necessária. Go é um bom exemploself/this/object, então se assemelha à abstração de dados no estilo de Cvoid. Além disso, a principal vantagem mencionada no texto dos desenvolvedores do kernel é economizar memória, deixando apenas um ponteiro para vtable em cada instância da struct, em vez de vários ponteiros de função. Ou seja, o ponto principal é a economia de memória, mas o OP está usando essa vtable como uma indireção para trocar métodos em tempo de execução e implementar polimorfismo. Esse padrão é diferente do que os desenvolvedores do kernel estavam descrevendovoid, e não ponteirovoid. Vtable é usada para implementar polimorfismo. Sem polimorfismo, nem se usaria vtable, então a economia de memória seria ainda maiorthis. Na prática, você está sempre passando a instânciathis, e othisexplícito evita confusão sobre se uma variável pertence à instância, é global ou veio de outro lugarthisao referenciar membros de instânciaobject->ops->start(object), o objeto precisa ser explicitado duas vezes: uma para resolver a vtable e outra para passá-lo à implementação da função em CmFoo,m_Foo,foo_etc.foo_é preferido por ser mais conciso quethis->foo. Claro, em C++ também é possível usarthisexplicitamentethisimplícito deixa o código mais conciso, e quando se usa um método de verdade não é preciso repetir o prefixo da struct em cada função. Por exemplo, fica mais natural escrevers->dosmth();em vez demystruct_dosmth(s);this. O exemplo destruct file_operationstem ponteiros de função que não recebemthis, então é difícil considerá-lo uma vtable de verdadefoo(thing, ...)em vez dething->vtable->foo(thing, ...)