Arquitectura Dual: La Necesidad de un Doble Núcleo
Incluso aplicando el parche PREEMPT_RT, el núcleo de Linux no puede garantizar latencias deterministas a nivel de microsegundos. Esto se debe a la existencia de regiones críticas no preemptibles, latencias de interrupción impredecibles y la gestión de colas de softirq/workqueue.
Para solucionar esto, Xenomai inserta un núcleo de tiempo real independiente llamado Cobalt sobre Linux, utilizando el I-pipe (Interrupt Pipeline) para asegurar una prioridad absoluta sobre las interrupciones:
Interrupción de Hardware
│
▼
┌──────────────────────────────────────┐
│ Tubería I-pipe │
│ │
│ ┌────────────────────────────────┐ │
│ │ Dominio Cobalt (Head Domain) │ │ ← Alta prioridad, intercepta todo
│ │ xnsched_realtime_domain │ │
│ └────────────────────────────────┘ │
│ │ │
│ │ Pasa si no hay tarea RT│
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ Dominio Raíz Linux (Root) │ │ ← Baja prioridad, interrupciones filtradas
│ │ ipipe_root │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
Un concepto clave es que un "dominio" no representa flujos de ejecución en CPUs distintas, sino capas de prioridad de interrupción en el mismo procesador. Cobalt no "roba" tiempo de CPU a Linux de forma continua; simplemente "se cuela" durante unos microsegundos al llegar una interrupción. Una vez atendida, la CPU regresa a Linux naturalmente. Sin embargo, si existen tareas Cobalt listas, la CPU no volverá a Linux hasta que estas finalicen, lo que significa que un diseño deficiente en Cobalt puede causar inanición en las tareas Linux.
Mecánica Interna del Planificador Cobalt
Máquina de Estados del Hilo
Cobalt utiliza una máscara de bits (task->status_mask) para representar el estado de un hilo, permitiendo la superposición de múltiples bits de bloqueo:
XNINACTIVE ──(start)──→ XNREADY ──(fetch_next)──→ RUNNING
↑ │
resume (limpiar bits) │ suspend(mask)
↑ ▼
BLOCKED ←──────────────── (BLOCK_BITS)
Reglas principales:
XNTHREAD_BLOCK_BITSagrupa los flags como XNSUSP, XNPEND, XNDELAY, XNDORMANT, XNRELAX, etc.- Si cualquier bit de bloqueo está activo, el hilo no es elegible.
- Los bits deben estar todos limpios para que el hilo regrese a XNREADY.
| Flag | Significado | Activación | Desactivación |
|---|---|---|---|
XNPEND |
Esperando objeto sincronización | mutex_lock / sem_wait | Recurso liberado |
XNDELAY |
Temporización en curso | sleep / espera con timeout | Expiración del temporizador |
XNSUSP |
Suspensión explícita | suspend() | resume() |
XNRELAX |
Ejecutándose en dominio Linux | relax (migración a Linux) | harden (retorno a Cobalt) |
XNDORMANT |
No iniciado | init() | start() |
Clases de Planificación y Selección de Hilos
Las políticas de planificación se implementan mediante una lista enlazada de clases, ordenadas por peso de mayor a menor:
rt(1024) → quota → sporadic → tp → weak → idle(0)
La función xnsched_fetch_next() recorre esta lista invocando sched_pick() de cada clase. La primera que devuelva un hilo válido será la elegida. La clase idle siempre retornará linux_proxy (el proxy del dominio Linux), evitando devolver NULL.
// kernel/xenomai/sched.c
struct cobalt_task *xnsched_fetch_next(struct cobalt_runqueue *rq)
{
// Si el hilo actual posee cerrojos de planificación, se rechaza la expulsión
if (running_task->preempt_locks > 0) {
xnsched_mark_local_resched(rq);
return running_task;
}
// Si el hilo actual no está bloqueado, se reinserta en la cola
if (!xnthread_test_state(running_task, XNREADY)) {
xnsched_reinsert(running_task); // Reinserta al frente de su grupo (LIFO)
xnthread_set_state(running_task, XNREADY);
}
// Itera sobre las clases para seleccionar la tarea de mayor prioridad
for_each_sched_class(cls) {
task = cls->sched_pick(rq);
if (task) {
mark_task_running(rq, task);
return task;
}
}
}
Replanificación Diferida (Lazy Rescheduling)
Cobalt no realiza cambios de contexto inmediatamente tras cada modificación de estado; en su lugar, acumula marcas y procesa en lotes:
┌─────────── Productor (solo marca) ──────────┐ ┌────── Consumidor (ejecuta cambio) ──────┐
│ xnthread_resume() │ │ Salida de interrupción (nesting→0) │
│ xnsched_set_policy() │────→│ El hilo se bloquea voluntariamente │
│ xnsched_putback() (fin de rodaja RR) │ ↑ │ Liberación de cerrojo de planificación │
│ xnsched_migrate() │ │ │ IPI en SMP │
└──────────────────────────────────────────────┘ │ └─────────────────────────────────────────┘
Establece flag XNRESCHEDULE ────────────┘ Limpia XNRESCHEDULE → fetch_next → switch
La ventaja es evidente: si xnsynch_flush despierta 5 hilos simultáneamente, solo se ejecuta una llamada a fetch_next y un único cambio de contexto.
Cerrojos de Planificación
running_task->preempt_locks++; // xnsched_lock() — Prohíbe la expulsión
running_task->preempt_locks--; // xnsched_unlock() — Si llega a 0, invoca xnsched_run()
Semántica: Se prohíbe la expulsión forzosa, pero se permite el bloqueo voluntario. Si el hilo que posee el cerrojo se bloquea, el contador se preserva y seguirá activo al reanudar.
Modelo de Hilo Sombra (Shadow Thread)
Concepto Central
Cada hilo de tiempo real en espacio de usuario posee una doble identidad:
┌──────────── Un hilo RT de usuario ────────────┐
│ │
│ Identidad Cobalt Identidad Linux │
│ ┌──────────────┐ ┌───────────────┐ │
│ │ cobalt_tcb │◄──────►│ task_struct │ │
│ │ - status_mask│ │ - mm (espacio)│ │
│ │ - cprio │ │ - files │ │
│ │ - regs │ │ - signal │ │
│ └──────────────┘ └───────────────┘ │
│ Comparten el mismo contexto de registros de la CPU │
└───────────────────────────────────────────────┘
Modos de Ejecución de Doble Dominio
En un instante dado, solo un planificador gobierna el hilo:
┌──── Modo Primario (Planificación Cobalt) ────┐
│ Gobernado por xnsched_run()/fetch_next │
│ Garantiza latencia determinista (μs) │
│ Prohibido llamar servicios bloqueantes Linux│
└──────────┬──────────────────────┬───────────┘
│ relax │ harden
│ (llamada Linux) │ (syscall Xenomai)
▼ │
┌──── Modo Secundario (Planificación Linux) ──┐
│ Gobernado por schedule() de Linux │
│ Acceso total a servicios Linux │
│ Sin garantías de tiempo real │
└─────────────────────────────────────────────┘
Mecanismo de conmutación:
- Harden: Limpia
XNRELAX→ el hilo se vuelve elegible → Cobalt puede planificarlo. - Relax: Establece
XNRELAX+post_wakeup(task)→ Se bloquea en Cobalt y se despierta en Linux.
Razones para este diseño en lugar de usar hilos de kernel puros:
- Espacio de direcciones: Los hilos RT requieren su propio
mm_struct, provisto por Linux. - Señales: La semántica POSIX requiere la infraestructura de Linux.
- Recursos:
open()/read()/write()son llamadas del sistema de Linux. - Depuración:
ptrace,/proc/pid/y gdb dependen detask_struct.
El modelo Shadow permite que un hilo RT opere en Cobalt cuando necesita determinismo, y cambie transparentemente a Linux cuando precisa sus servicios.
Proceso de Creación
Usuario: pthread_create(&tid, SCHED_FIFO/80, worker, arg)
│
▼ libcobalt intercepta (mecanismo --wrap del enlazador)
│
├─ ① Linux clone() → Crea un hilo Linux estándar (con mm, PID)
│
└─ ② El nuevo hilo arranca y emite una syscall Cobalt:
├─ xnthread_init() → Crea la identidad Cobalt
├─ cobalt_map_user() → Vincula la relación shadow
└─ xnthread_harden() → Migra al dominio Cobalt
→ El hilo ahora es planificado por Cobalt, el código de usuario comienza
Impulso de la Planificación: Interrupciones y Temporizadores
Ruta de Interrupción del Reloj
Interrupción del temporizador de hardware
│
▼ I-pipe → dispatch_irq_head()
▼ xnintr_core_clock_handler()
│
├─ xnclock_process_tick(&masterclk)
│ ├─ htimer expira → marca XNHTICK (hay tick pendiente para Linux)
│ ├─ rtimer expira → timeout_handler → xnthread_resume (limpia XNDELAY)
│ ├─ ptimer expira → periodic_handler → xnthread_resume
│ └─ rrbtimer expira → roundrobin_handler → xnsched_putback (rotación RR)
│
└─ Salida de interrupción (nesting→0) → xnsched_run()
└─ fetch_next → elige hilo de mayor prioridad → switch_context
Ruta de Interrupción de Dispositivo
Interrupción de hardware (ej. CAN/SPI/GPIO)
│
▼ I-pipe → dispatch_irq_head()
▼ xnintr_irq_handler()
│
├─ intr->isr(intr) ← ISR del controlador RTDM
│ └─ rtdm_event_signal(&event) → xnthread_resume(hilo, XNPEND)
│
└─ Salida de interrupción → xnsched_run()
└─ Cambio inmediato al hilo RT despertado
Latencia: IRQ hardware → Reanudación hilo RT ≈ pocos microsegundos (determinista)
Reenvío de Host Tick
Al tomar el control del temporizador, Linux deja de recibir interrupciones de reloj directamente. Cobalt las emula:
// Cuando expira el htimer:
rq->local_flags |= XNHTICK; // Marca tick pendiente
// Al cambiar a linux_proxy o en salida de IRQ si curr==linux_proxy:
xnintr_host_tick(rq):
ipipe_post_irq_root(HOST_TICK_IRQ); // Encola para Linux
// Linux lo consume en el próximo local_irq_enable():
__ipipe_sync_stage() → Gestor de tick Linux → schedule()
Modelo de Temporizador Oneshot
Cobalt evita los ticks periódicos; programa el temporizador en modo oneshot, apuntanndo siempre al próximo vencimiento más cercano:
Cola de temporizadores: [100ns] [500ns] [2ms]
↑
Hardware programado a 100ns
A los 100ns → IRQ → Procesa el primero → Reprograma a 500ns → ...
Beneficio: En estados inactivos no se generan interrupciones espurias, reduciendo consumo y fluctuaciones (jitter).
Bucle de Planificación Central: ___cobalt_exec_scheduler()
// kernel/xenomai/sched.c
int ___cobalt_exec_scheduler(struct cobalt_runqueue *rq)
{
xnlock_get_irqsave(&nklock, s);
running_task = rq->curr;
reschedule:
if (!check_resched_flags(rq)) // Verifica XNRESCHEDULE, envía IPI en SMP
goto out; // No se requiere replanificación
elected_task = xnsched_fetch_next(rq);
if (elected_task == running_task) {
if (XNROOT && XNHTICK)
xnintr_host_tick(rq); // Reenvía tick al host
goto out;
}
// ---- Cambio de contexto necesario ----
predecessor = running_task;
WRITE_ONCE(rq->curr, elected_task);
if (predecessor == linux_proxy)
leave_root(predecessor); // Salva contexto Linux, arranca watchdog
else if (elected_task == linux_proxy)
enter_root(elected_task); // Reenvía tick, detiene watchdog
switch_context(rq, predecessor, elected_task); // Intercambio real de registros
// ---- El código siguiente ejecuta en el contexto de 'elected_task' ----
if (is_shadow && ipipe_root_p)
goto shadow_epilogue; // Finaliza migración causada por relax
// Limpieza post-cambio en ruta normal
rq = xnsched_finish_unlocked_switch(rq);
running_task = rq->curr;
out:
xnlock_put_irqrestore(&nklock, s);
return switched;
}
Análisis de Escenarios Típicos
Escenario A: Despertar Periódico de Hilo RT
clock_nanosleep(1ms)
→ suspend(XNDELAY) + programa rtimer
→ fetch_next → linux_proxy → Linux se ejecuta
...
[A los 1ms] Interrupción temporizador
→ xnclock_process_tick → rtimer expira → resume(XNDELAY) → set_resched
→ Salida IRQ → xnsched_run → fetch_next → Hilo RT
→ leave_root + switch_context → Hilo RT reanuda (Latencia ≈ μs)
Escenario B: IRQ de Dispositivo Despierta Hilo RT
Hilo RT bloqueado en rtdm_event_wait (XNPEND)
...
[IRQ de dispositivo]
→ ISR: rtdm_event_signal → resume(XNPEND) → set_resched
→ Salida IRQ → xnsched_run → fetch_next → Hilo RT
→ leave_root + switch_context → Hilo RT procesa datos (Latencia ≈ μs)
Escenario C: Hilo RT Bloqueado, Linux Reanuda
Hilo RT invoca mutex_lock (recurso tomado)
→ suspend(XNPEND) → set_resched
→ xnsched_run → fetch_next → linux_proxy
→ enter_root + switch_context → Linux reanuda ejecución
→ __ipipe_sync_stage → Linux consume ticks pendientes → schedule()
Referencia Rápida de Estructuras de Datos Clave
Planificador por CPU (struct cobalt_runqueue)
struct cobalt_runqueue {
unsigned long status; // XNRESCHEDULE / XNINSW
unsigned long lflags; // XNHTICK / XNINIRQ / XNIDLE
struct cobalt_task *curr; // Tarea en ejecución actual
struct xnsched_rt rt; // Cola de clase RT
struct cobalt_task rootcb; // Proxy de Linux (prioridad idle)
struct xntimer htimer; // Temporizador emulación host tick
struct xntimer rrbtimer; // Temporizador de rodaja Round-Robin
struct xntimer wdtimer; // Temporizador watchdog
volatile unsigned nesting; // Contador de anidamiento de IRQs
};
Bloque de Control de Hilo (struct cobalt_task)
struct cobalt_task {
__u32 status_mask; // Máscara de estado del hilo
struct cobalt_runqueue *rq; // Planificador CPU local asociado
struct xnsched_class *policy; // Clase de planificación vigente
int base_prio; // Prioridad base
int effective_prio; // Prioridad efectiva (con herencia PI)
int preempt_locks; // Contador de cerrojos anidados
struct xntimer rtimer; // Temporizador de timeout
struct xntimer ptimer; // Temporizador periódico
xnticks_t rrperiod; // Rodaja RR en nanosegundos
struct xnsynch *wait_channel; // Objeto de sincronización en espera
};
Guía de Depuración y Resolución de Problemas
| Síntoma | Causa Probable | Dirección de Aálisis |
|---|---|---|
| Latencia RT con picos esporádicos | Hilo pasó a modo secundario | Verificar llamadas bloqueantes Linux; activar XNWARN |
| Hilo RT nunca se ejecuta | Hilo de mayor prioridad sin bloqueo | Revisar lógica de sleep/wait en prioridades superiores |
| Linux congelado/sin respuesta | Hilo RT acapara la CPU | Activar watchdog; asegurar que RT ceda CPU |
| Watchdog se dispara seguido | Tarea RT excede su tiempo previsto | Analizar duración de tareas; buscar bucles infinitos o inversión de prioridad |
| Cobalt inactivo tras el arranque | Parámetro de boot xenomai.state=disabled |
Verificar existencia de /proc/xenomai |
Archivos de Depuración Clave
/proc/xenomai/sched/threads — Estado de todos los hilos Cobalt
/proc/xenomai/sched/stat — Estadísticas de planificación (cambios de contexto y modo)
/proc/xenomai/sched/acct — Estadísticas de tiempo de ejecución
/proc/xenomai/irq — Estadísticas de interrupciones en dominio Cobalt
/proc/xenomai/clock/coreclk — Información del reloj del sistema
Señal SIGDEBUG
Al configurar el flag XNWARN en un hilo, se envía una señal SIGDEBUG bajo las siguientes anomalías:
SIGDEBUG_MIGRATE_SIGNAL: Forzado a relax por recepción de señal LinuxSIGDEBUG_MIGRATE_SYSCALL: Invocación de una syscall no perteneciente al dominio real-timeSIGDEBUG_WATCHDOG: Tiempo de observación excedido (Watchdog)SIGDEBUG_LOCK_BREAK: Intento de bloqueo manteniendo un cerrojo de planificación activo
Registrar un manejador para esta señal es vital para detectar migraciones de dominio no deseadas.
Índice de Código Fuente Clave
| Ruta del Archivo | Contenido Principal |
|---|---|
kernel/xenomai/init.c |
Punto de entrada de Cobalt: xenomai_init() |
kernel/xenomai/sched.c |
Núcleo de planificación: ___cobalt_exec_scheduler(), registro de clases |
kernel/xenomai/thread.c |
Ciclo de vida del hilo: harden, relax, suspend, resume |
kernel/xenomai/intr.c |
Gestión de IRQs: reloj, dispositivos, reenvío host tick |
kernel/xenomai/clock.c |
xnclock_process_tick(): Exploración y disparo de temporizadores |
kernel/xenomai/timer.c |
Gestión de temporizadores: grab_hardware, start/stop |
kernel/xenomai/synch.c |
Objetos de sincronización: mutex, semáforos, protocolo PI |
kernel/ipipe/core.c |
I-pipe: Despacho de IRQs, gestión de dominios |
kernel/ipipe/timer.c |
Temporizadores I-pipe: ipipe_timer_start, apropiación |
include/xenomai/cobalt/kernel/sched.h |
Definiciones de cobalt_runqueue, xnsched_class |
include/xenomai/cobalt/kernel/thread.h |
Definición de cobalt_task |
include/xenomai/cobalt/uapi/kernel/thread.h |
Flags de estado de hilo (espacio de usuario) |