- Los patrones de diseño orientado a objetos también permiten implementar polimorfismo y modularidad en kernels escritos en C, lo que hace posible diseñar sistemas más flexibles
- Mediante vtable (tabla de funciones virtuales) se estandariza la interfaz de dispositivos y servicios, y el cambio dinámico en tiempo de ejecución permite soportar distintos comportamientos
- Los servicios del kernel y el scheduler ofrecen interfaces consistentes para iniciar, detener y reiniciar a través de vtable, encapsulando los detalles de implementación
- En combinación con los módulos del kernel, se soporta la carga dinámica de drivers, lo que permite expandir el sistema sin recompilar
- Este enfoque ofrece flexibilidad y libertad para experimentar, aunque tiene la desventaja de una sintaxis compleja y de la verbosidad causada por el paso explícito de objetos
Libertad y patrones orientados a objetos en el desarrollo de OS
- Desarrollar tu propio OS permite experimentar libremente sin las restricciones de la colaboración o de aplicaciones reales
- Libera de vulnerabilidades de seguridad, mantenimiento de código y la carga de hacer lanzamientos
- Ese es parte del atractivo del desarrollo de OS: permite explorar patrones de programación no estándar
- El artículo de LWN “Object-oriented design patterns in the kernel” presenta casos en los que el kernel de Linux implementa principios orientados a objetos en C
- El polimorfismo se implementa con structs que incluyen punteros a función
- Gracias a la encapsulación, modularidad y extensibilidad, incluso un kernel de bajo nivel puede aprovechar las ventajas del diseño orientado a objetos
Concepto básico de vtable
- Una vtable es una struct con punteros a función que define la interfaz de un objeto
- Ejemplo: una struct para operaciones de dispositivo
struct device_ops { void (*start)(void); void (*stop)(void); }; struct device { const char *name; const struct device_ops *ops; };
- Ejemplo: una struct para operaciones de dispositivo
- Distintos dispositivos (por ejemplo,
netdev,disk) usan la misma API, pero con implementaciones diferentesnetdev.ops->start()invoca la operación del dispositivo de red, mientras quedisk.ops->start()llama la del dispositivo de disco
- Cambio en tiempo de ejecución: se puede reemplazar dinámicamente la vtable para modificar el comportamiento sin cambiar el código llamador
- Con una sincronización adecuada, esto permite una evolución dinámica del comportamiento de forma limpia
Casos de uso en un OS
Gestión de servicios
- Los servicios del kernel (administrador de red, worker pool, servidor de ventanas, etc.) pueden gestionarse con una interfaz consistente
- Struct de servicio:
struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; };
- Struct de servicio:
- Cada servicio implementa su propio comportamiento, pero desde la terminal puede iniciarse/detenerse/reiniciarse de manera estandarizada
- Se reduce el acoplamiento entre el código y los servicios, simplificando la administración
Scheduler
- El scheduler puede soportar distintas estrategias como round robin, shortest job first, FIFO y scheduling por prioridad
- La interfaz se simplifica a
yield,block,addynext - Definido mediante vtable, permite cambiar la política de scheduling en tiempo de ejecución
- Se puede cambiar toda la política sin modificar el resto del kernel
- La interfaz se simplifica a
Abstracción de archivos
- La struct file_operations de Linux implementa la filosofía de “todo es un archivo”
- Ejemplo: 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 *); ... };
- Ejemplo: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- Sockets, dispositivos y archivos de texto ofrecen la misma interfaz de read/write
- El código en espacio de usuario puede operar de manera uniforme sin necesidad de conocer los detalles de implementación
Integración con módulos del kernel
- Los módulos del kernel permiten cargar dinámicamente drivers o hooks mediante el reemplazo de vtables
- Como en los módulos de Linux, es posible extender el kernel sin recompilar ni reiniciar
- Al agregar nuevas funciones, basta con actualizar la vtable de la struct existente
Desventajas
- Complejidad sintáctica:
- Se debe pasar explícitamente el objeto, como en
object->ops->start(object) - Es más verboso que el paso implícito de C++
- Las firmas de función también son más largas:
static void object_start(struct object* this) { this->id = ... }
- Se debe pasar explícitamente el objeto, como en
- Ventaja: el paso explícito deja claras las dependencias de la función y hace transparente el acoplamiento entre objeto y comportamiento
- En código de kernel, es un tradeoff razonable entre complejidad y claridad
Implicaciones
- Las vtables ofrecen una forma simple de reducir complejidad manteniendo la flexibilidad
- Facilitan reemplazar comportamientos en tiempo de ejecución, mantener interfaces consistentes y agregar nuevas funciones
- Ofrecen una nueva manera de implementar diseño orientado a objetos en C, subrayando la diversión experimental del desarrollo de OS
- Material adicional: el proyecto xine (https://xine.sourceforge.net/hackersguide#id324430) muestra cómo gestionar variables privadas con vtables
- El desarrollo de OS es un espacio para la experimentación creativa, y demuestra que los patrones orientados a objetos son herramientas poderosas incluso en sistemas de bajo nivel
1 comentarios
Opiniones de Hacker News
NULLself/this/object, así que se asemeja a la abstracción de datos al estilo de Cvoid. Además, la ventaja principal mencionada por el desarrollador del kernel es ahorrar memoria al poner solo un puntero a vtable por instancia de struct, en vez de varios punteros a función. Es decir, el punto principal es el ahorro de memoria, pero el OP está usando esa vtable como indirección para reemplazar métodos en tiempo de ejecución e implementar polimorfismo. Ese patrón es distinto de lo que describía el desarrollador del kernelvoid, sino avoidcomo tipo de retorno de una función sin argumentos. La vtable sí se usa para implementar polimorfismo. Si no hubiera polimorfismo, ni siquiera se usaría una vtable, así que se ahorraría todavía más memoriathis. En la práctica, de todos modos se sigue pasando la instanciathis, y tenerla explícita evita confusiones sobre si una variable pertenece a la instancia o viene de un ámbito global o de otro lugarthisal referirse a miembros de instanciaobject->ops->start(object)hay que mencionar el objeto dos veces: una para resolver la vtable y otra para pasarlo a la implementación de la función en CmFoo,m_Foo,foo_, etc. Prefierefoo_porque es más conciso quethis->foo. Claro que en C++ también se puede usarthisexplícitamentethisimplícito hace que el código sea más conciso y, al usar métodos reales, ya no hace falta repetir el prefijo del struct en cada función. Por ejemplo,s->dosmth();se siente más natural quemystruct_dosmth(s);this. El ejemplo destruct file_operationstiene punteros a funciones que no recibenthis, así que cuesta verlo como una vtable realthing->vtable->foo(thing, ...)se pueda escribirfoo(thing, ...)