4.3POSIXskin的不兼容性

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,当新的内存被映射到进程地址空间时(例如通过 mallocmmap),这些新映射的页面也将自动被锁定,以防止它们被换出。但是,同样地,这并不会触发对这些页面的实际物理内存分配,直到它们被程序实际访问为止。

因此,虽然 mlockall 可以保证锁定的内存不会被交换到磁盘上,但它并不能保证所有的虚拟内存页在调用 mlockall 时都已经被分配了物理内存页。物理内存页的实际分配仍然遵循按需分页的原则,即在第一次访问某个页面时才会真正分配物理内存。 若要确保某些内存区域的物理页面已经被分配,你可能需要显式地访问这些页面的内容(例如,通过读写操作)来触发缺页处理和物理内存的分配。

2. 为什么需要 mlockall()

在标准 Linux 中,默认采用 按需分页(on-demand paging) 机制。也就是说,程序请求内存后并不会立即分配物理页,而是等到第一次访问该内存区域时才触发一次 页面错误(page fault),由内核动态分配物理页。

这种机制虽然节省了内存资源,但在实时系统中却存在严重问题:

  1. 页面错误会导致中断处理:发生 page fault 时,内核必须介入进行物理页分配,这会打断当前正在执行的线程。
  2. 切换执行模式:如果当前线程处于实时优先级(primary mode),则页面错误会强制将其降级为普通优先级(secondary mode),从而破坏实时性。
  3. 不可预测的延迟:在极端情况下(如内存不足且需要写回磁盘数据释放页),延迟可能达到毫秒级别。

因此,在 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_NORMALSCHED_FIFOSCHED_RR,Xenomai自行定义了其特有的调度策略:

Linux 调度策略Xenomai 调度策略Xenomai 调度类Xenomai 适应范围
SCHED_NORMALSCHED_NORMALxnsched_class_weak弱实时调度类,优先级为 0
SCHED_NORMALSCHED_NORMALxnsched_class_rt当没有打开 weak 调度类时,使用实时调度类,但是优先级强制为 0
SCHED_FIFOSCHED_FIFOxnsched_class_rt实时调度类,优先级支持范围1~256,Linux 实际只传入 1~99
SCHED_RRSCHED_RRxnsched_class_rt实时调度类,优先级支持范围1~256,Linux 实际只传入 1~99
N/ASCHED_IDLExnsched_class_idle用于空闲调度,优先级必须为 -1
N/ASCHED_COBALTxnsched_class_rt实时调度类,优先级范围0~259
N/ASCHED_WEAKxnsched_class_weak弱实时调度类,优先级范围 0~99
N/ASCHED_SPORADICxnsched_class_sporadic用于处理偶发任务,优先级范围 1~255
N/ASCHED_TPxnsched_class_tp用于时间分区调度,优先级范围 1~255
N/ASCHED_QUOTAxnsched_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_FIFOSCHED_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(&param, 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, &param);

    // 创建实时线程
    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, &param) != 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)都正确初始化;
  • 在销毁前确保没有线程正在等待;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值