车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager
车载消息中间件FastDDS 源码解析(八)TimedEvent
车载消息中间件FastDDS 源码解析(十)发送第一条PDP消息(上)
FastDDS 源码解析(十二)发送第一条PDP消息(下)---异步发送
在FastDDS 源码解析(十一)发送第一条PDP消息(中),FastDDS 源码解析(十二)发送第一条PDP消息(下)---异步发送中介绍了通过flowcontroller发送消息
这一篇我们介绍一下通过datasharing_delivery发送跨进程消息
1.datashare简介
datashare就是数据共享,包括进程间数据共享和进程内数据共享。fastdds的进程间数据发送使用的是信号量和共享内存的方式,实现了zero-copy的方式。从我个人的角度看这块内容比较鸡肋,在大部分的使用场景上这块内容不会被用到。总体而言:fastdds 跨进程通信这块用处不多,耗费的资源较多,性价比不高。
1.1fastdds跨进程实现的简介
fastdds 跨进程的主要作用就是实现消息的共享。
面对的业务场景:
1在同一台机器上,如果有多个fastdds的进程,fastdds进程之间互相发送消息。只需要使用共享内存,那么不需要把消息的内容传递给其他进程,就能实现消息的发送,就是将消息放入共享内存中。两个进程能同时使用这块内存空间,实现了zero-copy。
2.在同一台机器上,如果有多个fastdds的进程,那么从其他机器上发送的消息,如果想要发送给同一机器上的多个fastdds进程,这多个fastdds进程只需要保存1份消息就可以了。也是通过共享内存实现的,实现了zero-copy。
本质上就是以时间换空间,以cpu的占用换取空间的减少。
zero-copy
我们来看一下fastdds 如何实现跨进程通信,以及如何通过共享内存实现zero-copy的。
zero-copy 就是不copy数据实现数据的传输。
严格意义上来讲 fastdds的所谓 zero-copy其实是一次内存copy,socket收到消息,之后需要将数据解析,然后copy到共享内存上,然后各个进程就能实现,所以是一次copy,但对于其他进程而言,就是zero-copy。
对于跨进程通信的 管道 消息队列 socket来说都需要2次内存copy。
信号量则一般不用于传输大数据量。
fastdds跨进程
fastdds跨进程消息通信是 共享内存+信号量结合的方式来实现的
如图所示:App A (进程a)申请一块共享内存,然后映射到本地内存,这块共享内存就是和App B(进程 B)共用的,可以往里面读写数据,这就涉及到一个问题,如何让 APP A 和 APP B进行进程间同步,信号量就完成进程间同步的工作,比如加锁,释放锁,通知等等就是由信号量完成的。
1.2什么是共享内存
从网上找了一张共享内存的大概示意图:
首先 由一方向OS提出申请,此时OS在内核中创建一段物理内存用于进程之间的通信,然后将该共享内存的物理地址通过页表 分别映射到两个进程的虚拟内存的共享区,此时两个进程就能经过虚拟内存再通过页表映射 进而找到共享内存,从而看到同一份资源,然后相互通信。
能够达到节省空间的目的。
1.3fastdds跨进程数据传输的优缺点
在嵌入式使用场景中,fastdds 传输的数据,大部分都是高频小数据量的数据,本身需要大量存储的情况比较少。 就是说能够节省的存储资源比较少。
通过前面的代码分析我们可以看到fastdds其实是个相对比较大型的架构(PDP,EDP,WLP等等),在同一台机器上布置多个节点,开销比较大。所以没有必要在同一台机器上布置多个fastdds,如果有需要的话,以一个fastdds进程接收数据,然后分发给多个其他进程。如果数据量比较大的话,可以再适量增加fastdds的节点。这样更能节省开销。
所以以我多年的代码经验来看,整个这块的设计是比较鸡肋的。但是这块内容是fastdds一个相对比较重要的功能,所以还是需要介绍一下。
我们看一下这张图:
fastdds 就是处于某种比较杂乱的状态,同一台设备上有好几个fastdds的应用程序,各个应用程序可以对外通信,这样即使是同一台设备两个应用程序时间也都要走fastdds的相关通信(跨进程通信)。每个应用都需要集成一遍fastdds,造成了资源的浪费。由于同一台设备上使用了datasharing,数据内存能够得到一定的节省,但是运行内存是增加的(都需要运行fastdds)。
我所设想的设计图:
我们看一下这张图:每个设备只有一个fastdds 节点作为对外节点,其他应用程序对外交流都通过这个对外节点交流,这样节省了资源,fastdds 不用在每个应用程序都部署。设备内的应用程序交互可以使用传统的进程间通信进行。节省了资源,比如对于app B来说就不用fastdds 的相关代码,对于App D ,APPE来说也不用相关代码了。
除非有非常大的数据请求,大部分的应用场景都能使用这样的架构来解决。
2.datashare源码解析
我们看一下fastdds 跨进程通信,共享内存的实现:
2.1接收端的实现
1.RTPSReader初始化的时候调用init函数,这个函数主要是初始化共享内存相关的代码
主要干了3件事
a.创建DataSharingNotification 见2
b.根据DataSharingNotification创建DataSharingListener 见5
c.DataSharingListener的start函数 见6
2.调用DataSharingNotification的create_notification函数创建DataSharingNotification,create_notification调用了create_and_init_notification来创建共享内存
3.create_and_init_notification根据传进来的guid 和 shared_dir 来调用create_and_init_shared_segment_notification创建共享内存,如果shared_dir为空创建共享内存,如果不为空创建共享文件
4.create_and_init_shared_segment_notification创建并初始化内存,这个其实分为2块,一块是共享的内存,一块是对共享内存的管理,这个是用信号量实现的,比如我们改变了内存,那么需要通过信号量来通知其他进程,内存改变了,可以来处理新的数据了
5.new了一个DataSharingListener,根据之前我们创建的DataSharingNotification,来创建DataSharingListener
6.调用DataSharingListener的start函数,这儿启动了一个监听线程,监听共享内存的变化
arduino
enum DataSharingKind : fastrtps::rtps::octet
{
/**
* Automatic configuration.
* DataSharing will be used if requirements are met.
*/
AUTO = 0x01,
/**
* Activate the use of DataSharing.
* Entity creation will fail if requirements for DataSharing are not met
*/
ON = 0x02,
/**
* Disable the use of DataSharing
*/
OFF = 0x03
};
每个Reader或者Writer 都可以配置DataSharingKind。
DataSharingKind分为3类:
on 就是使用数据共享
off 就是不使用数据共享
auto 就是如果当前的Topic被配置成数据共享,那么这个reader 或者writer 就可以使用数据共享,如果topic没有配置成数据共享,那么当前的reader或者writer就不能使用数据共享。
默认是off。就是默认是不开启数据共享的。
步骤1.RTPSReader中的init函数
c
void RTPSReader::init(
const std::shared_ptr<IPayloadPool>& payload_pool,
const std::shared_ptr<IChangePool>& change_pool,
const ReaderAttributes& att)
{
payload_pool_ = payload_pool;
change_pool_ = change_pool;
fixed_payload_size_ = 0;
if (mp_history->m_att.memoryPolicy == PREALLOCATED_MEMORY_MODE)
{
fixed_payload_size_ = mp_history->m_att.payloadMaxSize;
}
if (att.endpoint.data_sharing_configuration().kind() != OFF)
{
using std::placeholders::_1;
std::shared_ptr<DataSharingNotification> notification =
DataSharingNotification::create_notification(
getGuid(), att.endpoint.data_sharing_configuration().shm_directory());
if (notification)
{
is_datasharing_compatible_ = true;
datasharing_listener_.reset(new DataSharingListener(
notification,
att.endpoint.data_sharing_configuration().shm_directory(),
att.matched_writers_allocation,
this));
// We can start the listener here, as no writer can be matched already,
// so no notification will occur until the non-virtual instance is constructed.
// But we need to stop the listener in the non-virtual instance destructor.
datasharing_listener_->start();
}
}
}
主要干了3件事
1.创建DataSharingNotification
2.根据DataSharingNotification创建DataSharingListener
3.DataSharingListener的start函数
步骤2.创建notification
c
std::shared_ptr<DataSharingNotification> DataSharingNotification::create_notification(
const GUID_t& reader_guid,
const std::string& shared_dir)
{
std::shared_ptr<DataSharingNotification> notification = std::make_shared<DataSharingNotification>();
if (!notification->create_and_init_notification(reader_guid, shared_dir))
{
notification.reset();
}
return notification;
}
步骤3.调用了create_and_init_notification
php
bool DataSharingNotification::create_and_init_notification(
const GUID_t& reader_guid,
const std::string& shared_dir)
{
if (shared_dir.empty())
{
return create_and_init_shared_segment_notification<fastdds::rtps::SharedMemSegment>(reader_guid,
shared_dir);
}
else
{
return create_and_init_shared_segment_notification<fastdds::rtps::SharedFileSegment>(reader_guid,
shared_dir);
}
}
调用create_and_init_shared_segment_notification
这里面分为2种情况:共享内存和共享文件,一个将内存映射到各自进程,一个将文件映射到各自进程
根据配置选择不同的方式。
步骤4.create_and_init_shared_segment_notification
ini
template <typename T>
bool create_and_init_shared_segment_notification(
const GUID_t& reader_guid,
const std::string& shared_dir)
{
segment_id_ = reader_guid;
//生成共享内存的名字
segment_name_ = generate_segment_name(shared_dir, reader_guid);
std::unique_ptr<T> local_segment;
try
{
uint32_t per_allocation_extra_size = T::compute_per_allocation_extra_size(
alignof(Notification), DataSharingNotification::domain_name());
uint32_t segment_size = static_cast<uint32_t>(sizeof(Notification)) + per_allocation_extra_size;
//Open the segment
T::remove(segment_name_);
local_segment.reset(
new T(boost::interprocess::create_only,
segment_name_,
segment_size + T::EXTRA_SEGMENT_SIZE));
}
------
try
{
// Alloc and initialize the Node
notification_ = local_segment->get().template construct<Notification>("notification_node")();
notification_->new_data.store(false);
}
------
segment_ = std::move(local_segment);
owned_ = true;
return true;
}
创建并初始化内存
步骤5.创建DataSharingListener
c
DataSharingListener::DataSharingListener(
std::shared_ptr<DataSharingNotification> notification,
const std::string& datasharing_pools_directory,
ResourceLimitedContainerConfig limits,
RTPSReader* reader)
: notification_(notification)
, is_running_(false)
, reader_(reader)
, writer_pools_(limits)
, writer_pools_changed_(false)
, datasharing_pools_directory_(datasharing_pools_directory)
{
}
步骤6.start DataSharingListener
arduino
void DataSharingListener::start()
{
std::lock_guard<std::mutex> guard(mutex_);
// Check the thread
bool was_running = is_running_.exchange(true);
if (was_running)
{
return;
}
// Initialize the thread
listening_thread_ = new std::thread(&DataSharingListener::run, this);
}
start就是启动一个线程监听共享内存的变动,处理新数据
这是这个线程中运行的函数:
rust
void DataSharingListener::run()
{
//获取锁
std::unique_lock<Segment::mutex> lock(notification_->notification_->notification_mutex, std::defer_lock);
//如果is_running_是个标志变量,外部可以通过这个变量关闭这个线程。
while (is_running_.load())
{
lock.lock();
//查看一下有没有新数据过来
notification_->notification_->notification_cv.wait(lock, [&]
{
return !is_running_.load() || notification_->notification_->new_data.load();
});
lock.unlock();
if (!is_running_.load())
{
// Woke up because listener is stopped
return;
}
do
{
//处理新数据
process_new_data();
// If some writer added new data, there may be something to read.
// If there were matching/unmatching, we may not have finished our last loop
} while (is_running_.load() &&
(notification_->notification_->new_data.load() || writer_pools_changed_.load(std::memory_order_relaxed)));
}
}
通过启动一个线程来等待,这个线程不断循环等待。首先是监听信号量的变化,没有变化就等待,有变化,就调用process_new_data处理新数据
fastdds 使用了c++的boost来实现共享内存,底层使用了共享内存+信号量。就是共享内存存储数据,信号量来对这些数据进行管理。
这是处理数据的函数process_new_data
scss
void DataSharingListener::process_new_data ()
{
std::unique_lock<std::mutex> lock(mutex_);
// It is safe to 'forget' any change now
notification_->notification_->new_data.store(false);
// All places where this is set to true is locked by the same mutex, memory_order_relaxed is enough
writer_pools_changed_.store(false, std::memory_order_relaxed);
// Loop on the writers looking for data not read yet
// 从writer_pools_中找到writer,处理新数据
for (auto it = writer_pools_.begin(); it != writer_pools_.end(); ++it)
{
//First see if we have some liveliness asertion pending
bool liveliness_assertion_needed = false;
// liveliness 相关
uint32_t new_assertion_sequence = it->pool->last_liveliness_sequence();
if (it->last_assertion_sequence != new_assertion_sequence)
{
liveliness_assertion_needed = true;
it->last_assertion_sequence = new_assertion_sequence;
}
// Take the pool to free the lock
std::shared_ptr<ReaderPool> pool = it->pool;
lock.unlock();
if (liveliness_assertion_needed)
{
reader_->assert_writer_liveliness(pool->writer());
}
uint64_t last_payload = pool->end();
bool has_new_payload = true;
while (has_new_payload)
{
CacheChange_t ch;
SequenceNumber_t last_sequence = c_SequenceNumber_Unknown;
//从共享内存中获取CacheChange_t对象
pool->get_next_unread_payload(ch, last_sequence, last_payload);
has_new_payload = ch.sequenceNumber != c_SequenceNumber_Unknown;
if (has_new_payload && ch.sequenceNumber > SequenceNumber_t(0, 0))
{
//gap消息是 Writer发送给Reader,标识HistoryCache中的一些Change不再可用,也不会再发给Reader
//根据收到的消息的sequenceNumber,来确定是否有些消息 已经永久丢失,相当于模拟了一条gap消息出来,处理一些永久丢失的消息
if (last_sequence != c_SequenceNumber_Unknown && ch.sequenceNumber > last_sequence + 1)
{
······
reader_->processGapMsg(pool->writer(), last_sequence + 1, SequenceNumberSet_t(ch.sequenceNumber));
}
if (last_sequence == c_SequenceNumber_Unknown && ch.sequenceNumber > SequenceNumber_t(0, 1))
{
······
reader_->processGapMsg(pool->writer(), SequenceNumber_t(0, 1), SequenceNumberSet_t(
ch.sequenceNumber));
}
······
//这儿是处理这个新收到的消息
if (reader_->processDataMsg(&ch))
{
pool->release_payload(ch);
pool->advance_to_next_payload();
}
}
if (writer_pools_changed_.load(std::memory_order_relaxed))
{
// Break the while on the current writer (it may have been removed)
break;
}
}
// Lock again for the next loop
lock.lock();
if (writer_pools_changed_.load(std::memory_order_relaxed))
{
// Break the loop over the writers (itearators may have been invalidated)
break;
}
}
}
这是共享内存接收端的相关逻辑。
主要是如何处理消息:
1.根据消息的seqnumber 来模拟gap message(gap消息是 Writer发送给Reader,标识writer的HistoryCache中的一些Change不再可用,也不会再发给Reader)
2.调用reader的processDataMsg函数来处理消息
上面是共享内存接收端的相关逻辑
2.2发送端的逻辑
下面我们简单介绍一下发送端的相关逻辑:
在发送端,会申请一个共享内存和接收端的共享内存一一对应,两边是同一块内存。
2.2.1发送端申请共享内存
在发送端申请共享内存是在这两个地方:
1.是RTPSWriter初始化的时候,如果这个RTPSWriter 设置了data_sharing,就会为这个RTPSWriter申请共享内存
2.是在StatefulReader 和 StatelessReader 匹配到新的远端的RTPSWriter的时候,会为这个RTPSWriter申请一个共享内存
这是第一种情况RTPSWriter初始化的时候创建共享内存
c
void RTPSWriter::init(
const std::shared_ptr<IPayloadPool>& payload_pool,
const std::shared_ptr<IChangePool>& change_pool,
const WriterAttributes& att)
{
------
if (att.endpoint.data_sharing_configuration().kind() != OFF)
{
std::shared_ptr<WriterPool> pool = std::dynamic_pointer_cast<WriterPool>(payload_pool);
if (!pool || !pool->init_shared_memory(this, att.endpoint.data_sharing_configuration().shm_directory()))
{
------
}
}
------
}
这是第二种情况,匹配到远端RTPSWriter的时候,申请一个共享内存。
c
bool DataSharingListener::add_datasharing_writer(
const GUID_t& writer_guid,
bool is_volatile,
int32_t reader_history_max_samples)
{
std::lock_guard<std::mutex> lock(mutex_);
------
std::shared_ptr<ReaderPool> pool =
std::static_pointer_cast<ReaderPool>(DataSharingPayloadPool::get_reader_pool(is_volatile));
if (pool->init_shared_memory(writer_guid, datasharing_pools_directory_))
{
if (0 >= reader_history_max_samples ||
reader_history_max_samples >= static_cast<int32_t>(pool->history_size()))
{
------
}
writer_pools_.emplace_back(pool, pool->last_liveliness_sequence());
writer_pools_changed_.store(true);
return true;
}
return false;
}
那么在两种场景下可以用到数据共享
1.RTPSWriter配置成可以使用数据共享,那么RTPSWriter会申请一块共享内存,在RTPSWriter发送数据的时候,所有和RTPSWriter在一台物理设备上的reader可以直接使用这个共享内存,来获取消息。
2.当RTPSReader匹配到远端的RTPSWriter的时候会为这个Writer申请一块共享内存,当这个RTPSWriter收到消息的时候,会将消息存储到共享内存中,这样这台机器上的所有RTPSReader都可以直接使用这块内存中的消息。
2.22发送端的源码解析
回到发送端,这个就是在第十篇中介绍到的,在发送消息的时候,会用datasharing_delivery 和 FlowController来发送消息。
datasharing_delivery就是使用跨进程通信来发送消息,FlowController就是使用网络通信来发送消息。
我们看一下datasharing_delivery是如何操作的。
1.StatelessWriter(PDP消息都是以StatelessWriter发送的,StatefulWriter类似) 调用datasharing_delivery
主要干了2件事
a.调用WriterPool的add_to_shared_history 见步骤2
b.调用ReaderLocator的datasharing_notify 见步骤3
2.WriterPool的add_to_shared_history 就是将change放入到共享内存中
3.调用ReaderLocator的datasharing_notify,
如果reader 和 writer是同一进程,直接调用DataSharingListener::notify 见步骤4
否则调用DataSharingNotifier 的notify见步骤5
4.直接调用process_new_data,处理消息
5.DataSharingNotifier 的notify调用了DataSharingNotification 的notify函数,见步骤6
6.通过信号量通知reader,数据已经到达
步骤1:
rust
bool StatelessWriter::datasharing_delivery(
CacheChange_t* change)
{
auto pool = std::dynamic_pointer_cast<WriterPool>(payload_pool_);
assert(pool != nullptr);
pool->add_to_shared_history(change);
EPROSIMA_LOG_INFO(RTPS_WRITER, "Notifying readers of cache change with SN " << change->sequenceNumber);
for (std::unique_ptr<ReaderLocator>& reader : matched_datasharing_readers_)
{
if (!reader_data_filter_ || reader_data_filter_->is_relevant(*change, reader->remote_guid()))
{
reader->datasharing_notify();
}
}
return true;
}
主要干了2件事:
1.add_to_shared_history 也就是步骤2
2.datasharing_notify,对reader发送通知 也就是步骤3
matched_datasharing_readers_ 这个一开始是空,需要在pdp edp阶段,收到消息之后,与之匹配的reader的数据才会放入。
步骤2
scss
void add_to_shared_history(
const CacheChange_t* cache_change)
{
assert(cache_change);
assert(cache_change->serializedPayload.data);
assert(cache_change->payload_owner() == this);
assert(free_history_size_ > 0);
// Fill the payload metadata with the change info
PayloadNode* node = PayloadNode::get_from_data(cache_change->serializedPayload.data);
node->status(ALIVE);
node->data_length(cache_change->serializedPayload.length);
node->source_timestamp(cache_change->sourceTimestamp);
node->writer_GUID(cache_change->writerGUID);
node->instance_handle(cache_change->instanceHandle);
if (cache_change->write_params.related_sample_identity() != SampleIdentity::unknown())
{
node->related_sample_identity(cache_change->write_params.related_sample_identity());
}
// Set the sequence number last, it signals the data is ready
node->sequence_number(cache_change->sequenceNumber);
// Add it to the history
// 通过address获取一个handle
// 这个address可以理解为本地地址,共享内存地址在本地内存上的映射
// 这个handle 可以理解为共享内存的一个全局地址,那么其他进程通过这个handle可以将共享内存映射到本地内存上,就能使用数据了
// 这个history_是一个数组,用于存放handle
history_[static_cast<uint32_t>(descriptor_->notified_end)] = segment_->get_offset_from_address(node);
EPROSIMA_LOG_INFO(DATASHARING_PAYLOADPOOL, "Change added to shared history"
<< " with SN " << cache_change->sequenceNumber);
advance(descriptor_->notified_end);
--free_history_size_;
}
1.通过cache_change 找到PayloadNode
对PayloadNode参数进行初始化
2.segment_->get_offset_from_address(node) 获取一个handle,
3.history_ 是存放全局地址的一个数组
这块操作就是将change 放入到共享内存中
步骤3:
scss
void ReaderLocator::datasharing_notify()
{
RTPSReader* reader = nullptr;
//是否同进程
if (is_local_reader())
{
reader = local_reader();
}
if (reader)
{
reader->datasharing_listener()->notify(true);
}
else
{
datasharing_notifier()->notify();
}
}
同进程,直接调用DataSharingListener::notify 见步骤4
否则调用DataSharingNotifier 的notify见步骤5
步骤4:
scss
void DataSharingListener::notify(
bool same_thread)
{
if (same_thread)
{
process_new_data();
}
else
{
//DataSharingNotification
notification_->notify();
}
}
直接process_new_data,处理消息
步骤5 DataSharingNotifier 的notify:
scss
void notify() override
{
if (is_enabled())
{
EPROSIMA_LOG_INFO(RTPS_WRITER, "Notifying reader " << shared_notification_->reader());
shared_notification_->notify();
}
}
最终还是走到DataSharingNotification 的notify函数
步骤6:
arduino
DataSharingNotification
inline void notify()
{
std::unique_lock<Segment::mutex> lock(notification_->notification_mutex);
notification_->new_data.store(true);
lock.unlock();
notification_->notification_cv.notify_all();
}
这儿就是发送消息了,在步骤2中已经将数据存储到共享内存中了,在这里只是通过信号量通知reader,数据已经到达。
见2.1的步骤6,reader 中正有一个线程在监听信息,收到信息后,从共享内存中读取消息,处理消息。
2.3类图
1.每个Writer,有1个到多个存储Reader信息的ReaderLocator
2.如果Reader是能够共享内存的reader,则会初始化DataSharingNotifier
3.DataSharingNotifier 有个DataSharingNotification对象
4.如果这个ReaderLocator对应的reader是同一个Participant中的reader
那么ReaderLocator 有一个指针指向这个reader
5.RTPSReader 有一个DataSharingListerner
6.DataSharingListerner有一个DataSharingNotification对象
车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager
车载消息中间件FastDDS 源码解析(八)TimedEvent