JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

讓系統跑得快:效能與 eBPF

你已經學會作業系統怎麼運作了;現在來學怎麼讓它跑得快——以及同樣要緊的:怎麼看清楚它到底正在做什麼。這篇導覽會帶你走過真正的成本藏在哪裡、那一整套追蹤與剖析工具(perf、ftrace,以及最重要的 eBPF),還有為什麼調校誠實的第一條規矩是:在動任何東西之前,先量。

首先,一條誠實的規矩:動手之前先量

你現在從內部理解了作業系統這套機械:行程與執行緒、排程器、分頁與 TLB、檔案系統、中斷與輸入輸出。這篇導覽談的是另一個問題——不是「它怎麼運作?」,而是「它為什麼慢,我要怎麼讓它快?」效能調校最重要的一課,也是最常被忽略的一課:你必須先量測。人對程式把時間花在哪裡的直覺,錯的次數遠比對的次數多,而憑猜測去改程式,通常只是把瓶頸搬到你看不見的地方去。

把它想成一個被叫到「水流很慢」的房子去的水管工。外行人換掉他看得見的那個水龍頭;專業的人則先在管線上裝一個壓力計,發現堵塞點在三個房間外。效能工作是同一種紀律。在你重寫一個迴圈、加一層快取,或撥動某個核心旋鈕之前,你先接上儀器,讓它們告訴你時間究竟花到哪去了。只有到那時,你才改動一件事,再量一次,並且只在數字真的動了的情況下,才保留那個改動。在黑暗中調校,你只會把程式裡錯誤的那 95% 給優化掉。

時間真正花在哪裡

要知道該量什麼,你得對主宰一台現代機器的那些成本有感覺。最深的一個是記憶體之牆:一次錯過所有快取、跑到主記憶體 RAM 去的存取,成本大約是一百個 CPU 週期,在那段時間裡,處理器本來可以做掉一百次算術。這正是為什麼參考局部性——讓你下一個會碰的資料,在實體上靠近你剛剛碰過的資料——悄悄地統治著真實世界的速度。一個漂亮地依序掃過陣列的迴圈,可以比一個在同樣大小的記憶體裡隨機跳躍的迴圈快上十倍,不是因為它做的工作比較少,而是因為它等得比較少。

第二大成本,是跨越你很久以前就認識的那條邊界:從使用者模式掉進核心、再回來的那趟旅程。每一次系統呼叫——每一次讀、寫,或網路傳送——都是一場受控的、穿門掉進核心模式的墜落,而那次跨越並不免費:暫存器得存起來、模式得切換、快取與 TLB 被部分擾動,回程時整趟旅程又得反過來走一遍。一次系統呼叫很便宜;一百萬次、一次一個位元組的系統呼叫,卻能主宰你的整個程式。真實調校有很大一部分,不過就是做更少、更大的跨越:一次讀 64 KB,而不是一個位元組讀上六萬五千次。

第三種成本,只在許多核心一起工作時才現身:爭用。當兩個核心上的兩個執行緒都想要同一把、或同一條快取線時,硬體必須在它們之間來回搬移所有權,於是它們把時間花在打架,而不是工作上。這是多核心可擴展性的核心:一個在單核心上跑得漂亮的程式,搬到十六核心上可能快不了——甚至更慢——如果這十六個核心全都排在同一把共用鎖後頭。解藥是少共用一點:給每個核心自己的資料,或改用無鎖結構,讓核心別再絆到彼此。

工具箱:perf、ftrace 與 eBPF

那你到底要怎麼看見這些成本?靠那套追蹤與剖析工具箱。它背後的兩個概念值得分清楚。剖析(profiling)是取樣:每秒許多次,一個中斷觸發,工具就快照一下「此刻正在跑哪個函式」。蒐集十萬張這種快照,出現最頻繁的那些函式,就是時間流向之處——一張 CPU 的統計 X 光片。追蹤(tracing)則是把特定事件在發生當下記錄下來:「一次系統呼叫剛開始」、「這把鎖剛被拿走」、「一個封包剛抵達」。剖析回答的是「CPU 把時間花在哪?」;追蹤回答的是「到底依序發生了什麼,每一步又花了多久?」

在 Linux 上,perf 是經典的剖析器:它倚靠 CPU 的硬體效能計數器,以及週期性取樣,來告訴你最燙的那些函式、快取未命中率、分支預測錯誤率——那些最原始的生命徵象。ftrace 則是核心內建的追蹤器:一套烤進 Linux 核心 裡的框架,能在核心運行時,記錄函式的進入與離開、以及一份龐大的預定義追蹤點選單。兩者都成熟而強大。但兩者都有一個容易被忽略的共同侷限:你大致上只能拿到工具作者事先決定要曝露的那些資料,而且形狀也大致是他們選好的。如果你的問題很不尋常,這些經典工具也許根本沒備好答案。

這正是 eBPF 填補的那道縫,也是它何以成為過去十年最重要的系統概念。eBPF 讓你寫一支微小、安全的程式,把它掛到運行中核心內部幾乎任何一個事件上——一次系統呼叫、一個網路封包、一次函式進入、一次情境切換——並在那裡即時跑你自己的邏輯,不必重開機、也不必重新編譯核心。你不再被限制在核心已經輸出的那些資料裡,而是用你自己的話、在答案所在的那個確切位置,問你自己的問題。這就好比,原本你只能讀別人鎖在引擎上的那些儀表,現在你卻能在引擎照常運轉的同時,往你喜歡的任何地方焊上一個新感測器。

eBPF 怎麼能在核心裡頭還安全

最後那個說法應該讓你緊張,而且本來就該如此。在核心內部跑你自己的程式碼,平常是想得到最危險的一件事:核心空間裡一個壞指標或一個無窮迴圈,就會讓整台機器崩潰,因為在那底下,沒有任何東西保護核心不被它自己弄壞。那麼 eBPF 怎麼可能讓普通程式把程式碼注入那塊神聖的空間,卻還安全?答案是一件漂亮的工程:核心拒絕直接信任你的程式,而是在它跑起來之前,先證明它無害。

  1. 你寫一支小程式(常用 C 的一個受限子集),描述在某個核心事件發生時要做什麼——例如「當任何行程呼叫 read 時,把一個依行程分開計數的計數器加一」。
  2. 它被編譯成 eBPF 位元組碼——一套微小、受限的指令集,刻意地不是一門擁有任意威力的完整程式語言。
  3. 位元組碼被交給核心的驗證器,它在載入之前,檢視程式裡每一條可能走過的路徑。
  4. 驗證器駁回任何不安全的東西:不准無界限的迴圈(所以它一定會結束)、不准讀任意記憶體、不准越界存取。如果它無法證明安全,這支程式就被拒絕——沒有商量。
  5. 只有通過的程式,才會被掛到事件上並執行,常常還經由一個即時編譯器,跑到接近原生的速度,安全地待在核心內部。

把那幾步再讀一遍,注意那筆取捨。eBPF 用放棄通用性來換取安全:驗證器只能核准簡單到可以被證明安全的程式,所以 eBPF 刻意不是一個拿來跑任意軟體的地方。那道限制正是重點所在——它讓「在核心裡跑不受信任的程式碼」成為一句說得通的話,而不是一個自相矛盾。結果就是,可觀測性、網路、與安全工具,如今都能就住在事情正在發生的那個地方,還帶著核心的祝福,而不必把成山的資料複製到一個遠遠旁觀的使用者空間程式去。

再快一點:繞過那條慢路徑

有時候,量測揭露出核心本身就是瓶頸。對一張每秒收到一千萬個封包的網路卡來說,就算是每個封包穿越核心一趟那個已經調得很好的成本——中斷、系統呼叫、複製——也變得負擔不起。激進的回應是核心旁路:把一個裝置直接交給一個使用者空間程式,讓它自己去跟硬體對話,完全跳過核心那條通用路徑。像 DPDK 這樣的框架就是這麼替網路做的;結果可以快得驚人,因為你不再為你已經不需要的那些跨越付錢了。

但對旁路的代價要誠實——速度從來不免費。藉著繞過核心,那個使用者空間程式也一併繞過了核心本來替大家做的一切:公平共享、安全檢查、那層讓其他程式也能使用同一張卡的乾淨抽象。旁路用通用性與保護,換取純粹的吞吐量,這對一台專用的高速設備是筆好交易,對一台共用的機器卻是筆糟透了的交易。在儲存與輸入輸出這一側,一條較溫和的中庸之路是 io_uring:一個較新的介面,它讓核心仍然掌權,卻靠著讓程式排入許多非同步請求、再成批收割結果,而不是一次發一個會阻塞的系統呼叫,大幅砍掉跨越的次數。

最後一個值得點名的前沿,因為它跑的方向和純粹速度相反:電源管理。在手機或筆電上,最快的設定很少是你想要的——把時脈全速燒到底,會榨乾電池、也煎熟你的大腿。所以現代作業系統不斷地在速度與能量之間斡旋:閒置時讓核心進入睡眠、工作量輕時降低時脈、在一陣爆量時衝刺著快點做完,好讓晶片早點睡。在靠電池的裝置上,「快」的意思是「用最少的能量把工作做完」,而不是「永遠跑在最大值」,而一套好的電源策略,本身就是另一種效能調校——只是用焦耳、而不是用秒來量。

把它串起來

退一步看,整篇導覽就收斂成一個你可以帶著走到任何地方的循環。量測,找出 CPU、記憶體、輸入輸出、爭用這四種成本裡,到底是哪一個在傷害你。拿起對的儀器:用 perf 剖析那些燙手的函式、用 ftrace 或 eBPF 追蹤那些確切的事件、用硬體計數器看快取與分支的行為。改動一件事——改善局部性、把系統呼叫成批、縮小那把鎖、只在真正划算的地方繞過核心。然後再量一次,並且只在數字真的動了的情況下,才保留那個改動。是那個循環,而不是任何單一招式,才真正是「讓系統跑得快」的意思。