利器解讀!Linux 核心調測中最最讓開發者頭疼的 bug 有解了|龍蜥技術

OpenAnolis小助手發表於2022-04-28

編者按: 一直持續存在 核心記憶體調測領域兩大行業難題: "記憶體被改" 和 "記憶體洩漏"何解?本文整理自 龍蜥大講堂第 13 期,有效地解決這兩大難題都需要什麼方案?快來看作者的詳細介紹吧!

一、背景

一直以來,核心記憶體調測領域一直持續存在著兩大行業難題:  "記憶體被改" 和 "記憶體洩漏"。記憶體問題行蹤詭異、飄忽不定,在 Linux 核心的調測問題中,是最讓開發者頭疼的 bug   之一,因為記憶體問題往往發生故障的現場已經是第 N 現場了,尤其是在生產環境上出現,截止目前並沒有一個很有效的方案能夠進行精準的線上  debug,導致難以排查、耗時耗力。
接下來讓我們來分別看一下"記憶體被改" 和 "記憶體洩漏"這兩大難題為什麼難。

1.1 記憶體被改

Linux 的使用者態的每個程式都單獨擁有自己的虛擬記憶體空間,由 TLB 頁表負責對映管理,從而實現程式間互不干擾的隔離性。然而,在核心態中,所有核心程式共用同一片核心地址空間,這就導致核心程式在分配和使用記憶體時必須小心翼翼。
出於效能考慮,核心中絕大多數的記憶體分配行為都是直接線上性對映區劃出一塊記憶體歸自己使用,並且對於分配後具體的使用行為沒有監控和約束。線性對映區的地址只是對真實實體地址做了一個線性偏移,幾乎可以視同直接操作實體地址,並且在核心態是完全開放共享的。這意味著如果核心程式的行為不規範,將可能汙染到其他區域的記憶體。這會引起許多問題,嚴重的情況下直接會導致當機。
一個典型的場景例子:現在我們假設使用者  A 向記憶體分配系統申請到了 0x00 到 0x0f 這塊地址,但這只是口頭上的“君子協定”,A不必強制遵守。由於程式缺陷,A 向隔壁的  0x10 寫入了資料,而 0x10 是使用者 B  的地盤。當B試圖讀取自己地盤上的資料的時候,就讀到了錯誤的資料。如果這裡原本存著數值,就會出現計算錯誤從而引起各種不可預估的後果,如果這裡原本是個指標,那整個核心就可能直接當機了。
上述的例子被稱為越界訪問(out-of-bound),即使用者  A 訪問了本不屬於 A  的地址。記憶體被改的其他情況還有釋放後使用(use-after-free)、無效釋放(invalid-free)等。這些情況就想成 A  釋放了這片空間後,核心認為這片已經空閒了從而分配給 B 用,然後 A 又殺了個回馬槍。
例如,我們可以透過以下的模組程式碼模擬各種記憶體修改的例子:
//out-of-bound
char *s = kmalloc(8, GFP_KERNEL);
s[8] = '1';
kfree(s);
//use-after-free
char *s = kmalloc(8, GFP_KERNEL);
kfree(s);
s[0] = '1';
//double-free
char *s = kmalloc(8, GFP_KERNEL);
kfree(s);
kfree(s);

1.1.1 為什麼調測難

在上面的例子中,當機最後將會由使用者  B 引發,從而產生的各種日誌記錄和 vmcore 都會把矛頭指向  B。也就是說,當機時已經是問題的第二現場了,距離記憶體被改的第一現場存在時間差,此時 A 可能早已銷聲匿跡。這時核心開發者排查了半天,認為 B  不應該出現這個錯誤,也不知道為什麼 B  的那片記憶體會變成意料之外的值,就會懷疑是記憶體被其他人改了,但是尋找這個“其他人”的工作是很艱難的。如果運氣好,當機現場還能找出線索(例如犯人還呆在旁邊,或是犯人寫入的值很有特徵),又或者發生了多次相似當機從而找到關聯等等。但是,也存在運氣不好時沒有線索(例如犯人已經釋放記憶體消失了),甚至主動復現都困難的情況(例如隔壁沒人,修改了無關緊要的資料,或者修改完被正主覆寫了等等)。

1.1.2 現有方案的侷限性

Linux 社群為了除錯記憶體被改的問題,先後引入了 SLUB DEBUG、KASAN、KFENCE等解決方案。
但這些方案都存在不少侷限性:

  • SLUB DEBUG 需要傳入 boot cmdline 後重啟,也影響不小的 slab 效能,並且只能針對 slab 場景;

  • KASAN 功能強大,同時也引入了較大的效能開銷,因此不適用於線上環境;後續推出的 tag-based 方案能緩解開銷,但依賴於 Arm64 的硬體特性,因此不具備通用性;

  • KFENCE 相對來講進步不少,可在生產環境常態化開啟,但它是以取樣的方式極小機率地發現問題,需要大規模叢集開啟來提升機率。而且只能探測 slab 相關的記憶體被改問題。

1.2 記憶體洩漏

相對記憶體被改,記憶體洩漏的影響顯得更為“溫和”一些,它會慢慢蠶食系統的記憶體。與大家所熟知的記憶體洩漏一樣,這是由於程式只分配記憶體而忘記釋放引起的。
例如,以下模組程式碼可以模擬記憶體洩漏:
char *s;
for (;;) {
    s = kmalloc(8, GFP_KERNEL);
    ssleep(1);
}

1.2.1 為什麼調測難

由於使用者態程式有自己的獨立地址空間管理,問題可能還算好定位(至少一開top   能看見哪個程式吃了一堆記憶體);而核心態的記憶體攪在一起,使得問題根源難以排查。開發者可能只能透過系統統計資訊觀察到某一種記憶體型別(slab/page)的佔用在增長,卻找不到具體是誰一直在分配記憶體而不釋放。這是因為核心對於線性對映區的分配是不做記錄的,也無從得知每塊記憶體的主人是誰。

1.2.2 現有方案的侷限性

Linux 社群在核心中引入了 kmemleak 機制,定期掃描檢查記憶體中的值,是否存在指向已分配區域的指標。
kmemleak 這種方法不夠嚴謹,也不能部署於線上環境,並且存在不少 false-positive 問題,因此定位不太精確。
另外,在使用者態,阿里雲自研運維工具集 sysAK 中也包含針對記憶體洩漏的探測。它透過動態採集分配/釋放的行為,結合記憶體相似性檢測,在某些場景下可以實現生產環境的記憶體洩露問題的精準排查。

二、解決方案

出現記憶體問題時,如果  vmcore 沒有捕獲到第一現場,無法發現端倪,這時核心同學的傳統做法是切換到 debug 核心使用 KASAN  線下除錯。然而線上環境複雜,有些十分隱蔽的問題無法線上下穩定復現,或者線上上時本身就屬於偶發。這類棘手的問題往往只能擱置,等待下一次出現時期望能提供更多線索。因此,我們看到了  KFENCE 本身的靈活性,對它進行改進,讓它成為一個能靈活調整用於線上/線下的記憶體問題除錯工具。
當前最新的 KFENCE 技術優點是可以靈活調節效能開銷(以取樣率,即捕獲bug的機率為代價),可不更換核心而透過重啟的方式開啟;而缺點則是捕獲機率太小,以及對於線上場景來說重啟也比較麻煩。
我們基於 KFENCE 技術的特點,進行了功能增強,並加上一些全新的設計,使其支援全量監控及動態開關,適用於生產環境,併發布在了龍蜥社群的Linux 5.10 分支,具體的實現有:

  • 可以在生產環境的kernel動態開啟和動態關閉。

  • 功能關閉時無任何效能回退。

  • 能夠100% 捕獲slab/order-0 page的out-of-bound、memory corruption, use-after-free、 invaild-free 等故障。

  • 能夠精準捕獲問題發生的第一現場(從這個意義上來看,可以顯著加速問題的復現時間)。

  • 支援 per-slab 開關,避免過多的記憶體和效能開銷。

  • 支援 slab/page 記憶體洩露問題的排查。

對具體技術細節感興趣的同學可訪問龍蜥社群的核心程式碼倉庫閱讀相關原始碼和文件

2.1 使用方法

2.1.1 功能開啟

  • (可選)配置按 slab 過濾

訪問    /sys/kernel/slab/<cache>/kfence_enable   對 每個 slab 單獨開關
訪問    /sys/module/kfence/parameters/order0_page    控制對於order-0 page 的監控開關

  • 取樣模式

使用者既可以設定啟動命令列   kfence.sample_interval=100   並重啟來設定系統啟動時直接開啟 KFENCE(upstream 原版用法),也可以在系統啟動後透過     echo 100 > /sys/module/kfence/parameters/sample_interval    手動開啟 KFENCE 的取樣功能。

  • 全量模式

首先我們需要對池子大小進行配置。
池子大小的估算方法:一個 object 約等於 2 個 page(也就是 8KB)。
考慮將 TLB 頁表分裂成PTE粒度對周圍的影響,最終的池子大小會按 1GB 向上對齊。(object 個數會自動按 131071 向上對齊)
如果配置了 slab 過濾功能,可以先不做修改,預設開啟 1GB 觀察情況。
如果沒配置過濾又需要全量監控,個人建議先開個 10GB 觀察情況。
決定好大小之後,將相應數字寫入     /sys/module/kfence/parameters/num_objects  中。
最後透過設定 sample_interval為-1 來開啟。(當然也可以把這倆引數寫在啟動命令列裡,開機即啟動)
如何觀察情況: kfence 啟動後讀取     /sys/kernel/debug/kfence/stats    介面,如果兩項  currently slab/page allocated 之和接近你設定的 object_size,說明池子大小不夠用了,需要擴容(先往  sample_interval 寫 0 關閉,再改 num_objects,最後往 sample_interval 寫 -1 開啟)。

2.1.2 記憶體被改

2.1.3 記憶體洩漏

2.2 使用效果

對於 記憶體被改,抓到該行為後會在 dmesg 列印現場的呼叫棧。從觸發現場到該記憶體的分配/釋放情況一應俱全,從而幫助精準定位問題。

對於記憶體釋放,可配合使用者態指令碼掃描   /sys/kernel/debug/kfence/objects     中活躍的記憶體(只有 alloc 沒有 free 的記錄),找出最多的相同呼叫棧。
實戰演示詳見影片回放

2.3 效能影響

2.3.1 hackbench

我們使用 ecs 上的裸金屬機器進行測試,104vcpu 的 Intel Xeon(Cascade Lake) Platinum 8269CY。使用 hackbench 將執行緒設滿(104),根據不同的取樣時間測得效能如下:
可以看到,在取樣間隔設定比較大(例如預設100ms)時,KFENCE 幾乎不產生影響。如果取樣間隔設定得比較激進,就能以不大的效能損失換取更高的捕獲 bug 成功率。
需要指出的是,hackbench  測試也是 upstream KFENCE 作者提到的他使用的 benchmark,該 benchmark  會頻繁分配記憶體,因此對kfence較為敏感。該測試用例可以反映 kfence 在較壞情況下的表現,對具體線上環境的效能影響還需因業務而定。

2.3.2 sysbench mysql

使用環境同上,使用 sysbench 自帶 oltp.lua 指令碼設定 16 執行緒進行測試。分別觀察吞吐(tps/qps)和響應時間 rt 的平均值和 p95 分位。

可以看到,在取樣模式下對該  mysql 測試的業務場景影響微乎其微,全量模式下則會對業務產生可見的影響(本例中約  7%),是否開啟全量模式需要結合實際場景具體評估。需要指出的是,本測試開啟了全量全抓的模式,如果已知有問題的 slab  型別,可以配合過濾功能進一步緩解 kfence 帶來的額外開銷。

三、總結

透過在Anolis 5.10 核心中增強 kfence 的功能,我們實現了一個線上的、精準的、靈活的、可定製的記憶體除錯解決方案,可以有效地解決線上核心記憶體被改和記憶體洩露這兩大難題,同時也為其新增了全量工作模式來確保在除錯環境快速抓到 bug 的第一現場。
當然,KFENCE 增強方案也存在一些缺點:

  • 理論上的覆蓋場景不全

例如,對於全域性/區域性變數、dma 硬體直接讀寫、複合頁、野指標等場景無法支援。然而,根據我們的記憶體問題的資料統計,線上上實際出現的問題裡,全都是 slab和order-0 page 相關的記憶體問題,說明本文的解決方案在覆蓋面上對於目前的線上場景已經足夠。

  • 記憶體開銷大

目前可以透過支援 per-slab 單獨開關、控制 interval 等手段極大地緩解,接下來我們也有計劃開發更多的應對記憶體開銷大的最佳化和穩定性兜底工作。

關於回放和課件獲取 

【影片回放】:影片回訪已上傳至龍蜥官網 (官網-動態-影片)
【PPT課件獲取】:關注微信公眾號(OpenAnolis),回覆“龍蜥課件” 即可獲取。有任何疑問歡迎隨時諮詢龍蜥助手—小龍 (微信:openanolis_assis)
相關連結可移步龍蜥公眾號(OpenAnolis龍蜥)2022年4月27日相同推送檢視。
—— 完 ——


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70004278/viewspace-2889274/,如需轉載,請註明出處,否則將追究法律責任。

相關文章