2026/4/7 11:14:45
网站建设
项目流程
网站开发环境是什么,长沙经开区建管站,wordpress5.0大更新,老干部局网站建设的意义4.3 POSIX skin的不兼容性
4.3.1 mlockall 与栈大小
在 Xenomai 等实时系统中#xff0c;确保程序运行的确定性和低延迟是至关重要的。为了实现这一点#xff0c;Xenomai 在其初始化过程中使用了一个关键的 Linux 系统调用 mlockall()#xff0c;以提升内存访问效率并避免潜…4.3 POSIX skin的不兼容性4.3.1 mlockall 与栈大小在 Xenomai 等实时系统中确保程序运行的确定性和低延迟是至关重要的。为了实现这一点Xenomai 在其初始化过程中使用了一个关键的 Linux 系统调用mlockall()以提升内存访问效率并避免潜在的页面错误page fault。本小节将深入解析mlockall的原理、作用及其对线程栈大小的影响并探讨如何在系统中合理配置。1. 什么是mlockall()mlockall()是一个 Linux 系统调用用于将进程的所有虚拟内存页锁定在物理内存中防止它们被交换到磁盘或因未分配而引发页面错误。该调用的原型如下intmlockall(intflags);其中常用的标志包括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-s8192这意味着每当创建一个新线程时系统都会立即为其分配 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二者中的最大值。#ifndefPTHREAD_STACK_DEFAULT#definePTHREAD_STACK_DEFAULT\({\int__retPTHREAD_STACK_MIN;\if(__ret65536)\__ret65536;\__ret;\})#endif/* !PTHREAD_STACK_DEFAULT */建议显式设置栈大小对于确实需要更大栈空间的线程应通过pthread_attr_setstacksize()显式指定更大的栈大小。示例代码pthread_attr_tattr;size_tstack_size64*1024;// 64 KiBpthread_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-s256# 将栈大小限制为 256 KiB./my_program此外即使启用了mlockall()主线程的栈仍可能在运行时增长因为它是自动扩展的。为了防止在关键实时路径上出现 page fault建议在进入实时模式之前主动“预触碰”主线程栈即通过访问栈上的变量或数组来强制分配物理页。示例代码chardummy[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_NORMALLinux 默认的分时调度策略不适合实时任务。所有调度策略SCHED_NORMAL的线程在pthread_create后优先级为0。为了给这些线程在Linux内核中进行优先级排序Linux通过nice值来重新调节优先级。在Linux系统中SCHED_OTHER 和 SCHED_NORMAL 实际上指的是同一个调度策略。SCHED_OTHER 是POSIX标准中定义的名称SCHED_NORMAL 是Linux内核内部使用的别名。下表列出了Xenomai支持的调度策略及其调度类。相比于Linux中常用的SCHED_NORMALSCHED_FIFO和SCHED_RRXenomai自行定义了其特有的调度策略:Linux 调度策略Xenomai 调度策略Xenomai 调度类Xenomai 适应范围SCHED_NORMALSCHED_NORMALxnsched_class_weak弱实时调度类优先级为 0SCHED_NORMALSCHED_NORMALxnsched_class_rt当没有打开 weak 调度类时使用实时调度类但是优先级强制为 0SCHED_FIFOSCHED_FIFOxnsched_class_rt实时调度类优先级支持范围1~256Linux 实际只传入 1~99SCHED_RRSCHED_RRxnsched_class_rt实时调度类,优先级支持范围1~256Linux 实际只传入 1~99N/ASCHED_IDLExnsched_class_idle用于空闲调度优先级必须为 -1N/ASCHED_COBALTxnsched_class_rt实时调度类优先级范围0~259N/ASCHED_WEAKxnsched_class_weak弱实时调度类优先级范围 0~99N/ASCHED_SPORADICxnsched_class_sporadic用于处理偶发任务优先级范围 1~255N/ASCHED_TPxnsched_class_tp用于时间分区调度优先级范围 1~255N/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_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.cstaticintpthread_create(structcobalt_thread**thread_p,intpolicy,conststructsched_param_ex*param_ex,structtask_struct*task){...snip...sched_classcobalt_sched_policy_param(param,policy,param_ex,tslice);if(sched_classNULL){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()之前设置调度策略和参数。#includepthread.h#includesched.hvoid*thread_func(void*arg){// 实时线程逻辑returnNULL;}intmain(){pthread_tthread;pthread_attr_tattr;structsched_paramparam;// 初始化属性pthread_attr_init(attr);// 设置继承调度策略为显式设置pthread_attr_setinheritsched(attr,PTHREAD_EXPLICIT_SCHED);// 设置调度策略为 SCHED_FIFOpthread_attr_setschedpolicy(attr,SCHED_FIFO);// 设置优先级0 ~ 99param.sched_priority50;pthread_attr_setschedparam(attr,param);// 创建实时线程pthread_create(thread,attr,thread_func,NULL);// 销毁属性对象pthread_attr_destroy(attr);// 等待线程结束pthread_join(thread,NULL);return0;}方法二修改已有线程的调度参数如果你希望修改主线程或其他已存在的线程的调度属性可以使用pthread_setschedparam()structsched_paramparam;param.sched_priority90;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都正确初始化在销毁前确保没有线程正在等待