owned this note
owned this note
Published
Linked with GitHub
# 2025-05-13 問答簡記
## 若語言和用詞不夠精確,思考都會模糊
> [出處](https://x.com/yungyuc/status/1921799104272924892)
「很多人以為工程與程式設計不需要精通語言。並非如此。若語言和用詞不夠精確,思考都會模糊,更不必談邏輯和結構,也寫不好程式。
套一個台灣最喜歡用,但十分模糊的詞:『純軟』。誰能給出明確的定義?英文世界裡準確的對應詞又是什麼?找不到的,因為這概念不存在明確定義。
只要見到討論中使用了這種無法定義的詞語,就能預知討論沒有結果。」
$\to$ [「了解詞源、語境,謹慎選擇用詞,是一種必要的態度」](https://hackmd.io/@sysprog/it-vocabulary)
## 職涯發展提點
> [出處](https://x.com/yungyuc/status/1921360318405734614)
以下翻譯自 Calum E. Douglas 的[推文](https://x.com/CalumDouglas1/status/1921313937091604786),不但適用於機械與航空工程,也適用於軟體工程。軟體工程師請使用腦內正則表示式把機械工程取代為軟體工程。
五項機械工程師 (mechanical engineer) 的職涯發展提點:
1. 若有哪件事情聽起來好得不像真的,那麼有 98% 的可能,它不會一蹴可幾,而需要數以千計的聰明人投入研究,拾級而上。成功沒有捷徑,只能逐步接近。
2. 我們都非常笨。放下你的自我。在工程業界,大家最看不起那種不懂卻到處放話的人。你只需要比昨天聰明一點,然後樂於隨時公開呈現你的結果,讓所有人都可以方便地審查 (scrutiny) 你。
3. 我不太喜歡現在對「大學」的想像。拿到學士、碩士、博士任何一個學位,都只是開始。實話實說,我在開始每個主要專案之前,幾乎都得要花費幾週的時間閱讀其它人是怎麼作這些事情的,才能決定自己要作的事。幾乎從來不會說「喔,我有這個學位,就知道該怎麼作」。在畢業之後的每一天都是「文獻回顧日」。學習永不停歇。一旦停下,就很難追上去。
4. 在工作上出現難解的意見分歧時,你有責任提供資料與數據,證明自己是對的。即使你知道自己是對的,仍然有責任向所有人證明你是對的。知道自己正確只是工作的第一步,而說服他人是下一步。若你實在證明不了,那就接受現實,你心智所認知的正確,無法成為現實世界中的證明。換句話說,若別人的意見比較差,但說明地更清楚,那你輸掉就是應該的。
若你說服失敗,可能的原因是你解釋的方式不夠自然,沒有使用通俗的常識。告訴你自己,當你說服不了人的時候,幾乎都是因為對基礎科學的掌握不夠。你只是以為你知道而已。若你真的學通了,就能在十秒之內說服任何人,無論對方是否具備專門知識。
所以,若你解釋不出來,就表示沒有學通。若你的對手能說清楚,即使你覺得他的想法比較差,他至少有學通。或者換句話說,簡單易懂的計畫 (或是更被廣泛接受) 比聰明難懂的更好。
5. 若你花一個月的時間,單獨一個人深刻投入研究一個技術問題,就會發現自己進入了機械工程的萬神殿,和其它神靈一樣發現自己找不到答案。沒關係,你只需要知道,現在自己最爛的答案比 99% 的其它人更好。和那些沒有花這一個月的時間研究的人相比,你是「專家」,也就是在這個問題上,你比其它人「錯得更多一點」。
若你能夠經年累月地作這些事情,並且工作努力,其他人就會開始詢問你的建議。記住,「努力工作有用」(working hard works),並且,其它所有對你的聰慧稱讚,都是因為你努力工作,然後不要停止閱讀。
---
> [2025-05-06 問答簡記](https://hackmd.io/8rwu2EOaQwW7AzjJOFK-gw)
## 從網路擁塞,談 Linux CPU 排程器的演化
在資訊高速傳輸的今日,無論是全球網際網路中海量資料串流的擁塞管理,抑或是多處理核系統上 CPU 時間的精細分配,資源的有效利用與 fairness 分配機制,始終是保障系統性能與穩定性的關鍵。電腦網路的封包排程與作業系統的行程排程,看似分屬不同領域,實則在其機制、挑戰及理論發展,存在深刻的共通性。尤其電腦網路中經典的擁塞控制問題,是理解 Linux CPU 排程器從 CFS 到 EEVDF 的重要變革,提供理論的依據。
試想典型的網路交換節點:多道資料串流在此匯聚,競相由網路介面控制器 (NIC) 進行轉發。在此之前,資料必須在有限的「緩衝區」(buffer) 中排隊。
```
多道異質資料串流 (速率各異、動態變化)
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
───────────────────────────────────────────────
│ 內部緩衝區 (排隊延遲時間累積與擁塞訊號形成區) │
───────────────────────────────────────────────
│ (受 NIC 服務速率制約的排空過程)
▼
───────────────────────────────────
│ NIC (實體頻寬瓶頸,服務速率受限) │
───────────────────────────────────
│
▼
經排程後的資料串流 (吞吐量受限於瓶頸與 fairness 策略)
```
此「緩衝-排程-轉發」機制是現代高速網路的基礎,用以緩解輸入流量的突發性與輸出 NIC 有限頻寬的不匹配,並力求達成某種程度的 fairness 或服務分化。網路工程的諸多投入,反映於改進 NIC 效能與緩衝區管理演算法 (如 RED, CoDel, Fair Queuing 變體等),以平衡吞吐量、延遲時間與 fairness。然而,當我們將視角從節點內部機制提升到資料串流的端到端動態特性及其與節點交互的宏觀審視時,尤其在網路規模擴張、流量模式多樣化且急劇變化的背景下,系統性瓶頸的成因與演化便顯現出來。
資料串流總量的指數級增長對網路基礎設施構成持續考驗。若 NIC 頻寬提升滯後於流量擴張,緩衝區將被迫吸納超額流量,導致排隊延遲時間顯著增加。過長的排隊延遲時間 (現象常稱為 "[bufferbloat](https://en.wikipedia.org/wiki/Bufferbloat)") 不僅損害延遲時間敏感應用的體驗,最終亦會因頻繁遺失封包而揭示網路擁塞,觸發如 TCP 等的擁塞控制機制,進而可能導致鏈路利用率劇烈波動。因此,理性的架構趨勢是 NIC 頻寬加速提升以快速排空佇列,同時緩衝區容量保持較小 (淺性緩衝),以降低非必要的排隊延遲時間。
傳統機制主要依賴觀測 RTT 增加或封包遺失作為擁塞訊號,並調整[擁塞視窗](https://en.wikipedia.org/wiki/TCP_congestion_control) (cwnd)。然而,這個基於負回饋的控制迴路存在固有問題:
1. 擁塞訊號的形成與穩定性:在淺性緩衝和高速排空下,佇列可能在形成穩定、可觀測的擁塞訊號前就被清空,導致訊號微弱或丟失
2. 擁塞訊號的回饋延遲時間:從擁塞發生到發送端感知並調整行為,至少經歷一個 RTT 的延遲時間。若此延遲時間相對於擁塞事件的持續時間過長,控制將嚴重滯後
這揭示本質的衝突:低延遲時間體驗要求緩衝區「透明」;而精準擁塞控制則需緩衝區「顯性化」以形成可觀測的負載狀態。換言之,擁塞控制的有效性與極致低延遲時間性能間存在設計張力。有時,為求穩定控制,需「刻意放緩」,犧牲瞬時性能。
此困境源於「待觀測網路事件的特徵持續時間」與「控制迴路的響應延遲」的比值。比值大,精確控制可行;比值小,則簡化的、可能帶隨機性的機制更佳,過度精細控制反引不穩。當代網際網路日益需要反應敏捷且具自適應能力的控制策略。
隨著骨幹網路頻寬持續攀升,傳輸瓶頸越來越可能轉為「應用端限制」(application-limited),而非傳統的「頻寬限制」(bandwidth-limited)。這要求控制策略的轉變,從資源稀缺下的精細分配,過渡到資源相對充裕下的效率與模式創新。
端到端 (end-to-end) 擁塞控制的訴求之一是 fairness,而非單純追求鏈結網路的使用率。顯著的 fairness 缺失會導致整體效率低下。然而,fairness 並非絕對均等,需引入「權重」(weights) 以區分服務優先級。如何在單一框架內達成有效的加權 fairness,並「定量」區分多樣化需求,是目前演算法面臨的難題。可能的演化方向是引入更具「自決性」(self-determination) 的機制,如讓資料串流主動宣告其「突發傳輸速率」(burst rate) 或需求特性作為控制參數。
這與 Linux 核心在 CPU 排程器領域從 CFS 到 EEVDF 的演化歷程相似:傳統 CPU 排程器為應對突發需求 (hardirq、softirq 及互動任務喚醒),引入大量啟發式規則 (heuristics)。但這些規則在面對源自應用或系統內部非同步事件引發的「短時高強度資源需求」時,常難以維持系統整體的 fairness 和響應穩定性,易引發系統抖動 (jitter)。EEVDF 的重要貢獻之一,是將此類突發因素更系統地納入統一排程框架,建立基於「信任但可驗證」的模型:賦予短的時間片段 (timeslice) 請求更高優先級 (體現為更早的「虛擬截止時間」,即 virtual deadline),隱含信任其急迫性。
網路傳輸的擁塞、抖動與 fairness 問題,其根源亦常與這些「短時高強度流量」(bursty traffic) 密切相關。有效管理 burst 是擁塞控制事半功倍的關鍵。CFS 的初衷是去除 $O(1)$ 排程器的啟發式規則,追求更簡潔和可證明的 fairness。但其發展中亦逐步加入各種 workaround。究其本質,傳統排程模型常缺少對急迫性 (urgency) 這一關鍵因素的整合,常將其視為獨立於權重和 vruntime 之外需額外處理的參數。
網路傳輸亦然。傳統 TCP 的 cwnd 主要反映允許的傳輸量 (可類比為資料重要性或權重),而 pacing rate 則關乎平滑性。但明確的、可協商的「突發速率」或急迫程度指標,長期未成為擁塞控制模型的主要構成。這與 CPU 排程器的發展歷程高度一致。EEVDF 提供借鑒:它「信任」請求較短時間片段的任務更緊急,並優先執行。這種基於可觀測行為的「信任式機制」,或許是未來擁塞控制走向「自決」的基石。
這條從網路擁塞控制的演化反思到 CPU 排程器設計的啟迪之路,揭示在複雜系統中平衡 fairness、效率與響應性的共通挑戰與解決思路。EEVDF 的出現,正是 Linux 在此道路上邁出的一大步。
## CFS 與 EEVDF 的 Fairness 探索
在資源管理領域,無論是電腦網路的頻寬分配抑或作業系統中的 CPU 時間排程,fairness 始終是衡量系統設計優劣的主要考量。早在 1980 年代末至 1990 年代初,電腦網路領域便已湧現出諸如 WFQ (Weighted Fair Queuing) 和 PGPS (Packetized Generalized Processor Sharing) 等一系列經典理論,目標是在離散化的服務環境下,達成對共享資源的 fairness 比例分配。這些源於網路排隊理論的想法,後來對 Linux 作業系統核心中 CPU 排程器的設計產生影響。從 2007 年引入的 CFS 到 2023 年的 EEVDF,Linux CPU 排程器的設計理念在追求 fairness 的道路上不斷精進,致力於強化微觀層次上的 fairness 控制精度與任務延遲的保障能力。
### 理想模型與離散現實:GPS、WFQ 與 Lag 的定義
> [重新看懂 GPS 相關的論文](https://hackmd.io/@salmonii/HJSvKVOlle)
- [ ] GPS 的理想基準
理解 fairness 分配的起點,往往是理想化的「通用處理器共享」(GPS) 模型。GPS 設想一個能將其服務能力 (如 CPU 運算能力或網路鏈路頻寬) 無限細分成極小單元,並同時服務所有活躍任務的處理器。若系統中有 $N$ 個任務 (或資料串流) 同時處於活躍狀態 (即有待處理的工作),且任務 $i$ 被賦予一個正權重 $w_i$,則在任何時間區間內,只要任務 $i$ 持續保持活躍,其獲得的服務量將精確地與其權重成正比。更具體地,若系統總服務能力 (例如,標準化的 CPU 處理速率) 為 1,則任務 $i$ 在時刻 $t$ 的瞬時理想服務速率 $f_i(t)$ 為:
$$
f_i(t) = \frac{w_i}{\sum_{j \in \mathcal{A}(t)} w_j}
$$
其中 $\mathcal{A}(t)$ 代表在時刻 $t$ 所有活躍任務的集合。相應地,在時間區間 $[t_0, t_1]$ 內,任務 $i$ 應獲得的理想累積服務量 $S_i(t_0, t_1)$ 即為 $f_i(t)$ 在該區間的積分:
$$
S_i(t_0, t_1) = \int_{t_0}^{t_1} f_i(\tau) \, d\tau
$$
GPS 模型提供完美的、理論上的 fairness 分配基準。然而,GPS 本質上是一種「流體模型」([fluid-flow model](https://en.wikipedia.org/wiki/Fluid_dynamics)),無法直接應用於真實的電腦系統,因為在實際操作中,CPU 時間的分配 (時間片段) 或網路資料的傳輸 (封包) 都只能以離散的、具有最小粒度的單元進行。
- [ ] 離散公平模型:WFQ 與 vruntime 的引入
為將 GPS 的理想 fairness 轉化為可在實際離散系統中實施的排程演算法,WFQ 及其變體 (如 PGPS) 引入 vruntime (virtual time) 的關鍵概念。系統維護全局的虛擬時鐘 $V(t)$,該時鐘的推進速率通常與目前活躍任務的總權重或系統的整體負載水平相關。當一個任務 $i$ 的一個離散工作單元 (如一個 CPU 時間片段請求) 到達或準備接受服務時,演算法會為其計算一個「虛擬完成時間」(virtual finish time),記為 $F_k$。這個虛擬完成時間點,代表在理想的 GPS 模型下,該工作單元應當完成服務的虛擬時刻。WFQ/PGPS 排程器在做出排程決策時,總是優先選擇具有最小虛擬完成時間 $F_k$ 的工作單元進行服務。此機制能在離散服務環境下,使每個任務獲得的服務量長期、近似地與其權重成正比,並提供可證明的延遲上界。
- [ ] 引入 Lag,量化離散服務與理想模型的差距
為了更精確地衡量在實際離散服務系統中,一個任務所獲得的服務量與其在理想 GPS 模型下應得的服務量之間的偏差,定義了「延遲量」(lag)。對於任務 $i$,在從 $t_0$ 到目前時刻 $t$ 的時間段內,其 $\operatorname{lag}_i(t_0, t)$ 為:
$$
\operatorname{lag}_i(t_0, t) = S_i(t_0, t) - s_i(t_0, t)
$$
其中 $S_i(t_0, t)$ 是理想 GPS 服務量,$s_i(t_0, t)$ 是實際服務量。若 $\operatorname{lag}_i > 0$,任務 $i$ 服務滯後;若 $\operatorname{lag}_i < 0$,則服務超前。Lag 是衡量 fairness 的重要指標,並與任務延遲特性密切相關。排程演算法致力於將 lag 控制在小且有界的範圍內。
### CFS 運用 vruntime 近似達成 Fairness
自 2007 年 Linux v2.6.23 起,CFS 成為 Linux 預設 CPU 排程策略,主要設計是為每個任務 $i$ 維護獨立的 vruntime,記為 $v_i(t)$。當任務 $i$ 在 CPU 上實際執行時,其 $v_i(t)$ 的增長速率與其自身權重的倒數成正比:
$$
\frac{dv_i(t)}{dt} = \frac{w_0}{w_i}
$$
其中 $w_0$ 是基準權重,$w_i$ 是任務權重 (由 nice 值映射)。CFS 排程器總是從紅黑樹中挑選 vruntime 最小的任務執行。此基於 vruntime 的 fairness 策略,近似 WFQ 中「選擇最欠服務者優先」的機制,長期確保任務 CPU 時間與權重成比例。然而,CFS 存在喚醒延遲較高和「最壞情況反應時間」(WCRT) 難以精確分析的缺陷,因其缺乏明確的虛擬截止時間與嚴格的資格檢查,且包含多種經驗參數與啟發式規則。
### EEVDF 以系統虛擬時鐘與虛擬截止時間嚴格控制 Lag
為克服 CFS 的限制,Linux 作業系統核心於 2023 年 (Linux v6.6+) 採用 EEVDF。EEVDF 延續 vruntime 與權重的框架,但引入以下關鍵機制:
1. 系統虛擬時鐘 (System Virtual Time), $V(t)$
EEVDF 引入全局虛擬時鐘 $V(t)$,其推進速率 $\frac{dV(t)}{dt} = \frac{w_0}{\sum_{j \in \mathcal{A}(t)} w_j}$,為所有任務的虛擬進度提供統一公平參考座標。基於此,$V(t)$ 的理想服務量可表示為 $S_i = \frac{w_i}{w_0} \Delta V$
2. 虛擬截止時間 (Virtual Deadline), $\mathrm{VD}_i$,與資格判斷 (Eligibility)
每個任務 $i$ 被賦予虛擬截止時間 $\mathrm{VD}_i = v_{elig_i} + \frac{s_i^{req} \cdot w_0}{w_i}$,其中 $v_{elig_i} = \max(v_i^{\text{last}}, V(t^{\text{wakeup}}))$ 是合格虛擬執行時間,$s_i^{req}$ 是請求的服務量。任務必須滿足資格條件 $v_i \le V(t)$ (即 $\operatorname{lag}_i \ge 0$) 才能競爭執行。排程器總是從合格任務中挑選具有最早虛擬截止時間的任務。
3. Lag 的強化控制
藉由資格條件與虛擬截止時間機制,EEVDF 的 lag 嚴格控制在一個時間片段內:$0 \le \operatorname{lag}_i(t) \le \frac{s_i^{req} \cdot w_0}{w_i}$。這種嚴密控制大幅改善互動式任務的延遲表現,並能有效防止長時間的 CPU 獨佔 (例如具有重尾分布特性的負載)。
### PELT:宏觀負載追蹤
無論 CFS 或 EEVDF,其主要職責是即時的排程決策:「哪個任務運行」及「運行多久」。而系統中各核處理核器及任務 (或任務組) 的長期歷史平均負載追蹤與估算,則由 PELT 機制負責。
PELT 提供的長期負載估算,是 CPU 排程器做出更宏觀決策的關鍵依據:
* CFS/EEVDF 的輸入參考:任務的權重 $w_i$ (間接影響 vruntime 增長率和 $\mathrm{VD}_i$ 的計算) 可能會參考 PELT 提供的歷史負載或利用率資訊進行動態調整。更重要的是,PELT 的負載估計是進行「負載均衡」(Load Balancing) 的基礎。排程器需要知道各個處理器核的繁忙程度,才能決定是否以及如何將任務從繁忙的處理器核遷移到較空閒的處理器核,以最大化系統整體吞吐量並維持 fairness
* 能源管理:PELT 估算的 CPU 利用率 (`util_avg`) 直接驅動 `schedutil` CPU 頻率調節器,後者進而控制動態電壓與頻率調整(DVFS)。這使得 CPU 排程的活躍程度能夠與處理器核的能耗狀態緊密掛鉤。
* 系統全局視角:PELT 從更長的時間窗口和更廣泛的系統視角來評估負載,彌補 CFS/EEVDF 主要關注單個佇列或即時狀態的不足。這種宏觀資訊對於 NUMA 架構下的排程改進以及 cgroups 的層次化資源分配至關重要。
EEVDF 與 PELT 在 Linux 核心中,因此形成功能明確分工且相互補充的關係:EEVDF 專注於管理微觀時間尺度 (即單個時間片段或請求級別) 的執行 fairness 和延遲 控制;而 PELT 則為宏觀層面 (基於更長歷史資料窗口) 的全局系統資源排程、負載均衡、及能源效率管理等策略提供關鍵的統計決策依據,輔助 CFS/EEVDF 做出更好的系統級 CPU 排程調整。
## 對數關係
以下探討 PELT、CFS 和 EEVDF 所存在與對數相關的數學概念:
- [ ] PELT
* 指數衰減與時間常數 ($e^{-\Delta t / \tau}$):PELT 的本質是藉由 EWMA 來追蹤歷史負載。其衰減因子 $y$ 可表示為 $y = e^{-\Delta t / \tau}$,其中 $\Delta t$ 是取樣時間間隔,$\tau$ 是時間常數。這裡的 $e$ 和指數函數,與自然對數 $\ln$ 互為反運算
* 半衰期 ($t_{1/2}$) 與自然對數 ($\ln 2$):PELT 使用固定的半衰期 $t_{1/2}$ (例如 32ms) 來定義衰減速度。時間常數 $\tau$ 與半衰期 $t_{1/2}$ 的關係是 $\tau = t_{1/2} / \ln 2$。這裡明確出現了自然對數 $\ln 2$
* Linux 核心中的實作手法:雖然理論模型涉及 $e$ 和 $\ln$,但在 Linux 核心的 `decay_load` 函數中,為了避免浮點運算,實際採用查表法。這個表格 (`runnable_avg_yN_inv`) 存儲的是 $y^k$ (其中 $y \approx (1/2)^{1/\text{LOAD_AVG_PERIOD}}$) 的定點數近似值。$y$ 的計算本身就需要 $(1/2)$ 的 $\text{LOAD_AVG_PERIOD}$ 次方根,這可以寫成 $y = \exp(\frac{1}{\text{LOAD_AVG_PERIOD}} \ln(1/2)) = \exp(-\frac{\ln 2}{\text{LOAD_AVG_PERIOD}})$。所以,在預先計算這個 $y$ 值以及表格內容時,隱含對數運算
- [ ] CFS
* 權重與 nice 值的映射:CFS 的 fairness 是基於權重。任務的權重 $w_i$ 由其 nice 值 (範圍 -20 到 +19) 映射而來。這個映射關係在 Linux 核心中是通過 `sched_prio_to_weight` 陣列實現的,其數值大致是指數關係:nice 值每降低 1 (優先級升高),權重大致增加 25% (即乘以 $1.25$)。反過來看,權重與 nice 值之間可看作是對數關係的近似
* `sched_prio_to_weight[prio]` 大致正比於 $1.25^{\text{NICE_0_LOAD - prio}}$,或者說 $1.25^{-\text{nice}}$ (若 nice=0 對應某個參考權重)。若 $W \propto a^N$,那麼 $N \propto \log_a W$
* 這種指數/對數關係使得 nice 值的調整對實際 CPU 時間分配比例的影響是非線性的,較低的 nice 值 (高優先級) 的微小變動會比較高的 nice 值 (低優先級) 的相同變動產生更大的影響力
* vruntime 的計算:任務的 `vruntime` 的增長速率是 $\Delta v_i = \Delta t_{exec} \cdot \frac{w_0}{w_i}$。這裡權重 $w_i$ 在分母上,而 $w_i$ 本身與 nice 值近似成指數關係。因此,`vruntime` 的增長速率與 $1.25^{-\text{nice}}$ 成反比,或者說與 $1.25^{\text{nice}}$ 成正比
* [紅黑樹](https://hackmd.io/@sysprog/linux-rbtree):CFS 使用紅黑樹來管理可執行佇列,並以 `vruntime` 為鍵值。紅黑樹是種自平衡二元搜尋樹,其操作 (插入、刪除、尋找最小/最大值) 的平均和最壞情況時間複雜度都是 $O(\log{N})$,其中 $N$ 是樹中節點的數量 (亦即,可執行任務的數量)。該對數時間複雜度是 CFS 能夠高效管理大量任務的關鍵
- [ ] EEVDF
* 繼承 CFS 的基礎:EEVDF 繼承 CFS 的權重體系和 `vruntime` 的基本概念。因此,上述 CFS 中關於權重與 nice 值映射的指數/對數關係,以及 `vruntime` 的計算方式,在 EEVDF 中依然適用
* Virtual Deadline 的計算:$\mathrm{VD}_i = v_{elig_i} + \frac{\text{slice_request}_i \cdot w_0}{w_i}$。這裡的 $w_i$ 是近似指數/對數關係的權重
* 系統虛擬時鐘 ($V(t)$) 的推進:$\frac{dV(t)}{dt} = \frac{w_0}{\sum_{j \in \mathcal{A}(t)} w_j}$。分母是所有活躍任務的權重之和。若活躍任務的 nice 值分布發生變化,這個總權重也會發生指數級的變化,從而影響 $V(t)$ 的推進速率
* `ilog2()` 的使用 (例如在 `sysctl_sched_base_slice` 的計算中):在某些與 CPU 排程參數相關的計算中,例如根據 CPU 數量調整基礎時間片段 (`sysctl_sched_base_slice`) 時,可能會用到 `ilog2()` 函式 (整數對數,以 2 為底)。例如,`kernel/sched/fair.c` 中 `update_sysctl()` 函式裡有類似 `(1 + ilog2(nr_cpu_ids))` 的計算,用於根據 CPU 數量對某些排程參數進行縮放。這是個明確的對數關係應用,使參數能適應不同規模的多核處理器系統
權重與 Nice 值的指數/對數映射
* 討論與 [Benford's Law](https://en.wikipedia.org/wiki/Benford%27s_law) 的關聯:如果我們將不同 nice 值對應的權重值視為一個資料集合,由於權重大致呈指數增長 (nice 值每減 1,權重約乘以 1.25),這個權重資料集合可能會展現 Benford's Law 的特性。這意味著較小的權重值 (例如以 "1" 開頭的某個範圍內的權重) 可能會比以較大數字開頭的權重值出現得更「頻繁」(若我們將 nice 值的不同級別視為不同的「事件」或「類別」)
* 這種指數/對數映射的設計初衷,並非為了符合 Benford's Law,而是達成感官上的線性調整對應非線性的資源分配比例:人類對優先級的感知可能是線性的 (使用者把 nice 值從 `0` 調到 `-1`,感覺只是「提升一級」),但希望其在資源競爭中的影響力是非線性的 (實際獲得的 CPU 時間比例顯著增加)。指數/對數關係恰好能達成這種效果。Benford's Law 也是在具有指數增長或尺度不變性的過程中出現,這裡的指數關係是共同點,但不是因果關係
> [這個定律,預言了你的人生進度條!](https://youtu.be/JdhRD4TKh48)
---
## CPU 排程的挑戰與新方向
儘管 EEVDF 相較於 CFS 在 fairness 控制的精確性和任務延遲的保障方面已是顯著的進步,其「主要」演算法邏輯依然是內建於作業系統核心的靜態實作。對於有著極端特定需求的使用者,例如運營超大規模資料中心的大型科技公司 (如 Google、Meta 等),或致力於探索下一代排程理論的研究者而言,這種傳統的開發模式在靈活性和迭代效率上仍顯不足。
正是在這樣的背景下,名為 [sched_ext](https://github.com/sched-ext/scx) (簡稱 scx) 的新興機制應運而生。它試圖藉由引入強大的 eBPF 技術,為 Linux CPU 排程器帶來新的途徑。
諸如 Google 和 Meta 這樣的大型科技企業,對 sched_ext 或類似的、允許將 CPU 排程邏輯部分移至使用者空間或利用 eBPF 進行客製化的技術,表現出了濃厚的興趣與積極的投入。其背後的驅動力包括:
* 應對超大規模資料中心與高度特化的工作負載:通用的 CPU 排程器,即便如 EEVDF 一般先進,也難以完美適應其內部運行著的極其複雜、規模龐大且高度客製化的應用場景 (例如,搜尋引擎的索引與查詢、社交網路的資訊流推薦、大規模機器學習訓練與推理等)
* 加速排程策略的創新與實驗迭代週期:利用 eBPF 或使用者空間進行排程邏輯的開發與部署,可以顯著縮短新排程思想從概念到驗證的週期,降低因修改核心而引入的風險,並允許更靈活的測試與線上調整
* 實作差異化服務品質 (QoS) 與精細化資源隔離:在多租戶雲端環境或內部共享的計算叢集中,需要為不同的業務或客戶提供可定制的 QoS 保證,並實作嚴格的 CPU 資源隔離與公平分配
* 保護基於內部資料與模型的專有 CPU 排程策略:某些高效的排程策略可能源於對公司內部特有工作負載的深入分析和複雜的性能模型,將這些策略以非開源的 BPF 程式形式實作,有助於保護其知識產權,同時又能利用 Linux 作業系統核心的穩定平台
* 適應與利用日趨複雜的硬體異質性:現代伺服器與行動裝置的 CPU 架構日益多樣化,例如包含不同性能與功耗特性的多處理器核 (如 Arm big.LITTLE, Intel P-core/E-core 的混合架構),以及複雜的 NUMA (Non-Uniform Memory Access) 拓撲
### sched_ext:CPU 排程器的 eBPF 擴充
`sched_ext` 的出現,正是為了直接回應和解決上述傳統排程器開發模式所面臨的痛點。它將 (e)BPF 技術的強大能力引入到 CPU 排程這一作業系統核心的關鍵領域,其主體可概括為以下:
1. 排程邏輯的 BPF 化:
`sched_ext` 在 Linux 作業系統核心中定義了一系列與排程器運作緊密相關的「操作鉤子」(operation hooks)。這些鉤子精確地對應排程器在各個「關鍵」決策點需要執行的行為,例如 `pick_next_task` (選擇下一個應當運行的任務)、`enqueue_task` (將一個任務加入可執行佇列)、`task_tick` (處理時鐘中斷引發的任務狀態更新) 等。開發者可撰寫符合特定規範的 BPF 程式,並將這些程式動態地附加 (attach) 到相應的操作鉤子上。當一個排程相關的事件在作業系統核心中發生時,核心不再執行固化在 C 程式碼中的傳統排程邏輯,而是轉而呼叫這些已附加的 eBPF 程式來做出排程決策。這實質上是將排程器的決策引擎部分或全部地轉移到可由使用者空間載入、更新和控制的 eBPF 程式之中。
2. eBPF 的安全性與執行效率保障:
eBPF 技術之所以可安全地應用於作業系統核心的敏感路徑,其關鍵優勢在於其內建的安全性保障機制和高執行效率。所有試圖載入到作業系統核心的 BPF 程式,都必須首先藉由極其嚴格的驗證器 (verifier) 的靜態分析檢查。驗證器會確保 eBPF 程式的記憶體存取行為是安全的 (例如,不允許任意指標解引用或越界存取)、控制流程是可預測的 (例如,不允許包含無法終止的迴圈)、並且只呼叫作業系統核心明確授權的輔助函式 (BPF helpers)。只有藉由驗證的 BPF 程式才能被載入。一旦載入,這些 eBPF 程式通常會被及時編譯」(Just-In-Time, JIT) 為原生處理器指令,從而能夠以接近原生 C 程式碼的效率在作業系統核心中執行。這使得基於 eBPF 的排程決策機能夠在滿足極低延遲要求的同時,保障作業系統核心的整體穩定性與安全性。
3. BPF Maps 作為狀態維護與通訊橋梁:
`sched_ext` 廣泛利用 BPF maps (作業系統核心空間內提供的高效鍵值對儲存結構) 來達成 CPU 排程狀態的管理及與使用者空間的雙向通訊。eBPF 排程程式可以使用 BPF maps 來儲存和查詢其運行所需的各種狀態資訊 (例如,類似於 CFS 中的任務虛擬執行時間 vruntime、任務的動態優先級、統計計數器等)。同時,使用者空間的控制程式或監控工具也可以藉由標準的 eBPF 系統呼叫介面,安全地讀取這些 BPF maps 以觀察排程器的內部狀態,或寫入特定的 BPF maps 以動態調整排程參數,甚至在某些設計下觸發 eBPF 排程邏輯的更新。
### scx 排程類別在 Linux 排程運作方式
在 Linux 作業系統核心複雜的排程體系中,`sched_ext` (常簡稱為 `scx`) 被實作為全新的、與其他排程策略並列的頂層排程類別(scheduling class)。Linux 的排程框架採用分層的排程類別設計,每個類別實作一套特定的排程策略,並按照預設的優先級順序接受排程仲裁。
* 優先級次序:`scx` 排程類別的優先級通常設定在諸如 `dl_sched_class` (Deadline 排程,用於即時任務) 和 `rt_sched_class` (Real-Time 排程,用於軟即時任務) 這些高優先級即時排程類別之下,但在常規的 `fair_sched_class` (其預設實作即為 EEVDF 或 CFS) 之上。這意味著,當一個 CPU核設定為由 `scx` 管理時,其上運行的普通非即時任務將完全由載入的 eBPF 排程器進行排程,而系統中的即時任務依然享有比這些 eBPF 排程任務更高的執行優先級
* CPU 核的歸屬與排程策略切換:系統管理者可藉由特定的介面,將系統中的一個或多個 CPU 核的排程策略指定為 `SCHED_EXT`。一旦指定,這些 CPU 核上的排程控制權就從預設的 `fair_sched_class` (例如 EEVDF) 轉移到了 `sched_ext` 框架
* 安全回退機制:為保障系統的整體穩定性,`sched_ext` 框架內建安全回退機制。若作業系統核心偵測到當前運行的 BPF 排程器出現異常行為 (例如,在預期時間內未能選出下一個可執行的任務,或者 eBPF 程式執行超時等),`sched_ext` 框架會自動將該 CPU 核的排程權回退到系統預設的 `fair_sched_class` (通常是 EEVDF)
`sched_ext` 的排程流程大致是:首先,使用者空間的管理程式將編譯好的 eBPF 排程程式碼和相關的 BPF maps 載入到作業系統核心,並將 BPF 程式附加到 `sched_ext` 預定義的各個操作鉤子上。然後,藉由系統呼叫將目標 CPU 核的排程策略設定為 `SCHED_EXT`。此後,當在這些 CPU「處理核」上發生任何排程相關的事件 (如時鐘中斷、任務喚醒、任務阻塞等) 時,作業系統核心的通用排程路徑會呼叫 `sched_ext` 類別註冊的相應處理函式,後者進而觸發執行已附加的 eBPF 程式。eBPF 程式根據其內部邏輯和從 BPF maps 或核心輔助函式獲取的狀態資訊做出排程決策 (例如,選出下一個要運行的任務)。最終,作業系統核心獲取 eBPF 程式的決策結果,並執行實際的底層操作 (如進行任務的上下文切換)。
### 以 `scx_lavd` 為例
由韓國科學技術院 (KAIST) 的 Changwoo Min 教授及其研究團隊精心設計的 `scx_lavd` (Latency-aware Virtual Deadline) 排程器,便是一個針對特定且極具挑戰性的應用場景 —— 高互動性、低延遲要求的現代電腦遊戲 —— 進行 CPU 排程改進的典型代表。
Changwoo Min 教授在其公開報告 (例如在 2024 年的 Linux Plumbers Conference) 中詳細闡述 `scx_lavd` 的核心設計理念與具體實作細節。其主體思想是將任務執行過程中的延遲敏感性 (latency sensitivity) 作為排程決策的關鍵依據,並緊密結合對現代處理器中常見的「異質多處理核」(Heterogeneous Multi-Processing, HMP) 架構 (例如 Arm big.LITTLE 技術,或 Intel 的 Performance-core/Efficient-core 混合設計的處理器) 的原生支援能力,以及一套基於系統實時負載狀況進行動態調整的排程策略。
遊戲應用程式的工作負載通常呈現出與傳統伺服器端批次處理任務或一般桌面辦公應用顯著不同的特徵:
1. 任務執行時間普遍較短,但頻率極高:在遊戲引擎的運行過程中,許多「關鍵」的計算任務,例如物理模擬的更新、人工智慧角色的邏輯演算、算繪命令的準備與提交等,其單次執行的 CPU 時間消耗往往非常短促,很多情況下僅在數十至數百微秒 ($\mu s$) 的量級。然而,這些短任務需要以極高的頻率 (例如,每秒 60 次、120 次甚至更高,對應遊戲的畫面更新率) 被精確地排程執行。
2. 任務間存在高度的、嚴格的執行順序依賴性:一幀遊戲畫面的成功生成,通常依賴於一個複雜的多階段算繪管線 (rendering pipeline) 的順利完成。在此管線中,各個階段的子任務 (例如,場景更新、幾何處理、光柵化、著色、後期處理等) 之間存在著嚴格的前後依賴關係。這些相互依賴的任務序列共同構成了一條或多條影響最終畫面能否按時呈現的「關鍵路徑」(critical path)。此「關鍵路徑」上任何一個環節的任務發生預期之外的執行延遲,都會直接傳導並可能被放大到最終畫面的生成時間,進而嚴重影響遊戲的平均畫面更新率 (Frames Per Second, FPS) 以及使用者感知的視覺流暢體驗 (例如,可能導致畫面卡頓 stutter、延遲感增加或畫面撕裂 tearing 等問題)。
`scx_lavd` 的排程策略正是精確地圍繞這些遊戲工作負載的特有屬性展開設計。它嘗試藉由靜態分析、運行時啟發式判斷,或者由遊戲應用程式本身藉由特定的 API 介面進行標記,來識別出那些處於關鍵路徑上、對整體畫面延遲影響最大的任務。然後,根據每個任務在遊戲關鍵路徑中所扮演的角色及其對整體延遲的敏感程度,為其賦予一個量化的延遲敏感性評估值。基於這個敏感性評估值,`scx_lavd` 為每個被排程的任務計算其 virtual deadline。那些處於算繪管線更前端、或對後續一系列任務的啟動時間有更廣泛連鎖影響的「關鍵」任務,其延遲敏感性通常被評估得更高,相應地,其 virtual deadline 也會被設定得更短 (即排程器認為其更為「緊急」),從而在排程決策過程中獲得更高的實際執行優先級。
在分配 CPU 時間片段的具體機制,`scx_lavd` 則借鑒類似 CFS 的按比例分配想法,但針對遊戲場景的適應性調整。系統總的可用時間片段 (或一個基礎的排程週期長度) 可能是固定的,但每個任務實際能獲得的時間片段長度,會依據當時可執行任務的數量、各任務的權重以及它們的延遲敏感性等因素進行動態分配。目標是在多個競爭 CPU 資源的遊戲相關執行緒之間維持一定的 fairness,防止單一執行緒因不合理的長時間獨佔 CPU 而導致其他關鍵執行緒饑餓。
特別值得一提的是,在具有不同性能與功耗特性的異質多核硬體平台上,`scx_lavd` 能夠根據系統的實時負載狀況以及各任務的特性,自動偵測並動態切換不同的運行模式,以期在遊戲性能與系統能耗之間達致更優的平衡:
* 輕負載模式:當系統整體 CPU 負載較低時 (例如,遊戲可能處於相對靜態的選單介面,或負載不高的過場動畫等場景),`scx_lavd` 會傾向於將任務(包括主要的遊戲執行緒)排程到功耗較低的小核 (Efficient-cores) 上執行,以此最大限度地節省設備的能源消耗,延長電池續航時間 (對於移動設備而言尤為重要)。
* 重負載模式:當系統負載顯著升高時 (例如,遊戲進入到包含複雜 3D 算繪、大規模物理運算或眾多 AI 角色活動的激烈場景),`scx_lavd` 則會積極地將那些計算密集型或延遲敏感型的關鍵遊戲執行緒遷移並優先排程到性能更為強勁的「大核」(Performance-cores) 上執行。這樣可以確保遊戲能夠獲得充足的計算資源支持,從而最大限度地提升處理效能、保障畫面更新率的穩定,並減少卡頓。
* 中間負載狀況:在系統負載介於輕載與重載之間的過渡狀態下,`scx_lavd` 會採取更為精細和動態的混合策略。例如,它可能僅將那些被識別為延遲高度敏感的關鍵路徑任務優先分配給大核執行,以確保其最優先的響應速度和執行效率;而其他非關鍵路徑的背景任務、或者對延遲容忍度相對較高的遊戲輔助執行緒 (例如,音效處理、網路同步等),則可能繼續在「小核」上運行,或者根據當時大核的即時空閒情況進行機會性的動態分配。
Changwoo Min 教授在其報告中展示的初步實驗資料表明,他們在多款實際的 PC 及移動平台遊戲應用程式中的測試結果顯示,相較于 Linux 作業系統核心預設的 EEVDF 排程器,`scx_lavd` 能夠在這些特定的遊戲工作負載下,實作更高的平均畫面更新率,同時達成略微降低的系統整體功耗,並且能夠顯著減少因排程不當而導致的畫面卡頓現象,從而全面提升了遊戲的整體流暢度和使用者的互動體驗。`scx_lavd` 的成功實踐,清晰地展示了 `sched_ext` 機制在針對特定應用領域 (如遊戲、即時互動等) 進行深度排程策略改進方面所具備的巨大潛力。這也積極地預示著,未來 Linux 平台上的高性能應用體驗,有望藉由更多類似的、更精細化、更具場景感知能力的 BPF 排程器得到進一步的提升和最佳化。
除了 `scx_lavd` 這樣針對特定應用 (遊戲) 的排程器之外,`sched_ext` 的技術社群中還不斷湧現出其他值得關注的、具有不同設計目標和應用側重的實作案例:
* `scx_bpfland`:此排程器的設計目標是實作一種普適性的、旨在最大限度地最小化一般任務回應延遲的策略。它可能特別適用於那些對桌面應用程式的互動流暢性、或者伺服器端微服務請求處理延遲有較高要求的場景。
* `scx_rustland`:這是個頗具開創性的實驗性專案,它最大膽的嘗試是將排程決策的主體邏輯從作業系統核心的 eBPF 環境完全轉移到使用者空間,並採用 Rust 程式語言進行實作。作業系統核心中的 eBPF 程式在此模型下主要扮演事件捕獲與通訊中繼的角色。儘管這種設計無可避免地面臨著諸如核心態與使用者空間之間通訊開銷較大、以及使用者空間排程器自身可能因分頁錯誤等原因發生阻塞而影響系統整體排程的固有挑戰,但其在極大簡化排程器開發流程、顯著加速測試與迭代速度、以及方便開發者對排程決策過程進行細緻觀察與除錯等方面的優勢同樣不容忽視。若未來 `sched_ext` 的核心框架能夠提供更高效的批次事件通知機制或更低開銷的核心與使用者空間通訊機制,此類使用者空間排程模型的實用性與吸引力將有望得到大幅增強。
`sched_ext` 提供全新的、更具開放性的平台,使得更多針對特定需求的、甚至是具有革命性思想的排程演算法,能夠擺脫傳統作業系統核心開發模式在週期、風險和接納度等方面的束縛。
延伸閱讀:
* [Sched_ext at LPC 2024](https://lwn.net/Articles/991205/)
## leowu0411
### fork 後,親代與子代行程檔案描述子行為
> The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent. This means that the two file descriptors share open file status flags, file offset, and signal-driven I/O attributes
在 fork 呼叫之後,親代與子代行程都會繼承親代行程中所有的 open file description(即系統層級的開啟檔案表)。因此即便作業系統透過 Copy-On-Write(COW)機制能確保兩個行程在記憶體等資源上的互不干擾,對於檔案的操作卻不然:親代與子代行程共享同一份 open file description,因此不僅檔案的偏移量(offset),還有檔案狀態(status flags)等也會被共享,若親代與子代行程在 fork 後各自對該檔案進行讀寫,雙方之檔案偏移量會同時改變。
此情況對於 socket 亦然,親代與子代行程將共享相同的 kernel-level socket 物件,等同共用以下資源:
* Socket 狀態(如連線是否建立)
* 傳送與接收緩衝區(send/receive buffers)
* Socket 設定選項
當親代與子代行程同時對該 socket 物件進行操作(如 `send()` 或 `recv()`),可能導致資料交錯、race condition 或其他非預期行為。
為了避免上述描述符共用所造成的問題,常見的作法之是在 `fork()` 之後,於子行程中立即關閉不需要繼承的共享檔案描述符。
### dup
* `int dup(int oldfd)`
此系統呼叫會回傳一個非負整數,代表呼叫行程中 per-process file descriptor table 中最小的未使用索引值,並讓該索引指向與 oldfd 相同的系統級 open file description。
須注意的一點是,新的描述符號雖與舊描述符共享檔案偏移量、檔案狀態,但不共享檔案描述符旗標(file descriptor flags),即 close-on-exec flag,若此 flag 被設置,當行程呼叫 `execve()` 時,設置此旗幟之檔案描述符將被自動關閉。 `dup()` 預設新描述符此 flag 不會被設置
* `int dup2(int oldfd, int newfd)`
此系統呼叫與 `dup()` 行為基本一致,不同的是此系統呼叫能夠透過參數 `newfd` 指定新描述符之數值。
假使 `newfd` 在呼叫函式前已經被使用,將在使用前關閉原本的描述符 -- `close(newfd)`,須注意 `dup2()` 不回傳任何呼叫 `close()` 時發生之錯誤訊息。
此外,`dup2()` 會將「關閉 `newfd`」與「將其指向 `oldfd`」這兩個步驟以 atomic 方式執行。這一點相當重要,因為若試圖以下列方式自行模擬 `dup2()`:
```c
close(newfd);
dup(oldfd);
```
將可能在兩步驟之間發生 race condition:
* 如主程式被 signal handler 打斷,該 handler 在此時開啟了一個新的檔案描述元,恰巧佔用了剛關閉的 `newfd`
* 或是其他執行緒同時分配了新的檔案描述元
導致 `dup()` 無法取得原本預期的 `newfd` 數值,造成預期之外的行為。因此,若需要精確指定描述元並避免 race,應優先使用 `dup2()`。
* `int dup3(int oldfd, int newfd, int flags)`
此系統呼叫與 `dup2()` 行為基本一致,但能夠針對 `newfd` 設置 close-on-exec flag,此旗幟行為於上述已提及。
此旗幟設置方法有兩個,一個是透過 `fcntl()`,而另一個方式是在開啟檔案時,透過 `open()` 設置,而設計 `dup3()` 的考量與設計 `open()` 能夠設置此旗幟之考量一致
> Note that the use of this flag is essential in some multithreaded programs, because using a separate fcntl(2) F_SETFD operation to set the FD_CLOEXEC flag does not suffice to avoid race conditions where one thread opens a file descriptor and attempts to set its close-on-exec flag using fcntl(2) at the same time as another thread does a fork(2) plus execve(2). Depending on the order of execution, the race may lead to the file descriptor returned by open() being unintentionally leaked to the program executed by the child process created by fork(2).
即在多執行緒環境中,若在建立檔案描述符的同時立即設置 `FD_CLOEXEC` 旗幟,能夠避免以下 race condition 發生:
當採取先建立描述符再使用 `fcntl()` 設定旗幟,在 `fcntl()` 尚未成功執行前,若其他執行緒同時呼叫 `fork() + execve()`,該描述符將在尚未設定 `FD_CLOEXEC` 就被 fork 出去,導致子行程執行的新程式意外繼承原本應該自動關閉的描述符,造成 file descriptor 洩漏
`dup()` 並不會建立新的 open file description,而是產生一個新的 file descriptor 指向相同的系統層級 entry,因此新舊描述符會共用 file offset 與 open flags,但不共用 `FD_CLOEXEC`;若希望 parent 與 child 在 `fork()` 後能各自操作獨立的檔案 offset,仍需在 `fork()` 後於子行程中重新 open 該檔案,取得新的 open file description。
但對於預計會 `fork()` 的行程來說,設置 `O_CLOEXEC` 能夠安全的確保子行程在 `execve()` 後能夠自動關閉描述符,避免誤用。
## HenryChaing
* 如何確認實體記憶體頁面 (page) 內容相同。
* Reclaim --> 形式不變
* LRU --> Least Recently Used
* Radix Tree --> Space Optimized
* rmap (r: reverse)
## Andrushika
### 在統計學中,什麼手法可以將離散分布轉換為連續的形式?
我回頭查閱了大二使用的機率統計課本,找到了「常態近似」(Normal Approximation) 這個手段。
這個方法基於中央極限定理,抽樣時的隨機變數應該要是獨立同分布;換句話說,母體分布的平均值及變異數應該維持不變。
接下來舉例子說明常態近似:
假設一隨機變數,$X \sim Binominal(n, p)$;根據定理,已知二項式分布平均數 $\mu = np$,標準差 $\sigma = \sqrt{np\space(1-p)}$。而我們知道二項式分布是離散的:例如丟 100 次硬幣的成功次數,只會有 0次、1次、2次、...、100次。
根據中央極限定理,「大量獨立變數的平均會趨近於常態分佈」,因此可以列式得到:
$$\frac{X-np}{\sqrt{np\space(1-p)}} \approx Z \sim N(0,1)$$
最後,為了修正離散轉換到連續的誤差,會加上 0.5 的 continuity correction。
$$P(X\leq k) \approx P(Z\leq \frac{k+0.5-np}{\sqrt{np(1-p)}})$$ 加上 0.5 的原因是,在離散分布中,每個整數值是一個單位;而在常態分布中只能計算面積,所以會使用「寬度為1」的區間中點來對應離散點。例如,離散分布中的 $P(X=45)$ 會對應到常態分布的 $P(44.5\leq X\leq 45.5)$。
#### 在 fork() 後,Parent 和 Child 的 page 哪些部分會不一樣?
在 man page 的 [fork()](https://man7.org/linux/man-pages/man2/fork.2.html) 中有一系列的描述,在此列舉幾個:
* Semaphore adjustment
這個比較特別,上課聽老師描述的時候,以為是 child process 使用的 semaphore 計數和 parent process 完全隔離,看了文件之後才發現不是這麼一回事。semaphore adjustment 指的是 SEM_UNDO 的操作(詳見 [semop()](https://man7.org/linux/man-pages/man2/semop.2.html)),會去維護一個 per-process 和 per-semaphore 的計數值,表示當前占用了多少 semaphore。
> A semaphore adjustment (semadj) value is a per-process, per-semaphore integer that is the negated sum of all operations performed on a semaphore specifying the SEM_UNDO flag. Each process has a list of semadj values—one value for each semaphore on which it has operated using SEM_UNDO.
由以下敘述得知,在 process 被終止之前,會先將所有占用的 semaphore 歸還:
> When a process terminates, each of its per-semaphore semadj values is added to the corresponding semaphore, thus undoing the effect of that process's operations on the semaphore.
在 [linux/ipc/sem.c](https://github.com/torvalds/linux/blob/088d13246a4672bc03aec664675138e3f5bff68c/ipc/sem.c#L2335) 的實作裡面,存在一個 `exit_sem()` 函式,裡面包裝了一段程式碼:
```c
void exit_sem(struct task_struct *tsk){
/* ...ignore */
/* perform adjustments registered in un */
for (i = 0; i < sma->sem_nsems; i++) {
struct sem *semaphore = &sma->sems[i];
if (un->semadj[i]) {
semaphore->semval += un->semadj[i];
/* Range checks of the new semaphore value */
if (semaphore->semval < 0)
semaphore->semval = 0;
if (semaphore->semval > SEMVMX)
semaphore->semval = SEMVMX;
ipc_update_pid(&semaphore->sempid, task_tgid(current));
}
}
/* ...ignore */
}
```
`semaphore->semval += un->semadj[i]` 這段實際上就是前面提及的 UNDO 操作,`exit_sem()` 的使用又可以在 [linux/kernel/exit.c](https://github.com/torvalds/linux/blob/088d13246a4672bc03aec664675138e3f5bff68c/kernel/exit.c#L891) 的 `do_exit()` 中找到,這個函式實際上就是 system call 的實作,`exit()` 時會執行的行為。可見 semaphore adjustment 是一個在 exit 時會被常態執行的步驟。
```c
SYSCALL_DEFINE1(exit, int, error_code){
do_exit((error_code&0xff)<<8);
}
```
因此,在 fork 出 child process 的時候,child 絕對不能繼承 parent 的 semaphore adjustment,不然就會有重複 UNDO 的狀況。所以 semaphore 本身其實是 parent 和 child 都可見的、兩者共用的。
* 不繼承 parent process 的 memory locks
memory locks 指的是將特定的 virtual address space 鎖進 RAM 當中,這樣就不會被放到 swap。好處是延遲極低,壞處是直到 process 明確指定 memory unlock 前,都不會釋放該 RAM 的資源;如果每個 child 在 fork 時也把 parent 的 memory lock 複製一份,那會大量耗用資源
此外,memory lock 在執行 `execve()` 或 process terminates 會被自動銷毀。
> Memory locks are not inherited by a child created via fork(2) and are automatically removed (unlocked) during an execve(2) or when the process terminates. —— [mlock(2)](https://man7.org/linux/man-pages/man2/mlock.2.html?utm_source=chatgpt.com)
* resource utilization 和 CPU time counter 會被歸零
* child 不繼承 timer