【原创】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实时系统,分别是cobalt 和 mercury详见,mercury构建时,printf接口仍是非实时的。
实时应用POSIX接口库
libcobalt
提供的printf()
,完全解决了上节中的不足:- 应用无需调用额外初始化,编译链接即可使用
- 预先分配打印内存池,无需每次通过glibc动态申请
- IPC使用共享内存,freelock(无锁)
- 引入线程特有数据,多线程安全,临界区无需锁保护
以下内容仅做概要,不对源码逐行分析,若有兴趣可自行阅读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工作流程
实时线程
-
每个实时线程打印时,先从pool中分配printf buffer
-
成功分配后,将分配的buffer设置为线程特有存储数据
pthread_setspecific(buffer_key, buffer)
,此后该线程只操作这个buffer; -
若线程过多,预先分配的pool已无法分配,使用
malloc
增加一个printf buffer,放到全局队first_buffer
里,并设置为该线程特有存储数据,供后续每次打印输出使用。 -
将打印消息格式化到buffer的数据区
非实时线程
以一定周期从first_buffer遍历链表,处理每一个buffer中的entry_head,按顺序取出entry_head,按照entry_head指定目的进行IO输出。
到此上个实现中的不足全部解决,其中关于xenomai如何实现"无缝衔接,应用代码无需修改编译链接即可使用",这个已在之前的文章中解析,详见。
4. 总结
以上就是一个实时linux下开发实时应用程序,由一个普普通通的
printf()
引发的实时性能问题解决,可以看出不起眼的printf()
要做好远比我们想象的复杂,做底层就是这样,得坐冷板凳耐得住寂寞。几句话共勉:
"万丈高楼平地起,勿在浮沙筑高台"。
"或许做上层业务能快速出活,大家看得到,不用了解其内部的实现和对底层的依赖,美其名日“站在巨人的肩膀上”。效率提升了,但同时也导致我们对巨人的成长过程不闻不问。殊不知巨人倒下之后,我们将无所适从,就算巨人只是生个病(发生漏洞)带来的损失也不可估量"。更多xenomai原理见本博客其他文章,关于更多PREEMPT-RT的原理和坑敬请关注本博客。