# 增强管道数据流转技术（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 <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的输入。

如：
```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 <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解调，并收听广播；另一方面，又想要进行频谱显示，如下：

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


