从前向数据复制(FDR)到增强管道数据流转(EPDR)-taskBus的前世今生

发布于:2022-11-02 ⋅ 阅读:(442) ⋅ 点赞:(0)

增强管道数据流转技术(Enhanced Pipeline Data Routing,EPDR)是 taskBus 跨平台多进程合作框架创立的开源数据分发技术,在软件无线电方向已经具有了较为完整的应用场景。应一些玩家要求,介绍一下这个技术的起源,简述从最初的前向数据复制(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”这样的函数手工打开的,进程一旦启动,其自然存在。因此,可以像读写一般文件一样,读写这三个文件:

#include <stdio.h>
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的输入。

如:

$  ls -na | grep "Sep" | less

则是把列出文件(ls)的结果送给 grep程序,进行字符串“ Sep ” 的搜索匹配,以只列出九月份的文件。而后,如果九月份的文件很多,则进行分屏显示(less)。这里要注意的是一般只能把 stdout 送给 stdin,stderr一直都连着默认的设备。在命令行下,“|” 是可以传输二进制数据的。很多开源的工具借助这种方式完成复杂的任务。如RTL-SDR的广播收听:

$ rtl_fm -f 91.8e6 -s 200000 -r 48000 - | ffplay -f s16le -ar 48000  -showmode 1 -i -

但是要注意的是,在windows下自己写程序时,由于要避免\r\n替换、EOF转义等问题,要指定二进制模式, 见如下代码:

#include <io.h>
#include <fcntl.h>
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解调,并收听广播;另一方面,又想要进行频谱显示,如下:

在这里插入图片描述
由于"|"链接的管道是顺序的、单向的,因此上图中的两路处理必须合成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贴近内核,但是它的带宽也是有限制的。具体的测试参考这个测试,引用结论见下表。
  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三个指针。因此,若管理者进程启动了一堆子进程,则可以用一个矩阵来交换他们的数据。
生产消费
上表对应的流转图如下:

进程1
进程2
进程3
进程4
进程5

2.2 taskBus的命名

当我们着手开始把EPDR的想法实现为平台时,遇到了命名的问题。如何命名这个数据交换总线呢?就叫EPDR Platform如何?

团队里的成员都觉得不好。EPDR比较拗口,还是要找一个简明的词。由于我们几个好基友在入门计算机时,接触的第一个DOS命令行操作的编程是在Turbo-BASIC 1.0下完成的, 而第一个管道操作是在Turbo C2.0下完成的,于是自然就想用一个缩写为 TB的词,如Turbo Bus。

Turbo Badic
但是,擅用专属于Borland大神的Turbo这个名讳,显然是对Borland的历史有所冲撞。考虑到计算机里task是与“进程”比较贴切的词语,很自然的想到任务管理器等计算机词汇,便用了 taskBus,任务总线,来命名未来的产品。如此这般,取首字母还是TB,真是亲切。更为讨巧的是,词组 Taskbus Platform的缩写是TP,与另一个Borland的名著 Turbo-Pascal 一致,再次致敬,缅怀Borland。

2.3 taskBus的LOGO由来

taskBus的LOGO是一个着了火的兔子。这个Logo是由团队成员的孩子引出的。当吃着一盒巴士饼干,只认得Bus而不识Task的孩子,把白板上taskBus的字母读成了兔巴士,taskBus的中文翻译就定了。同时,也取兔子敏捷、巴士装得多的意义。

而后,孩子们手绘了兔子巴士的样子,并认为应该用纸糊一个。此外,为了突出兔子巴士开的快,就让它的钛合金表面被大气层摩擦出火花来,就有了下图。
taskBus Logo

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语言来重构交换逻辑,性能会好的多。