7.2 实时进程如何打印输出
7.2.1 实时进程打印输出的必要性和约束条件
在代码中添加打印输出作为调试和监控手段,在软件开发中是最常见的作法。打印输出只需一行 printf() 或 其它语句,就能输出变量值、执行路径等信息,简单而直接,适合快速验证逻辑。
在实时应用程序中,调试与监控信息的打印输出必须满足确定性和低延迟的要求,以确保不会破坏系统的实时性、也不会掩盖与时序相关的问题。因此,实时系统中打印输出的实现方式应符合以下要求:
-
保留执行时序完整性
- 实时打印应尽量轻量、快速、无阻塞,使得调试信息的插入不会显著改变任务调度顺序或触发新的并发问题。
-
避免掩盖时序相关缺陷
- 若打印操作太慢或不可预测,可能掩盖某些竞态条件或超时错误。
-
支持在线分析与故障诊断
- 实时系统往往部署在嵌入式设备或工业控制等场景,现场难以使用调试器,只能依赖日志进行事后分析。
-
满足硬实时约束
- 实时打印的操作时间必须是可预测且有上限的,这样可以在调度分析中纳入其成本,保证整体任务满足截止时间要求。
但是,常规的打印输出函数(如 printf())不适用于实时路径:
-
不可预测的延迟
printf()等标准 I/O 操作依赖于缓冲机制和系统调用,可能会阻塞当前任务。- 输出到终端或文件时,I/O 操作耗时不确定,可能导致实时任务错过截止时间(deadline)。
-
影响程序执行时序
- 在硬实时系统中,某些问题只在特定的时间序列下出现。
- 但若打印本身引入了较大的、不确定的延迟,也会影响程序行为。
-
锁竞争与线程安全开销
printf()是线程安全的,但在多线程环境中可能会引入锁竞争,增加响应延迟。
7.2.2 Xenomai 3 的解决方案
Xenomai 3 通过符号包装(Symbol Wrapping)来劫持常规打印输出函数并按照实时要求重新实现。只要使用 xeno-config 获取编译链接参数并使用,默认会开启符号包装功能。具体参考符号包装相关章节。
查看 lib/cobalt/cobalt.wrappers 文件,可以看到 Xenomai 3 已经为常见的 I/O 函数进行了符号包装。
--wrap vfprintf
--wrap vprintf
--wrap fprintf
--wrap printf
--wrap puts
--wrap fputs
--wrap fputc
--wrap putchar
--wrap fwrite
--wrap fclose
--wrap syslog
--wrap vsyslog
--wrap __vfprintf_chk
--wrap __vprintf_chk
--wrap __fprintf_chk
--wrap __printf_chk
--wrap __vsyslog_chk
--wrap __syslog_chk
Xenomai 3 在 lib/cobalt/printf.c 中,重新实现上述打印输出函数。所有输出内容先暂存到内存缓冲区 ,由非实时线程 cobalt_printf 异步处理。实时线程得以不直接执行耗时 I/O 操作(如写文件、终端输出),避免因系统调用阻塞而破坏实时性。
它的核心设计要点包括:
- 环形缓冲区: 预先分配内存池,避免频繁分配内存。预先分配的内存池默认会划分为 4 个环形缓冲区。每个环形缓冲区默认大小为 16 KB,仅能被一个子线程独占而不可共用。
- 无锁写入:实时线程通过无锁(lockless)方式将输出内容写入环形缓冲区。
- 异步输出 :一个普通的(非实时的)Linux 线程
cobalt_printf定期将缓冲区内容刷新到目标流(如stdout、stderr或其他文件流)。cobalt_printf线程是在应用程序启动过程中自动创建的,与实时线程同属于一个进程,具体过程参考应用启动相关章节。
除了劫持常规打印输出函数之外,Xenomai 3 还提供了 rt_printf() 等系列函数,可在实时线程中直接调用。完整的函数列表,参考 POSIX skin 相关章节。
二者的使用差异如下表所示:
| 函数 | 实时线程处于实时域 | 实时线程处于非实时域 | 适合场景 |
|---|---|---|---|
printf() |
无锁写入环形缓冲区 | 执行标准 I/O 操作 | POSIX 兼容 |
rt_printf() |
无锁写入环形缓冲区 | 无锁写入环形缓冲区 | 实时进程,且兼容 Xenomai 2 |
Xenomai 3 的实时打印输出功能,支持通过以下参数进行调节:
-
--print-buffer-size=<num-bytes>- 功能:设置单个环形缓冲区的大小。
- 默认值 :16 KB(即
16384字节)。#define RT_PRINT_DEFAULT_BUFFER 16*1024
- 调整场景 :
- 高频输出 :若实时线程频繁调用
printf,可增大缓冲区以减少溢出风险。 - 大块数据:若单次输出数据量较大,需确保缓冲区足够容纳。
- 高频输出 :若实时线程频繁调用
-
--print-buffer-count=<num-buffers>- 功能:设置环形缓冲区的数量。
- 默认值 :4 个缓冲区。
#define RT_PRINT_DEFAULT_BUFFERS_COUNT 4
- 调整逻辑 :
- 如果子线程数量超过 4 ,则在打印输出时需要动态分配内存,生成额外的环形缓冲区。
- 可以根据线程总数来调整参数,避免动态申请内存。
-
--print-sync-delay=<ms>- 功能 :设置辅助线程
cobalt_printf刷新缓冲区的最大周期(单位:毫秒)。 - 默认值 :100 毫秒。
#define RT_PRINT_DEFAULT_SYNCDELAY 100 /* ms */
- 平衡策略 :
- 更短的延迟减少输出滞后,但增加系统负载;
- 更长的延迟降低刷新频率,可能造成输出堆积。
- 功能 :设置辅助线程
通过合理配置这些参数,开发者可在实时性 与输出可靠性之间实现最佳平衡。