实战高效RPC方案在嵌入式环境中的应用与揭秘

news2024/9/27 15:54:08

实战高效RPC方案在嵌入式环境中的应用与揭秘

开篇

  在嵌入式系统开发中,大型项目往往采用微服务架构来构建,其核心思想是将一个庞大的单体应用分割成一系列小型、独立、松耦合的服务模块,这些模块可以是以线程或进程形式存在的多个服务单元。各服务间为了协同工作,不可避免地需要进行进程间通信(IPC, Inter-Process Communication)。

  已有的IPC方案众多,包括但不限于信号、管道、消息队列和Socket通信等。此前也分享过系列文章,详细介绍过这些方案的使用方式(可以在公众号聊天界面获取历史文章目录)。不过,大多数传统IPC方案主要侧重于单向数据传递,对于服务调用后的同步返回值处理并未提供直接的支持。

  鉴于此,本文参照Android平台中的Binder机制,设计并实现了一套具备同步返回值功能的RPC(Remote Procedure Call,远程过程调用)方案。这套方案汲取了Binder的优点,能够有效地在进程间进行服务调用并同步接收返回结果,解决了传统IPC方案在双向通信方面的局限性,提升了嵌入式应用中服务间通信的效率和灵活性。

选择共享环形缓冲区的缘由

  首先,对于RPC的实现要求,数据传输的顺序必须按照接口传入参数的顺序依次传输。调用者和被调用者保持相同的内存偏移同步写入和读取,确保数据不乱套。

为什么选用共享内存,而非其他的IPC方案?

  • 零拷贝(Zero-copy)优势:共享内存允许进程直接访问同一块内存区域,省去了数据在用户态和内核态之间的多次复制,对于RPC会存在的高频调用,可以显著降低系统开销,提升性能。
  • 实时性与低延迟:由于数据在内存层面直接交互,共享内存的通信延迟较低,能够提升同步参数与返回值过程的耗时。
  • 灵活的访问模式:不同于管道、消息队列等其他IPC方式,共享内存支持多个进程同时读写,通过合理的同步机制可以实现并发访问,适用于复杂的数据交互模式。

为什么采用环形缓冲区?

  • 先进先出(FIFO)特性:环形缓冲区天然符合FIFO数据传输的需求,保证了数据的有序传输,适用于RPC调用时参数和返回值的有序传递。
  • 资源复用与空间管理:环形缓冲区通过循环利用内存空间,有效避免了频繁分配和回收内存资源,从而减少内存碎片,提高内存利用率。
  • 简化同步复杂性:通过维护读写指针,环形缓冲区可以相对简单地实现多进程间的同步和数据一致性,相较于非循环结构的缓冲区,更容易管理何时可以安全地读写数据。

设计思路

  我们的目的是实现进程间接口的远程调用,外部的需求主要两点:1.参数传递 2. 结果同步返回。

基于此,大致时序如下:

共享环形缓冲区时序图

首先约定:服务端与客户端各创建一片共享内存和信号量。同时持有彼此的共享内存和信号量。(方便调试的做法,实际项目应该统一管理分配)

  1. 服务进程持先启动,初始化共享内存S和信号量S,同时持有客户端的共享内存C和信号量C。
  2. 服务端初始化完毕后,阻塞监听信号量S。
  3. 客户端后启动,初始化共享内存C和信号量C,同时持有服务端的共享内存S和信号量S。
  4. 客户端发起远程调用,将参数写入共享内存S。信号量S通知服务端,阻塞等待信号量C。
  5. 服务端解除阻塞,读取共享内存S。读取到参数,并调用本地接口,获取返回值。
    并将返回值写入共享内存C,通过信号量C通知客户端。
  6. 客户端解除阻塞,读取共享内存C,获取到返回值。本次调用完毕。

源码实现

编程环境

  • 编译环境: Linux环境
  • 语言: C++11

接口定义

  • 环形缓冲区接口(SharedRingBuffer)
struct Root
{
    uint8_t  work;      // 使能状态
    uint8_t  busy;      // 忙碌状态
    uint8_t  rwStatus;  // 可读状态
    uint32_t wp;        // 写入位置
    uint32_t rp;        // 读取位置
};

enum ECmdType
{
    CMD_WRITEABLE   = 0x01,
    CMD_READABLE    = 0x02,
    CMD_BUTT,
};

class SharedRingBuffer
{
public:
    SharedRingBuffer(std::string path, uint32_t capacity);
    ~SharedRingBuffer();

    bool IsReadable()  const noexcept;
    bool IsWriteable() const noexcept;
    int  write(const void* data, uint32_t len);
    int  read(void* data, uint32_t len);

private:
    uint32_t AvailSpace()   const noexcept;
    uint32_t AvailData()    const noexcept;
    void     SetRWStatus(ECmdType type) const noexcept;
    void     DumpMemory(const char* pAddr, uint32_t size);
    void     DumpErrorInfo();

private:
    Root*       mRoot;
    void*       mData;
    uint32_t    mCapacity;
    std::mutex  mMutex;
    std::string mShmPath;
};

SharedRingBuffer对外仅暴露四个接口,主要用于数据的检查和读写。

  • 数据封装接口(Parcel)
class Parcel
{
public:
    Parcel(std::string path, int key, bool master);
    ~Parcel();

    int WriteBool(bool value);
    int ReadBool(bool& value);
    int WriteInt(int value);
    int ReadInt(int& value);
    int WriteString(const std::string& value);
    int ReadString(std::string& value);
    int WriteData(void* data, int size);
    int ReadData(void* data, int& size);
    int wait();
    int post();

private:
    bool                mMaster;
    int                 mShmKey;
    sem_t*              mSem ;
    std::string         mShmPath;
    SharedRingBuffer*   mRingBuffer;
};

Parcel持有共享环形缓冲区和信号量,负责数据的封装。对外提供各种数据类型的写入和读取,同时提供数据同步机制接口wait()post()

关键接口实现
  篇幅有限,文章仅列举关键实现接口(完整代码可在聊天界面输入标题获取)

  • SharedRingBuffer::write(const void* data, uint32_t len)
int SharedRingBuffer::write(const void* data, uint32_t len) {
    int ret = -1;
    int retry = RETRY_TIMES;

    // It's hard to believe, but it actually happened:
    // Although post after it is written in the shared memory, synchronization still might not be timely,
    // and the AvailSpace() returns 0. Only add a retry to avoid it
    while (retry > 0) {
        std::lock_guard<std::mutex> lock(mMutex);
        int32_t avail = AvailSpace();
        if (avail >= len) {

            memcpy(static_cast<char*>(mData) + mRoot->wp, data, len);
            mRoot->wp = (mRoot->wp + len) % mCapacity;
            SetRWStatus(CMD_READABLE);
            ret = 0;
            break;
        } else {
            SPR_LOGE("AvailSpace invalid! avail = %d\n", avail);
            DumpErrorInfo();
            retry--;
            usleep(RETRY_INTERVAL_US);
        }
    }

    return ret;
}

write 接口实现的是将数据写入共享内存,并同步写入偏移量和相关状态。这里加了失败重试机制和一些线程同步。

  • SharedRingBuffer::read(void* data, uint32_t len)
int SharedRingBuffer::read(void* data, uint32_t len)
{
    int ret = -1;
    int retry = RETRY_TIMES;

    // Refer to write comments
    while (retry > 0) {
        std::lock_guard<std::mutex> lock(mMutex);
        int32_t avail = AvailData();
        if (avail >= len) {

            memcpy(data, static_cast<char*>(mData) + mRoot->rp, len);
            mRoot->rp = (mRoot->rp + len) % mCapacity;
            SetRWStatus(CMD_WRITEABLE);
            ret = 0;

            break;
        } else {
            SPR_LOGE("AvailData invalid! avail = %d, len = %d\n", avail, len);
            DumpErrorInfo();
            retry--;
            usleep(RETRY_INTERVAL_US);
        }
    }

    return ret;
}

read 接口实现的是将数据从共享内存读取出。大致流程与write一致。

测试效果

  实现一个简单的例子,客户端远程调用服务端的一个接口 CalculateSum(int val1, int val2)

  • 服务端代码
static int CalculateSum(int val1, int val2)
{
    return val1 + val2;
}

void ServerHandleRequest(Parcel& req, Parcel& reply)
{
    int cmd;
    req.ReadInt(cmd);

    switch (cmd)
    {
        case PARCEL_CMD_CACULATE_SUM:
        {
            int val1 = 0;
            int val2 = 0;
            req.ReadInt(val1);
            req.ReadInt(val2);

            int sum = CalculateSum(val1, val2);
            reply.WriteInt(sum);
            break;
        }

        default:
            SPR_LOGE("Invaild Cmd(0x%x)!\n", cmd);
            break;
    }

    reply.post();
}

int main()
{
    Parcel replyParcel("client_rpc", 88888, false);
    Parcel reqParcel("server_rpc", 12345, true);

    while (true)
    {
        reqParcel.wait();
        ServerHandleRequest(reqParcel, replyParcel);
    }

    return 0;
}
  • 客户端代码
Parcel reqParcel("server_rpc", 12345, false);
Parcel replyParcel("client_rpc", 88888, true);

int CalculateSum(int val1, int val2)
{
    int sum = 0;
    reqParcel.WriteInt(PARCEL_CMD_CACULATE_SUM);
    reqParcel.WriteInt(val1);
    reqParcel.WriteInt(val2);
    reqParcel.post();

    replyParcel.wait();
    replyParcel.ReadInt(sum);

    return sum;
}

int main() {
    char in = 0;

    do {
        SPR_LOGD("Input: ");
        scanf("%c", &in);
        getchar();

        switch (in)
        {
            case '3':
            {
                int val1 = 0;
                int val2 = 0;
                SPR_LOGD("Input val1 val2: ");
                scanf("%d %d", &val1, &val2);
                getchar();
                int sum = CalculateSum(val1, val2);
                SPR_LOGD("sum = %d\n", sum);
                break;
            }

            default:
                break;
        }
    } while (in != 'q');

    return 0;
}
  • 测试结果
Client D: Input val1 val2: 11 22
Client D: sum = 33
Client D: Input val1 val2: 10 10
Client D: sum = 20

总结

  • 本文介绍了一种实用高效的RPC(远程过程调用)解决方案。传统的IPC机制在处理服务间的双向通信时存在挑战,比如无法很好地支持同步返回结果。于是,受Android Binder机制的启发,运用共享环形缓冲区,实现一套轻量化RPC框架。

  • 共享内存配合上数据结构,用起来还是挺高效和方便的。例如之前的《高性能共享内存》 用的是二叉树和共享内存;这篇文章是环形缓冲区和共享内存。应该还有其他数据结构配合共享内存用于新的场景,等待学习。

  • 之所以选择共享内存,主要是因为它具有零拷贝、低延迟、高实时性等优点,能显著降低资源开销,尤其频繁调用的RPC场景。而环形缓冲区的引入,则因其自带的先进先出特性,确保了数据传输的有序性,同时通过循环利用内存空间,减少了内存碎片,提高了内存使用效率。

  • 在实现过程中,设计SharedRingBuffer类来管理共享内存中的环形缓冲区,提供了判断缓冲区状态和进行读写操作的方法。Parcel类则充当了数据的打包和解包角色,它可以方便地处理不同数据类型的读写,并通过控制信号量实现了服务调用的同步等待与响应。

  • 通过具体的示例——远程调用CalculateSum函数,展示如何在客户端和服务端利用上述类实现RPC通信。经过实际测试,达成预期。

  • 实现共享环形缓冲区,是因为个人在Linux应用项目中,遇到了需要RPC的场景。但流行的RPC框架,要么代码量太大,移植费劲;要么资源消耗大,不适合用于嵌入式环境。最主要原因的是,个人技术有限,移植一套RPC框架心有余而力不足。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1541520.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言动态内存的管理

前言 本篇博客就来探讨一下动态内存&#xff0c;说到内存&#xff0c;我们以前开辟空间大小都是固定的&#xff0c;不能调整这个空间大小&#xff0c;于是就有动态内存&#xff0c;可以让我们自己选择开辟多少空间&#xff0c;更加方便&#xff0c;让我们一起来看看动态内存的有…

yolov5训练并生成rknn模型部署在RK3588开发板上,实现NPU加速推理

简介 RK3588是瑞芯微&#xff08;Rockchip&#xff09;公司推出的一款高性能、低功耗的集成电路芯片。它采用了先进的28纳米工艺技术&#xff0c;并配备了八核心的ARM Cortex-A76和Cortex-A55处理器&#xff0c;以及ARM Mali-G76 GPU。该芯片支持多种接口和功能&#xff0c;适…

python写爬虫爬取京东商品信息

工具库 爬虫有两种方案&#xff1a; 第一种方式是使用request模拟请求&#xff0c;并使用bs4解析respond得到数据。第二种是使用selenium和无头浏览器&#xff0c;selenium自动化操作无头浏览器&#xff0c;由无头浏览器实现请求&#xff0c;对得到的数据进行解析。 第一种方…

分布式技术知识体系

分布式架构知识与技术 1.分布式相关理论与组件原理 理解分布式基础理论&#xff08;CAP/BASE&#xff09; 掌握分布式必知必会的核心知识与技能 摸清分布式系统研发与设计的各个环节 2.分布式相关技术及实践 掌握分布式各应用场景与实践技术栈 熟练运用分布式中间件 完成软件…

java面向对象编程基础

对象&#xff1a; java程序中的对象&#xff1a; 本质上是一种特殊的数据结构 对象是由类new出来的&#xff0c;有了类就可以创建对象 对象在计算机的执行原理&#xff1a; student s1new student();每次new student(),就是在堆内存中开辟一块内存区域代表一个学生对象s1变…

第十届蓝桥杯大赛个人赛省赛(软件类)真题- CC++ 研究生组-最短路

6 肉眼观察&#xff0c; 看起来短的几条路对比下来是6~ #include <iostream> using namespace std; int main() {printf("6");return 0; }

学习或复习电路的game推荐:nandgame(NAND与非门游戏)、Turing_Complete(图灵完备)

https://www.nandgame.com/ 免费 https://store.steampowered.com/app/1444480/Turing_Complete/ 收费&#xff0c;70元。据说可以导出 Verilog &#xff01;

蓝桥杯需要掌握的几个案例(C/C++)

文章目录 蓝桥杯C/C组的重点主要包括以下几个方面&#xff1a;以下是一些在蓝桥杯C/C组比赛中可能会涉及到的重要案例类型&#xff1a;1. **排序算法案例**&#xff1a;2. **查找算法案例**&#xff1a;3. **数据结构案例**&#xff1a;4. **动态规划案例**&#xff1a;5. **图…

Python文件读写操作

文件操作注意点 注意点&#xff1a; 1. for line in file --> 会将偏移量移到末尾 2. buffering1 --> 缓冲区中遇到换行就刷新&#xff0c;即向磁盘中写入 3. 读操作结束后&#xff0c;文本偏移量就会移动到读操作结束位置 """编写一个程序,循环不停的写入…

SQLServer TRY_CONVERT函数

TRY_CONVERT&#xff1a;数据库中的安全转换利器 在数据库操作中&#xff0c;数据类型转换是一个常见的需求。然而&#xff0c;传统的转换方法在面对无法转换的数据时&#xff0c;往往会抛出错误&#xff0c;影响程序的稳定性和用户体验。为了解决这个问题&#xff0c;SQL Serv…

Mysql数据库:事务管理

目录 一、Mysql事务的概述 1、Mysql事务的概念 2、事务的ACID四大特性 3、事务之间的相互影响 4、事务的四种隔离级别 5、MySQL与Oracle自动提交事务的区别 6、事务隔离级别的作用范围 二、Mysql事务相关操作 1、查询和设置事务隔离级别 1.1 全局级事务隔离级别 1.1…

手撕算法-买卖股票的最佳时机 II(买卖多次)

描述 分析 使用动态规划。dp[i][0] 代表 第i天没有股票的最大利润dp[i][1] 代表 第i天持有股票的最大利润 状态转移方程为&#xff1a;dp[i][0] max(dp[i-1][0], dp[i-1][1] prices[i]); // 前一天没有股票&#xff0c;和前一天有股票今天卖掉的最大值dp[i][1] max(dp[i-1…

广州迅腾文化传播助力品牌传播的力量:以声塑形

在市场竞争日益激烈的今天&#xff0c;品牌传播成为企业塑造形象、提升竞争力的关键一环。通过精准的品牌传播策略&#xff0c;企业能够迅速吸引目标消费者的注意&#xff0c;实现新产品的快速推广和市场的占领。品牌传播不仅关乎企业的形象塑造&#xff0c;更关乎企业与消费者…

RIP,EIGRP,OSPF的区别

1.路由协议 能否选择出最优路径 2.路由协议 是否能够完成故障切换/多久能够完成故障切换 3.路由协议 是否会占用过大硬件资源 -- RIP -- 路由信息协议 跳数:一次三层设备的转发算一跳 中间隔的设备数量 不按照链路带宽来算 Rip认为路径一样,这个时候。 下面这个跳数不…

Linux:点命令source

相关阅读 Linuxhttps://blog.csdn.net/weixin_45791458/category_12234591.html?spm1001.2014.3001.5482 source命令用于读取一个文件的内容并在当前Shell环境&#xff08;包括交互式Shell或是非交互式Shell&#xff09;执行里面的命令。它被称为点命令是因为命令名source也可…

(三维重建学习)已有位姿放入colmap和3D Gaussian Splatting训练

这里写目录标题 一、colmap解算数据放入高斯1. 将稀疏重建的文件放入高斯2. 将稠密重建的文件放入高斯 二、vkitti数据放入高斯 一、colmap解算数据放入高斯 运行Colmap.bat文件之后&#xff0c;进行稀疏重建和稠密重建之后可以得到如下文件结构。 1. 将稀疏重建的文件放入高…

稀碎从零算法笔记Day23-LeetCode:二叉树的最大深度

题型&#xff1a;链表、二叉树的遍历 链接&#xff1a;104. 二叉树的最大深度 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上…

JAVA面向对象编程 JAVA语言入门基础

类与对象的概念 类 (Class) 和对象 (Object) 是面向对象程序设计方法中最核心的概念。 类是对某一类事物的描述(共性)&#xff0c;是抽象的、概念上的定义&#xff1b;而对象则是实际存在的属该类事物的具体的个体&#xff08;个性&#xff09;&#xff0c;因而也称为实例(In…

《边缘计算:连接未来的智慧之桥》

随着物联网、5G等技术的快速发展&#xff0c;边缘计算作为一种新兴的计算模式&#xff0c;正逐渐引起人们的广泛关注。边缘计算通过将数据处理和存储功能放置在距离数据产生源头更近的位置&#xff0c;实现了更快速、更可靠的数据处理和交换&#xff0c;为各行各业带来了前所未…

JS13-事件的绑定和事件对象Event

绑定事件的两种方式/DOM事件的级别 DOM0的写法&#xff1a;onclick element.onclick function () {}举例&#xff1a; <body> <button>点我</button> <script>var btn document.getElementsByTagName("button")[0];//这种事件绑定的方式…