# 增强管道数据流转技术(EPDR) 增强管道数据流转技术(Enhance Pipeline Data Routing,EPDR)是 [taskBus 跨平台多进程合作框架](https://blog.csdn.net/goldenhawking/article/details/84191631)创立的开源数据分发技术,在软件无线电方向已经具有了较为完整的应用场景。本文介绍技术的起源,简述从最初的前向数据复制(Forward Data Replication,FDR)发展到EPDR的过程。 # 1. 前向数据复制(FDR)技术 FDR技术通过定义一些简单的标记格式,允许通过 A.exe|B.exe|C.exe级联三个可执行文件时,C.exe也能直接获得A.exe的输出(靠B来复制)。 ## 1.1 标准输入输出设备与重定向 “标准输入输出设备”是在DOS时代就非常常用的系统文件设备。执行一个DOS/Bash命令行时,命令进程内通常会自动打开三个标准文件句柄(指针),即标准输入文件(stdin),通常对应终端的键盘;标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。这三个文件是不需要通过“file.open”这样的函数手工打开的,进程一旦启动,其自然存在。因此,可以像读写一般文件一样,读写这三个文件: ```c #include int main(int argc, char *argv[]) { unsigned int a[4]; //scanf("%u,%u,%u,%u",a,a+1,a+2,a+3); //等效为 fscanf(stdin,"%u,%u,%u,%u",a,a+1,a+2,a+3); for(int i = 0; i < 4; ++i) a[i] = 0xFFFFFFFF ^ a[i]; //printf("%u,%u,%u,%u\n",a[0],a[1],a[2],a[3]); //等效为 fprintf(stdout,"%u,%u,%u,%u\n",a[0],a[1],a[2],a[3]); fprintf(stderr,"%u,%u,%u,%u\n",a[0],a[1],a[2],a[3]); return 0; } ``` 这种连接着输入输出设备的文件句柄,是一种特殊的文件,管道(pipeline)。管道也是一类流式设备。经常接触的另一种流式的设备是套接字的Stream模式,对应的是TCP协议。管道则是单机版本的底层的流式数据,在层级上和TCP完全不同。在默认状态,如bash交互时,stdin连着键盘缓存,stdout\stderr连着显示缓存。 操作系统允许重定向这些管道。通过符号“<”、">"、"|"可以进行重定向。其中, - "A.exe < 1.txt" 是从某个文件里读取内容,丢给当前程序。 - "B.exe > 2.txt" 是把输出定向到文件,写入到磁盘。 - "A.exe| B.exe" 则是把A进程的输出(stdout)交给下一个进程B的输入。 如: ```bash $ ls -na | grep "Sep" | less ``` 则是把列出文件(ls)的结果送给 grep程序,进行字符串“ Sep ” 的搜索匹配,以只列出九月份的文件。而后,如果九月份的文件很多,则进行分屏显示(less)。这里要注意的是一般只能把 stdout 送给 stdin,stderr一直都连着默认的设备。在命令行下,“|” 是可以传输二进制数据的。很多开源的工具借助这种方式完成复杂的任务。如RTL-SDR的广播收听: ```bash $ rtl_fm -f 91.8e6 -s 200000 -r 48000 - | ffplay -f s16le -ar 48000 -showmode 1 -i - ``` 但是要注意的是,在windows下自己写程序时,由于要避免\r\n替换、EOF转义等问题,要指定二进制模式, 见如下代码: ```c #include #include int main(int argc,char * argv) { //用于在windows下吞吐二进制数据。 setmode(fileno(stdout), O_BINARY); setmode(fileno(stdin), O_BINARY); unsigned int a[4]; fread(a,sizeof(int),4,stdin); for(int i = 0; i < 4; ++i) a[i] = 0xFFFFFFFF ^ a[i]; fwrite(a,sizeof(int),4,stdout); return 0; } ``` ## 1.2 前向数据复制(FDR)技术 在早期进行控制台SDR程序设计时,遇到一种情况需要进行不同层级的数据访问。比如,SDR获取的数据,一方面要进行fm解调,并收听广播;另一方面,又想要进行频谱显示,如下: ![FDR](images/FDR.png) 由于"|"链接的管道是顺序的、单向的,因此上图中的两路处理必须合成1路,其中前序的某些EXE要为后续处理流程转发数据。 采用的思路是统一各个exe的输入输出格式,给原本无边界的流,切割为包。而后,标记包的ID,并复制到后一级。为了有效的完成这类请求,流式数据被切割为包,并加入头部: |包序号|数据类型ID|长度|内容| |--|--|--|--| |1|2字节|2字节|N字节| |2|2字节|2字节|N字节| |3|2字节|2字节|N字节 ||……|||| - 程序都会解析所有的stdin输入。 - 被解析的输入包会立刻丢往stdout。 - 程序产生的数据也要封装后,送往 stdout。 - 程序需要自行确保包的原子性。 ## 1.3 FDR的问题 FDR有几个问题: 1. **流量浪费**。所有前序的流量会在下一级叠加,导致巨大的浪费。要知道尽管管道比起TCP贴近内核,但是它的带宽也是有限制的。具体的测试参考[这个测试](https://goldenhawking.blog.csdn.net/article/details/124575558),引用结论见下表。 2. **无法闭环**。整个数据流程是前序的,没有环结构。也就是进程A无法消费C的请求。这样一来,很多需要反馈的结构就很难做了。如控制吞吐节奏的水位操作。 管道虽然是内存设备,但是流量也是有上限的. 基于教研室的 i7 6700K @ 4GHz, 内存 64GB DDR4 @ 2666MHz. Linux发行版为 Manjaro,内核5.10, 进行的最大流量测试结果如下表: |操作系统|编译器|级联层数|整体速率|sc16单路带宽| |--|--|--|--|--| |windows 10 x64|Mingw64|1|7016.842 MB/s|1754.21 MHz| |windows 10 x64|Mingw64|2|3621.510 MB/s|905.37 MHz| |windows 10 x64|Mingw64|3|2419.891 MB/s|604.97 MHz| |windows 10 x64|Mingw64|4|1813.055 MB/s|453.26 MHz| |windows 10 x64|vc2022|1|6843.942 MB/s|1710.98 MHz| |windows 10 x64|vc2022|2|3567.249 MB/s|891.81 MHz| |windows 10 x64|vc2022|3|2429.300 MB/s|607.325 MHz| |windows 10 x64|vc2022|4|1899.506 MB/s|474.87 MHz| |Linux x64|gcc|1|4046.54 MB/s|1011.63 MHz| |Linux x64|gcc|2|2122.48 MB/s|530.62 MHz| |Linux x64|gcc|3|2126.54 MB/s|531.64 MHz| |Linux x64|gcc|4|1985.11 MB/s|496.28 MHz| 因此,如果一味的进行前向复制,很容易塞满系统的IO带宽。 # 2. 增强管道数据流转(EPDR)技术 FDR有不足,它的主要限制是BASH和DOS命令行的语法决定的。这种语法是前向(无环)、单列的。于是,自然而然会想到要自己做一个增强的Shell出来,要更加灵活地流转数据。 参考分布式计算和消息队列的概念,把数据分为专题(Subject、Topic之类)、并由一个专门的程序管理他们之间的消费关系。这个程序应是实现EPDR 的关键,它构造了一个总线,或者是交换Hub,用来流转各个进程的包数据。 ## 2.1 EPDR 交换矩阵 无论是在windows还是在Linux下,一个父进程启动了某个子进程,便可以调用API获取它的stdin, stdout, stderr三个指针。因此,若管理者进程启动了一堆子进程,则可以用一个矩阵来交换他们的数据。 ![生产消费](images/Matrix.png) 上表对应的流转图如下: ![生产消费](images/flow.png) ## 2.2 taskBus的命名 当我们着手开始把EPDR的想法实现为平台时,遇到了命名的问题。如何命名这个数据交换总线呢?就叫EPDR Platform如何? 团队里的成员都觉得不好。EPDR比较拗口,还是要找一个简明的词。由于我们几个好基友在入门计算机时,接触的第一个DOS命令行操作的编程是在Turbo-BASIC 1.0下完成的, 而第一个管道操作是在Turbo C2.0下完成的,于是自然就想用一个缩写为 TB的词,如Turbo Bus。但是,擅用专属于Borland大神的Turbo这个名讳,显然是对Borland的历史有所冲撞。考虑到计算机里task是与“进程”比较贴切的词语,很自然的想到任务管理器等计算机词汇,便用了 taskBus,任务总线,来命名未来的产品。如此这般,取首字母还是TB,真是亲切。 ## 2.3 taskBus的LOGO由来 taskBus的LOGO是一个着了火的兔子。这个Logo是由团队成员的孩子引出的。当吃着一盒巴士饼干,只认得Bus而不识Task的孩子,把白板上taskBus的字母读成了兔巴士,taskBus的中文翻译就定了。同时,也取兔子敏捷、巴士装得多的意义。 而后,孩子们手绘了兔子巴士的样子,并认为应该用纸糊一个。此外,为了突出兔子巴士开的快,就让它的钛合金表面被大气层摩擦出火花来,就有了下图。 ![taskBus Logo](images/logo.png) # 3. 优势和不足 基于管道的吞吐,具有很多优势: 1. **编程简单**。在跨进程通信中,直接使用stdin, 要比其他技术的代码量小得多。无论是共享内存、第三方消息队列,还是DCOM、Socket、DBus,都不如 fread来的直接、简单。 2. **平台无关**。无论在Arm,PC,还是MIPS上,标准输入输出管道都是一致的。 3. **工具链低耦合**。主流的开发语言都支持标准输入输出管道,它是操作系统提供的底层特性,是语言无关的。同时,不依赖任何MQ中间件,开发程序时,无需配置链接特殊的库。 4. **传输友好**。经过平台的处理,结合管道本身的流式可靠性,进程间的传输是**按包传输、无需连接、无损保序**的,这种特性如果用网络来处理,UDP/TCP都无法兼顾,只有考虑上更高级的消息队列。但消息队列会带来工具链依赖。 5. **离线调试**。可以通过文件来进行调试。把录制的文件作为输入,直接用"<"重定向给这个模块,就能脱离平台在IDE里调试了。 但与集约化的平台(如 GNURadio, Pothos FLow)相比,在某些方面有显著的不足。 1. **界面零碎**。用不同语言开发的进程,会有各自的窗口。风格也不统一。 2. **控制复杂**。A进程要精确控制B进程的行为,比如设置音量、更新配置,需要额外的协议定义。比如taskBus的指令系统。这种协议会稍微增加复杂度。 3. **平台即瓶颈**。通过矩阵交换的数据流转,在平台上是资源密集的调度活动。在数据速率很高的情况下,CPU会吃紧。目前通过Qt的实现,吞吐率不是最优的。后续如果使用无动态内存分配的C语言来重构交换逻辑,性能会好的多。