Análisis Profundo de la Planificación en Xenomai Cobalt: Del Manejo de Interrupciones al Cambio de Contexto en Microsegundos

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_BITS agrupa 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:

  1. Espacio de direcciones: Los hilos RT requieren su propio mm_struct, provisto por Linux.
  2. Señales: La semántica POSIX requiere la infraestructura de Linux.
  3. Recursos: open()/read()/write() son llamadas del sistema de Linux.
  4. Depuración: ptrace, /proc/pid/ y gdb dependen de task_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 Linux
  • SIGDEBUG_MIGRATE_SYSCALL: Invocación de una syscall no perteneciente al dominio real-time
  • SIGDEBUG_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)

Etiquetas: xenomai cobalt ipipe real-time-linux kernel-scheduling

Publicado el 10-14 03:28