實(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)一管理分配)
- 服務(wù)進(jìn)程持先啟動(dòng)妻熊,初始化共享內(nèi)存S和信號(hào)量S夸浅,同時(shí)持有客戶端的共享內(nèi)存C和信號(hào)量C。
- 服務(wù)端初始化完畢后扔役,阻塞監(jiān)聽(tīng)信號(hào)量S帆喇。
- 客戶端后啟動(dòng),初始化共享內(nèi)存C和信號(hào)量C亿胸,同時(shí)持有服務(wù)端的共享內(nèi)存S和信號(hào)量S坯钦。
- 客戶端發(fā)起遠(yuǎn)程調(diào)用预皇,將參數(shù)寫(xiě)入共享內(nèi)存S。信號(hào)量S通知服務(wù)端婉刀,阻塞等待信號(hào)量C吟温。
- 服務(wù)端解除阻塞,讀取共享內(nèi)存S突颊。讀取到參數(shù)鲁豪,并調(diào)用本地接口,獲取返回值律秃。
并將返回值寫(xiě)入共享內(nèi)存C,通過(guò)信號(hào)量C通知客戶端爬橡。 - 客戶端解除阻塞,讀取共享內(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框架心有余而力不足掖桦。