實(shí)戰(zhàn)高效RPC方案在嵌入式環(huán)境中的應(yīng)用與揭秘

實(shí)戰(zhàn)高效RPC方案在嵌入式環(huán)境中的應(yīng)用與揭秘

開(kāi)篇

? 在嵌入式系統(tǒng)開(kāi)發(fā)中,大型項(xiàng)目往往采用微服務(wù)架構(gòu)來(lái)構(gòu)建蜗帜,其核心思想是將一個(gè)龐大的單體應(yīng)用分割成一系列小型、獨(dú)立、松耦合的服務(wù)模塊耙蔑,這些模塊可以是以線程或進(jìn)程形式存在的多個(gè)服務(wù)單元。各服務(wù)間為了協(xié)同工作孤荣,不可避免地需要進(jìn)行進(jìn)程間通信(IPC, Inter-Process Communication)甸陌。

? 已有的IPC方案眾多,包括但不限于信號(hào)盐股、管道钱豁、消息隊(duì)列和Socket通信等。此前也分享過(guò)系列文章疯汁,詳細(xì)介紹過(guò)這些方案的使用方式(可以在公眾號(hào)聊天界面獲取歷史文章目錄)牲尺。不過(guò),大多數(shù)傳統(tǒng)IPC方案主要側(cè)重于單向數(shù)據(jù)傳遞幌蚊,對(duì)于服務(wù)調(diào)用后的同步返回值處理并未提供直接的支持谤碳。

? 鑒于此,本文參照Android平臺(tái)中的Binder機(jī)制溢豆,設(shè)計(jì)并實(shí)現(xiàn)了一套具備同步返回值功能的RPC(Remote Procedure Call蜒简,遠(yuǎn)程過(guò)程調(diào)用)方案。這套方案汲取了Binder的優(yōu)點(diǎn)漩仙,能夠有效地在進(jìn)程間進(jìn)行服務(wù)調(diào)用并同步接收返回結(jié)果搓茬,解決了傳統(tǒng)IPC方案在雙向通信方面的局限性,提升了嵌入式應(yīng)用中服務(wù)間通信的效率和靈活性队他。

選擇共享環(huán)形緩沖區(qū)的緣由

? 首先垮兑,對(duì)于RPC的實(shí)現(xiàn)要求,數(shù)據(jù)傳輸?shù)捻樞虮仨毎凑战涌趥魅雲(yún)?shù)的順序依次傳輸漱挎。調(diào)用者和被調(diào)用者保持相同的內(nèi)存偏移同步寫(xiě)入和讀取系枪,確保數(shù)據(jù)不亂套。

為什么選用共享內(nèi)存磕谅,而非其他的IPC方案私爷?

  • 零拷貝(Zero-copy)優(yōu)勢(shì):共享內(nèi)存允許進(jìn)程直接訪問(wèn)同一塊內(nèi)存區(qū)域雾棺,省去了數(shù)據(jù)在用戶態(tài)和內(nèi)核態(tài)之間的多次復(fù)制,對(duì)于RPC會(huì)存在的高頻調(diào)用衬浑,可以顯著降低系統(tǒng)開(kāi)銷(xiāo)捌浩,提升性能。
  • 實(shí)時(shí)性與低延遲:由于數(shù)據(jù)在內(nèi)存層面直接交互工秩,共享內(nèi)存的通信延遲較低尸饺,能夠提升同步參數(shù)與返回值過(guò)程的耗時(shí)。
  • 靈活的訪問(wèn)模式:不同于管道助币、消息隊(duì)列等其他IPC方式浪听,共享內(nèi)存支持多個(gè)進(jìn)程同時(shí)讀寫(xiě),通過(guò)合理的同步機(jī)制可以實(shí)現(xiàn)并發(fā)訪問(wèn)眉菱,適用于復(fù)雜的數(shù)據(jù)交互模式迹栓。

為什么采用環(huán)形緩沖區(qū)?

  • 先進(jìn)先出(FIFO)特性:環(huán)形緩沖區(qū)天然符合FIFO數(shù)據(jù)傳輸?shù)男枨蠹蠡海WC了數(shù)據(jù)的有序傳輸克伊,適用于RPC調(diào)用時(shí)參數(shù)和返回值的有序傳遞。
  • 資源復(fù)用與空間管理:環(huán)形緩沖區(qū)通過(guò)循環(huán)利用內(nèi)存空間华坦,有效避免了頻繁分配和回收內(nèi)存資源愿吹,從而減少內(nèi)存碎片,提高內(nèi)存利用率惜姐。
  • 簡(jiǎn)化同步復(fù)雜性:通過(guò)維護(hù)讀寫(xiě)指針洗搂,環(huán)形緩沖區(qū)可以相對(duì)簡(jiǎn)單地實(shí)現(xiàn)多進(jìn)程間的同步和數(shù)據(jù)一致性,相較于非循環(huán)結(jié)構(gòu)的緩沖區(qū)载弄,更容易管理何時(shí)可以安全地讀寫(xiě)數(shù)據(jù)。

設(shè)計(jì)思路

? 我們的目的是實(shí)現(xiàn)進(jìn)程間接口的遠(yuǎn)程調(diào)用撵颊,外部的需求主要兩點(diǎn):1.參數(shù)傳遞 2. 結(jié)果同步返回宇攻。

基于此,大致時(shí)序如下:

[圖片上傳失敗...(image-5ff8d6-1711275889497)]

首先約定:服務(wù)端與客戶端各創(chuàng)建一片共享內(nèi)存和信號(hào)量倡勇。同時(shí)持有彼此的共享內(nèi)存和信號(hào)量逞刷。(方便調(diào)試的做法,實(shí)際項(xiàng)目應(yīng)該統(tǒng)一管理分配)

  1. 服務(wù)進(jìn)程持先啟動(dòng)妻熊,初始化共享內(nèi)存S和信號(hào)量S夸浅,同時(shí)持有客戶端的共享內(nèi)存C和信號(hào)量C。
  2. 服務(wù)端初始化完畢后扔役,阻塞監(jiān)聽(tīng)信號(hào)量S帆喇。
  3. 客戶端后啟動(dòng),初始化共享內(nèi)存C和信號(hào)量C亿胸,同時(shí)持有服務(wù)端的共享內(nèi)存S和信號(hào)量S坯钦。
  4. 客戶端發(fā)起遠(yuǎn)程調(diào)用预皇,將參數(shù)寫(xiě)入共享內(nèi)存S。信號(hào)量S通知服務(wù)端婉刀,阻塞等待信號(hào)量C吟温。
  5. 服務(wù)端解除阻塞,讀取共享內(nèi)存S突颊。讀取到參數(shù)鲁豪,并調(diào)用本地接口,獲取返回值律秃。
    并將返回值寫(xiě)入共享內(nèi)存C,通過(guò)信號(hào)量C通知客戶端爬橡。
  6. 客戶端解除阻塞,讀取共享內(nèi)存C友绝,獲取到返回值堤尾。本次調(diào)用完畢。

源碼實(shí)現(xiàn)

編程環(huán)境

  • 編譯環(huán)境: Linux環(huán)境
  • 語(yǔ)言: C++11

接口定義

  • 環(huán)形緩沖區(qū)接口(SharedRingBuffer)
struct Root
{
    uint8_t  work;      // 使能狀態(tài)
    uint8_t  busy;      // 忙碌狀態(tài)
    uint8_t  rwStatus;  // 可讀狀態(tài)
    uint32_t wp;        // 寫(xiě)入位置
    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對(duì)外僅暴露四個(gè)接口迁客,主要用于數(shù)據(jù)的檢查和讀寫(xiě)郭宝。

  • 數(shù)據(jù)封裝接口(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持有共享環(huán)形緩沖區(qū)和信號(hào)量,負(fù)責(zé)數(shù)據(jù)的封裝掷漱。對(duì)外提供各種數(shù)據(jù)類(lèi)型的寫(xiě)入和讀取粘室,同時(shí)提供數(shù)據(jù)同步機(jī)制接口wait()post() 卜范。

關(guān)鍵接口實(shí)現(xiàn)
? 篇幅有限衔统,文章僅列舉關(guān)鍵實(shí)現(xiàn)接口(完整代碼可在聊天界面輸入標(biāo)題獲取)

  • 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 接口實(shí)現(xiàn)的是將數(shù)據(jù)寫(xiě)入共享內(nèi)存海雪,并同步寫(xiě)入偏移量和相關(guān)狀態(tài)锦爵。這里加了失敗重試機(jī)制和一些線程同步。

  • 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 接口實(shí)現(xiàn)的是將數(shù)據(jù)從共享內(nèi)存讀取出奥裸。大致流程與write一致险掀。

測(cè)試效果

? 實(shí)現(xiàn)一個(gè)簡(jiǎn)單的例子,客戶端遠(yuǎn)程調(diào)用服務(wù)端的一個(gè)接口 CalculateSum(int val1, int val2)

  • 服務(wù)端代碼
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;
}
  • 測(cè)試結(jié)果
Client D: Input val1 val2: 11 22
Client D: sum = 33
Client D: Input val1 val2: 10 10
Client D: sum = 20

總結(jié)

  • 本文介紹了一種實(shí)用高效的RPC(遠(yuǎn)程過(guò)程調(diào)用)解決方案湾宙。傳統(tǒng)的IPC機(jī)制在處理服務(wù)間的雙向通信時(shí)存在挑戰(zhàn)樟氢,比如無(wú)法很好地支持同步返回結(jié)果。于是侠鳄,受Android Binder機(jī)制的啟發(fā)埠啃,運(yùn)用共享環(huán)形緩沖區(qū),實(shí)現(xiàn)一套輕量化RPC框架伟恶。

  • 共享內(nèi)存配合上數(shù)據(jù)結(jié)構(gòu)碴开,用起來(lái)還是挺高效和方便的。例如之前的《高性能共享內(nèi)存》 用的是二叉樹(shù)和共享內(nèi)存博秫;這篇文章是環(huán)形緩沖區(qū)和共享內(nèi)存叹螟。應(yīng)該還有其他數(shù)據(jù)結(jié)構(gòu)配合共享內(nèi)存用于新的場(chǎng)景鹃骂,等待學(xué)習(xí)。

  • 之所以選擇共享內(nèi)存罢绽,主要是因?yàn)樗哂辛憧截愇废摺⒌脱舆t、高實(shí)時(shí)性等優(yōu)點(diǎn)良价,能顯著降低資源開(kāi)銷(xiāo)寝殴,尤其頻繁調(diào)用的RPC場(chǎng)景。而環(huán)形緩沖區(qū)的引入明垢,則因其自帶的先進(jìn)先出特性蚣常,確保了數(shù)據(jù)傳輸?shù)挠行蛐裕瑫r(shí)通過(guò)循環(huán)利用內(nèi)存空間痊银,減少了內(nèi)存碎片抵蚊,提高了內(nèi)存使用效率。

  • 在實(shí)現(xiàn)過(guò)程中溯革,設(shè)計(jì)SharedRingBuffer類(lèi)來(lái)管理共享內(nèi)存中的環(huán)形緩沖區(qū)贞绳,提供了判斷緩沖區(qū)狀態(tài)和進(jìn)行讀寫(xiě)操作的方法。Parcel類(lèi)則充當(dāng)了數(shù)據(jù)的打包和解包角色致稀,它可以方便地處理不同數(shù)據(jù)類(lèi)型的讀寫(xiě)冈闭,并通過(guò)控制信號(hào)量實(shí)現(xiàn)了服務(wù)調(diào)用的同步等待與響應(yīng)。

  • 通過(guò)具體的示例——遠(yuǎn)程調(diào)用CalculateSum函數(shù)抖单,展示如何在客戶端和服務(wù)端利用上述類(lèi)實(shí)現(xiàn)RPC通信萎攒。經(jīng)過(guò)實(shí)際測(cè)試,達(dá)成預(yù)期矛绘。

  • 實(shí)現(xiàn)共享環(huán)形緩沖區(qū)耍休,是因?yàn)閭€(gè)人在Linux應(yīng)用項(xiàng)目中,遇到了需要RPC的場(chǎng)景货矮。但流行的RPC框架羊精,要么代碼量太大,移植費(fèi)勁次屠;要么資源消耗大,不適合用于嵌入式環(huán)境雳刺。最主要原因的是劫灶,個(gè)人技術(shù)有限,移植一套R(shí)PC框架心有余而力不足掖桦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末本昏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子枪汪,更是在濱河造成了極大的恐慌涌穆,老刑警劉巖怔昨,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異宿稀,居然都是意外死亡趁舀,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)祝沸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)矮烹,“玉大人,你說(shuō)我怎么就攤上這事罩锐》畋罚” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵涩惑,是天一觀的道長(zhǎng)仁期。 經(jīng)常有香客問(wèn)我,道長(zhǎng)竭恬,這世上最難降的妖魔是什么跛蛋? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮萍聊,結(jié)果婚禮上问芬,老公的妹妹穿的比我還像新娘。我一直安慰自己寿桨,他們只是感情好此衅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著亭螟,像睡著了一般挡鞍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上预烙,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天墨微,我揣著相機(jī)與錄音,去河邊找鬼扁掸。 笑死翘县,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谴分。 我是一名探鬼主播锈麸,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼牺蹄!你這毒婦竟也來(lái)了忘伞?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎氓奈,沒(méi)想到半個(gè)月后翘魄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舀奶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年暑竟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片伪节。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡光羞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出怀大,到底是詐尸還是另有隱情纱兑,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布化借,位于F島的核電站潜慎,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏蓖康。R本人自食惡果不足惜铐炫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蒜焊。 院中可真熱鬧倒信,春花似錦、人聲如沸泳梆。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)优妙。三九已至乘综,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間套硼,已是汗流浹背卡辰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留邪意,地道東北人九妈。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像雾鬼,于是被迫代替她去往敵國(guó)和親萌朱。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容