ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

fastdds:传输层SHM和DATA-SHARING的区别

fastdds:传输层SHM和DATA-SHARING的区别 下图是fastdds官方的图,清晰地展示了dds支持的传输层:根据通信双方的相对位置(跨机器、同机器跨进程、同进程)的不同,选择合适的传输层,是通信中间件必须要考虑的事情。跨机器:udp、tcp跨机器通信,只能通过网络, fastdds支持UDP和TCP。同机器,跨进程:SHM、DATA-SHARING在同一个机器中,使用UDP和TCP进行通信,当然也是可以的。但是,从性能的角度来考虑,更推荐使用SHM,SHM和UDP/TCP相比有2点优势:1、减少系统调用次数共享内存创建完成之后,后边使用的时候直接是内存操作,而UCP/TCP每次发送或者接收的的时候,都要通过系统调用。每次系统调用,都会有上下文的保存和恢复的动作,性能不友好。2、支持超长消息UDP/TCP受协议栈的影响,可能会对消息进行分片和组装,1500。SHM也不完全是优点,也有自己的缺点,比如实现复杂:1、使用UDP/TCP来收发包,功能成熟,接口简单,拿来就用。使用SHM进行通信的话,很多功能需要自己实现,比如进程间的同步(通过信号量来实现),buffer的管理(segment)等。同进程:INTRA-PROCESS如果writer和reader在同一个进程,那么可以使用这种传输方式,writer线程会直接调用reader的回调函数,两者在同一个线程中。如下图所示,使用命令gdb --args ./delivery_mechanisms pubsub -m INTRA-PROCESS,并对函数PubSubApp::on_data_available设置断点,可以看到调用栈,使用INTRA-PROCESS的时候,subscriber的接收回调函数直接被发送线程调用,在同一个线程中。这是一种效率最高的方式,去掉了中间缓存的环节,减少了数据拷贝次数。在默认情况下,同进程的通信使用intra-process,同机器跨进程使用SHM,跨机器使用UDP。同机器的不同进程间通信可以使用SHM,也可以使用DATA-SHARING,那么两者有什么区别呢?本文使用fastdds中的例子delivery_mechanisms,这个例子可以测试不同的传输层。本文先介绍SHM和DATA-SHARING传输层的数据收发流程,最后总结两者之间的异同。1SHM收发流程简单来说,如果要看SHM的发送流程,那么可以以函数SharedMemTransport::send为中心进行梳理,通过gdb对这个函数设置断点,可以查看函数的调用栈,阅读函数的代码,可以看函数的发送操作;如果要看SHM的接收过程,可以通过函数SharedMemChannelResource::perform_listen_operation来梳理,对于TCP、UDP、SHM来说,三种传输层都实现了自己的函数perform_listen_operation,这个函数监听接收数据。上图中左边是TranportDescriptor,右边是Transport。用户在创建传输层的时候,并不需要直接创建一个Transport,而是构造一个传输层的描述符即可,fastdds内部根据描述符来创建传输层。这是典型的设计模式中的建造者模式,当构造一个对象时,如果需要传递较多的参数,并且参数之间还有一些依赖关系,需要对参数的关系进行检查,那么构造函数实现起来就会比较复杂,这个时候就可以使用建造者模式,在建造者中对参数进行检查,如果没有问题则直接构造对象,让构造函数只专注于对象的构造,至于参数的检查工作,放到构造器中来完成。参数检查与对象创建进行了解耦,设计模式的主要出发点和落脚点就是解耦,但是解耦也不是盲目地将代码进行拆分,而是有意义的拆分:满足模块之间的边界划分,提升代码的复用性,对修改关闭、对扩展开放等。上图中左图的SenderResource负责发送数据,右图的ChannelResource负责接收数据。SenderResource和ChannelResource都是ChannelResource,负责维护底层的通信channel,对于UDP、TCP来说,channel就是socket;对于SHM来说,channel就是共享内存文件。ChannelResource中保存的是收发数据的通道,Transport管理ChannelResource。UDP、TCP和SHM的类有平行的关系,它们都有自己的TransportDescriptor、Tranport、SenderResource、ChannelResource类。通过一个基类或者接口派生出3个传输层,体现了c++中多态的使用。Locator用来描述一个端点,其中kind包括LOCATOR_KIND_TCPv4、LOCATOR_KIND_UDPv4、 LOCATOR_KIND_TCPv6、LOCATOR_KIND_UDPv6、LOCATOR_KIND_SHM;port就是通信的端口,比如7400、7410这些;address是地址,在UDP、TCP中就是ip地址。只要知道要发送对象的Locator信息,那么数据就可以发送出去。Locator { int32_t kind; uint32_t port; std::string address; }1.1发送下图是对SharedMemTransport::send设置断点,看到的调用栈。发送流程:①用户调用DataWriter::write进行发送,所以dds提供的接口是write,并不是publish。在我们实际工作中,往往会对dds接口进行封装,封装成publish接口,publish更符合dds发布数据的语义,更直观,好理解。②在DataWriterHistory中,将要发送的数据封装到一个CacheChange_t中③在发送侧,dds和rtps之间的接口类是dds侧的DataWriterHistory和rtps侧的WriterHistory④rtps中的BaseWriter进行发送⑤RtpsParticipantImpl::sendSync进行发送,SenderResource属于Participant中的资源,在Participant中调用SenderResource的send函数,最终调用到SharedMemTransport的send然后我们再来看看SharedMemTransport::send函数内部的实现:bool SharedMemTransport::send( const std::vectorNetworkBuffer buffers, uint32_t total_bytes, fastdds::rtps::LocatorsIterator* destination_locators_begin, fastdds::rtps::LocatorsIterator* destination_locators_end, const std::chrono::steady_clock::time_point max_blocking_time_point) { ... fastdds::rtps::LocatorsIterator it = *destination_locators_begin; bool ret = true; std::shared_ptrSharedMemManager::Buffer shared_buffer; try { while (it != *destination_locators_end) { if (IsLocatorSupported(*it)) { if (shared_buffer == nullptr) { shared_buffer = copy_to_shared_buffer(buffers, total_bytes, max_blocking_time_point); } ret = send(shared_buffer, *it); ... } ++it; } } ... return ret; }①对于第一次循环,需要通过函数copy_to_shared_buffer将数据保存到共享内存中在函数copy_to_shared_buffer中打印shared_mem_segment_的信息,可以看到segment name是fastdds_aec0cadd732cebe2,这个就是发送数据要拷贝的共享内存。所以说,使用共享内存发送数据时,会进行一次数据拷贝。将数据拷贝到共享内存之后,就是通知接收者,通知接收者需要两个步骤来实现:①构造一个BufferDescrptor(简称BD),并将BD发送到对应的端口②唤醒监听者构造一个BufferDescriptor:唤醒监听者:1.2接收对Subscriber的接收回调设置断点,调用栈如下:fastdds SHM的writer和reader通过信号量进行唤醒。怎么去查看相关的代码,去确认通过哪个信号量进行唤醒的?(1)在gdb中对subscriber的数据接收函数设置断点,如下函数eprosima::fastdds::examples::delivery_mechanisms::SubscriberApp::on_data_available当该函数被调用时,通过info thread查看哪个这个函数在哪个进程中被调用。如下图所示,该函数在进程6中被调用。(2)停止publisher,让发布侧不再写数据,这个时候因为没有数据,subscriber侧收不到数据,那么一定是通过信号量在等待。此时再使用bt查看线程6的调用栈如下。使用boost库中的信号量boost::interprocess::interprocess_semaphore,这个信号量是基于共享内存的信号量。信号量属于一个Port。从调用栈可以看出来,只有创建接收通道的时候,才会创建信号量。接收侧通过这个信号量进行wait,发送侧唤醒的时候也是找到接收侧的信号量进行唤醒。所以,创建发送资源的时候不需要创建信号量。由上图红线可知,用于创建信号量的共享内存创建在了端口共享内存上。2DATA-SHARING收发流程DATA-SHARING不是一个标准的传输层,没有TransportDescriptor、Transport、SenderResource、ChannelResource这些资源。2.1发送delivery mechanisms这个例子,publish的时候,并不是构造了一个临时的变量发送出去的,而是通过load_sample首先从底层申请了一块内存,而这块内存就是从data sharing的writer pool中申请的,这样数据直接就是保存在这里的,payload pool就是从这里申请的,申请的是data sharing的writer pool,而writer pool就是基于共享内存创建的。所以说,在发送时,没有拷贝。std::shared_ptrIPayloadPool DataWriterImpl::get_payload_pool(){ if (!payload_pool_) { // Avoid calling the serialization size functors on PREALLOCATED mode fixed_payload_size_ = pool_config_.memory_policy == PREALLOCATED_MEMORY_MODE ?
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表