查看原文
其他

技术文档丨Cyber RT 进程间通信

Apollo CarOS团队 Apollo开发者社区 2022-07-29



共享内存是指 (shared memory)在多处理器的计算机系统中,可以被不同中央处理器(CPU)访问的大容量内存。由于多个CPU需要快速访问存储器,这样就要对存储器进行缓存(Cache)


在Linux中,每个进程都有属于自己的进程控制块(PCB)地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。


进程通信是指进程之间的信息交换。PV操作是低级通信方式(P操作和V操作,P表示申请,V表示释放)。高级通信方式是指以较高的效率传输大量数据的通信方式。高级通信方法主要有以下三类:


  1. 共享存储

    在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换。

    在对共享空间进行写/读操作时,需要使用同步互斥工具(如P操作,V操作),对共享空间的写/读进行控制。

  2. 消息传递系统

    在消息传递系统中,进程间的数据交换是以消息(Message)为单位的。程序员直接利用系统提供的一组通信命令(原语)来实现通信。

    操作系统隐藏了通信的实现细节,大大简化了通信程序编制的复杂性,因而获得了广泛的应用。在消息传递系统中,源进程可以直接或间接地将消息传送给目标进程。

  3. 管道通信系统

    管道通信是消息传递的一种特殊方式。所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。它是读写进程的一个特殊文件(外存,数据量大),允许按照先来先服务的方式传送数据,也能使进程同步执行。



以下,ENJOY





用户基于Cyber RT进行开发时,会将自己的模块抽象为一系列的Component。这些Component既可以加载到同一个进程内运行,也可以加载到不同进程中运行。


目前,同主机不同进程的Component间默认通过共享内存进行通信。相比于其他进程间通信方式,共享内存具备以下几个优点:


  1. 由多个进程共享,适合多进程读/写;

  2. 通信过程中无系统调用,减少了运行态切换的开销;

  3. 没有用户内存和内核内存之间的数据拷贝;




UNIX系统上有着各式各样的IPC工具。根据工具的功能特点,可以将它们分为三类:


UNIX系统上IPC的结构图


  • communication: 这类工具关注的是进程之间的数据传输;

  • signal: 信号既可以用于同步,还可以用于通信(传递signal number)。实时信号上还可以绑定数据。

  • synchronization: 这类工具关注的是进程之间操作的同步;


当然,不同工具的适用场景有所不同。比如pipe只能用于同一台机器上的进程间通信,而socket却可以用于网络通信。


对于communication工具而言,我们可以将其分为两类:


  • data transfer: 这类工具一般需要在用户空间和内核空间之间进行两次数据传输。以pipe为例,ProcessA在写入数据的时候,需要将数据从用户空间拷贝至内核空间;ProcessB在读取数据时,需要将数据从内核空间拷贝至用户空间。


Process的数据传输


  • shared memory: 共享内存允许进程通过将数据放到进程间共享的一块内存中来完成信息交换。在通信过程中,一方面不需要系统调用,另一方面也没有用户空间和内核空间之间的数据传输。因此共享内存通信速度比其他data transfer工具要快。




自动驾驶系统中,一些传感器的消息较大,例如点云,图像等。另外,录制全量数据时,消息总的频率高(2000Hz~3000Hz),传输量大(约7GBytes/s)。因此,Cyber RT采取共享内存来进行进程间通信。


Cyber RT进程间通信主要有以下三个步骤:


  1. 消息写入:发送方将消息写入共享内存区域;

  2. 同步:发送方通知接收方有新消息可读;

  3. 消息读取:接收方从共享内存区域读取消息;


下面介绍Cyber RT共享内存区域和通知机制的设计。



Cyber RT中所有消息都有对应的channel来标识,比如感知模块输出的障碍物消息的channel为"/apollo/perception/obstacles"。一个channel可能有多个writer和多个reader。为了减少内存拷贝,每个channel只有唯一的一块共享内存区域(Segment)。该channel的所有writer和reader都通过这块区域进行消息传输。


Segment在内存中的布局如下:


内存中Segment的布局


  • state: 描述了当前Segment的状态,包含已经写入的消息数量等信息;

  • block: 用于标记block buffer的读写状态。block的数量是由该Segment传输消息的大小决定的。比如消息小于16KBytes时,block数量为512。

  • block buffer: 用于存放序列化后的消息,数量与block一致。


Segment对外的主要接口如下:

1// struct WritableBlock {
2//   uint32_t index = 0;
3//   Block* block = nullptr;
4//   uint8_t* buf = nullptr;
5// };
6// using ReadableBlock = WritableBlock;
7
8class Segment {
9public:
10...
11bool AcquireBlockToWrite(std::size_t msg_size, WritableBlock* writable_block);
12void ReleaseWrittenBlock(const WritableBlock& writable_block);
13
14bool AcquireBlockToRead(ReadableBlock* readable_block);
15void ReleaseReadBlock(const ReadableBlock& readable_block);
16...
17};


Acquire与Release应当成对出现。



发送方将消息写入到共享内存区域后,需要通知接收方有新消息可读。为了减少线程使用,Cyber RT采取了单线程监听。有一个专门的Dispatcher来执行监听操作,同一进程内的所有接收者都委托这个Dispatcher来进行监听。


在具体实现上,经典的做法是采用mutex+condition_variable。当没有新消息可读时,Dispatcher在condition_variable上执行wait操作。发送方写入消息后,执行notify操作。然而,实际测试发现notify有一定的抖动。因此,最终还是采取了轮询的方式实现。


另外,Cyber RT还实现基于UDP组播的通知机制。目前,默认采取UDP组播来通知。


开发者可以通过修改cyber/conf/cyber.pb.conf中transport_conf的notifier_type来更改通知机制。


1transport_conf {
2shm_conf {
3    # "multicast" "condition"
4    notifier_type: "multicast"
5    shm_locator {
6        ip: "239.255.0.100"
7        port: 8888
8    }
9}
10participant_attr {
11    lease_duration: 12
12    announcement_period: 3
13    domain_id_gain: 200
14    port_base: 10000
15}
16communication_mode {
17    same_proc: INTRA
18    diff_proc: SHM
19    diff_host: RTPS
20}
21resource_limit {
22    max_history_depth: 1000
23}
24}



消息从ShmTransmitter发出,到ShmReceiver收到的完整过程如下图所示:


SHM Transport通信过程


  1. ShmTransmitter:消息发送方。

  2. ShmReceiver:消息接收方。

  3. Segment:共享内存区域的抽象,内部包含一个循环队列(一系列blocks),每个block均可进行读写操作。

  4. Notifier:用于发送/监听共享内存可读通知。

  5. ShmDispatcher:代替ShmReceiver进行共享内存监听和数据读取。




Cyber RT与Apollo ROS的进程间通信都是使用的共享内存。相比于Apollo ROS,Cyber RT主要做了以下两个方面的改进。



之前消息传输过程中有4次数据拷贝:


  1. serialize message to a string;

  2. copy data of string to shared memory area;

  3. copy data of shared memory area to a string;

  4. deserialize message from string;


改进后,去除了中间变量string的使用,只保留必要的2次数据拷贝。



Cyber RT实现了一系列高性能的基础数据结构,如ReadWriteLock, AtomicHashMap等。


在通信相关的代码实现中,大量使用了上述基础数据结构,从而进一步提升了性能。

 

对比发现,Cyber RT进程间传输性能优于Apollo ROS。


Cyber RT与Apollo ROS传输性能比较




本文首先介绍了常见的IPC工具,然后结合自动驾驶的实际场景,解释了使用共享内存进行进程间通信的原因。之后,围绕通信的过程,介绍了共享内存区域和通知机制的设计。最后,对比了Cyber RT和Apollo ROS的进程间传输性能。


下一篇我们将介绍Cyber RT中节点间的互相发现机制。






您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存