C++高性能通信:了解Iceoryx与零拷贝技术的实现与应用

0. 引言

Iceoryx是一个开源的实时通信框架,特别适用于需要高性能和低延迟的嵌入式系统,如自动驾驶系统、机器人控制、航空航天等。

详细介绍请看官网

img

iceoryx 使用真正的零拷贝共享内存方法,允许将数据从发布者传输到订阅者而无需任何副本。这可确保数据传输具有恒定的延迟,无论有效负载的大小是多少。

img
关于进程间通信,Iceoryx与Nanomsg的对比请看从 Nanomsg 到 Iceoryx: 发布-订阅模式的性能对比

1. Iceoryx使用到的零拷贝技术

1.1 零拷贝技术概述

零拷贝是指在数据传输过程中,避免不必要的数据拷贝操作。传统的数据传输通常涉及将数据从一个缓冲区复制到另一个缓冲区,这会产生额外的开销。而零拷贝技术允许数据在不进行拷贝的情况下直接传递到目标缓冲区,从而提高传输效率。

Iceoryx使用共享内存进行进程间通信,将数据放置在共享内存区域,然后通过指针引用实现数据传递,避免了数据的额外复制。

1.2 零拷贝的优势

  1. 减少CPU开销:零拷贝减少了CPU的复制操作,提高了系统性能。
  2. 降低内存占用:由于数据不需要在不同缓冲区之间复制,内存使用更为高效。
  3. 降低传输延迟:数据直接传递,无需复制等待时间。

1.3 Iceoryx零拷贝的实现

Iceoryx通过以下方式实现真正的零拷贝:

  • 利用共享内存技术,预先开辟内存块(chunk),publisher将数据写入。
  • Subscriber通过指针获取chunk中的信息,数据被写入时,subscriber收到一个指针。
  • Iceoryx维护每个chunk的引用记录,确保资源不被浪费。

1.4 信息轮询与信号触发

为了提升数据获取效率,Iceoryx提供两种方式:

  • WaitSet:采用react设计模式,绑定对应的subscribers,数据到来时触发通知。
  • Listener:直接触发用户定制的callback,数据到来时调用回调函数。

2. Iceoryx的核心概念

下图是Iceoryx的核心架构内容
在这里插入图片描述

2.1 RouDi (iox-roudi)

2.1.1 定义

RouDi是Iceoryx的中间件守护进程(daemon),负责管理和协调不同应用之间的通信。它是Iceoryx通信框架中的核心组件,所有使用Iceoryx的应用都需要与RouDi建立连接才能进行正常的数据交换。

在启动任何使用Iceoryx的应用之前,必须先启动RouDi守护进程。

2.1.2 RouDi 解决的问题

在早期版本的 Iceoryx 中,为了实现高效的内存管理和通信机制,使用了一些全局变量。这些全局变量有:

  • 内存池管理:记录内存池的状态和配置信息。

    std::unordered_map<std::string, SharedMemoryPool*> g_memoryPools;
    
  • 节点注册和发现:记录已注册的节点信息。

    std::unordered_map<std::string, Node*> g_registeredNodes;
    
  • 通信上下文:存储通信上下文数据。

    CommunicationContext* g_communicationContext = nullptr;
    
  • 系统状态和配置:记录系统的全局状态和配置信息。

    SystemConfig g_systemConfig;
    
  • 错误处理和日志记录:存储错误码和日志记录器。

    int g_lastErrorCode = 0;
    Logger* g_logger = nullptr;
    

尽管这些全局变量帮助实现了基本功能,但也带来了问题:

  • 模块间耦合:多个模块依赖同一个全局变量,增加耦合度。
  • 调试困难:全局变量可在任何地方被修改,增加调试难度。
  • 测试困难:单元测试时需考虑全局变量影响,增加复杂性。
  • 扩展性差:系统扩展或修改时,需同步修改多个模块,增加工作量。
2.1.3 RouDi 的引入解决这些问题
  • 路由与分发:作为中心节点,接收并分发数据。
  • 资源管理与优化:管理共享内存,确保资源合理分配。
  • 集中管理状态:集中管理系统的状态信息。
  • 明确的接口定义:提供明确接口,模块通过这些接口与 RouDi 交互。
  • 更好的错误处理:监控系统状态,及时发现和处理错误。

通过引入 RouDi,Iceoryx 解决了早期版本中全局变量带来的问题。

2.2 Runtime

定义
Runtime是Iceoryx为每个应用提供的运行时环境。在应用启动时,需要初始化其对应的Runtime,以便应用能够接入Iceoryx的通信框架。

功能

  • 初始化:为应用提供必要的初始化步骤,使其能够注册为Iceoryx通信框架的一部分。
  • 资源分配:为应用分配必要的资源,如共享内存段、消息队列等。
  • 通信管理:管理应用与其他参与者(如其他应用、RouDi等)之间的通信。

使用

constexpr char APP_NAME[] = "iox-publisher";
iox::runtime::PoshRuntime::initRuntime(APP_NAME);

2.3 Publisher

定义
Publisher是Iceoryx中的数据发送器,负责将数据发布到指定的Topic上,以便订阅者可以接收。

功能

  • 数据发送:将数据写入共享内存中的指定位置,并通知RouDi该数据已准备好被分发。
  • Topic绑定:Publisher需要与特定的Topic绑定,以便订阅者能够识别并接收其发布的数据。

使用

iox::popo::Publisher<Data> publisher({"Group", "Topic", "Instance"});

2.4 Subscriber

定义
Subscriber是Iceoryx中的数据接收器,负责订阅指定的Topic并接收来自发布者的数据。

功能

  • 数据接收:从共享内存中读取发布者发布的数据。
  • Topic绑定:Subscriber需要与特定的Topic绑定,以便接收该Topic上的所有数据。
  • 回调处理:当接收到新数据时,可以触发回调函数来处理数据。

使用

iox::popo::Subscriber<Data> subscriber({"Group", "Topic", "Instance"});

2.5 Topic

定义
Topic是Iceoryx中的数据载体,用于在发布者和订阅者之间传递数据。Publisher将数据发送到指定的Topic,而Subscriber则订阅该Topic以接收数据。

功能

  • 数据传递:作为数据传递的媒介,确保数据能够从发布者正确地传输到订阅者。
  • 命名约定:Topic通过组(Group)、主题(Topic)和实例(Instance)来唯一标识,以便发布者和订阅者能够准确匹配。

使用
在创建Publisher和Subscriber时,需要指定它们要绑定或订阅的Topic名称(包括组、主题和实例)。

3. Iceoryx使用示例

3.1 发布者程序(publisher_iceoryx.cpp)

#include <chrono>
#include <cstring>
#include <iostream>
#include <thread>

#include "iceoryx_posh/popo/publisher.hpp"
#include "iceoryx_posh/runtime/posh_runtime.hpp"
#include "iox/signal_watcher.hpp"

struct Data {
  char message[128];
};

int main() {
  constexpr char APP_NAME[] = "iox-publisher";
  iox::runtime::PoshRuntime::initRuntime(APP_NAME);

  iox::popo::Publisher<Data> publisher({"Radar", "FrontLeft", "Object"});

  while (!iox::hasTerminationRequested()) {
    publisher.loan()
        .and_then([&](auto& sample) {
          auto start = std::chrono::high_resolution_clock::now();
          std::strcpy(sample->message, "Hello from Publisher");
          sample.publish();
          auto end = std::chrono::high_resolution_clock::now();
          std::chrono::duration<double> elapsed = end - start;
          std::cout << "Send Time: " << elapsed.count() << " seconds\n";
        })
        .or_else([](auto& error) { std::cerr << "Loaning sample failed: " << error << std::endl; });

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
  }

  return 0;
}

3.2 订阅者程序(subscriber_iceoryx.cpp)

#include <chrono>
#include <iostream>
#include <thread>

#include "iceoryx_posh/popo/subscriber.hpp"
#include "iceoryx_posh/runtime/posh_runtime.hpp"
#include "iox/signal_watcher.hpp"

struct Data {
  char message[128];
};

int main() {
  constexpr char APP_NAME[] = "iox-subscriber";
  iox::runtime::PoshRuntime::initRuntime(APP_NAME);
  iox::popo::Subscriber<Data> subscriber({"Radar", "FrontLeft", "Object"});

  while (!iox::hasTerminationRequested()) {
    subscriber.take()
        .and_then([&](const auto& sample) {
          auto start = std::chrono::high_resolution_clock::now();
          std::cout << "Received: " << sample->message << std::endl;
          auto end = std::chrono::high_resolution_clock::now();
          std::chrono::duration<double> elapsed = end - start;
          std::cout << "Receive Time: " << elapsed.count() << " seconds\n";
        })
        .or_else([](auto& error) {
          if (error != iox::popo::ChunkReceiveResult::NO_CHUNK_AVAILABLE) {
            std::cerr << "Taking sample failed: " << error << std::endl;
          }
        });

    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }

  return (EXIT_SUCCESS);
}

3.3 编译和运行

确保已安装Iceoryx,并正确配置CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)
project(iceoryx_test)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

include(GNUInstallDirs)

find_package(iceoryx_posh CONFIG REQUIRED)
find_package(iceoryx_hoofs CONFIG REQUIRED)

get_target_property(ICEORYX_CXX_STANDARD iceoryx_posh::iceoryx_posh CXX_STANDARD)
include(IceoryxPlatform)

add_executable(publisher_iceoryx ./publisher_iceoryx.cpp)
target_link_libraries(publisher_iceoryx iceoryx_posh::iceoryx_posh)
target_compile_options(publisher_iceoryx PRIVATE ${ICEORYX_WARNINGS} ${ICEORYX_SANITIZER_FLAGS})

add_executable(subscriber_iceoryx ./subscriber_iceoryx.cpp)
target_link_libraries(subscriber_iceoryx iceoryx_posh::iceoryx_posh)
target_compile_options(subscriber_iceoryx PRIVATE ${ICEORYX_WARNINGS} ${ICEORYX_SANITIZER_FLAGS})

add_executable(iceoryx_test ./iceoryx_test.cpp)
target_link_libraries(iceoryx_test pthread)

set_target_properties(subscriber_iceoryx publisher_iceoryx PROPERTIES
    CXX_STANDARD_REQUIRED ON
    CXX_STANDARD ${ICEORYX_CXX_STANDARD}
    POSITION_INDEPENDENT_CODE ON
)

然后在项目根目录创建并运行CMake:

mkdir build
cd build
cmake ..
make

运行RouDi(Iceoryx的守护进程):

iox-roudi &

运行发布者和订阅者程序:

./publisher_iceoryx &
./subscriber_iceoryx &

3.4 简单的测试程序(iceoryx_test.cpp)

示例程序参考自iceoryx_examples目录

#include <chrono>
#include <cstdlib>
#include <iostream>
#include <system_error>
#include <thread>
#include <vector>

void run_roudi() {
  if (system("iox-roudi &") != 0) {
    throw std::system_error(errno, std::generic_category(), "Failed to start iox-roudi");
  }
}

void run_publisher(const std::string& uniqueName) {
  std::string command = "PUB_NAME=" + uniqueName + " ./publisher_iceoryx";
  if (system(command.c_str()) != 0) {
    std::cerr << "Failed to run publisher: " << uniqueName << std::endl;
  }
}

void run_subscriber(const std::string& uniqueName) {
  std::string command = "SUB_NAME=" + uniqueName + " ./subscriber_iceoryx";
  if (system(command.c_str()) != 0) {
    std::cerr << "Failed to run subscriber: " << uniqueName << std::endl;
  }
}

int main() {
  const int num_publishers = 2;
  const int num_subscribers = 2;

  try {
    run_roudi();
    // Wait a bit to ensure RouDi has started
    std::this_thread::sleep_for(std::chrono::seconds(2));
  } catch (const std::system_error& e) {
    std::cerr << e.what() << std::endl;
    return EXIT_FAILURE;
  }

  std::vector<std::thread> threads;

  threads.emplace_back(run_publisher, "publisher_0");
  threads.emplace_back(run_subscriber, "subscriber_0");
  for (auto& t : threads) {
    t.join();
  }

  return 0;
}

执行结果

$ ./iceoryx_test 
2024-07-26 14:33:56.514 [Info ]: No config file provided and also not found at '/etc/iceoryx/roudi_config.toml'. Falling back to built-in config.
2024-07-26 14:33:56.731 [Info ]: Resource prefix: iox1
2024-07-26 14:33:56.731 [Info ]: Domain ID: 0
2024-07-26 14:33:56.731 [Info ]: RouDi is ready for clients
2024-07-26 14:33:58.523 [Info ]: Domain ID: 0
2024-07-26 14:33:58.523 [Info ]: Domain ID: 0
Send Time: 4.469e-06 seconds
Send Time: 6.2659e-05 seconds
Received: Hello from Publisher
Receive Time: 4.9024e-05 seconds
Send Time: 8.887e-06 seconds
Received: Hello from Publisher
Receive Time: 3.2011e-05 seconds
Send Time: 1.3976e-05 seconds
Received: Hello from Publisher
Receive Time: 5.9703e-05 seconds
Send Time: 8.265e-06 seconds
Received: Hello from Publisher
Receive Time: 3.4045e-05 seconds
Send Time: 8.326e-06 seconds
Received: Hello from Publisher
Receive Time: 5.782e-05 seconds
Send Time: 1.1362e-05 seconds
Received: Hello from Publisher
Receive Time: 3.2662e-05 seconds
Send Time: 8.596e-06 seconds
Received: Hello from Publisher
Receive Time: 8.1926e-05 seconds
Send Time: 1.1341e-05 seconds
Received: Hello from Publisher
Receive Time: 3.0638e-05 seconds

4. 参考文章

iceoryx源码阅读
iceoryx_github

下面是一个iceoryx发布订阅的C代码示例,其中一个发布者发布一个消息,而两个订阅者订阅这个消息: ```c #include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include "iceoryx_posh/popo/subscriber.hpp" #include "iceoryx_posh/popo/publisher.hpp" #include "iceoryx_posh/runtime/posh_runtime.hpp" #include "iceoryx_posh/roudi/introspection_types.hpp" int main() { // 初始化iceoryx runtime iox::runtime::PoshRuntime::initRuntime("publisher"); // 创建发布者和订阅者 iox::popo::Publisher publisher({"Radar", "FrontLeft", "Object"}); iox::popo::Subscriber subscriber1({"Radar", "FrontLeft", "Object"}); iox::popo::Subscriber subscriber2({"Radar", "FrontLeft", "Object"}); // 订阅者1等待消息 subscriber1.subscribe(); printf("Subscriber 1 waiting for messages...\n"); // 订阅者2等待消息 subscriber2.subscribe(); printf("Subscriber 2 waiting for messages...\n"); // 发布者发布消息 printf("Publisher publishing message...\n"); publisher.publish("Hello, world!"); // 等待订阅者接收消息 while (true) { if (subscriber1.hasData()) { printf("Subscriber 1 received message: %s\n", subscriber1.getChunk()->userPayload()); break; } if (subscriber2.hasData()) { printf("Subscriber 2 received message: %s\n", subscriber2.getChunk()->userPayload()); break; } } // 清理资源并退出 iox::runtime::PoshRuntime::shutdownRuntime(); return 0; } ``` 需要注意的是,这个示例代码需要使用iceoryx库,需要将iceoryx库链接到您的项目中。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘色的喵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值