FastDDS之DataSharing
目录
- 原理说明
- 限制条件
- 配置
- Data-Sharing delivery kind
- Data-sharing domain identifiers
- 最大domain identifiers数量
- 共享内存目录
- DataReader和DataWriter的history耦合
- DataAck
- 阻塞复用
本文详细记录Fast DDS中Data Sharing的实现原理和代码分析。
DataSharing的概念:FastDDS中在同一台机器内通过利用共享内存共享DataWriter和DataReader的history实现通信加速,这种方式避免了传输层中的任何开销,有效减少了数据在DataWriter和DataReader之间复制所带来的额外负担。
Fast DDS 使用DomainParticipant的 GuidPrefix_t
来识别运行在同一主机上的其他participant。如果两个participant的 GuidPrefix_t
的前四个字节相同,则认为它们运行在同一主机上。提供了 is_on_same_host_as()
API 来检查这一条件。
使用Data-sharing并不能阻止数据在应用层和DataWriter或DataReader之间的拷贝,要避免这种形式需要使用zero-copy通信方式(TODO:)
尽管Data Sharing使用了共享内存,但是与共享内存不同的是,共享内存是一种完全符合传输规范的方式,这意味着使用共享内存传输时,被传输的数据必须从DataWriter history 拷贝到传输层再从传输层拷贝到DataReader中。使用Data-Sharing可以避免这类拷贝操作。
原理说明
当DataWriter创建后,Fast DDS会预先分配一个位于共享内存映射文件,大小为 max_samples+extra_samples大小的样本数的pool,当publish数据时,DataWriter从这个pool中取出一个sample并将其添加到它的history中,并通知DataReader哪个pool中的sample包含了新数据。然后DataReader访问相同的共享内存映射文件,就可以拿到由DataWriter发布的数据了。
限制条件
只有当以下条件满足时该特性才可以使用:
- DataWriter和DataReader可以访问相同的共享内存
- Topic有一个有界限的 TopicDataType(也就是说该数据类型有一个固定的大小限制),其成员函数 is_bounded() 返回 true
- Topic没有keyed
- DataWriter 使用
PREALLOCATED_MEMORY_MODE
或PREALLOCATED_WITH_REALLOC_MEMORY_MODE
.
还有一个限制,对于DataReader的HistoryQos。使用Data-sharing机制,DataWriter的history被共享给DataReaders,这意味着DataReaders上有效的HistoryQos的depth最多等于DataWriter的HistoryQos的depth。为避免混淆,将DataReaders的 history depth设置为小于等于DataWriter的 history depth。
配置
使用 DataSharingQosPolicy 配置DataWriter和DataReader。配置内容有四种:
Data-Sharing delivery kind
有三种模式
- AUTO:默认值,如果DataWriter 和 DataReader符合上述的限制条件,data-sharing的传输机制会生效
- ON:类似AUTO,但是如果条件不满足,entity的创建就会失败
- OFF:不会启用Data-sharing
DataWriter和DataReader的兼容性:
《TODO https://fast-dds.docs.eprosima.com/en/latest/fastdds/transport/datasharing.html 6.5.3.1 图片》
Data-sharing domain identifiers
每个entity定义了一组identifiers用于标识它所属的domain(TODO:一组?)。只有当两个entity至少有一个共同的domain时它们之间才能够使用Data-sharing。
用户通过DataSharingQosPolicy来定义DataWriter或DataReader的domain。如果没有提供domain的identifiers,系统会自动创建一个,这种自动的data-sharing domain对于entity运行所在的机器将是唯一的。也就是说,所有在同一台机器上运行的entities,且用户没有配置特定用户的domains的情况下,都能够使用data-sharing传输(前提是满足其余的要求)。
在服务发现阶段,entities之间会交换它们的domain identifiers,并检查它们是否能够使用data-sharing来进行通信。
尽管data-sharing domain identifiers是一个64位的整数,但是用户自定义的identifiers被限制为16位的整数。这意味着系统内部可能处理更大的64位整数作为domain identifiers,然而用户在配置或指定domain identifiers时只能使用较小范围的16位整数。这样的设计可能是为了简化用户接口,同时也确保了domain identifiers的一致性和兼容性。
最大domain identifiers数量
在发现过程中,从远程实体预期接收到的域标识符的最大数量。如果远程实体定义(并发送)的域标识符数量超过了这个数值,那么发现过程将会失败。
默认情况下,标识符的数量是没有限制的。可以通过 max_domains() 函数来改变这个默认值。定义一个有限的数量允许在实体创建时预分配接收标识符列表所需的内存,从而避免之后的动态内存分配。需要注意的是,数值0意味着没有限制。
共享内存目录
如果指定了用户定义的目录用于共享内存文件,那么这个目录将被用来存储用于数据共享传输的共享内存映射文件。如果没有指定,则会使用当前系统配置的默认目录。
配置用户定义的目录在某些场景下可能是有用的:
- 选择一个启用了 Huge TLB(透明的大页)的文件系统来存放记忆映射文件。
- 允许挂载了相同容器的容器之间进行数据共享传输。
DataReader和DataWriter的history耦合
普通的transport传输,DataReader和DataWriter保持独立的history,每个都有自己的sample的拷贝。一旦sample通过传输被DataReader接收,DataWriter就可以自由地从其history中删除sample,而不影响DataReader。
使用data-sharing方式,DataReader直接访问由DataWriter创建的数据实例,这意味着DataReader和DataWriter的history中的samples都指向共享内存的同一对象。因此DataWriter和DataReader的history之间存在着很强的耦合。
这也意味着一种情况:如果DataWriter复用一个旧的sample发布新的数据,而不是创建一个新的sample,那么这个被更新的sample在共享内存中的内容旧变成了新的数据,DataReader就无法获取这个sample的原始数据,因为已经被覆盖了。
DataWriter如果从其history中移除一个sample,只要它没有被复用,那么DataReader仍然能够访问到这些旧的数据,但如果DataWriter使用同一sample发布了新数据,则旧数据会被新数据所替代,DataReader也就无法再访问到原先的数据了。
DataAck
使用data-sharing传输,sample 的ack在应用程序的DataReader第一次接收到sample时开始(通过 DataReader::read_next_sample()
, DataReader::take_next_sample()
,或其他方式)。一旦数据被应用程序接收,DataWriter就可以自由使用sample发布新的数据,DataReader会探测到当一个sample被重复使用时自动从它的history中移除。这也意味着如果尝试连续访问DataReader的相同的sample或许没有sample返回。
阻塞复用
使用KEEP_LAST_HISTORY_QOS
或者 BEST_EFFORT_RELIABILITY_QOS
配置,DataWriter会从它的history中移除samples并添加新的,即使是没有收到DataReader的ack。有一种情况:如果发送的频率大于接收处理的频率,这会导致在每个smaple被处理前被重复使用。
为了避免这种情况,位于预先分配的pool中的samples不会被复用,除非它们收到了ack,比如它们至少被应用程序处理过一次。如果在pool中没有可复用的sample,DataWriter端的writer操作会被阻塞指导有一个可复用的,或max_blocking_time
到达。
这种行为不会影响DataWriter的history。samples仍然会按照规则从history中删除,唯一受影响的是pool中samples的服用哦个。这意味着即使DataWrtier的history为空,但如果pool中的所有samples都没有被确认接收,writer操作仍然可能是被阻塞的。
可用通过使用extra_samples
来减少DataWriter在writer操作时被阻塞的可能性,这会使pool的分配的大小比history的大小多更多samples,从而使DataWriter有更多的机会获取free sample,同时DataReader仍然可以访问那些已经被从DataWriter的history中删除的sample。