Ciao~ 各位 OpenBMC 的戰友們,不知道你有沒有跟我一樣的經驗:在任何一個 service 的實作中,你總會看到 Boost.Asio 像空氣一樣無所不在。雖然原作者們早已經把架構搭得穩穩妥妥,該有的 class 也設計得服服貼貼,我們身為『後人』,幾乎只需要動動小手術——新增或微調 member function 就好。然而,當我們對一個頻繁相遇的基礎設施一無所知時,帶我進入這一行的老闆常說:「你不知道他怎麼好的?有一天他怎麼死的你就也會告訴我你不知道...」。To be honest, I've actually said that... I really didn't know! 囧!
首先:Boost.Asio 是什麼?
Boost.Asio 是一個提供非同步 I/O 支援的 C++ 函式庫,核心是 io_context(或舊稱 io_service),用來管理事件(event)的排程與執行。其核心運作如下:
boost::asio::io_context io;
io.run(); // 進入事件循環
io_context 內部維護一個 事件佇列(event queue),當有任務、計時器、或 I/O 完成事件加入時,loop 會逐一取出並執行 callback handler。
為什麼我們需要「非同步」?
如果前面這段介紹看完,你心中對於這個問題已經有答案,表示你的基礎打得很不錯。相信也有很多人一頭霧水,覺得同步vs非同步有很重要嗎?差別是什麼?還沒有很明白。接著我用說故事的方式,不專業的講一下我對於這件事情的理解。想像一下,你開了一家餐廳,你是櫃檯的老闆,你的工作是接待客人、點菜、收錢。你可能會有兩種經營的模式:
同步(Synchronous / Blocking)—— 一次只能做一件事
在這家「同步餐廳」裡,服務流程是這樣的:
- 客人 A 點了一份「宮保雞丁」(I/O 任務)。 (我自從從國外回來好愛吃宮保雞丁,笑死!)
- 老闆(主程式):「好的,請稍候。」
- 老闆走進廚房,開始盯著廚師把宮保雞丁炒完(等待 I/O 完成)。
- 期間,門外排了長長的隊伍,客人 B、C、D... 都卡住了。
(他們都不能點餐, 一直在排隊,好幾次在歐洲吃飯都這個狀態,
我都想說是沒有服務人員了嗎?) - 宮保雞丁終於上桌,老闆走回櫃檯。
- 老闆:「下一位。」(我如果是下一位,我已經去下一家了)
【技術解析】:在程式世界裡,這叫做 Blocking I/O (阻塞式 I/O)。當你的主程式發出一個耗時的請求(例如:發送網路封包、讀取感測器),它會停下來,等待 I/O 操作完成並返回結果,期間完全不能處理其他任務。
非同步(Asynchronous / Non-blocking)—— 做完就通知我
在這家「非同步餐廳」(也就是 Boost.Asio 採用的模式)裡,流程煥然一新:
- 客人 A 點了一份「宮保雞丁」(I/O 任務)。
- 老闆(主程式):「好的,點菜單給廚房。下一位。」
- 老闆繼續接待客人 B、C、D,收錢、倒水、處理投訴(處理其他任務)。
- 廚房(作業系統/硬體)炒完菜後,搖鈴通知老闆:「A 客人的宮保雞丁好了!」
- 老闆聽到鈴響,把菜端給客人 A。
【技術解析】:這就是 Non-Blocking I/O (非阻塞式 I/O)。主程式發出 I/O 請求後,立刻返回,可以繼續執行其他程式碼(例如處理 D-Bus 請求、處理定時器)。當 I/O 任務(例如網路連線、感測器讀取)完成時,系統會發出一個「完成通知」給主程式。
從 Hwmon Sensor Monitoring 來看看 Boost.Asio 的角色
Sensor Monitoring是BMC中很重要的功能之一,如果你有打開過"dbus-sensors"這個Repo就會看到有很多不同的services專門為不同種類的sensor在不斷回報著主機板的溫度、電壓、風扇轉速與電源狀態,讓 BMC 能在任何時刻掌握機器的健康狀況。讀取sensor這件事情其實並不簡單,首先,要先建立每個sensor的基本資訊,這些資訊包含大家熟知的sensor name, sensor number, threshold value, hysteresis...etc. 同時要留意兩件很重要的事情,一是presence的狀態,因為現在很多系統都是設計某些板子或小卡可以熱插拔,如果卡拔走sensor也就不在了。二是Power的狀態,如果sensor所在的位置供電狀態為power off,無法對他進行讀值也是非常合理的了,就怕這時候你誤判他為sensor failure。那最後就是....對!你要可以判斷何為sensor failure? 這對data center很重要,只有知道真正的錯誤才有辦法解決問題。
每支sensor monitoring service都大同小異,我們就來看一下Hwmon這裡的main function都做了些什麼?首先,會先看到環境設定相關的部分:boost::asio::io_context 是事件中樞(event loop),所有任務——不論是 D-Bus signal、I/O 操作或 timer——都要透過它排隊執行。systemBus 負責與 D-Bus 互動,而 objectServer 則提供一個「在 BMC 上註冊出來的服務名稱」。從這裡開始,BMC 就有了一個在 D-Bus 上能被尋址的 sensor 伺服器。(看到這邊如果對於dbus還不是非常了解的朋友,可以先去看我先前寫的Dbus的內容。)這種設計的關鍵在於,整個程式在 io.run() 被呼叫之前,其實什麼都還沒開始做。所有的callback、非同步監聽與初始化任務,都只是被「註冊」到 io_context 的內部。等到 io.run() 啟動後,Boost.Asio 才會進入一個永不結束的事件循環,持續處理當前佇列中的任務,並隨時準備響應和執行在服務運行期間由硬體或作業系統「推入」的非同步 I/O 完成事件。 這些事件可能是sensor reading發生變化,系統configuration發生變化,或者是power satus發生變化...etc.
Sensor Polling: 讀值發生改變
當 createSensors() 被呼叫時,程式會為每個 sensor 建立一個物件。每個物件都會在建構時啟動一個屬於自己的 timer,週期性地觸發讀值:
void HwmonTempSensor::setupRead()
{
if (!readingStateGood())
{
markAvailable(false);
updateValue(std::numeric_limits<double>::quiet_NaN());
restartRead();
return;
}
std::weak_ptr<HwmonTempSensor> weakRef = weak_from_this();
inputDev.async_read_some_at(
0, boost::asio::buffer(readBuf),
[weakRef](const boost::system::error_code& ec, std::size_t bytesRead) {
std::shared_ptr<HwmonTempSensor> self = weakRef.lock();
if (self)
{
self->handleResponse(ec, bytesRead);
}
});
}
這段設計的精髓在於——整個 polling 運作於同一個 event loop 裡,而不是每個 sensor 各開一個 thread。async_wait 並不會阻塞程式,它只會向 io_context 登記「一秒後請執行這個 callback」。等到時間到時,Boost.Asio 會在事件佇列中排程這個任務,讀取溫度檔案、更新 D-Bus 屬性,然後再次呼叫 restartRead() 自我排程下一輪。
Configuration 變化與防抖動的策略
關鍵程式如下:
std::function<void(sdbusplus::message_t&)> eventHandler = [&](auto& message) {
sensorsChanged->insert(message.get_path());
filterTimer.expires_after(1s);
filterTimer.async_wait([&](const boost::system::error_code& ec) {
if (ec == boost::asio::error::operation_aborted) return;
createSensors(io, objectServer, sensors, systemBus, sensorsChanged, false);
});
};
setupPropertiesChangedMatches(*systemBus, sensorTypes, eventHandler);
這裡的 eventHandler 是用來監聽 PropertiesChanged 訊號——當 Entity-Manager 偵測到新的卡被插入或拔除時,它會在 D-Bus 上發出這個 signal。
有趣的是,這段程式不是立刻重建 sensor,而是先用一個 filterTimer 延遲一秒。這樣設計的原因在於「防抖動(debounce)」:在 power stress 或熱插拔測試中,Configuration 可能在短時間內連續變動數次,如果每次都立刻呼叫 createSensors(),整個系統會陷入大量重建、刪除、再重建的循環。來看看filterTimer 的用法:
- 當有新的事件進來時,就重設 timer。
- 若在 1 秒內沒有新的事件,就執行重建。
- 若 timer 被重設,前一個
async_waitcallback 會收到operation_aborted,自動終止。
這讓程式能夠在事件風暴中維持穩定,不會頻繁地消耗系統資源。
電源狀態發生變化的處理
接著我們看到這段程式:
auto powerCallBack = [&sensors, &io, &objectServer, &systemBus]
(PowerState type, bool state) {
powerStateChanged(type, state, sensors, io, objectServer, systemBus);
};
setupPowerMatchCallback(systemBus, powerCallBack);
這裡註冊了一個電源狀態的 callback。當Power State Manager(負責觀察power change的service) 發出「上電」或「掉電」訊號時,這個callback就會被觸發。在這個時刻,Boost.Asio 發揮的作用是事件排程的中介者。它確保這個回呼會被安全地、同步地加入 io_context 的事件佇列,而不會和正在執行的 sensor polling 發生衝突。若系統掉電,callback 會呼叫 timer.cancel() 停止所有 sensor 的讀值排程;若重新上電,則會重新建立 sensor 物件並啟動新的 timer。
如果看到這邊,你的心中有浮現出一個時間差會導致sensor failure發生的corner case,這邊我先不細談,有興趣的朋友可以了解一下另外兩個機制:
- Sensor::updateValue() 會再查一次 readingStateGood()
- Sensor::incrementError() 也先檢查 readingStateGood()
結語
如果要用一句話來總結 Boost.Asio 在這裡的角色,那就是:
它讓整個 sensor-monitor 變成一個「有節奏、可預測、可恢復」的系統。
Boost.Asio 提供的 io_context 不只是個事件容器,而是一套完整的非同步執行框架。它負責處理所有 timer、callback、signal,確保它們不會彼此衝突。這種架構讓 OpenBMC 的程式在單執行緒下依然能具備非同步的彈性,並自然地融合 D-Bus 事件、I/O 操作與時間驅動邏輯。
在沒有 Asio 的情況下,我們可能需要手動管理多執行緒、mutex、條件變數、sleep 等複雜結構。Boost.Asio 把這些煩瑣的底層同步細節抽象化成事件驅動模型,讓韌體工程師能專注於「邏輯」。

















