关于Linux Preempt RT补丁的实时编程问题?

1、Preempt-RT Linux的上层实时任务如何设计,仅仅就是创建实时线程,设置实时调度方式,锁定内存这些就可以了吗?2、实时补丁对设备驱动有没…
关注者
75
被浏览
105,357

5 个回答

本来是发一下rk3588 xenomai实时数据的,居然有好几个收藏,那整体回答一下,这是一个很大的话题,涉及面广,每个拎出来都是很大的话题,所以回答略显粗糙,还请见谅。

一、首先,什么是实时操作系统(RTOS)?

什么是实时

实时的分类

常见的RTOS

latency和jitter

总结一下,实时其实说的是系统响应事件需要的时间的确定性,时间必须确定,打死都不能超过这个时间。

二、 linux为什么不是实时操作系统?

要保证系统实时性(事件响应计算输出的时间确定),我们可以将整个过程时延分别分解如下,只有保证了各个部分时间的确定性,才能保证整体响应时间的确定性。而操作系统仅能保证系统层面时延,也就是完成实时任务的调度执行,至于你的实时任务执行时间确不确定取决于你的软件算法设计,然后输出计算或控制结果。

为什么linux不是实时操作系统

下面对操作系统延时进行分解,可分解为系统延时=中断响应延时+中断处理延时+调度延时+进程切换延时,所以我们一一来看,linux为什么不实时?

  • 中断响应时间

任何条件下,linux中断响应时间确定吗(也就是最大中断屏蔽时间确定吗)?全球这么多开发者一起开发,linux支持这么多外设驱动,谁的驱动屏蔽中断执行多久谁知道呢?

  • 中断处理时间

那linux的中断处理延时确定吗?同样未知!不同的板子,不同的外设,中断处理需要的时间均不一样。

  • 任务调度时间

那一个事件产生,现在要找到下一个使用cpu的任务,如果系统中有成千上万个任务,linux从中选择运行的任务的时间要多少,这个调度过程的时间确定吗? 这就是为什么要有不同调度类和调度算法,不同调度类之间还要有不同的优先级,EDF调度类>RT调度类>完全公平调度调度类>idle调度类。但linux的实时问题不是出在这,而是什么时候能调度的问题!!!

任何操作系统的调度追求2个目标:吞吐率大和延迟低,但这个目标是相互矛盾的。调度和上下文切换过程本身也是要CPU时间的,频繁的上下文切换会导致CPU时间的浪费,降低系统的吞吐量,

Linux是通用操作系统(GPOS), linux定位是尽量缩短系统的平均响应时间,提高吞吐量,注重操作系统的整体功能需求,达到更好的平均性能。(过度追求响应时,过多的上下文切换把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降),所以规定在哪可以调度很重要,也就是我们说的调度点或者抢占点。

说白了,调度也是一段代码,一个函数,什么时候可以调用这个函数呢?不同的抢占模型抢占点不同,目前linux主线提供了3种抢占模型。

1、No Forced Preemption(Server)

不可抢占调度模型
不可抢占调度模型

早期linux为追求最大吞吐量,只能在内核态返回用户态时中断处理返回用户态时进行调度,也就是整个内核态执行的过程中都不能抢占,那外部事件产生到p3任务执行,这时间就可大了去了。

这是传统的抢占模型,目标是使吞吐量最大化。大多数时候提供良好的延迟,但是没有保证,可能偶尔出现长的延迟。这种模型主要用于服务器和科学计算系统。如果希望使内核的处理能力最大化,不考虑调度延迟,那么应该选择这种模型。

2、Voluntary Kernel Preemption(Desktop)

自愿抢占模型
自愿抢占时延示例

后来呢,为降低系统整体的响应时间,加入了自愿调度,也就是我是内核开发者,我在开发这个驱动或者子系统的时候,我觉得这里我占用CPU太久了,我主动加一行代码调用might_resched()增加抢占点,让别的任务执行一下,相比No Forced Preemption P3事件响应时间明显减少了,这种方式对吞吐量几乎没影响。

3、Preemptible Kernel(Low-Latency Desktop)

可抢占内核
可抢占内核示例

可抢占内核,就是内核态执行过程中,除了临界区以外的所有内核代码可抢占,也就是在上面的基础上增加了中断处理返回内核态(内核态抢占)的抢占点。

提供了很低的响应延迟,最坏情况的延迟时间是几毫秒,代价是稍微降低吞吐量和稍微增加运行时开销。相比Voluntary Kernel Preemption,从事件发生到高优先级的p3到得到运行之间的时间更少了。

这种模型主要用于有毫秒级别延迟需求的桌面系统和嵌入式系统(linux 2.6 以上),比如音频处理。

可抢占内核实时性测试

那为什么到这linux还不是一个硬实时操作系统呢?因为增加了中断处理返回内核态的抢占点,内核态中,只要不是关中断的地方,都可能发生抢占,这时候内核开发就变复杂了,任何内核代码都要考虑不同的上下文会不会有重入/死锁问题,多核下死锁的问题,进而增加了更多的临界区,这些临界区是不能抢占的。

所以就算linux启用可抢占内核,还有很多临界区和机制是默认不能抢占的,spinlock默认禁止抢占(整个内核态有10万+地方使用),同时硬中断的执行时间不确定,软中断总是抢占应用上下文等等影响任务调度时间的地方。

4、Full Real Time Preemption(PREEMPT-RT)、

也就是我们说的实时补丁,linux实时化的方案之一。

PREEMPT-RT

使能RT补丁,得到硬实时kernel,几乎任何地方都可以发生抢占,这种模型主要用于延迟要求为100微秒或稍低(几十微秒)的实时系统。 降低吞吐量、增加更多运行时开销。

PREEMPT-RT包含了hrtimer、优先级翻转、可抢占RCU、中断线程化、Full Tickless、EDF调度等等这些机制来保证系统的整体实时性。其中的90%多已经进入主线内核。

(1)仓库git.kernel.org/cgit/lin

(2)仓库git.kernel.org/cgit/lin

PREEMPT-RT实时性测试
  • 上下文切换时间

那下一个运行的任务选出来了,linux完成上下文切换需要多久?得益于现在的CPU针对性优化设计,linux上下文切换时间是很短的,与处理器架构相关。

三、linux实时化的方案有哪些?

详见:实时linux概述?

linux实时化方案

方案一:直接修改现有Linux代码

修改现有内核部分机制达到实时(基本上涉及内核中的每个文件)

RT-patch(PREEMPT-RT)已成为Linux实时增强的标准。它被广泛使用在各行各业:电信,工业自动化,专业音频,医疗设备,数据采集,汽车等。

方案二:双内核

内核空间添加一个实时调度核(linux作为实时核的低优先级任务受实时核调度),常用的双内核法有RT-Linux、RTAI(Real-Time Application Interface)和 Xenomai

实时linux

方案三:多核异构+实时虚拟化

AMP CPU上,一个或多个核跑linux,其他核跑裸机或RTOS,操作系统调度层面完全隔离。

SMP CPU通过虚拟化实现,实时虚拟化介绍详见:虚拟化技术及实时虚拟化概述

四、 xenomai的实时性

最近在RK3588上适配了xenomai和igh,并初步测试了一下,很强。

优化前
优化后

更多实时linux相关文章详见:沐多_xenomai内核解析,linux,X86-CSDN博客

更多实时linux相关文章详见:沐多_xenomai内核解析,linux,X86-CSDN博客

原文链接:有利于提高xenomai/PREEMPT-RT 实时性的一些配置建议

原文链接: 嵌入式实时linux概述

原文链接:【原创】xenomai内核解析之xenomai初探 - 沐多 - 博客园

参考链接:blog.csdn.net/fightings

最近在研究Preempt RT,即apply rt patch后让Linux达到硬实时。

关于RT patch的详细介绍,岛主打算后面详细地总结一下。今天先讲一下,假设成功apply rt patch之后,那么如何验证实时性呢?

最常用的工具就是cyclictest,本文较为粗略地介绍该工具及其原理,供感兴趣或工作中恰好会用到的同学们参考。

注:Debian/Ubuntu系统下可以直接使用apt-get install rt-tests来安装cyclictest。而本文主要介绍基于ARM的交叉编译方式。

安装cyclictest

1. 下载rt_tests源码

两种方法:
方法1:
如果你能正常访问github,那么直接运行:
$ git clone git://git.kernel.org/pub/scm/utils/rt-tests/rt-tests.git
$ git checkout origin/stable/v1.0

如果不能,使用方法2:
$ wget https://mirrors.edge.kernel.org/pub/linux/utils/rt-tests/rt-tests-1.10.tar.gz
$ gzip -d rt-tests-1.10.tar.gz
$ tar -xf rt-tests-1.10.tar.gz

2. 交叉编译

需要修改Makefile文件

CC = arm-none-linux-gnueabi-gcc -static
AR = arm-none-linux-gnueabi-ar

这里我们直接使用static build的方式,这样生成的bin文件不依赖于其他.so文件,方便测试。

3. 解决报错

执行make指令如下:

    报错:
        src/cyclictest/rt_numa.h:29:18: fatal error: numa.h: 没有那个文件或目录
    解决:
        1)安装apt-file 来找到依赖库
            apt-get install apt-file
            apt-file update #更新源
        2)寻找 numa.h
            apt-file install numa.h
        3) 安装相应的库
            apt-get install libnuma-dev 
            
    测试:
    $ file cyclictest 
    cyclictest: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 4.9.0, with debug_info, not stripped

最后,通过tftp/adb push等方式将bin文件传到board上即可。
参数完整参数参数含义
常用的基本选项
-a--affinity设置测试线程的亲核性,或设置线程的cpuset
-A--aligned=USECalign thread wakeups to a specific offset
-d--distance=DIST当每个CPU上只有一个线程时,建议将它设置为0。若CPU上有多个实时线程,需要设置一个distance用来隔开线程唤醒的时间,distance会累加到interval的值上,默认是500us
-D--duration=TIME --latency=PM_QOS指定测试持续事件,可以加单位 'm' 'h' 'd' 分钟 小时 天 write PM_QOS to /dev/cpu_dma_latency
-F--fifo=在指定路径下创建一个管道,用来向它写stats
-i--interval=INTV线程睡眠的时间,默认是1000us,即实时线程1000us被唤醒一次
-l--loops=LOOPS --laptop循环次数,默认次数是0(无数次),cyclictest运行的时间等于interval×loops,或是--duration=TIME 省电模式运行cyclictest,得到的结果相对不那么realtime。
-m--mlockall将当前和接下来的内存通过mlock锁定,防止发生swap影响测试
-n--nanosleep --notrace使用精度更高的纳秒睡眠api:clock_nanosleep 抑制追踪的行为(因为ftrace会有一定的overhead)
-p--priority=PRIO指定实时线程的优先级
关于timer和时间的选项
-c--clock=CLOCK选择时钟类型。默认是=0 0代表CLOCK_MONOTONIC 1代表CLOCK_REALTIME
-N--nsecs测试结果使用精度更高的ns显示(默认是us)
-r--relative使用相对时间的timer来代替绝对时间的timer
-R--resolution --secaligned [USEC]check clock resolution. align thread wakeups to the next full second
-s--system使用sys_nanosleep()和sys_setitimer()设置定时器
-t--threads每个处理器分配一个线程
-t--threads=NUM --tracemark指定总线程数,若不指定,线程数等于max_cpus;若不使用-t参数,则线程数为1。 write a trace mark when -b latency is exceeded
-S--smp --smi标准smp架构测试,所有的线程将使用相同的-a -t -n和优先级 Enable SMI counting
关于输出打印的选项
-h--histogram=US测试完成后输出一个直方图,并输出延时小于US的次数统计。
-H--histofall=US --histfile=类似于-h,输出一个柱状图 指定输出文件的路径
-M--refresh_on_maxdelay updating the screen until a new max latency is hit. Userful for low bandwidth.
-q--quiet测试结束后再输出结果
-o--oscope=RED使用'示波器'模式,以减少RED冗长的输出信息
-u--unbufferedforce unbuffered output for live processing
-v-verbose把测试信息输出到stdout
tracing相关的选项
-b--breaktrace=USEC当出现大于USEC的延时后,立即退出测试
-O--traceopt=TOPTtrace option
-T--tracer=TRACER(同-b一起使用)用来指定ftrace的tracer类型,例如:function_graph function nop blk
-B--preemptirqs(同-b一起使用)追踪preempt和irqoff
-E--event(同-b一起使用)进行event tracing
-I--irqsoff(同-b一起使用)追踪irqsoff
-P--preemptoff --policy=NAME --priospread(同-b一起使用)追踪Preempt off 设置实时进程的调度策略:other, normal, batch, idle, fifo or rr. spread priority levels starting at specified value
-w--wakeup(同-b一起使用)追踪wakeup
-f--ftrace(同-b一起使用)使用ftrace
-C--context(同-b一起使用)追踪context switch
-W--wakeuprt --dbg_cyclictest(同-b一起使用)追踪wakeuprt。 打印cyclictest的调试信息。

例子:

 #运行5个线程,线程优先级80,无限循环
sudo cyclictest -t 5 -p 80 -n  
#clock_nanosleep 线程优先级80,间隔10000微秒,10000次循环,无负载
cyclictest -t1 -p 80 -n -i 10000 -l 10000  
#POSIX间隔计时器 线程优先级80,间隔10000微秒,10000次循环,无负载
cyclictest -t1 -p 80 -i 10000 -l 10000

实测:

$ sudo cyclictest -t5 -p 80 -n -i 10000 -l 10000
# /dev/cpu_dma_latency set to 0us
policy: fifo: loadavg: 0.04 0.03 0.07 3/757 6676          

T: 0 ( 6672) P:80 I:10000 C:    830 Min:     30 Act:   96 Avg:  626 Max:    6198
T: 1 ( 6673) P:80 I:10500 C:    791 Min:     13 Act:  428 Avg:  508 Max:    9664
T: 2 ( 6674) P:80 I:11000 C:    755 Min:     14 Act:  807 Avg:  558 Max:    9003
T: 3 ( 6675) P:80 I:11500 C:    722 Min:     13 Act:  758 Avg:  536 Max:    2933
T: 4 ( 6676) P:80 I:12000 C:    692 Min:     31 Act:  717 Avg:  485 Max:    1882

打印参数含义:

T线程
P线程优先级
C计数器。线程的时间间隔每达到一次,计数器加1
I时间间隔(us)
Min最小延时(us)
Act最近一次的延时(us)
Avg平均延时(us)
Max最大延时(us)

cyclictest 的原理浅谈

从src/cyclictest/cyclictest.c 的 main函数入手:

main进程再启动指定数量指定优先级的实时进程,实时进程会设置一个timer周期性的唤醒自己(从timer溢出触发中断并进入ISR调用wake_up_process()唤醒实时进程,到进程真正能被运行,这中间的时间即我们需要测量的延时)。实时进程得到运行后会再次获取当前系统时间,减去睡眠时间时的时间以及睡眠的时间即可得到延时时间,并通过共享内存将该值传递给main进程进行统计,如此周而复始,最终由main进程将结果输出。

注意:当造成延时的事件发生在timer溢出之前,那么这样的延时将不会被捕捉到,所以我们需要足够久的运行cyclictest才能更大概率的抓取全面的延时数据。

master线程的逻辑:先创建num_threads个指定优先级的实时线程,然后在while循环中读取实时线程放在共享内存中的数据。伪代码如下:

int main(int argc, char **argv)
{ ...
    for (i = 0; i < num_threads; i++) {
        if (histogram)print_hist(parameters, num_threads)
        stat->min = 1000000;
        stat->max = 0;
        stat->avg = 0.0;
        stat->threadstarted = 1;
        status = pthread_create(&stat->thread, &attr, timerthread, par);
 ...
    }
    while (!shutdown) {
        for (i = 0; i < num_threads; i++) 
            print_stat((stats[i]), i))
        usleep(10000)
    } 
 ...
}

重点放在线程函数timerthread:

     void *timerthread(void *param)
        { ...
        interval.tv_sec = par->interval  // 首先将参数中的间隔数赋给函数中的间隔数
        interval.tv_nsec = (par->interval % USEC_PER_SEC) * 1000; 
     ...
        /* Get current time  */
        clock_gettime(par->clock, &now); // 获取当前时间,存在 now 中
        next = now;    //\
        next.tv_sec += interval.tv_sec; // = 这三行是将当前时间(now 的值)加上间隔数(interval)算出下次间隔的时间,存在next     
        next.tv_nsec += interval.tv_nsec; ///
        tsnorm(&next);
     ...
     /* Wait for next period */ 等到下次循环
      ... 
     if ((ret = clock_gettime(par->clock, &now))) { //下次循环中记录循环时的时间到now 中,此时now 值中存的数是真实的下次循环的值,而上面存在next 的值是上次循环加上间隔值所以是理论上的下个循环的值。
                if (ret != EINTR)
                    warn("clock_getttime() failed. errno: %d\n", errno);
                goto out;
            }   
       if (use_nsecs)
            diff = calcdiff_ns(now, next);         // 上面已经说过了,now 中是下次循环的真值,而next是理论的值,所以两者的差就是延时!延时赋值给diff
        else
            diff = calcdiff(now, next);
        if (diff < stat->min)   // 假如延时比min 小,将min 改为这个更小的延时值diff
            stat->min = diff;
        if (diff > stat->max) {   // 假如延时比max 大,将max 改为这个更大的延时值diff
            stat->max = diff;
            if (refresh_on_max)
                pthread_cond_signal(&refresh_on_max_cond);
        }
        stat->avg += (double) diff;  // 计算新的平均延时
     ...
        /* Update the histogram */  // 更新histogram中存的延时统计数据
        if (histogram) {
            if (diff >= histogram) { // 假如延时比histogram大,添加一次溢出
                stat->hist_overflow++; 
                                if (stat->num_outliers < histogram)
                    stat->outliers[stat->num_outliers++] = stat->cycles;
            }
            else      // 如果没有溢出,将histogram 中的相应值加1
                stat->hist_array[diff]++;
        }
         stat->cycles++;     // 循环加1
        next.tv_sec += interval.tv_sec; // 继续计算下次循环的值 ...
        next.tv_nsec += interval.tv_nsec;
     ...
    }

可见,实时线程的逻辑为:在while循环中先睡眠interval微秒,醒来后获取当前时间,计算延时,然后周而复始。伪代码如下:

clock_gettime((&now))
next = now + par->interval
while (!shutdown) {
    clock_nanosleep((&next))
    clock_gettime((&now))       //获取指定类型时钟的时间,例如CLOCK_MONOTONIC
    diff = calcdiff(now, next)  //计算此次延时
    # update stat-> min, max, total latency, cycles
    # update the histogram data
    next += interval
}

具体测试时通过创建线程,在线程中进行测试以及记录.刚开始记录开始时间为t_1,时间间隔为l,理论值为t_1 + l

执行结束后再次记录时间t_2, latency即为t_2 - (t_1+l)

延时在不同的上下文有不同的含义,而cyclictest所测得的延时是中断延时加调度延时,如下图。中断延时(interrupt latency),即中断发生到进入中断处理程序ISR的延时。调度延时(scheduling latency),即当任务被唤醒到任务真正获得CPU使用权中间的延时。

实时性测试:cyclictest详解