05-xenomai任务创建与调度
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_OTHER | Linux默认策略,非实时 |
pthread_attr_setschedparam() — 设置优先级
struct sched_param param;
param.sched_priority = 80; /* 1~99, 99最高 */
pthread_attr_setschedparam(&attr, ¶m);
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, ¶m);
/* 步骤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, ¶m);
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, ¶m);
/* 查询当前调度参数 */
int policy;
pthread_getschedparam(pthread_self(), &policy, ¶m);
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, ¶m);
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, ¶m);
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, ¶m);
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调用自动获得硬实时调度能力。
在下一篇文章中,我们将探讨任务间通信,包括管道、共享内存和信号量等机制,这些是构建复杂实时系统的基础。
参考资料
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐


所有评论(0)