05-Xenomai任务创建与调度

本文是Xenomai系列文章第5篇,主要介绍如何使用POSIX接口在Xenomai上创建实时线程、实现周期任务以及理解优先级调度机制。全文代码均基于Xenomai 3 POSIX皮肤(Cobalt核心)。

1. 概述

在前几篇文章中,我们了解了实时系统的基本概念、Xenomai的架构以及如何编译运行第一个Xenomai程序。从本篇开始,我们将深入Xenomai的核心编程模型。

Xenomai 3提供了多种API皮肤(skin),其中POSIX皮肤是最推荐的选择:

  • 标准POSIX接口,开发者无需学习私有API
  • 代码可移植到其他POSIX兼容系统(如RT-PREEMPT Linux、QNX等)
  • Cobalt核心自动拦截POSIX调用,将其路由到实时域执行

本篇将覆盖以下内容:

  • 实时线程的生命周期与双模式运行机制
  • 使用pthread创建和管理实时线程
  • 基于clock_nanosleep的周期任务实现
  • SCHED_FIFO / SCHED_RR调度策略与优先级
  • 多线程抢占调度示例

2. Xenomai POSIX皮肤的工作原理

Xenomai 3在Cobalt模式下,通过**符号包装(symbol wrapping)**机制拦截标准POSIX调用。当你的程序使用 xeno-config --posix 编译时,pthread_create()clock_nanosleep() 等函数会被自动重定向到Cobalt实时核心的实现。

┌─────────────────────────────────────────────────────────────┐
│                    应用程序代码                                │
│         pthread_create() / clock_nanosleep() / ...          │
└──────────────────────┬──────────────────────────────────────┘
                       │ 符号包装 (--wrap)
                       ▼
┌─────────────────────────────────────────────────────────────┐
│                  libcobalt.so                                │
│          Xenomai POSIX皮肤实现层                              │
│    __wrap_pthread_create() / __wrap_clock_nanosleep()        │
└──────────┬──────────────────────────────────┬───────────────┘
           │                                  │
           ▼                                  ▼
┌─────────────────────┐          ┌─────────────────────┐
│   Cobalt 实时核心    │          │    Linux 内核        │
│   (primary mode)    │          │   (secondary mode)   │
│   硬实时调度         │          │   普通调度            │
└─────────────────────┘          └─────────────────────┘

这意味着你写的就是标准POSIX代码,但在Xenomai环境下自动获得硬实时能力。

3. 实时线程的双模式运行

Xenomai线程在运行时会在两种模式间切换:

┌──────────────────┐                    ┌──────────────────┐
│  Primary Mode    │    模式切换         │  Secondary Mode  │
│  (Cobalt实时域)   │ ◄──────────────► │  (Linux域)        │
│                  │                    │                  │
│ • pthread API    │                    │ • printf()       │
│ • clock_nanosleep│  ←── 调用Linux ──  │ • malloc()       │
│ • sem_wait()     │      服务触发       │ • 文件I/O        │
│ • 硬实时延迟保证  │  ── 调用Xenomai ─→ │ • 网络操作        │
│                  │      服务返回       │                  │
└──────────────────┘                    └──────────────────┘

核心原则: 在实时关键路径上,必须避免调用任何Linux系统服务,否则线程会切换到secondary mode,丧失实时性。

4. 创建实时线程的核心步骤

使用POSIX接口创建一个Xenomai实时线程,需要以下关键步骤:

┌─────────────────────────────────────────────────────────┐
│                创建实时线程的标准流程                       │
│                                                         │
│  1. mlockall()           锁定内存,防止页面换出            │
│         │                                               │
│         ▼                                               │
│  2. pthread_attr_init()  初始化线程属性                    │
│         │                                               │
│         ▼                                               │
│  3. 设置调度策略          SCHED_FIFO 或 SCHED_RR          │
│     pthread_attr_setschedpolicy()                       │
│         │                                               │
│         ▼                                               │
│  4. 设置优先级            1~99, 99为最高                   │
│     pthread_attr_setschedparam()                        │
│         │                                               │
│         ▼                                               │
│  5. 设置显式调度          不继承父线程调度属性               │
│     pthread_attr_setinheritsched(PTHREAD_EXPLICIT_SCHED)│
│         │                                               │
│         ▼                                               │
│  6. pthread_create()     创建并启动线程                    │
│         │                                               │
│         ▼                                               │
│  7. pthread_join()       等待线程结束                      │
│     pthread_attr_destroy()  释放属性资源                   │
└─────────────────────────────────────────────────────────┘

4.1 关键API说明

pthread_attr_setschedpolicy() — 设置调度策略

int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
policy说明
SCHED_FIFO先进先出,同优先级不抢占,高优先级抢占低优先级
SCHED_RR时间片轮转,同优先级按时间片轮流执行
SCHED_OTHERLinux默认策略,非实时

pthread_attr_setschedparam() — 设置优先级

struct sched_param param;
param.sched_priority = 80;  /* 1~99, 99最高 */
pthread_attr_setschedparam(&attr, &param);

pthread_attr_setinheritsched() — 设置调度继承

pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

这一步非常关键。默认情况下新线程会继承创建者的调度属性。如果创建者是普通Linux线程(SCHED_OTHER),那么即使你设置了SCHED_FIFO也不会生效。必须设置为 PTHREAD_EXPLICIT_SCHED 强制使用显式指定的策略和优先级。

5. 实战:创建第一个实时线程

/*
 * example1_simple_thread.c
 * 最简单的Xenomai POSIX实时线程
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#include <time.h>

void *rt_thread_entry(void *arg)
{
    const char *name = (const char *)arg;
    struct timespec ts;
    int i;

    for (i = 0; i < 10; i++) {
        clock_gettime(CLOCK_MONOTONIC, &ts);
        printf("[%s] 第 %d 次执行, 时间: %ld.%09ld s\n",
               name, i + 1, ts.tv_sec, ts.tv_nsec);

        /* 休眠500ms */
        usleep(500000);
    }

    printf("[%s] 线程结束\n", name);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    /* 步骤1: 锁定内存,防止页面换出 */
    mlockall(MCL_CURRENT | MCL_FUTURE);

    /* 步骤2: 初始化线程属性 */
    pthread_attr_init(&attr);

    /* 步骤3: 设置调度策略为SCHED_FIFO */
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);

    /* 步骤4: 设置优先级为80 */
    param.sched_priority = 80;
    pthread_attr_setschedparam(&attr, &param);

    /* 步骤5: 使用显式调度属性 */
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    /* 步骤6: 创建并启动线程 */
    ret = pthread_create(&thread, &attr, &rt_thread_entry, "RT-Thread");
    if (ret) {
        fprintf(stderr, "pthread_create 失败: %s\n", strerror(ret));
        return 1;
    }

    printf("实时线程已启动 (SCHED_FIFO, prio=80)\n");

    /* 步骤7: 等待线程结束 */
    pthread_join(thread, NULL);
    pthread_attr_destroy(&attr);

    printf("程序退出\n");
    return 0;
}

编译命令:

# 使用Xenomai POSIX皮肤编译
gcc -o example1 example1_simple_thread.c \
    $(xeno-config --posix --cflags --ldflags)

运行(需要root权限设置实时调度):

sudo ./example1

预期输出:

实时线程已启动 (SCHED_FIFO, prio=80)
[RT-Thread] 第 1 次执行, 时间: 12345.678901234 s
[RT-Thread] 第 2 次执行, 时间: 12346.179012345 s
...
[RT-Thread] 线程结束
程序退出

注意: 上面的示例中使用了 printf()usleep(),在Xenomai Cobalt模式下这些调用会导致模式切换。这里仅作为入门演示,后续示例会展示如何避免模式切换。

6. 实战:周期任务

周期任务是实时系统中最常见的模式——以固定频率采集传感器数据、执行PID控制算法等。POSIX接口中实现周期任务的最佳方式是使用 clock_nanosleep() 配合绝对时间。

6.1 为什么用clock_nanosleep + 绝对时间?

方案对比:

❌ 相对时间休眠 (nanosleep / usleep)
   ┌──────┐  sleep(T)  ┌──────┐  sleep(T)  ┌──────┐
   │ 执行  │──────────►│ 执行  │──────────►│ 执行  │
   └──────┘            └──────┘            └──────┘
   ◄─ t1 ─►◄─── T ───►◄─ t2 ─►◄─── T ───►
   实际周期 = t1 + T ≠ t2 + T  (执行时间不同导致周期漂移)

✅ 绝对时间休眠 (clock_nanosleep TIMER_ABSTIME)
   ┌──────┐           ┌──────┐           ┌──────┐
   │ 执行  │           │ 执行  │           │ 执行  │
   └──────┘           └──────┘           └──────┘
   ◄──────── T ───────►◄──────── T ───────►
   每次都睡到下一个绝对时间点,不受执行时间影响

使用 CLOCK_MONOTONIC 而非 CLOCK_REALTIME,因为后者可能被NTP等服务修改导致时间跳变。

6.2 周期任务完整示例

/*
 * example2_periodic_task.c
 * 基于clock_nanosleep的周期任务 - 1ms周期
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <time.h>
#include <limits.h>

#define PERIOD_NS   1000000L    /* 周期: 1ms = 1,000,000 ns */
#define ITERATIONS  100         /* 执行100个周期 */
#define NSEC_PER_SEC 1000000000L

/* 周期信息结构体 */
struct period_info {
    struct timespec next_period;
    long period_ns;
};

/* 时间递增:计算下一个周期的绝对时间 */
static void inc_period(struct period_info *pinfo)
{
    pinfo->next_period.tv_nsec += pinfo->period_ns;

    while (pinfo->next_period.tv_nsec >= NSEC_PER_SEC) {
        pinfo->next_period.tv_sec++;
        pinfo->next_period.tv_nsec -= NSEC_PER_SEC;
    }
}

/* 初始化周期任务 */
static void periodic_task_init(struct period_info *pinfo)
{
    pinfo->period_ns = PERIOD_NS;
    clock_gettime(CLOCK_MONOTONIC, &pinfo->next_period);
}

/* 等待到下一个周期 */
static void wait_rest_of_period(struct period_info *pinfo)
{
    inc_period(pinfo);

    /* 使用绝对时间休眠,避免周期漂移 */
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME,
                    &pinfo->next_period, NULL);
}

void *periodic_thread(void *arg)
{
    struct period_info pinfo;
    struct timespec now, prev;
    long delta_ns;
    int i;

    periodic_task_init(&pinfo);
    clock_gettime(CLOCK_MONOTONIC, &prev);

    for (i = 0; i < ITERATIONS; i++) {
        wait_rest_of_period(&pinfo);

        clock_gettime(CLOCK_MONOTONIC, &now);

        /* 计算实际周期间隔 */
        delta_ns = (now.tv_sec - prev.tv_sec) * NSEC_PER_SEC
                 + (now.tv_nsec - prev.tv_nsec);

        printf("周期 %3d | 实际间隔: %ld.%03ld ms | 偏差: %+ld us\n",
               i + 1,
               delta_ns / 1000000,
               (delta_ns % 1000000) / 1000,
               (delta_ns - PERIOD_NS) / 1000);

        prev = now;

        /* ===== 在这里执行实时任务 ===== */
        /* do_rt_task(); */
    }

    printf("周期任务完成,共执行 %d 个周期\n", ITERATIONS);
    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t thread;
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    mlockall(MCL_CURRENT | MCL_FUTURE);

    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    param.sched_priority = 80;
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    /* 可选:设置栈大小 */
    pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + 0x4000);

    ret = pthread_create(&thread, &attr, &periodic_thread, NULL);
    if (ret) {
        fprintf(stderr, "pthread_create 失败: %s\n", strerror(ret));
        return 1;
    }

    printf("周期任务运行中 (1ms周期, SCHED_FIFO, prio=80)...\n");

    pthread_join(thread, NULL);
    pthread_attr_destroy(&attr);

    return 0;
}

编译运行:

gcc -o example2 example2_periodic_task.c \
    $(xeno-config --posix --cflags --ldflags)
sudo ./example2

预期输出:

周期任务运行中 (1ms周期, SCHED_FIFO, prio=80)...
周期   1 | 实际间隔: 1.002 ms | 偏差: +2 us
周期   2 | 实际间隔: 0.999 ms | 偏差: -1 us
周期   3 | 实际间隔: 1.001 ms | 偏差: +1 us
周期   4 | 实际间隔: 1.000 ms | 偏差: +0 us
...

在Xenomai Cobalt核心下,偏差通常在微秒级别,远优于普通Linux内核。

6.3 周期任务执行时序

时间轴 ──────────────────────────────────────────────────────────►

        ┌──┐              ┌──┐              ┌──┐              ┌──┐
线程    │任│              │任│              │任│              │任│
执行    │务│              │务│              │务│              │务│
        └──┘              └──┘              └──┘              └──┘
        t0   clock_       t0+T  clock_      t0+2T clock_      t0+3T
             nanosleep()        nanosleep()       nanosleep()

        ◄──── T=1ms ─────►◄──── T=1ms ─────►◄──── T=1ms ─────►

        clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL)
        每次休眠到绝对时间点 next_period,不累积误差

7. 调度策略与优先级

7.1 Xenomai POSIX皮肤支持的调度策略

┌─────────────────────────────────────────────────────────────────┐
│              Xenomai POSIX 调度策略                               │
├────────────────┬────────────────────────────────────────────────┤
│  SCHED_FIFO    │ 先进先出(最常用的实时策略)                      │
│                │ • 高优先级线程抢占低优先级线程                     │
│                │ • 同优先级线程按先到先得顺序执行                   │
│                │ • 线程主动让出CPU才会切换(pthread_yield/sleep)   │
├────────────────┼────────────────────────────────────────────────┤
│  SCHED_RR      │ 时间片轮转                                      │
│                │ • 高优先级仍然抢占低优先级                        │
│                │ • 同优先级线程按时间片轮流执行                     │
│                │ • 时间片耗尽自动切换到下一个同优先级线程            │
├────────────────┼────────────────────────────────────────────────┤
│  SCHED_OTHER   │ Linux默认策略(非实时)                           │
│                │ • 线程运行在secondary mode                       │
│                │ • 用于非实时的辅助线程                            │
└────────────────┴────────────────────────────────────────────────┘

7.2 SCHED_FIFO vs SCHED_RR 对比

SCHED_FIFO(同优先级=50的线程A和B):

线程A ──────────────────────────────────────────────►
线程B                                                (永远得不到执行)

线程A不主动让出CPU,线程B就无法运行。


SCHED_RR(同优先级=50的线程A和B,时间片=T):

线程A ─────┐     ┌─────┐     ┌─────┐     ┌─────►
           │     │     │     │     │     │
线程B      └─────┘     └─────┘     └─────┘
           ◄─ T ─►◄─ T ─►◄─ T ─►◄─ T ─►

同优先级线程按时间片轮流执行。

7.3 优先级范围

POSIX实时优先级范围为 1 ~ 99

优先级值    含义
──────────────────────────
  99       最高实时优先级(谨慎使用,可能影响系统关键线程)
  ...
  80       高优先级(推荐用于关键控制任务)
  ...
  50       中等优先级
  ...
  20       较低实时优先级
  ...
   1       最低实时优先级
──────────────────────────
   0       SCHED_OTHER(非实时)

抢占规则图示:

高优先级线程 (prio=80)    ──┐     ┌──────────────────────►
                            │就绪  │ 恢复执行
                            ▼     │
低优先级线程 (prio=30)  ────┘     └──  被抢占,暂停执行

时间 ──────────────────────────────────────────────────────►

当prio=80的线程就绪时,调度器立即抢占prio=30的线程。
prio=80的线程阻塞或退出后,prio=30的线程恢复执行。

7.4 运行时动态修改调度参数

/* 在线程运行过程中修改自身的调度策略和优先级 */
struct sched_param param;
param.sched_priority = 90;
pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

/* 查询当前调度参数 */
int policy;
pthread_getschedparam(pthread_self(), &policy, &param);
printf("当前策略: %s, 优先级: %d\n",
       policy == SCHED_FIFO ? "FIFO" : "RR",
       param.sched_priority);

8. 实战:多线程抢占调度

下面的示例创建三个不同优先级的实时线程,演示SCHED_FIFO下的抢占行为:

/*
 * example3_preemption.c
 * 多线程抢占调度演示
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

#define NSEC_PER_SEC 1000000000L

struct thread_arg {
    const char *name;
    int priority;
};

static struct timespec start_time;

/* 获取相对启动时间(微秒) */
static long elapsed_us(void)
{
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    return ((now.tv_sec - start_time.tv_sec) * NSEC_PER_SEC
          + (now.tv_nsec - start_time.tv_nsec)) / 1000;
}

void *thread_body(void *arg)
{
    struct thread_arg *ta = (struct thread_arg *)arg;
    int i;

    for (i = 0; i < 5; i++) {
        printf("[%8ld us] %s (prio=%d): 第 %d 次执行\n",
               elapsed_us(), ta->name, ta->priority, i + 1);

        /* 休眠100ms,模拟周期性工作 */
        usleep(100000);
    }

    printf("[%8ld us] %s: 完成\n", elapsed_us(), ta->name);
    return NULL;
}

/* 辅助函数:创建指定优先级的实时线程 */
static int create_rt_thread(pthread_t *thread, int priority,
                            void *(*entry)(void *), void *arg)
{
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    param.sched_priority = priority;
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);

    ret = pthread_create(thread, &attr, entry, arg);
    pthread_attr_destroy(&attr);

    return ret;
}

int main(int argc, char *argv[])
{
    pthread_t t_high, t_mid, t_low;
    struct thread_arg arg_high = { "HighPrio", 80 };
    struct thread_arg arg_mid  = { "MidPrio",  50 };
    struct thread_arg arg_low  = { "LowPrio",  20 };

    mlockall(MCL_CURRENT | MCL_FUTURE);
    clock_gettime(CLOCK_MONOTONIC, &start_time);

    printf("启动三个线程 (SCHED_FIFO: High=80, Mid=50, Low=20)\n\n");

    /* 按从低到高的顺序启动,观察抢占行为 */
    create_rt_thread(&t_low,  20, &thread_body, &arg_low);
    create_rt_thread(&t_mid,  50, &thread_body, &arg_mid);
    create_rt_thread(&t_high, 80, &thread_body, &arg_high);

    pthread_join(t_high, NULL);
    pthread_join(t_mid, NULL);
    pthread_join(t_low, NULL);

    printf("\n所有线程执行完毕\n");
    return 0;
}

编译运行:

gcc -o example3 example3_preemption.c \
    $(xeno-config --posix --cflags --ldflags)
sudo ./example3

你会观察到高优先级线程总是优先获得执行权。在所有线程同时就绪时,prio=80的线程先执行,然后是prio=50,最后是prio=20。

9. 优先级反转与优先级继承

9.1 优先级反转问题

在多线程共享资源(如互斥锁)的场景下,可能出现优先级反转:

场景:三个线程共享一个 pthread_mutex

线程H (prio=80)  ─────────┐ 等待锁 ┌──────────────────►
                           │blocked │
线程M (prio=50)  ──┐       │        │
                    │ 运行   │        │
线程L (prio=20)  ──┘───────┘ 持有锁 └──释放锁──────────►

问题:线程H被线程L阻塞(因为L持有锁),
      而线程M(不需要锁)却能抢占线程L运行,
      导致高优先级线程H间接被中优先级线程M延迟!
      这就是"优先级反转"。

9.2 使用PTHREAD_PRIO_INHERIT解决

POSIX提供了优先级继承协议来解决此问题:

pthread_mutex_t mutex;
pthread_mutexattr_t mutex_attr;

/* 初始化互斥锁属性 */
pthread_mutexattr_init(&mutex_attr);

/* 启用优先级继承协议 */
pthread_mutexattr_setprotocol(&mutex_attr, PTHREAD_PRIO_INHERIT);

/* 创建互斥锁 */
pthread_mutex_init(&mutex, &mutex_attr);
pthread_mutexattr_destroy(&mutex_attr);

启用优先级继承后的行为:

使用 PTHREAD_PRIO_INHERIT 后:

线程H (prio=80)  ─────────┐ 等待锁 ┌──────────────────►
                           │        │
线程M (prio=50)  ──────────│────────│──┐ 运行 ─────────►
                           │        │  │
线程L (prio=20→80) ───────┘ 继承80 └──┘ 恢复20 ───────►
                    持有锁   快速执行   释放锁

效果:线程L持有锁期间,临时继承线程H的优先级80,
      不会被线程M(50)抢占,从而尽快释放锁。

9.3 完整的优先级继承示例

/*
 * example4_prio_inherit.c
 * 优先级继承互斥锁演示
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>

pthread_mutex_t shared_mutex;

static struct timespec start_time;

static long elapsed_us(void)
{
    struct timespec now;
    clock_gettime(CLOCK_MONOTONIC, &now);
    return ((now.tv_sec - start_time.tv_sec) * 1000000000L
          + (now.tv_nsec - start_time.tv_nsec)) / 1000;
}

void *high_prio_thread(void *arg)
{
    usleep(50000);  /* 延迟50ms启动,确保低优先级线程先获取锁 */

    printf("[%8ld us] HIGH(80): 尝试获取锁...\n", elapsed_us());
    pthread_mutex_lock(&shared_mutex);
    printf("[%8ld us] HIGH(80): 获取到锁!\n", elapsed_us());

    usleep(10000);  /* 持有锁10ms */

    pthread_mutex_unlock(&shared_mutex);
    printf("[%8ld us] HIGH(80): 释放锁\n", elapsed_us());
    return NULL;
}

void *mid_prio_thread(void *arg)
{
    usleep(60000);  /* 延迟60ms启动 */

    printf("[%8ld us] MID(50):  开始计算(不需要锁)\n", elapsed_us());
    usleep(100000); /* 模拟100ms计算 */
    printf("[%8ld us] MID(50):  计算完成\n", elapsed_us());
    return NULL;
}

void *low_prio_thread(void *arg)
{
    printf("[%8ld us] LOW(20):  获取锁\n", elapsed_us());
    pthread_mutex_lock(&shared_mutex);
    printf("[%8ld us] LOW(20):  持有锁,开始长时间操作...\n", elapsed_us());

    usleep(200000); /* 持有锁200ms */

    pthread_mutex_unlock(&shared_mutex);
    printf("[%8ld us] LOW(20):  释放锁\n", elapsed_us());
    return NULL;
}

static int create_rt_thread(pthread_t *t, int prio,
                            void *(*fn)(void *), void *arg)
{
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    pthread_attr_init(&attr);
    pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
    param.sched_priority = prio;
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    ret = pthread_create(t, &attr, fn, arg);
    pthread_attr_destroy(&attr);
    return ret;
}

int main(int argc, char *argv[])
{
    pthread_t t_high, t_mid, t_low;
    pthread_mutexattr_t mattr;

    mlockall(MCL_CURRENT | MCL_FUTURE);
    clock_gettime(CLOCK_MONOTONIC, &start_time);

    /* 创建带优先级继承的互斥锁 */
    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_setprotocol(&mattr, PTHREAD_PRIO_INHERIT);
    pthread_mutex_init(&shared_mutex, &mattr);
    pthread_mutexattr_destroy(&mattr);

    printf("=== 优先级继承演示 ===\n\n");

    create_rt_thread(&t_low,  20, &low_prio_thread, NULL);
    create_rt_thread(&t_mid,  50, &mid_prio_thread, NULL);
    create_rt_thread(&t_high, 80, &high_prio_thread, NULL);

    pthread_join(t_high, NULL);
    pthread_join(t_mid, NULL);
    pthread_join(t_low, NULL);

    pthread_mutex_destroy(&shared_mutex);
    printf("\n演示结束\n");
    return 0;
}

10. 实战:封装可复用的实时线程工具函数

在实际项目中,建议将线程创建和周期任务的样板代码封装起来:

/*
 * rt_utils.h
 * 实时线程工具函数
 */
#ifndef RT_UTILS_H
#define RT_UTILS_H

#include <pthread.h>
#include <sys/mman.h>
#include <time.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define NSEC_PER_SEC 1000000000L

struct period_info {
    struct timespec next_period;
    long period_ns;
};

/* 初始化实时环境 */
static inline void rt_init(void)
{
    mlockall(MCL_CURRENT | MCL_FUTURE);
}

/* 创建实时线程 */
static inline int rt_thread_create(pthread_t *thread, int policy,
                                   int priority,
                                   void *(*entry)(void *), void *arg)
{
    pthread_attr_t attr;
    struct sched_param param;
    int ret;

    pthread_attr_init(&attr);
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
    pthread_attr_setschedpolicy(&attr, policy);
    param.sched_priority = priority;
    pthread_attr_setschedparam(&attr, &param);
    pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
    pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + 0x4000);

    ret = pthread_create(thread, &attr, entry, arg);
    pthread_attr_destroy(&attr);

    if (ret)
        fprintf(stderr, "rt_thread_create 失败: %s\n", strerror(ret));

    return ret;
}

/* 初始化周期信息 */
static inline void period_init(struct period_info *pinfo, long period_ns)
{
    pinfo->period_ns = period_ns;
    clock_gettime(CLOCK_MONOTONIC, &pinfo->next_period);
}

/* 等待下一个周期 */
static inline void period_wait(struct period_info *pinfo)
{
    pinfo->next_period.tv_nsec += pinfo->period_ns;
    while (pinfo->next_period.tv_nsec >= NSEC_PER_SEC) {
        pinfo->next_period.tv_sec++;
        pinfo->next_period.tv_nsec -= NSEC_PER_SEC;
    }
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME,
                    &pinfo->next_period, NULL);
}

/* 创建带优先级继承的互斥锁 */
static inline int rt_mutex_init(pthread_mutex_t *mutex)
{
    pthread_mutexattr_t attr;
    int ret;

    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
    ret = pthread_mutex_init(mutex, &attr);
    pthread_mutexattr_destroy(&attr);
    return ret;
}

#endif /* RT_UTILS_H */

使用封装后的代码非常简洁:

/*
 * example5_with_utils.c
 * 使用rt_utils.h的简洁示例
 */
#include "rt_utils.h"
#include <stdio.h>

#define PERIOD_1MS  1000000L

void *control_loop(void *arg)
{
    struct period_info pinfo;
    int i;

    period_init(&pinfo, PERIOD_1MS);

    for (i = 0; i < 1000; i++) {
        period_wait(&pinfo);

        /* 在这里执行控制算法 */
        /* read_sensor(); */
        /* compute_pid(); */
        /* write_actuator(); */
    }

    return NULL;
}

int main(void)
{
    pthread_t thread;

    rt_init();
    rt_thread_create(&thread, SCHED_FIFO, 80, &control_loop, NULL);
    pthread_join(thread, NULL);

    return 0;
}

11. 编程注意事项

11.1 mlockall()

mlockall(MCL_CURRENT | MCL_FUTURE);

mlockall() 用于将进程的内存页锁定在物理内存中,防止被换出到swap。MCL_CURRENT 锁定当前已映射的页,MCL_FUTURE 锁定后续新映射的页。

实际上在Xenomai 3的Cobalt核心下,实时线程的调度由Cobalt接管,即使不调用 mlockall() 也不太容易出现明显的延迟尖峰。不过作为POSIX实时编程的惯例,加上它是一个好习惯,尤其是在系统开启了swap或内存压力较大的场景下能提供额外保障。

11.2 避免模式切换的函数对照表

避免使用替代方案原因
printf()缓冲输出或非实时线程打印触发Linux系统调用
malloc() / free()预分配内存池可能触发页面错误
open() / read() / write()在初始化阶段完成文件I/O不确定
sleep() / usleep()clock_nanosleep()精度和实时性更好
gettimeofday()clock_gettime(CLOCK_MONOTONIC)不受NTP影响

11.3 栈预热(Stack Prefaulting)

除了 mlockall(),还建议在实时循环开始前预热栈空间:

static void prefault_stack(void)
{
    unsigned char dummy[8192];
    memset(dummy, 0, sizeof(dummy));
}

void *rt_thread(void *arg)
{
    /* 进入实时循环前预热栈 */
    prefault_stack();

    /* 开始实时工作... */
    while (1) {
        /* ... */
    }
    return NULL;
}

这确保栈内存页已经映射到物理内存,避免在实时循环中触发缺页异常。

12. 调试技巧

12.1 查看实时线程状态

# 查看Xenomai实时线程信息
cat /proc/xenomai/sched/stat

# 输出示例:
# CPU  PID    MSW     CSW      PF    STAT    %CPU  NAME
#   0  1234     0      42       0    00500   12.3  control_loop
#   0  1235     2      10       0    00300    5.1  monitor
字段说明
MSW模式切换次数(Mode SWitch),越少越好,理想值为0
CSW上下文切换次数
PF页面错误次数,应为0

12.2 检测模式切换

模式切换是实时性能的大敌。可以通过信号机制检测:

#include <signal.h>

/* SIGDEBUG: Xenomai在发生模式切换时发送此信号 */
void sigdebug_handler(int sig, siginfo_t *si, void *context)
{
    /* 注意:这里不能调用printf等非异步信号安全函数 */
    /* 可以设置标志位,在安全的地方打印 */
    static volatile int switch_count = 0;
    switch_count++;
}

void setup_sigdebug(void)
{
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigdebug_handler;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGDEBUG, &sa, NULL);
}

12.3 使用latency工具测量延迟

Xenomai自带的latency测试工具可以评估系统的实时性能:

# 测量用户态周期任务延迟
sudo /usr/xenomai/bin/latency -p 1000 -T 60

# 参数说明:
# -p 1000  周期1000us (1ms)
# -T 60    测试60秒

输出中关注 worst 列,这是最坏情况延迟,直接反映系统的实时能力。

13. 总结

┌──────────────────────────────────────────────────────────────────┐
│                       本篇知识图谱                                │
│                                                                  │
│  线程管理 (POSIX)          调度机制              编程要点          │
│  ─────────────           ────────            ────────          │
│  • pthread_create()      • SCHED_FIFO        • mlockall()      │
│  • pthread_join()        • SCHED_RR          • clock_nanosleep()│
│  • pthread_setschedparam • 优先级 1~99        • CLOCK_MONOTONIC │
│  • pthread_yield()       • 抢占式调度          • 避免模式切换     │
│  • pthread_attr_set*()   • 优先级继承          • 栈预热          │
│                                                                  │
│  周期任务                  互斥锁                                 │
│  ────────                ────────                               │
│  • clock_gettime()       • PTHREAD_PRIO_INHERIT                 │
│  • clock_nanosleep()     • pthread_mutexattr_setprotocol()      │
│  • TIMER_ABSTIME         • 防止优先级反转                        │
│  • 绝对时间避免漂移                                               │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

使用POSIX接口的最大优势在于代码的可移植性——同样的代码稍作修改就能运行在RT-PREEMPT Linux、QNX等其他实时平台上。而在Xenomai Cobalt核心下,这些标准POSIX调用自动获得硬实时调度能力。

在下一篇文章中,我们将探讨任务间通信,包括管道、共享内存和信号量等机制,这些是构建复杂实时系统的基础。

参考资料

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐