【原创】linux实时应用如何printf输出不影响实时性?


版权声明:本文为本文为博主原创文章,转载请注明出处 https://www.cnblogs.com/wsg1100。如有错误,欢迎指正。
@

目录
  • 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实时系统,分别是cobaltmercury详见,mercury构建时,printf接口仍是非实时的。

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

    1. 应用无需调用额外初始化,编译链接即可使用
    2. 预先分配打印内存池,无需每次通过glibc动态申请
    3. IPC使用共享内存,freelock(无锁)
    4. 引入线程特有数据,多线程安全,临界区无需锁保护

    以下内容仅做概要,不对源码逐行分析,若有兴趣可自行阅读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如何实现"无缝衔接,应用代码无需修改编译链接即可使用",这个已在之前的文章中解析,详见。

    4. 总结

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

    更多xenomai原理见本博客其他文章,关于更多PREEMPT-RT的原理和坑敬请关注本博客。