实时Linux主要有两类方案:
- 单内核方案:对主线传统的Linux内核打入PREEMPT_RT补丁,使内核成为硬实时操作系统
- 双内核方案:主线传统Linux内核+实时内核的双内核方案,常见的主流方式有:RT-Linux,RTAI、Xenomai。
本文重点讲解单内核PREEMPT_RT补丁方案。
概念基础
实时性
实时分为硬实时和软实时,两者主要的区别主要就是就绪运行时间确定性。然后,主线Linux是软实时系统,加入实时补丁后将其改造为硬实时系统。
可抢占性
实现实时内核很重要的特点是可抢占性,就绪的高优先级的任务能够抢占低优先级任务。Linux内核在2.6版本之后引入了抢占模型,支持任务抢占。
主线Linux支持如下三种抢占模型:
(1) No Forced Preemption (Server): 传统的 Linux 抢占模型,面向吞吐量。系统调用返回和中断是唯一的抢占点。(2) Voluntary Kernel Preemption (Desktop): 此选项通过向内核代码 [. . . ] 以稍微降低吞吐量为代价。除了显式抢占点外,系统调用返回和中断返回都是隐式抢占点 (3) Preemptible Kernel (Low-Latency Desktop): 此选项通过使所有内核代码(不在临界区中执行的)可抢占来减少内核的延迟。隐式抢占点位于每个抢占禁用部分之后。
对于软实时的嵌入式系统来说,内核抢占模式配置为Preemptible Kernel (Low-Latency Desktop)最佳。
进程调度策略
Linux内核支持实时进程和非实时进程调度(无PREEMPT_RT补丁支持也是支持实时进程调度,只是软实时,有补丁后就是硬实时)。
对于Linux进程任务来说,Linux 内核实现了多种调度策略。它们分为非实时和实时策略。调度策略已经在主线 Linux 中实现。
非实时策略:
- SCHED_OTHER: 每个任务都有一个所谓的“nice值”。它是一个介于 -20(最高 nice 值)和 19(最低 nice 值)之间的值。任务执行时间的平均值取决于相关的 nice 值。
- SCHED_BATCH: 此策略源自 SCHED_OTHER 并针对吞吐量进行了优化。
- SCHED_IDLE: 它也是从 SCHED_OTHER 派生的,但它的值比 19 弱。
实时策略:
- SCHED_FIFO: 任务的优先级介于 1(低)和 99(高)之间。在此策略下运行的任务将被调度,直到它完成或更高优先级的任务抢占它。
- SCHED_RR: 此策略源自 SCHED_FIFO。与 SCHED_FIFO 的区别在于任务在定义的时间片的持续时间内运行(如果它没有被更高优先级的任务抢占)。一旦时间片用完,它可以被具有相同优先级的任务中断。时间片定义在 procfs (/proc/sys/kernel/sched_rr_timeslice_ms) 中导出。
- SCHED_DEADLINE: 此策略实施全局最早deadline优先 (GEDF) 算法。在此策略下调度的任务可以抢占使用 SCHED_FIFO 或 SCHED_RR 调度的任何任务。
设置api:
sched_setscheduler()
PREEMPT_RT 实时补丁
打入PREEMPT_RT补丁,可实现硬实时内核。
PREEMPT_RT下载地址:http://cdn.kernel.org/pub/linux/kernel/projects/rt/
PREEMPT_RT补丁主要做了如下修改,修改背后的原理后续文章再深入描述:
- 高分辨率定时器
- 中断线程化
- 自旋锁spinlock_t改为互斥锁rt_mutex,要使用自旋锁则使用raw_spinlock_t
打入PREEMPT_RT后内核抢占模型配置则会多了如下两项:
- Preemptible Kernel (Basic RT): 这种抢占模型类似于“抢占内核(低延迟桌面)”模型。除了上面提到的属性外,线程中断处理程序是强制的(就像使用内核命令行参数时一样
threadirqs
)。该模型主要用于 PREEMPT_RT 补丁实现的替代机制的测试和调试。 - Fully Preemptible Kernel (Real-Time): 除了少数选定的关键部分之外,所有内核代码都是可抢占的。线程中断处理程序是强制的。此外,还实现了几种替代机制,如睡眠自旋锁和 rt_mutex,以减少抢占禁用部分。此外,大的抢占禁用部分被单独的锁定结构取代。必须选择这种抢占模型以获得硬实时行为。
PREEMPT_RT 实时补丁的限制
某些环境当前不能很好地与 Preempt-RT 配合使用。由于不兼容,CONFIG_PREEMPT_RT_FULL=y 禁用了几个功能。
禁用的配置选项,主要是虚拟化配置:
- CONFIG_TRANSPARENT_HUGEPAGE
- CONFIG_OPROFILE
- CONFIG_XEN (arm64)
- CONFIG_X86_POWERNOW_K8
- CONFIG_BCACHE
- CONFIG_HIGHMEM(mips,powerpc)
- CONFIG_KVM_MPIC (powerpc)
- CONFIG_RT_GROUP_SCHED
- CONFIG_CPUMASK_OFFSTACK
PREEMPT_RT 实时补丁修改步骤
1、下载PREEMPT_RT补丁
对应内核版本选择PREEMPT_RT补丁的版本,不对应版本选择则可能出现不可预估性的问题或者打补丁出现大量冲突。
下载地址:http://cdn.kernel.org/pub/linux/kernel/projects/rt/
2、打补丁以及配置内核
传统的打补丁方法:
git apply xxx.patch
# 或
git am xxx.patch
传统的配置内核方法:
# 1、输入配置内核命令
make menuconfig
# 2、选中全功能实时抢占配置,保存退出
(*)Fully Preemptible Kernel (Real-Time)
因为我用的是yocto来构建系统,如下是我的菜谱追加文件
FILESEXTRAPATHS_prepend := "${THISDIR}/preempt-rt-patch:" ## 存放补丁和配置的目录
PATCHTOOL = "git"
SRC_URI += "file://preempt_full.cfg" ## 配置片段
SRC_URI += "file://patch-5.10.8-rt24.patch" ## 补丁
preempt_full.cfg配置片段:
CONFIG_HAVE_POSIX_CPU_TIMERS_TASK_WORK=y
CONFIG_POSIX_CPU_TIMERS_TASK_WORK=y
CONFIG_PREEMPT_LAZY=y
# CONFIG_PREEMPT is not set
CONFIG_PREEMPT_RT=y
CONFIG_RCU_BOOST=y
CONFIG_RCU_BOOST_DELAY=500
CONFIG_EXPERT=y
# CONFIG_SGETMASK_SYSCALL is not set
# CONFIG_DEBUG_RSEQ is not set
CONFIG_EMBEDDED=y
# CONFIG_PC104 is not set
# CONFIG_SLUB_MEMCG_SYSFS_ON is not set
# CONFIG_SUSPEND_SKIP_SYNC is not set
# CONFIG_DPM_WATCHDOG is not set
# CONFIG_FIRMWARE_MEMMAP is not set
# CONFIG_VIRTUALIZATION is not set
CONFIG_ARCH_SUPPORTS_RT=y
# CONFIG_CFG80211_CERTIFICATION_ONUS is not set
# CONFIG_PCIE_BUS_TUNE_OFF is not set
CONFIG_PCIE_BUS_DEFAULT=y
# CONFIG_PCIE_BUS_SAFE is not set
# CONFIG_PCIE_BUS_PERFORMANCE is not set
# CONFIG_PCIE_BUS_PEER2PEER is not set
# CONFIG_WIRELESS_WDS is not set
# CONFIG_TTY_PRINTK is not set
# CONFIG_DRM_DEBUG_DP_MST_TOPOLOGY_REFS is not set
# CONFIG_DRM_FBDEV_LEAK_PHYS_SMEM is not set
# CONFIG_SND_SOC_SOF_DEVELOPER_SUPPORT is not set
CONFIG_MXC_GPU_VIV=y
# CONFIG_DEBUG_FORCE_FUNCTION_ALIGN_32B is not set
# CONFIG_DEBUG_MEMORY_INIT is not set
3、编译以及烧写镜像
make Image
4、下载实时测试工具套件
运行下面指令下载实时测试工具,然后按照你的环境对其交叉编译:
git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
5、实时性测试
实时性测试主要有两个点:模拟负载和cyclictest。
模拟负载就是通过运行一些程序让系统处于高负载的状态。
cyclictest就是开源的Linux实时性测试工具。
模拟负载有多种方式,比如stress、stress-ng、hackbench、脚本模拟。
如下是我模拟负载脚本(stress_loades.sh ),内部使用了dd、hackbench、ping、taskset、cyclictest命令,其中cyclictest、hackbench是上面下载的实时测试工具套件内部自带的,其他三个命令也是一般系统都会自带的:
#!/bin/bash# 如果参数小于1
if [ "$1" == "-h" ]
then echo "example:"echo " ./stress_loades.sh [Level]"echo "option:"echo " 1: heavy cpu load"echo " -h: help "exit 0
fi# 如果参数等于1
if [ "$1" == "1" ]
thenwhile true; do dd if=/dev/zero of=bigfile bs=1024000 count=1024; done > /dev/null &while true; do killall hackbench; sleep 5; done > /dev/null &while true; do ./hackbench 20; done > /dev/null &
fitaskset -c 0 ping -l 65535 -q -s 10 -f localhost > /dev/null &
taskset -c 1 ping -l 65535 -q -s 10 -f localhost > /dev/null &
taskset -c 2 ping -l 65535 -q -s 10 -f localhost > /dev/null &
taskset -c 3 ping -l 65535 -q -s 10 -f localhost > /dev/null &while true; do taskset -c 3 du / ; done > /dev/null &time ./cyclictest -t50 -p 80 -i 10000 -s -l 100000000000 -d 86400 -a 3 > /dev/null &
实时测试脚本(cyclictest.sh):
#!/bin/bash
./cyclictest -S -p 99 -i 1000 -D 5m -m -s -h400 -q >no_load_output ./stress_loades.sh ./cyclictest -S -p 99 -i 1000 -D 5m -m -s -h400 -q >light_load_output./stress_loades.sh 1./cyclictest -S -p 99 -i 1000 -D 5m -m -s -h1000 -q >heavy_load_output
这里为了展示效果,我只运行了5分钟测试,实际上应以产品的应用场景时长做测试,这样比较能反应其实时稳定性。
6、实时测试总结
我测试 Voluntary Kernel Preemption (Desktop)、Preemptible Kernel (Low-Latency Desktop)、Fully Preemptible Kernel (Real-Time) 三种抢占模型在无负载、一般负载、超重负载下的实时性。
在无负载的情况下,三种抢占模型基本上都能稳定实时响应,在一般负载和超重负载下,Voluntary和Preemptible模型出现了比较大延时抖动,其中Voluntary在一般负载下,最大延时达到395us,在超重负载下最大延时达到2599us,而Preemptible在一般负载下,最大延时达到241us,但是在超重负载下,最大延时达到了11920us。然而Fully Preemptible模型,在三种负载都表现出稳定实时性能,无负载下,最大延时49us,一般负载下,最大延时55us,超重负载下,最大延时79us。
由于测试的时间短以及实时性也跟处理器性能有关系,因此该测试仅作为简单参考。
最后,测试结果的直方图如下:
Voluntary Kernel Preemption (Desktop)抢占模型:
无负载:
一般负载:
超重负载:
Preemptible Kernel (Low-Latency Desktop)抢占模型:
无负载:
一般负载:
超重负载:
Fully Preemptible Kernel (Real-Time) 抢占模型:
无负载:
一般负载:
超重负载:
实时应用程序开发
对于有实时需求的应用程序,一般需要设置调度策略,优先级等等参数。一般常用的api函数如下:
sched_setscheduler()
pthread_attr_setschedpolicy()
pthread_attr_setschedparam()
pthread_attr_setinheritsched()
示例如下:
/* POSIX Real Time Example* using a single pthread as RT thread */#include <limits.h>
#include <pthread.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>void *thread_func(void *data)
{/* Do RT specific stuff here */return NULL;
}int main(int argc, char* argv[])
{struct sched_param param;pthread_attr_t attr;pthread_t thread;int ret;/* Lock memory */if(mlockall(MCL_CURRENT|MCL_FUTURE) == -1) {printf("mlockall failed: %m\n");exit(-2);}/* Initialize pthread attributes (default values) */ret = pthread_attr_init(&attr);if (ret) {printf("init pthread attributes failed\n");goto out;}/* Set a specific stack size */ret = pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN);if (ret) {printf("pthread setstacksize failed\n");goto out;}/* Set scheduler policy and priority of pthread */ret = pthread_attr_setschedpolicy(&attr, SCHED_FIFO);if (ret) {printf("pthread setschedpolicy failed\n");goto out;}param.sched_priority = 80;ret = pthread_attr_setschedparam(&attr, ¶m);if (ret) {printf("pthread setschedparam failed\n");goto out;}/* Use scheduling parameters of attr */ret = pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED);if (ret) {printf("pthread setinheritsched failed\n");goto out;}/* Create a pthread with specified attributes */ret = pthread_create(&thread, &attr, thread_func, NULL);if (ret) {printf("create pthread failed\n");goto out;}/* Join the thread and wait until it is done */ret = pthread_join(thread, NULL);if (ret)printf("join pthread failed: %m\n");out:return ret;
}
FAQ
Scheduling - RT throttling
实时任务具有较高优先级,Linux内核优先调度高优先级任务。
如果实时应用程序中的编程失败会导致整个系统挂起,这种失败可能就像调用了一个“while(true){}”循环。
当实时应用程序具有尽可能高的优先级并使用 SCHED_FIFO 策略进行调度时,没有其他任务可以抢占它。
这会导致系统阻塞所有其他任务并以 100% 的 CPU 负载调度此循环。
实时节流是一种通过限制每个实时任务的周期执行时间来避免这种情况的机制。
这些设置被导出到 proc 文件系统中。
默认设置为:
# cat /proc/sys/kernel/sched_rt_period_us
1000000
# cat /proc/sys/kernel/sched_rt_runtime_us
950000
如果要使实时任务的 CPU 使用率仅达到 50% ,可以使用以下命令更改这些值:
# echo 2000000 > /proc/sys/kernel/sched_rt_period_us
# echo 1000000 > /proc/sys/kernel/sched_rt_runtime_us
如果想要禁用实时节流, 通过将 -1
写入 sched_rt_runtime_us
来自动完成的:
# echo -1 > /proc/sys/kernel/sched_rt_runtime_us