王顺刚:linux实时应用如何printf输出不影响实时性?

原创 Linux阅码场 2023-02-18 22:30
王顺刚(网名:沐多),一线码农,从事工控行业,目前在一家工业自动化公司从事工业实时现场总线开发工作,喜欢钻研Linux内核及xenomai,个人博客 wsg1100,欢迎大家关注!
顺刚,公众号:Linux阅码场xenomai3.1+linux构建linux实时操作系统-基于X86_64和arm

本文介绍为什么linux实时任务不能直接调用printf(),首先简单介绍一下终端输出原理,然后就如何实现终端输出不影响实时任务实时性给出一个方案,最后介绍xenomai中是如何做到完美printf()的。

1. 前言

开始前,回顾下实时(Real-Time):

实时的本质是确定性、可预期性。即实时系统是必须在设置的截止时间内特定环境中的事件做出反应的系统,不仅依赖于计算结果的正确性,还依赖于计算结果的 返回时间。实时任务运行过程中,不论软件硬件,一切造成时间不确定的因素都是实时性的影响因素。

我们在linux上开发普通应用程序时,最常用的调试手段是gdb单步、终端打印。除调试外,一般应用程序运行过程中或多或少都会输出一些应用运行信息、错误信息、警告信息等,这些信息格式化后可能会输出到终端、syslog、记录到文件等(本文仅介绍终端打印操作,其他的类似)。

但如果我们开发的是实时应用程序,还能一样吗?硬实时应用开发调试,部分情况下可以使用gdb跟踪调试,但在一些涉及时间敏感的业务调试时,程序不能停下来,这时好的调试方式只有打印。非调试时也需要打印输出和纪录一些应用信息,总之我们要在实时路径上打印信息,就需要考虑打印这个操作的实时性,即打印操作耗时必须是确定的,同时耗时不能影响实时应用结果输出的deadline。

这个问题的本质是:实时任务该如何进行非实时IO 操作

(1) 任务具有高优先级,不代表该任务所有IO操作实时 。

(2) 部分IO操作可能会带来严重的不确定性,如实时任务中通过标准输入输出打印、读写文件等。

那glibc中printf()操作是实时的吗?为什么?

2. linux终端输出

在linux中,glibc提供了标准IO接口(printf、fwrite(stdout)...),其底层通过读写linux内核tty设备进行IO输入输出,终端输出简单流程如下所示。

应用程序终端打印可以直接通过系统调用write()输出,这样的话我们要处理更多的底层细节,比如指定文件描述符,要区分向终端打印字符还是写入到文件。为屏蔽底层操作细节,C标准库提供了统一和通用的IO接口,让我们不必关注底层操作系统相关细节,做到一次编码到处编译。

但是,系统调用的过程涉及到进程在用户模式与内核模式之间的转换,过多的系统调用和上下文切换,会将原本运行应用的CPU时间,消耗在寄存器、内核栈以及虚拟内存数据保护和恢复上,缩短应用程序真正运行的时间,其成本较高。为了提升 IO 操作的性能,同时保证开发者所指定的 IO 操作不会在程序运行时产生可观测的差异,标准 IO 接口在实现时通过添加缓冲区的方式,尽可能减少了低级 IO 接口的调用次数。使用标准 IO 接口实现的程序,会在用户输入的内容达到一定数量或程序退出前,再更新文件中的内容。而在此之前,这些内容将会被存放到缓冲区中。

通过系统调用进入系统后,数据经过TTY 核心、线路规程、tty驱动最终到达硬件外设,如果终端是串口的话,由UART driver操作串口外设发送,如果终端是VGA显示器或xtrem虚拟终端,则通过对应的路径进行输出。

综上printf()由linux C标准库提供,其执行时间的长短取决于用户态glibc缓冲方式、内存分配,内核态TTY driver、UART driver的具体实现(全路径是否实时)等。所以glibc提供的标准IO并不是个实时的接口(低端arm平台,实测glibc缓冲后输出到波特率为115200的串口终端,执行需要330ms左右,如果在实时上下文使用,对实时应用来说这就是灾难)。

虽然PREEMPT-RT通过修改Linux内核使linux内核提供硬实时能力,但整个路径不仅仅只有内核,还涉及内核中的各种子系统,还有硬件驱动,应用层的标准库glibc等,存在很多非实时的行为,没有明确说明哪些是执行时间确定的,哪些是不确定的,只能遇到问题解决问题。

3. 常见的NRT IO输出方案

实时应用中,对于此类问题,一般将非实时的IO操作交给非实时任务来处理,实时任务与非实时IO操作任务之间通过实时进程间通信IPC(共享内存、消息队列…)交互,这个IPC通讯时间是确定的,如下所示。

3.1 一种实现方式

根据上图,我们容易实现如下可在实时上下文调用的打印输出接口。

实时与非实时任务使用消息队列通信,创建的消息队列大小固定,实时方通过非阻塞的方式发送消息,非实时方阻塞接收消息。

rt_printf()接口每次调用先分配一片内存msg,然后将要打印的内容通过sprintf()格式化到该内存中,接着将内存首地址通过非阻塞方式放到消息队列,待高优先级的任务让出CPU,低优先级的任务printf_task得到运行后,从消息队列取出消息,最后通过printf()进行输出,输出完成后将内存释放。

该实现方式有没有问题?这个rt_printf接口并不是实时的,我们在一个PREMPT-RT的生产环境中就是这样实现的,在实时应用中应用时发现有很大问题。

你可能觉得不实时是因为不能在实时上下文使用glibc提供的malloc()来动态分配内存,这里malloc()是原因之一,这是显而易见的问题。我们在排查问题时,也一度以为抖动是malloc或实时应用其他业务部分产生的。但经过排查,发现一些过大的抖动产生时与内存分配并没有关系,并且抖动比malloc()分配内存产生的pagefult抖动还大,能达到几百ms,这明显不正常。

这里简单吐槽一下,linux虽然有很多debug和training的工具,如gdb、ftrace、tracepoint、bpf、strace、...,但这些都是会严重影响实时任务的运行实时序,在debug一个实时应用的问题时,由于这些工具的干预,要么问题不复现,要么整个系统卡死等等,特别是在一些资源受限的小型嵌入式linux系统上,很难排查系统或应用实时性问题,共性问题最好在x86上调试。

笔者这里要给大家介绍该实现里我们遇到的坑,从应用角度来看格式化字符串接口sprintf()与打印输出接口printf()是两种行为,他们之间没有什么直接联系。但通过调试发现,在glibc的实现中它们底层共用一个函数,存在锁互斥,就会导致低优先级任务的printf()持有锁刷新缓冲区,前面说到刷新缓冲区的时间可长达300ms,这时候高优先级任务只能阻塞等待锁释放,影响高优先级实时性。

这里想说的是,用户态的glibc诞生之初就是针对高吞吐量设计的,而非实时性。此外虽然PREEMPT-RT在内核调度层面保证了linux的实时性,但内核中仍有许多机制和子系统、driver是非实时的,最严重的是driver,目前linux内核代码量三千多万行,其中85%以上为bsp驱动,这些驱动来自全球无数开发者和芯片厂商,这些驱动编写之初就不是为实时应用而设计,这只是upstream的代码,代码质量比较优秀,问题相对好查找,但还有未上游化的驱动,那才是痛苦的根源。

由于ARM IP核授权方式,各个芯片厂商不同芯片外设各式各样,这些外设驱动代码并没有上游化,只存在于芯片厂商提供的SDK中,如果厂商没有明确支持PREEMPT-RT,那使用到的实时外设对应的实时驱动基本得debug一遍,特别是一些国产ARM芯片需要注意。

总之我们在开发实时应用时,全路径都需要注意,分清楚哪些实时的哪些是非实时的,这也是为什么xenomai用户库、调度核、中断、驱动到底层硬件全路径实时。

3.3 改进

如何解决这个问题?printf()的作用是输出到终端,所有直接使用fwrite写终端stdout替换即可解决。

需要注意,fwrite需要知道写的数据长度,所以通过消息队列发送给实时任务的就不仅仅是个内存地址了,我们可以为每个输出流添加如下头,申请内存附加这个头,这里就不赘述了。

struct out_head {
size_t len; /*数据长度*/
char data[0]; /*格式化后的数据*/
};

到此,只要不是在实时上下文频繁调用,一个基本满足实时应用调试的rt_printf()接口就完成了,如果我们要实现一个完美的rt_printf()接口,那它还有以下不足:

  • 存在动态内存分配,导致不确定性增加。

  • IPC方式效率过低,消息队列需要内核频繁参与。

  • 共用一个消息队列、malloc内存分配,多线程同时调用时这些会成为瓶颈(消息队列在内核中也存在锁),相互影响实时性。

  • 消息队列的大小有限,若某个实时线程突发大量信息打印时,可能导致消息队列耗尽,其他实时任务的消息无法输出到终端,造成打印信息丢失。

  • 原实时应用源代码需要修改,应用中所有printf()接口都要修改为rt_printf(),导致应用代码可移植性,可维护性差。

  • 使用需要添加初始化代码相关,如消息队列创建、非实时线程创建等。

3. Xenomai3 printf()接口

xenomai3于2015年正式发布,在xenomai3之前的xenomai2,实时应用程序打印需要调用特定的接口rt_printf(),从xenomai3开始实时应用无需修改printf(),只有正确编译链接实时应用POSIX接口库libcobalt就可实现实时上下文调用printf()不影响实时性。

需要说明的是:xenomai3支持两种方式构建linux实时系统,分别是cobalt 和 mercury详见【原创】xenomai内核解析之xenomai初探,mercury构建时,printf接口仍是非实时的。

实时应用POSIX接口库libcobalt提供的printf(),完全解决了上节中的不足:

  1. 应用无需调用额外初始化,编译链接即可使用

  2. 预先分配打印内存池,无需每次通过glibc动态申请

  3. IPC使用共享内存,freelock(无锁)

  4. 引入线程特有数据,多线程安全,临界区无需锁保护

  5. 无缝连接,应用代码无需修改标准IO接口

以下内容仅做概要,不对源码逐行分析,若有兴趣可自行阅读libcobalt源码。

3.1 应用运行前环境初始化

用户无需调用代码初始化,那只能在应用代码执行前将环境printf相关准备好,如何做?回想我们使用C语言开发裸机程序时,我们通常认为CPU是从main()函数开始执行的,但实际上裸机开发时需要先用汇编为C程序执行准备环境,然后再调用main()开始执行,这种情况下我们可以在main()执行前做一些额外操作。

回到我们linux环境,这时我们要在main()之前做一些操作,又该如何实现?到这熟悉C++的同学应该会联想到C++中全局对象,它们在main()之前就调用构造函数完成全局对象的创建了,而且main()结束后,程序即将结束前其析构函数也会被执行。

1. GCC特定语法

在GCC中,可以通过GCC提供的两个GCC特定语法实现:

  • __attribute__((constructor)) 当与一个函数一起使用时,则该函数将会在main()函数之前。

  • __attribute__((destructor)) 当与一个函数一起使用时,则该函数将会在main()函数之后执行。

它们的工作原理为:共享文件 (.so) 或者可执行文件包含特殊的部分(ELF上的.ctors Section和.dtors Section,可用通过readelf -S查看Section信息),GCC编译时会将标有构造函数和析构函数属性的函数符号放到这两个Section中,当库被加载/卸载时,动态加载器程序检查这些部分是否存在,如果存在,则调用其中引用的函数。

关于这些,有几点是值得注意的。

a. 当一个共享库被加载时,__attribute__((constructor))运行,通常是在程序启动时。

b. 当共享库被卸载时,__attribute__((destructor))运行,通常在程序退出时。

c. 两个小括号大概是为了区分它们与函数调用。

d. __attribute__是GCC特有的语法;不是一个函数或宏。

使用destructor和constructor的好处是,如果我们有很多模块,原来的方式是每个模块内的初始化都需要去调用一遍,删除某一个模块就需要删除相应的初始化代码,然后重新编译。有了destructor和constructor,我们就可以为每一个模块设置对应的constructor,应用程序使用时就不需要统一写代码一个模块一个模块进行初始化,只需要编译链接需要对应的模块即可,爽歪歪

xenomai 实时库libcobalt利用该特性在实时应用程序前执行了大量初始化,如如Alchemy API、VxWorks® emulator、pSOS® emulator 等 API环境的初始化,这样我们才能无缝使用libcobalt提供的服务。

这样的应用很多,比如DPDK中,我们需要支持什么网卡驱动直接选中编译链接即可,业务代码还未执行,就已经完成所有网卡驱动注册了,应用程序后续执行扫描硬件,匹配直接执行对应驱动进行probe。

2. libcobalt printf初始化流程

3.2 libcobalt printf内存管理

1. print_buffer

实时线程与负责打印输出的非实时线程通过一片共享内存来实现IPC,该内存为环形队列,print_buffer是管理这片内存的结构,与环形队列缓冲区一一对应,其维护着环形队列生产者与消费者的位置,print_buffer每个线程一个。

2. entry_head

entry_head用来抽象每条消息,从缓冲队列中分配,包含消息长度,序号,目的(stdio、syslog)等信息。

3. printf pool

cobalt_print_init初始化过程中,预先分配打印内存池pool,分配成N份,其分配信息通过bitmap来记录,无需每次通过glibc动态申请,当实时线程第一次调用printf()接口时,查询bitmap未分配的print_buffer,取出设置为该线程的特有数据,并将其添加到全局链表first_buffer

注:线程特有数据(TSD)是解决多线程临界区需要保护,影响多线程并发性能的一种方式。更多详见《Linux/UNIX系统编程手册 第31章 线程:线程安全与每线程存储》

3.2 libcobalt printf工作流程

实时线程

  1. 每个实时线程打印时,先从pool中分配printf buffer

  2. 成功分配后,将分配的buffer设置为线程特有存储数据pthread_setspecific(buffer_key, buffer),此后该线程只操作这个buffer;

  3. 若线程过多,预先分配的pool已无法分配,使用malloc增加一个printf buffer,放到全局队first_buffer里,并设置为该线程特有存储数据,供后续每次打印输出使用。

  4. 将打印消息格式化到buffer的数据区

非实时线程

以一定周期从first_buffer遍历链表,处理每一个buffer中的entry_head,按顺序取出entry_head,按照entry_head指定目的进行IO输出。

到此上个实现中的不足全部解决,其中关于xenomai如何实现"无缝衔接,应用代码无需修改编译链接即可使用",这个已在之前的文章中解析,详见【原创】xenomai内核解析--双核系统调用(二)--应用如何区分xenomai/linux系统调用或服务 。

4. 总结

以上就是一个实时linux下开发实时应用程序,由一个普普通通的printf()引发的实时性能问题解决,可以看出不起眼的printf()要做好远比我们想象的复杂,做底层就是这样,得耐得住寂寞。几句话共勉:
"万丈高楼平地起,勿在浮沙筑高台"。
"或许做上层业务能快速出活,成果直接,不用了解其内部的实现和对底层的依赖,美其名日“站在巨人的肩膀上”。效率提升了,但同时也导致我们对巨人的成长过程不闻不问。殊不知巨人倒下之后,我们将无所适从,就算巨人只是生个病(发生漏洞)带来的损失也不可估量"。

加微信进群与作者交流

Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
评论
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 223浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 108浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 141浏览
  • 故障现象一辆2017款东风风神AX7车,搭载DFMA14T发动机,累计行驶里程约为13.7万km。该车冷起动后怠速运转正常,热机后怠速运转不稳,组合仪表上的发动机转速表指针上下轻微抖动。 故障诊断 用故障检测仪检测,发动机控制单元中无故障代码存储;读取发动机数据流,发现进气歧管绝对压力波动明显,有时能达到69 kPa,明显偏高,推断可能的原因有:进气系统漏气;进气歧管绝对压力传感器信号失真;发动机机械故障。首先从节气门处打烟雾,没有发现进气管周围有漏气的地方;接着拔下进气管上的两个真空
    虹科Pico汽车示波器 2025-01-08 16:51 70浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 116浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 122浏览
  •  在全球能源结构加速向清洁、可再生方向转型的今天,风力发电作为一种绿色能源,已成为各国新能源发展的重要组成部分。然而,风力发电系统在复杂的环境中长时间运行,对系统的安全性、稳定性和抗干扰能力提出了极高要求。光耦(光电耦合器)作为一种电气隔离与信号传输器件,凭借其优秀的隔离保护性能和信号传输能力,已成为风力发电系统中不可或缺的关键组件。 风力发电系统对隔离与控制的需求风力发电系统中,包括发电机、变流器、变压器和控制系统等多个部分,通常工作在高压、大功率的环境中。光耦在这里扮演了
    晶台光耦 2025-01-08 16:03 58浏览
  • 本文介绍编译Android13 ROOT权限固件的方法,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。关闭selinux修改此文件("+"号为修改内容)device/rockchip/common/BoardConfig.mkBOARD_BOOT_HEADER_VERSION ?= 2BOARD_MKBOOTIMG_ARGS :=BOARD_PREBUILT_DTB
    Industio_触觉智能 2025-01-08 00:06 92浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 202浏览
  • 「他明明跟我同梯进来,为什么就是升得比我快?」许多人都有这样的疑问:明明就战绩也不比隔壁同事差,升迁之路却比别人苦。其实,之间的差异就在于「领导力」。並非必须当管理者才需要「领导力」,而是散发领导力特质的人,才更容易被晓明。许多领导力和特质,都可以通过努力和学习获得,因此就算不是天生的领导者,也能成为一个具备领导魅力的人,进而被老板看见,向你伸出升迁的橘子枝。领导力是什么?领导力是一种能力或特质,甚至可以说是一种「影响力」。好的领导者通常具备影响和鼓励他人的能力,并导引他们朝着共同的目标和愿景前
    优思学院 2025-01-08 14:54 61浏览
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 158浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 164浏览
我要评论
1
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦