編者按:之前我們介紹了內核并發消殺器KCSAN,有小伙伴希望介紹下內核內存方面的工具,這次為大家帶來適合生產環境的工具:kfence。
以下文章來自OpenAnolis龍蜥,作者Kernel SIG成員

一、背景
一直以來,內核內存調測領域一直持續存在著兩大行業難題: "內存被改" 和 "內存泄漏"。內存問題行蹤詭異、飄忽不定,在 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 又殺了個回馬槍。例如,我們可以通過以下的模塊代碼模擬各種內存修改的例子:char *s = kmalloc(8, GFP_KERNEL);s[8] = '1';kfree(s);
char *s = kmalloc(8, GFP_KERNEL);kfree(s);s[0] = '1';
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 功能開啟
訪問???/sys/kernel/slab//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 的第一現場。例如,對于全局/局部變量、dma 硬件直接讀寫、復合頁、野指針等場景無法支持。然而,根據我們的內存問題的數據統計,在線上實際出現的問題里,全都是 slab和order-0 page 相關的內存問題,說明本文的解決方案在覆蓋面上對于目前的線上場景已經足夠。目前可以通過支持 per-slab 單獨開關、控制 interval 等手段極大地緩解,接下來我們也有計劃開發更多的應對內存開銷大的優化和穩定性兜底工作。關于回放和課件獲取?
【視頻回放】:視頻回訪已上傳至龍蜥官網(官網-動態-視頻,可閱讀原文直達)查看。【PPT課件獲取】:關注微信公眾號(OpenAnolis),回復“龍蜥課件” 即可獲取。有任何疑問歡迎隨時咨詢龍蜥助手—小龍(微信:openanolis_assis)。https://gitee.com/anolis/cloud-kernel/blob/devel-5.10/Documentation/dev-tools/kfence.rsthttps://openanolis.cn/video/加入微信群:添加社區助理-龍蜥社區小龍(微信:openanolis_assis),備注【龍蜥】與你同在;加入釘釘群:掃描下方釘釘群二維碼。歡迎開發者/用戶加入龍蜥社區(OpenAnolis)交流,共同推進龍蜥社區的發展,一起打造一個活躍的、健康的開源操作系統生態!

龍蜥社區(OpenAnolis)由企事業單位、高等院校、科研單位、非營利性組織、個人等在自愿、平等、開源、協作的基礎上組成的非盈利性開源社區。龍蜥社區成立于 2020 年 9 月,旨在構建一個開源、中立、開放的Linux 上游發行版社區及創新平臺。
龍蜥社區成立的短期目標是開發龍蜥操作系統(Anolis OS)作為 CentOS 停服后的應對方案,構建一個兼容國際 Linux 主流廠商的社區發行版。中長期目標是探索打造一個面向未來的操作系統,建立統一的開源操作系統生態,孵化創新開源項目,繁榮開源生態。
加入我們,一起打造面向未來的開源操作系統!
https://openanolis.cn
1.以南大通用為例,講一講如何完成與龍蜥操作系統的兼容驗證2.易捷行云EasyStack 加入龍蜥社區,共同打造多樣化算力創新云平臺3.聊一聊龍蜥硬件兼容性 SIG 那些事兒 | 龍蜥 SIG4.系列解讀 SMC-R (二):融合 TCP 與 RDMA 的 SMC-R 通信 | 龍蜥技術5.「龍蜥大講堂」4月預告來了,多位大咖帶你共享技術盛宴!