在Server領域裡,BMC(Baseboard Management Controller)負責與不同的硬體模組或外部設備進行通訊。這些溝通過程往往透過標準化的協定來完成,例如 PLDM(Platform Level Data Model)。而 PLDM 在每一筆 request 與 response 的封包之間,都依賴一個小小的欄位來識別「這是哪一筆請求的回覆」,這個欄位就叫做 Instance ID。就好像我們去鹹酥雞攤點好菜,你會拿到一個號碼牌,等待製作完成之後,機器叫號,你就知道這包餐點是我的。
這麼看來,你學完了。Ha....
還沒,難的在後面。它的設計卻牽涉到多個層次的考量——如何確保唯一性、如何避免重複使用、如何讓多個 process 同時運作仍不會衝突,甚至還要在跨作業系統與硬體的情境下保持穩定。要理解它,最好的方式不是直接翻規格書,而是回到實作層面來看一看。
什麼是 PLDM 的 Instance ID
用賣鹹酥雞的方式來說明其實不夠精確。我們用比較艱澀的詞彙來再次說明。PLDM 協定在每個封包的 Header 中,都保留了幾個固定欄位,例如 Type、Command、Request Bit,以及這個我們今天要講的 Instance ID。顧名思義,它代表這筆封包是「哪一個請求」。當 BMC 向遠端的 terminus(例如主機、電源模組、風扇控制板)發出指令時,會先分配一個 Instance ID。遠端收到封包、完成動作後再回傳 Response,裡面會帶回相同的 ID。Requester 就能根據這個值,把回覆與原始請求正確配對起來。這個欄位只有 5 個 bit,因此每個 terminus 最多可以同時存在 32 筆尚未結束的請求。當一筆請求完成、ID 被釋放後,就能再重新使用。這聽起來很簡單,但如果有多個程式同時在要不同 terminus 的 ID 呢?如果同一個 terminus 的 ID 被兩個不同的 thread 搶到又該怎麼辦?這時候就需要一個管理員,能替整個系統追蹤哪些 ID 已被佔用、哪些可以重新發放,而這個角色在這裡就是 InstanceIdDb。
Note: 根本不知道header是什麼的讀友們,麻煩下方留言。我可能必須從傳輸的時候一個封包的具體長相開始跟大家好好聊聊。
InstanceIdDb 與 PLDM 的核心資料結構
在pldm source code中,真正儲存 Instance ID 狀態的structure叫作: pldm_instance_db。它的內容其實非常樸素:
struct pldm_tid_state {
uint8_t prev;
uint32_t allocations;
};
struct pldm_instance_db {
struct pldm_tid_state state[PLDM_TID_MAX];
int lock_db_fd;
};
每個 terminus 都對應一個 pldm_tid_state。裡面的 allocations 是一個 32-bit 的 bitmap,每一個 bit 代表一個可使用的 Instance ID;若 bit 為 1,表示該 ID 已被佔用;若為 0,則可分配。而 prev 則紀錄上一次發出去的 ID,方便下一次分配時從下一個位置開始搜尋。Leercode裡面是不是也會考到循環分配的機制,這邊使用的就是這樣的方法。下一段我們會細講分配機制。
除此之外還有一個關鍵欄位 lock_db_fd,它是一個開啟的檔案描述符。pldm 會在系統裡建立一個小檔案,例如 /var/lib/pldm/instance_id_db,並利用這個檔案進行 OFD file lock(Open File Description Lock)。這個設計確保即使有多個 daemon、甚至多個 process 同時呼叫 libpldm,也不會搶到同一個 Instance ID。這點我們稍後再細講。
Instance ID 的分配機制
在 PLDM 的世界裡,每個 terminus 都擁有自己的一組 Instance ID 空間。當某個 requester 想發一筆 request 時,它會透過 pldm_instance_id_alloc() 向 InstanceIdDb 申請一個新的 ID。這個函式的流程大致如下:
- 讀出上一個發出去的 ID,也就是
prev。 - 從
(prev + 1)開始尋找下一個可用的 bit。如果到 31 之後又會回到 0,這個取mod運算就包裝在iid_next()這個函式裡。 如果想在往外面一層看可以參考多研究一下這個function的工作:pldm_instance_id_alloc()。
#define PLDM_INST_ID_MAX 32
static inline int iid_next(pldm_instance_id_t cur)
{
return (cur + 1) % PLDM_INST_ID_MAX;
}
- 若找到一個未被佔用的 bit,就試圖在檔案中對應位置上鎖。
- 鎖成功表示這個 ID 沒被其他 process 使用,於是標記該 bit 為 1、更新
prev,並回傳這個新的 ID。
if (flop.l_type == F_UNLCK) {
ctx->state[tid].prev = l_iid;
ctx->state[tid].allocations |= BIT(l_iid);
*iid = l_iid;
return 0;
}
- 若一整圈都找不到空位,就回傳 -EAGAIN,讓上層程式稍後再試。
while ((l_iid = iid_next(l_iid)) != ctx->state[tid].prev) {
每次迴圈一開始,l_iid = iid_next(l_iid) 會把 l_iid 往下一格(循環)推進一次,iid_next(x) 等價於 (x + 1) % PLDM_INST_ID_MAX。迴圈終止條件是「繞回到 prev 就停」,代表整圈都找過了仍沒找到可用的 ID,就會跳出 while(最後回 -EAGAIN)。
這段邏輯的本質,其實就像是一道 Leetcode 題:「給定一個長度為 32 的陣列,每格代表一個可用 ID,請實作 allocate() 與 free(),確保多執行緒安全。」只不過在真實世界中,我們不能只用 mutex,因為 PLDM 可能同時被不同的程式使用。這就需要「跨 process」的同步方法。
OFD File Lock:跨行程同步的祕密武器
這裡是整個設計最巧妙的地方。原code作者並沒有為每個 terminus 建立一把全域鎖,而是用一個單一的檔案,搭配 fcntl() 提供的 byte-range lock 機制,實現細部的鎖定。在分配ID的時候,pldm會計算offset:
loff = tid * PLDM_INST_ID_MAX + l_iid;
如果我們有三個 terminus(TID 1、2、3),每個有 32 個 instance ID,那麼:
- TID1 的 instance ID 對應檔案 offset 0–31
- TID2 對應 32–63
- TID3 對應 64–95
這意味著不同 terminus 永遠鎖不到同一個 byte。當程式呼叫:
fcntl(ctx->lock_db_fd, F_OFD_SETLK, &flock);
它其實只鎖住檔案的其中一個 byte,而不是整個檔案。OFD lock 由核心負責維護,跨 process 也能生效。結果就是:
- 不同 terminus 的 ID 分配可以同時進行;
- 同一 terminus 的不同執行緒若搶同一個 ID,會被 fcntl 擋下;
- 當程式結束或關閉檔案,鎖會自動釋放。
這是一種我覺得優雅的設計:以最少的資源,實現跨process、跨執行緒的安全互斥。
釋放 ID 的過程
當一筆請求完成(例如收到了回覆或逾時),PLDM 需要把原本佔用的 Instance ID 釋放回可用的pool。這個釋放的動作並不由應用程式手動呼叫,而是由 PLDM 的「請求管理者」自動負責。
在pldm的設計裡,所有與遠端 terminus 溝通的請求都會經過一個集中管理的物件。這個物件負責記錄每一筆 request 對應的 Instance ID、計時逾時事件、處理回覆封包, 當它偵測到一筆請求已經完成時,就會呼叫 pldm_instance_id_free(),確認該 bit 目前確實是使用中,然後將其清除、釋放檔案鎖。
這樣的設計讓上層應用完全不需要關心 Instance ID 的分配與回收。對開發者而言,PLDM 的整個請求生命週期就像一次「送出與回覆」的操作, 而底層的分配、鎖定與釋放,都被這個請求管理層默默地完成。
結語
我每次都覺得自己寫的文章,最後一段都要有一個「結語」感覺好老氣...但是還是會習慣在最後寫一段結語。將就看一下吧!在現代的韌體系統裡,這樣的問題無所不在,PLDM 的 Instance ID 只是其中一個縮影。很多的溝通都需要這種拿取Unique ID的方式來知道request和response之間如何匹配。PLDM這樣的設計方式你喜歡嗎?可以來留言跟我們分享你看過的類似這種行為的實作方法?或者你認為作者這樣的設計有沒以可能在不同的硬體架構下會有怎樣的potential risk? 跟我分享一下吧!