目录
transport层负责为DDS用户数据收发和服务发现提供通信。本文以Fast DDS 2.14版本介绍共享内存。
使用DomainParticipant的GuidPrefix_t
来判断对端是否在同一个主机运行。同一主机中两个participants的GuidPrefix_t
的前四个字节是相同的。可以通过API is_on_same_host_as()
来检查这一条件。
使用共享内存的原因如下:
- 支持大消息,网络协议需要对数据进行分片,以符合特定协议和网络栈的要求,这会增加通信开销。共享内存传输允许复制完整的消息,唯一的大小限制是机器的内存容量。
- 减少内存拷贝:发送相同数据到不同端点时,SHM只需要给所有目的端点共享同一内存缓存,其他协议只需要为每个端点执行一次消息拷贝
- 减少操作系统负载:初始化设置完成之后,相比较网络通信,共享内存的系统调用更少,因此,使用SHM可以在性能和时间消耗上获得提升。
数据类型和概念
Segment
Segment 是一块共享内存,不同的进程都可以访问。 每个DomainParticipant都会配置和创建一个共享内存用于读写。注意:以更高权限的用户(例如,root)启动任何进程都可能导致通信问题,因为由非特权用户运行的进程可能无法将数据写入该内存段。每个segment都有一个16位的UUID唯一标识这一块内存,这个segment ID用于识别和访问每个DomainParticipant的段。Segment ID在构造SharedMemManager::Segment
时通过调用generate_segment_id_and_name
来生成:
void generate_segment_id_and_name(const std::string& domain_name) {
static constexpr uint32_t MAX_COLLISIONS = 16;
uint32_t collisions_count = MAX_COLLISIONS;
do {
// No collisions are most probable
segment_id_.generate();
...
}
UUID具体的生成算法:
void generate(
uint8_t* uuid,
size_t len)
{
uint64_t now = std::chrono::high_resolution_clock::now().time_since_epoch().count();
{
// Two IDs cannot be generated in this process at the same exact time
std::lock_guard<std::mutex> lock(gen_mutex_);
while (now == last_gen_time_)
{
std::this_thread::sleep_for(std::chrono::microseconds(1));
now = std::chrono::high_resolution_clock::now().time_since_epoch().count();
}
last_gen_time_ = now;
}
// Mersenne twister generator
std::mt19937 gen;
// The seed is derived from the ProcessID and the steady clock
std::array<uint32_t, 4> seq;
uint64_t pid = this_process_pid();
seq[0] = static_cast<uint32_t>(pid);
seq[1] = static_cast<uint32_t>(pid >> 32);
seq[2] = static_cast<uint32_t>(now);
seq[3] = static_cast<uint32_t>(now >> 32);
std::seed_seq seed_seq(seq.begin(), seq.end());
gen.seed(seed_seq);
std::uniform_int_distribution<> dis(0, 255);
for (size_t i = 0; i < len; i++)
{
uuid[i] = static_cast<uint8_t>(dis(gen));
}
}
Segment Buffer
Segment Buffer是在Segment中分配的具体的内存,用于存放DDS消息。Domainparticipant写入segment的每条消息都会放到不同的buffer中。对应到代码中就是SharedMemBuffer对象。每次发送时都会构造一个SharedMemBuffer对象,用于管理这条消息的内存
std::shared_ptr<Buffer> alloc_buffer(
uint32_t size,
const std::chrono::steady_clock::time_point& max_blocking_time_point)
{
...
try
{
buffer_node = pop_free_node();
data = segment_->get().allocate(size);
free_bytes_ -= size;
buffer_node->data_offset = segment_->get_offset_from_address(data);
buffer_node->data_size = size;
auto validity_id = buffer_node->status.load(std::memory_order_relaxed).validity_id;
new_buffer = std::make_shared<SharedMemBuffer>(segment_, segment_id_, buffer_node,
static_cast<uint32_t>(validity_id));
...
《TODO 插图》
Buffer Descriptor
指向特定segment中的一个segment buffer,包含了segment id和segment buffer中基于segment base的偏移。当通过共享内存方式在不同的domainparticipant之间通信时,只会分发这个Buffer Descriptor,而不是复制消息本身,共享内存的通信值发送Buffer Descriptor,避免的消息体的拷贝,接收方的domainparticipant可以通过Buffer Descriptor访问共享内存缓冲区中的消息,因为该描述符唯一的标识了segment ID和偏移量。代码中对应SharedMemGlobal::BufferDescriptor
这个类,定义如下:
struct BufferDescriptor
{
BufferDescriptor()
: buffer_node_offset(0)
, validity_id(0)
{
}
BufferDescriptor(
const SharedMemSegment::Id& segment_id,
SharedMemSegment::Offset offset,
uint32_t validity)
: source_segment_id(segment_id)
, buffer_node_offset(offset)
, validity_id(validity)
{
}
SharedMemSegment::Id source_segment_id;
SharedMemSegment::Offset buffer_node_offset;
uint32_t validity_id;
};
在讲消息写入共享内存后,会构造出一个SharedMemGlobal::BufferDescriptor
对象存入port的共享内存,然后通知对端通过读port中的SharedMemGlobal::BufferDescriptor
进而再读取Segment Buffer中的数据,具体可以参考下面的数据发送部分。
Port
Port是另一块共享内存,用于收发Buffer Descriptor的通道。是一个环形buffer,任意domainparticipant都可以读写。有一个唯一的32位id标识Port,这个id在服务发现阶段会被分享给远端。每个DomainParticipant都会配置和创建一个port用于接收Buffer Descriptors,每个DomainParticipant会为接收Port创建一个Listener,所以当推送一个新的Buffer Descriptor到port的时候会收到通知。
《TODO 插图》
Port Health Check
每次DomainParticipant读或写一个Port的时候会执行Health Check检查它的正确性。这是因为有可能进程正在使用Port的时候crash了,此时Port是无效的。如果attach的Listener在给定的超时时间内没有响应,则认为该端口已经损坏,将会被销毁并重新创建。
SharedMemTransportDescriptor
除了从TransportDescriptorInterface中继承的字段外,SharedMemTransportDescriptor还包含以下字段:
Member | Data type | Default | Accessor / Mutator | Description |
---|---|---|---|---|
segment_size_ | uint32_t | 512*1024 | segment_size() | Size of the shared memory segment |
port_queue_capacity_ | uint32_t | 512 | port_queue_capacity() | The size of the listening port |
healthy_check_timeout_ms_ | uint32_t | 1000 | healthy_check_timeout_ms() | Timeout for the health check of ports |
rtps_dump_file_ | string | “” | rtps_dump_file() | Full path of the protocol dump file. |
default_reception_threads | ThreadSettings | default_reception_threads | Default ThreadSettings for the reception threads. | |
reception_threads | std::map<uint32_t, ThreadSettings> | reception_threads | ThreadSettings for the reception threads on specific ports. | |
dump_thread() | ThreadSettings |