4.3 POSIX skin的不兼容性
4.3.1 mlockall 与栈大小
在 Xenomai 等实时系统中,确保程序运行的确定性和低延迟是至关重要的。为了实现这一点,Xenomai 在其初始化过程中使用了一个关键的 Linux 系统调用 mlockall(),以提升内存访问效率并避免潜在的页面错误(page fault)。
本小节将深入解析 mlockall 的原理、作用及其对线程栈大小的影响,并探讨如何在系统中合理配置。
1. 什么是 mlockall()
mlockall() 是一个 Linux 系统调用,用于将进程的所有虚拟内存页锁定在物理内存中,防止它们被交换到磁盘或因未分配而引发页面错误。该调用的原型如下:
int mlockall(int flags);
其中,常用的标志包括:
MCL_CURRENT:锁住当前已分配的所有内存页。MCL_FUTURE:锁住将来可能分配的内存页。
当一个程序调用 mlockall(MCL_CURRENT | MCL_FUTURE) 后,Linux 内核会尝试将所有当前和未来使用的内存页都映射到物理内存中,并禁止这些页被换出(swap out)。
调用 mlockall(MCL_CURRENT | MCL_FUTURE) 不会直接导致立即分配物理页面。mlockall 的作用是锁定当前和将来映射的内存页,防止它们被交换到磁盘上的交换空间(swap)。然而,这并不意味着所有虚拟内存都会立即对应到物理内存页。
当你使用 mlockall(MCL_CURRENT | MCL_FUTURE) 时:
- MCL_CURRENT 标志表示锁定当前已映射的内存页面。
- MCL_FUTURE 标志表示锁定将来映射的内存页面。
这意味着对于已经分配并映射的内存区域(由 MCL_CURRENT 指示),操作系统将确保这些页面不会被交换出去。但是,如果这些页面之前没有被访问过(即没有发生缺页异常),那么它们可能还没有对应的物理内存页。在这种情况下,物理内存页会在首次访问这些页面时通过缺页处理机制分配。
对于 MCL_FUTURE,当新的内存被映射到进程地址空间时(例如通过 malloc 或 mmap),这些新映射的页面也将自动被锁定,以防止它们被换出。但是,同样地,这并不会触发对这些页面的实际物理内存分配,直到它们被程序实际访问为止。
因此,虽然 mlockall 可以保证锁定的内存不会被交换到磁盘上,但它并不能保证所有的虚拟内存页在调用 mlockall 时都已经被分配了物理内存页。物理内存页的实际分配仍然遵循按需分页的原则,即在第一次访问某个页面时才会真正分配物理内存。 若要确保某些内存区域的物理页面已经被分配,你可能需要显式地访问这些页面的内容(例如,通过读写操作)来触发缺页处理和物理内存的分配。
2. 为什么需要 mlockall()
在标准 Linux 中,默认采用 按需分页(on-demand paging) 机制。也就是说,程序请求内存后并不会立即分配物理页,而是等到第一次访问该内存区域时才触发一次 页面错误(page fault),由内核动态分配物理页。
这种机制虽然节省了内存资源,但在实时系统中却存在严重问题:
- 页面错误会导致中断处理:发生 page fault 时,内核必须介入进行物理页分配,这会打断当前正在执行的线程。
- 切换执行模式:如果当前线程处于实时优先级(primary mode),则页面错误会强制将其降级为普通优先级(secondary mode),从而破坏实时性。
- 不可预测的延迟:在极端情况下(如内存不足且需要写回磁盘数据释放页),延迟可能达到毫秒级别。
因此,在 Xenomai 实时框架中,为了避免任何非预期的页面错误,通常会在初始化阶段调用 mlockall(),一次性提交并锁定所有内存,从而消除 page fault 带来的不确定性。
在Xenomai应用程序启动时,cobalt_init()初始化过程会自动执行mlockall(MCL_CURRENT | MCL_FUTURE)。
//lib/cobalt/init.c
cobalt_init()
|
|--> cobalt_init_1()
| |
| |--> cobalt_init_2()
| |
| |--> low_init()
| |
| |--> mlockall(MCL_CURRENT | MCL_FUTURE)
3. mlockall 的副作用:线程栈大小问题
尽管 mlockall() 可以显著提高系统的确定性,但它也会带来一些副作用,尤其是在多线程环境中,最明显的就是 线程栈大小的分配问题。
(1)默认栈大小的问题
在大多数 Linux 平台上,线程默认的栈大小为 2MiB甚至更高。例如Ubuntu22.04/RHEL8.9的默认栈大小为8MiB。
ulimit -s
8192
这意味着,每当创建一个新线程时,系统都会立即为其分配 8MiB 的物理内存空间。在内存受限的嵌入式系统中,如果有大量线程同时运行,这可能会迅速耗尽可用内存。
更糟糕的是,由于 mlockall() 已经启用,这些栈空间会被立即提交并锁定在内存中,无法延迟分配,进一步加剧内存压力。
(2) Xenomai 的应对策略
为了解决这个问题,Xenomai 对线程栈进行了优化:
- 默认栈大小被减小:缩小到一个更合理的默认值,以减少内存占用。
PTHREAD_STACK_MIN 是定义在 POSIX 线程库(pthreads)中的一个宏,用于表示创建线程时允许的最小栈大小。这个值并不是指实际分配给每个新线程的栈大小,而是系统支持的最小安全栈大小,以确保程序能正常运行而不出现栈溢出等问题。
在 Linux 系统中,PTHREAD_STACK_MIN 的具体数值可能会根据不同的处理器架构有所不同,但通常它被设置为一个足够小的值,用来作为开发者设定自定义栈大小时的下限标准。例如,在适配ARM64的glibc 2.41上,它的默认值2个64KB的页面,即128KB。
Xenomai定义的宏PTHREAD_STACK_DEFAULT代表了默认栈大小,它取PTHREAD_STACK_MIN和64KB二者中的最大值。
#ifndef PTHREAD_STACK_DEFAULT
#define PTHREAD_STACK_DEFAULT \
({ \
int __ret = PTHREAD_STACK_MIN; \
if (__ret < 65536) \
__ret = 65536; \
__ret; \
})
#endif /* !PTHREAD_STACK_DEFAULT */
- 建议显式设置栈大小:对于确实需要更大栈空间的线程,应通过
pthread_attr_setstacksize()显式指定更大的栈大小。
示例代码:
pthread_attr_t attr;
size_t stack_size = 64 * 1024; // 64 KiB
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, stack_size);
pthread_create(&thread_id, &attr, thread_func, NULL);
⚠️ 注意:某些标准库函数(如
printf)会在内部使用较多栈空间。如果将栈大小设得太小(如 4KiB),可能导致栈溢出并引发段错误(Segmentation Fault)。
4. 主线程的特殊处理
与普通线程不同,主线程(main thread)并不是通过 pthread_create() 创建的,因此不能使用 pthread_attr_setstacksize() 来修改其栈大小。此时,可以通过 shell 命令 ulimit 来调整主线程的栈限制。
例如,在启动程序前运行:
ulimit -s 256 # 将栈大小限制为 256 KiB
./my_program
此外,即使启用了 mlockall(),主线程的栈仍可能在运行时增长(因为它是自动扩展的)。为了防止在关键实时路径上出现 page fault,建议在进入实时模式之前主动“预触碰”主线程栈,即通过访问栈上的变量或数组来强制分配物理页。
示例代码:
char dummy[64 * 1024];
memset(dummy, 0, sizeof(dummy)); // 强制分配栈空间
因此,在开发 Xenomai 应用程序时,开发者应在保证功能正确性的前提下,合理配置线程栈大小,结合 mlockall() 和预分配策略,最大化系统的实时性能与稳定性。
4.3.2 实时线程的调度策略
1. 实时线程的基本要求
原生Linux中,使用pthread_create创建的线程,支持以下几种调度策略:
- SCHED_FIFO:先进先出调度策略。线程一旦开始运行,除非被更高优先级的线程抢占,或者主动放弃 CPU(如调用
sched_yield()),否则将持续运行。优先级范围1~99。 - SCHED_RR:轮转调度策略,类似于 FIFO,但每个线程有一个时间片,时间片用完后会排到队列末尾等待下一轮执行。优先级范围等同于SCHED_FIFO,优先级范围1~99。
- SCHED_NORMAL:Linux 默认的分时调度策略,不适合实时任务。所有调度策略SCHED_NORMAL的线程,在
pthread_create后优先级为0。为了给这些线程在Linux内核中进行优先级排序,Linux通过nice值来重新调节优先级。在Linux系统中,SCHED_OTHER 和 SCHED_NORMAL 实际上指的是同一个调度策略。SCHED_OTHER 是POSIX标准中定义的名称,SCHED_NORMAL 是Linux内核内部使用的别名。
下表列出了Xenomai支持的调度策略及其调度类。相比于Linux中常用的SCHED_NORMAL,SCHED_FIFO和SCHED_RR,Xenomai自行定义了其特有的调度策略:
| Linux 调度策略 | Xenomai 调度策略 | Xenomai 调度类 | Xenomai 适应范围 |
|---|---|---|---|
| SCHED_NORMAL | SCHED_NORMAL | xnsched_class_weak | 弱实时调度类,优先级为 0 |
| SCHED_NORMAL | SCHED_NORMAL | xnsched_class_rt | 当没有打开 weak 调度类时,使用实时调度类,但是优先级强制为 0 |
| SCHED_FIFO | SCHED_FIFO | xnsched_class_rt | 实时调度类,优先级支持范围1~256,Linux 实际只传入 1~99 |
| SCHED_RR | SCHED_RR | xnsched_class_rt | 实时调度类,优先级支持范围1~256,Linux 实际只传入 1~99 |
| N/A | SCHED_IDLE | xnsched_class_idle | 用于空闲调度,优先级必须为 -1 |
| N/A | SCHED_COBALT | xnsched_class_rt | 实时调度类,优先级范围0~259 |
| N/A | SCHED_WEAK | xnsched_class_weak | 弱实时调度类,优先级范围 0~99 |
| N/A | SCHED_SPORADIC | xnsched_class_sporadic | 用于处理偶发任务,优先级范围 1~255 |
| N/A | SCHED_TP | xnsched_class_tp | 用于时间分区调度,优先级范围 1~255 |
| N/A | SCHED_QUOTA | xnsched_class_quota | 用于配额调度,优先级范围 1~255 |
注意,Xenomai 默认情况下只支持两种调度类:实时调度类(xnsched_class_rt)和 空闲调度类(xnsched_class_idle)。其它调度类需要通过编译选项开启,而且一般来说并不常用。
[*] Xenomai/cobalt --->
Core features --->
[*] Extra scheduling classes
[ ] Weak scheduling class (NEW)
[ ] Temporal partitioning (NEW)
[ ] Sporadic scheduling (NEW)
[ ] Thread groups with runtime quota (NEW)
要使一个线程被 Xenomai 调度器识别为实时线程,必须使用 SCHED_FIFO 或 SCHED_RR 调度策略。
如果一个线程的调度策略被设置为SCHED_NORMAL,会被等同于SCHED_WEAK对待,对应的调度类为xnsched_class_weak,提供相对较弱的实时性保障。虽然在 Linux 中 SCHED_NORMAL 线程的优先级必须为 0,但在 Xenomai 中,SCHED_NORMAL 线程的优先级可以设置为 0~99。如果没有打开 weak 调度类,则 SCHED_NORMAL 线程的调度类为 xnsched_class_rt,优先级强制为 0,且实时线程的状态被标记为 XNWEAK。
Linux的调度策略到Xenomai的调度策略的映射关系,可以参考POSIX skin中的pthread_createAPI执行过程。
在用户层应用程序中,可以调用POSIX skin中的pthread_createAPI来创建Xenomai 实时线程。pthread_create的执行过程比较复杂,其中有一个环节,会执行sc_cobalt_thread_create系统调用,陷入内核层并执行Xenomai系统调用函数CoBaLt_thread_create。
CoBaLt_thread_create经过层层调用,会执行Cobalt内核实现的pthread_create函数。
// kernel/cobalt/posix/thread.c
static int pthread_create(struct cobalt_thread **thread_p,
int policy,
const struct sched_param_ex *param_ex,
struct task_struct *task)
{
...snip...
sched_class = cobalt_sched_policy_param(¶m, policy,
param_ex, &tslice);
if (sched_class == NULL) {
xnfree(thread);
return -EINVAL;
}
...snip...
}
在上述pthread_create的代码片段中,主要关注 cobalt_sched_policy_param 函数。
cobalt_sched_policy_param 函数的核心功能是将用户空间指定的调度策略(如 SCHED_FIFO、SCHED_RR 等)和调度参数,转换为内核空间使用的调度类和调度策略参数。它会根据不同的调度策略进行相应处理,验证优先级是否合法,最终返回对应的调度类指针。
2. 设置线程的调度属性
有两种方式可以设置线程的调度策略和参数:
- 方法一:创建线程前设置属性
使用 pthread_attr_t 属性对象,在调用 pthread_create() 之前设置调度策略和参数。
#include <pthread.h>
#include <sched.h>
void* thread_func(void* arg) {
// 实时线程逻辑
return NULL;
}
int main() {
pthread_t thread;
pthread_attr_t attr;
struct sched_param param;
// 初始化属性
pthread_attr_init(&attr);
// 设置继承调度策略为显式设置
pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);
// 设置调度策略为 SCHED_FIFO
pthread_attr_setschedpolicy(&attr, SCHED_FIFO);
// 设置优先级(0 ~ 99)
param.sched_priority = 50;
pthread_attr_setschedparam(&attr, ¶m);
// 创建实时线程
pthread_create(&thread, &attr, thread_func, NULL);
// 销毁属性对象
pthread_attr_destroy(&attr);
// 等待线程结束
pthread_join(thread, NULL);
return 0;
}
- 方法二:修改已有线程的调度参数
如果你希望修改主线程或其他已存在的线程的调度属性,可以使用 pthread_setschedparam():
struct sched_param param;
param.sched_priority = 90;
if (pthread_setschedparam(pthread_self(), SCHED_FIFO, ¶m) != 0) {
perror("Failed to set real-time priority");
}
3. 使用 SCHED_FIFO注意事项
⚠️ 注意:使用
SCHED_FIFO时,若线程进入死循环而没有让出 CPU,整个系统可能“冻结”。务必保证线程能定期释放 CPU,或设计合理的退出机制。
以下代码片段可能导致线程永远阻塞,尤其是在未正确初始化互斥量或条件变量时:
pthread_mutex_lock(&mutex);
while (!cond)
pthread_cond_wait(&cond, &mutex); // 如果 cond/mutex 未初始化,可能造成死锁
pthread_mutex_unlock(&mutex);
为了避免此类问题,请确保:
- 所有同步对象(mutex、cond)都正确初始化;
- 在销毁前确保没有线程正在等待;

被折叠的 条评论
为什么被折叠?



