0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Linux跟蹤系統(tǒng)和BPF的整體認(rèn)知

Linux閱碼場 ? 來源:Linux閱碼場 ? 作者:Linux閱碼場 ? 2022-12-01 10:57 ? 次閱讀

第一章 Linux 跟蹤技術(shù)

1.1 前言

本文目的是給大家建立一個對 Linux 跟蹤系統(tǒng)和 BPF 的整體認(rèn)知.

1.2 跟蹤系統(tǒng)

系統(tǒng)和軟件的可觀測性是系統(tǒng)和應(yīng)用性能分析和故障排查的基礎(chǔ).最小化生產(chǎn)環(huán)境中性能觀測帶來的額外負(fù)擔(dān)是一件很有挑戰(zhàn)性的工作, 但其回報(bào)是豐厚的.在 Linux 系統(tǒng)上, 有一些很有用的跟蹤工具, 如 strace 和 ltrace 分別可用來跟蹤哪些系統(tǒng)調(diào)用和哪些動態(tài)庫被調(diào)用, 這些工具能提供一些有用的信息但是有限, 同時使用這些工具也會給性能帶來不小的額外影響, 這使得它們不是非常適合用于生產(chǎn)環(huán)境中的調(diào)試和觀測.

BPF 提供了一種全新的跟蹤技術(shù)方案, 它與其它的 Linux 跟蹤技術(shù)最大的不同之處在于 1) 它是可編程的:BPF 程序被鏈接到內(nèi)核作為內(nèi)核的一部分運(yùn)行;2) 它同時具備高效率和生產(chǎn)環(huán)境安全性的特點(diǎn), 我們可以在生產(chǎn)環(huán)境中直接運(yùn)行 BPF 程序而無須增加新的內(nèi)核組件.

在開始正式介紹 BPF 之間我們首先來看一下 Linux 跟蹤技術(shù)的的整體圖景.概括地來說,Linux 上的跟蹤系統(tǒng)由三層構(gòu)成: 前端, 跟蹤框架和事件源

1.2.1 概覽

f653282c-711a-11ed-8abf-dac502259ad0.png

圖 1.1. 跟蹤系統(tǒng)框架

事件源是跟蹤數(shù)據(jù)的來源, 它們是內(nèi)核提供的事件跟蹤最底層接口, 由跟蹤框架使用,Linux 提供了豐富的事件源.跟蹤框架運(yùn)行于內(nèi)核, 根據(jù)前端提供的參數(shù)注冊要跟蹤的事件, 負(fù)責(zé)事件發(fā)生時的數(shù)據(jù)采集和計(jì)數(shù), 如果跟蹤框架支持可編程的跟蹤器, 那么它還是直接在內(nèi)核態(tài)對數(shù)據(jù)進(jìn)行聚合、匯總、過濾、統(tǒng)計(jì)等處理.前端運(yùn)行在用戶態(tài), 它讀取跟蹤框架收集的數(shù)據(jù)進(jìn)行處理和展示.用戶根據(jù)前端展示的結(jié)果來進(jìn)行性能分析和故障排查.

1.2.2 術(shù)語

可觀測性 (observability) 是指通過全面觀測來理解一個系統(tǒng), 可以實(shí)現(xiàn)這一目標(biāo)的工具就可以歸類為可觀測性工具.這其中包括跟蹤工具、采樣工具和基于固定計(jì)數(shù)器的工具.但不包括基準(zhǔn)測量(benchmark)工具, 基準(zhǔn)測量工具在系統(tǒng)上模擬業(yè)務(wù)負(fù)載, 會更改系統(tǒng)的狀態(tài).BPF 工具就屬于可觀測性工具, 它們使用 BPF 技術(shù)進(jìn)行可編程型跟蹤分析.

跟蹤 (tracing) 是基于事件的記錄—-這也是 BPF 所使用的監(jiān)測方式.例如 Linux下的 strace(1)就是一個跟蹤工具, 它可以記錄和打印系統(tǒng)調(diào)用事件的信息.有許多監(jiān)測工具并不跟蹤事件, 而是使用固定的計(jì)數(shù)器統(tǒng)計(jì)監(jiān)測事件的頻次, 然后打印出摘要信息:Linux top(1)便是這樣的例子.跟蹤工具的一個顯著標(biāo)志是, 它具備記錄原始事件和事件元數(shù)據(jù)的能力.但這類數(shù)據(jù)的數(shù)量不少, 因此可能需要經(jīng)過后續(xù)處理生成摘要信息.BPF 技術(shù)催生了可編程的跟蹤工具的實(shí)現(xiàn), 這些工具可以在事件發(fā)生時, 通過運(yùn)行一段小程序(一個或幾個函數(shù))來進(jìn)行定制化的實(shí)時統(tǒng)計(jì)摘要生成或其他動作.

采樣 (sampling) 工具通過獲取全部觀測量的子集來描繪目標(biāo)的大致圖像; 這也被稱作生成性能剖析樣本或 profiling.有一個 BPF 工具就叫 profile(8), 它基于計(jì)時器來對運(yùn)行中的代碼定時采樣.采樣工具的一個優(yōu)點(diǎn)是, 其性能開銷比跟蹤工具小, 因?yàn)橹祵Υ罅渴录囊徊糠诌M(jìn)行測量.采樣的缺點(diǎn)是, 它只提供了一個大致的畫像, 或遺漏事件.

探針 (probe) 軟件或者硬件中的探測點(diǎn), 它產(chǎn)生一個事件, 該事件將導(dǎo)致內(nèi)核中的一段程序被運(yùn)行.

靜態(tài)跟蹤 (static tracing) 跟蹤的事件由硬編碼的探針產(chǎn)生, 這類探針的位置在編譯時就已經(jīng)寫死比如 tracepoint.因?yàn)檫@些探針是固定的, 所以它們的 API 比較穩(wěn)定, 可以對它們編寫文檔, 但同時這也意味著這類事件源是不靈活的.

動態(tài)跟蹤 (dynamic tracing) 跟蹤的事件可以在運(yùn)行時動態(tài)的創(chuàng)建和撤銷, 這使得我們可以跟蹤軟件中的任何事件比如任意函數(shù)的調(diào)用和返回, 具有極大的靈活性, 但是軟件接口是發(fā)展變化的, 這類事件接口是不穩(wěn)定, 因此和也很難為其編寫文檔.

事件 (event) 直接描述什么是事件可能是一件很困難的事情, 因?yàn)槭录赡苡捎布?、?nèi)核和用戶程序觸發(fā), 事件產(chǎn)生時的硬件和軟件環(huán)境也很復(fù)雜.不過好在用戶從來都不需要去直接處理事件, 對事件的直接處理是由內(nèi)核自動完成的.因此我們可以抖機(jī)靈的把事件定義為: 內(nèi)核在特定的條件下執(zhí)行的一段特定的程序, 這個程序會收集該條件發(fā)生時系統(tǒng)硬件和軟件環(huán)境的一些信息(“事件上下文”), 并將這些信息保存在內(nèi)核緩沖區(qū)或者更新計(jì)數(shù)器.所以從用戶的角度看, 發(fā)生一個事件等效于內(nèi)核產(chǎn)生了一個“事件上下文”或者更新了計(jì)數(shù)器, 如果跟蹤框架支持可編程的事件跟蹤(如 BPF,systemtap)那么事件發(fā)生時內(nèi)核還會執(zhí)行由用戶注入到內(nèi)核中關(guān)聯(lián)到該事件的程序.

1.2.3 Linux 跟蹤技術(shù)的發(fā)展時間線

f6630de6-711a-11ed-8abf-dac502259ad0.png

圖 1.2. Linux 跟蹤技術(shù)發(fā)展時間線

1.3 Linux 跟蹤技術(shù)棧

f676932a-711a-11ed-8abf-dac502259ad0.png

圖 1.3. Linux 跟蹤技術(shù)棧

1.3.1 事件源

kernel tracepoint

tracepoint 是內(nèi)核預(yù)先定義的的靜態(tài)跟蹤事件源.tracepoint 可以用來對內(nèi)核進(jìn)行靜態(tài)插樁.內(nèi)核開發(fā)著在內(nèi)核函數(shù)中的特定邏輯位置出, 有意放置了這些插樁點(diǎn), 對于內(nèi)核開發(fā)者來說,tracepoint 有一定的維護(hù)成本, 而且它的使用范圍比 kprobe 要窄得多.使用tracepoint 的主要優(yōu)勢是它的 API 穩(wěn)定; 基于 tracepoint 的工具, 在內(nèi)核版本升級后一般仍然可以正常工作.同時它帶來的額外負(fù)擔(dān)也較輕.在能滿足探測需要時應(yīng)優(yōu)先考慮使用 tracepoint.Linux tracepoint 事件名的格式是“子系統(tǒng): 事件名”.

tracepoint 出于不啟用狀態(tài)時, 性能開銷要盡可能小, 這是為了避免對不使用的東西“交性能稅”, 為此 Linux 使用了一項(xiàng)叫做“靜態(tài)跳轉(zhuǎn)補(bǔ)?。╯tatic jump patching)”的技
術(shù).其工作原理如下:

  1. 在內(nèi)核編譯階段會在 tracepoint 所在的位置插入不做任何具體工作的指令, 在 x86架構(gòu)下就是有 5 個字節(jié)的 nop 指令, 這個長度的選擇是為了確保之后可以將它替換為一個 5 字節(jié)的 jmp 指令.

  2. 在函數(shù)尾部插入一個 tracepoint 處理函數(shù), 也叫做蹦床函數(shù), 這個函數(shù)會遍歷一個存儲 tracepoint 回調(diào)函數(shù)的數(shù)組.這會導(dǎo)致函數(shù)編譯結(jié)果稍稍變大.(之所以稱之為蹦床函數(shù), 是因?yàn)樵趫?zhí)行過程中函數(shù)會跳入, 然后再跳出這個處理函數(shù)), 這很可能會對指令緩存會有一些小影響.

  3. 在執(zhí)行過程中, 當(dāng)某個跟蹤器啟動 tracepoint 時(該 tracepoint 可能已經(jīng)被其他跟蹤器啟用):

a. 在 tracepoint 回調(diào)函數(shù)數(shù)組中插入一條新的 tracepoint 回調(diào)函數(shù), 以 RCU 的形式進(jìn)行同步更新

b. 如果 tracepoint 之前出于禁用狀態(tài),nop 指令的地址會被重寫為跳轉(zhuǎn)到蹦床函數(shù)的指令.

  1. 當(dāng)跟蹤器禁用某個 tracepoint 時:

a. 在 tracepoint 回調(diào)函數(shù)數(shù)組中刪除該跟蹤器的回調(diào)函數(shù), 以 RCU 的形式進(jìn)行

同步更新

b. 如果最后一個回調(diào)函數(shù)也被刪除了, 那么將 jmp 再重寫為 nop 指令.

這樣可以最小化出于禁用狀態(tài)的 tracepoint 的性能開銷, 幾乎可以忽略不計(jì).

tracepoint 有以下兩個接口:

  • 基于 Ftrace 的接口, 通過/sys/kernel/debug/tracing/events: 每個 traceppoint 子系統(tǒng)有一個子目錄, 每個 tracepoint 對應(yīng)該子目錄下的一個文件(通過向這些文件中寫入內(nèi)容來開啟或者關(guān)閉跟蹤點(diǎn)).

  • perf_event_open(): 這是 perf(1) 工具一直以來使用的接口, 近來 BPF 跟蹤也開始
    用(通過 perf_tracepoint PMU).

kprobes

kprobes 提供了針對內(nèi)核的動態(tài)插樁支持.kprobes 可以對任何內(nèi)核函數(shù)進(jìn)行插樁, 它還可以對函數(shù)內(nèi)部的指令進(jìn)行插樁.它可以實(shí)時在生產(chǎn)環(huán)境系統(tǒng)中啟用, 不需要重啟系統(tǒng),也不需要以特殊方式重啟內(nèi)核.這是一項(xiàng)令人驚嘆的能力, 這意味著我們可以對 Linux 中數(shù)以萬計(jì)的內(nèi)核函數(shù)任意插樁.根據(jù)需要生成指標(biāo).

kprobes 技術(shù)還有另外一個接口, 即 kretprobes(其實(shí)還有一個 jprobes 接口, 但已經(jīng)廢棄不再維護(hù)), 用來對 Linux 內(nèi)核函數(shù)返回時進(jìn)行插樁以獲取返回值.當(dāng)用 kprobes 和kretprobes 同時對同一個內(nèi)核函數(shù)進(jìn)行插樁時, 可以使用時間戳來記錄函數(shù)執(zhí)行的時長,這在性能分析中是一個重要的指標(biāo).

使用 kprobes 對內(nèi)核進(jìn)行動態(tài)插樁的過程如下:

A. 對于一個 kprobe 插樁來說:

  1. 把要插樁的目標(biāo)地址中的字節(jié)內(nèi)容復(fù)制并保存(為的是給單步斷點(diǎn)指令騰出

位置).

  1. 以單步中斷指令覆蓋目標(biāo)地址: 在 x86_64 上是 int3 指令.(如果 kprobes 開
    了優(yōu)化, 則使用 jmp 指令).

  2. 當(dāng)指令流執(zhí)行到斷點(diǎn)時, 斷點(diǎn)處理函數(shù)會檢查這個斷點(diǎn)是否是由 kprobes 注冊的, 如果是, 就會執(zhí)行 kprobes 處理函數(shù).

  3. kprobes 處理函數(shù)執(zhí)行完后原始的指令會接著執(zhí)行, 指令流繼續(xù).

  4. 當(dāng)不再需要(deactivate)kprobes 時, 原始的字節(jié)內(nèi)容會被復(fù)制回目標(biāo)地址上,

這樣這些指令就回到了它們的原始狀態(tài).

B. 如果這個 kprobe 是一個 Ftrace 已經(jīng)做過插樁的地址(一般位于函數(shù)的入口處),
那么可以基于 Ftrace 進(jìn)行 kprobe 優(yōu)化, 過程如下:

  1. 將一個 Ftrace kprobe 處理函數(shù)注冊為對應(yīng)函數(shù)的 Ftrace 處理器

  2. 當(dāng)在函數(shù)起始處執(zhí)行內(nèi)建入口函數(shù)時 (在 x86 架構(gòu) gcc 4.6 下是 fentry),
    該函數(shù)會調(diào)用 Ftrace,Ftrace 接下來會調(diào)用 kprobe 處理函數(shù).

  3. 當(dāng) kprobe 不在會被調(diào)用時, 從 Ftrace 中移除 Ftrace-kprobe 處理函數(shù).

C. 如果是一個 kretprobe:

  1. 對函數(shù)入口進(jìn)行 kprobe 插樁.

  2. 當(dāng)函數(shù)入口被 kprobe 命中是, 將函數(shù)返回地址保存并替換為一個“蹦床”(tram-
    poline)函數(shù)地址.

  3. 當(dāng)函數(shù)最終返回時,CPU 將控制權(quán)交給蹦床函數(shù)處理.

  4. 在 kretprobe 處理完成之后在返回到之前保存的地址.

  5. 當(dāng)不再需要 kretprobe 時, 函數(shù)入口的 krobe 和 kretprobe 處理函數(shù)就被移除了.

根據(jù)當(dāng)前系統(tǒng)和體系結(jié)構(gòu)的一些其它的因素,kprobe 的處理過程可能需要禁止搶占和中斷.另外在線修改內(nèi)核函數(shù)體是風(fēng)險(xiǎn)極大的操作, 但是 kprobe 從設(shè)計(jì)上就已經(jīng)保證了自身的安全性.在設(shè)計(jì)中包括了一個不允許 kprobe 動態(tài)插樁的函數(shù)黑名單, 其中 kprobe自身就在名單之列, 可防止出現(xiàn)遞歸陷阱的情形.kprobe 同時利用的是安全的斷電插入技術(shù), 比如使用 x86 內(nèi)置的 int3 指令.當(dāng)使用 jmp 指令時, 也會先調(diào)用 stop_machine()函數(shù), 來保證在修改代碼的時候其它 CPU 核不會執(zhí)行指令.在實(shí)踐中, 最大的風(fēng)險(xiǎn)是, 在需要對一個執(zhí)行頻率非常高的函數(shù)插樁時, 每次對函數(shù)調(diào)用小的開銷都會疊加, 這會對系統(tǒng)的性能產(chǎn)生一定的影響.

kprobe 在某些 ARM 64 位系統(tǒng)上不能正常工作, 出于安全性的考慮, 這些平臺上的內(nèi)核代碼區(qū)不允許被修改.

有以下三種接口可以訪問 kprobe:

  • kprobe API: 如 register_kprobe() 等

  • 基于 Ftrace 的, 通過/sys/kernel/debug/tracing/kprobe_events: 通過向這個文件寫入字符串, 可以配置開啟和停止 kprobe

  • perf_event_open(): 與 perf(1) 工具所使用的一樣, 近來 BPF 跟蹤工具也開始使用
    這些函數(shù).在 Linux 內(nèi)核 4.17 中加入了相關(guān)的支持(perf_kprobe PMU).

uprobe

uprobe 提供了用戶態(tài)程序的動態(tài)插樁, 與 kprobe 相似, 只是在用戶態(tài)程序使用.up- robe 可以在用戶態(tài)程序的以下位置插樁: 函數(shù)入口, 特定偏移處, 以及函數(shù)返回處.uprobe也是基于文件的, 當(dāng)一個可執(zhí)行文件中的一個函數(shù)被跟蹤時, 所有使用到這個文件的進(jìn)程都會被插樁, 包括奈雪兒尚未啟動的進(jìn)程.這樣就可以在全系統(tǒng)范圍內(nèi)跟蹤系統(tǒng)庫調(diào)用.

uprobe 的工作方式和 kprobe 相似: 將一個快速斷點(diǎn)指令插入目標(biāo)指令處, 該指令將控制權(quán)轉(zhuǎn)交給 uprobe 處理函數(shù).當(dāng)不再需要 uprobe 時, 目標(biāo)指令被恢復(fù)成原來的樣子.對于 uretprobe, 也是在函數(shù)入口處使用 uprobe 進(jìn)行插樁, 而在函數(shù)返回之前則使用一個蹦床函數(shù)對返回地址進(jìn)行劫持, 和 kretprobe 類似.gdb 就使用了這種方式來調(diào)試應(yīng)用程序.

uprobe 有以下兩個可以使用的接口:

  • 基于 Ftrace 的, 通過/sys/kernel/debug/tracing/uprobe_events: 可以通過向這個配
    置文件中寫入特定的字符串來打開或者關(guān)閉 uprobe.

  • perf_event_open(): 和 perf(1) 工具的用法一樣,BPF 跟蹤工具也開始頻繁地這樣使
    用了.相關(guān)的支持已經(jīng)加入 Linux 內(nèi)核 4.17 版本(perf_uprobe PMU).

在內(nèi)核中同時包含了 register_uprobe_event() 函數(shù), 和 register_kprobe() 函數(shù)類似, 但是并沒有以 API 地形式顯露.

USDT

用戶態(tài)預(yù)定義靜態(tài)跟蹤 (user-level statically defined tracing, USDT) 提供了一個用戶空間版本地 tracepoint 機(jī)制.USDT 的與眾不同之處在于, 它依賴于外部的系統(tǒng)跟蹤器來喚起.如果沒有外部跟蹤器, 應(yīng)用中的 USDT 點(diǎn)不會做任何事情, 也不會開啟.給應(yīng)用程序添加 USDT 探針有兩種可選方式: 通過 systemtap-sdt-dev 包提供的頭文件和工具或者使用自定義的頭文件.這些探針定義了可以被放置在代碼中各個邏輯位置上的宏, 以此生成 USDT 的探針.

當(dāng)編譯應(yīng)用程序時, 在 USDT 探針的地址放置了一個 nop 指令.當(dāng) USDT 探針被激活時, 這個地址會由內(nèi)核使用 uprobe 動態(tài)地將器修改位一個斷點(diǎn)指令.當(dāng)該斷點(diǎn)被觸發(fā)時, 內(nèi)核會執(zhí)行相應(yīng)的 BPF 程序.BPF 跟蹤器前端 BCC 和 bpftrace 均支持 USDT, 如果不使用這些前端, 則不能直接使用 USDT, 需要前端自己實(shí)現(xiàn) USDT 支持.

動態(tài) USDT

上一節(jié)介紹的 USDT 探針技術(shù)是需要添加到源代碼并編譯到最終的二進(jìn)制文件中的, 在插樁點(diǎn)留下 nop 指令, 在 ELF notes 段中保存元數(shù)據(jù).然而有一些編程語言, 比如Java, 是在運(yùn)行是的時候解釋或者編譯的.動態(tài) USDT 可以用來給 Java 代碼添加插樁點(diǎn).

JVM 已經(jīng)在內(nèi)置的 C++ 代碼中包含了許多 USDT 探針—-比如對 GC 事件、類加載, 以及其它高級行為.這些 USDT 探針會對 JVM 的函數(shù)進(jìn)行插樁.但是 USDT 探針不能被添加到動態(tài)進(jìn)行編譯的 Java 代碼中.USDT 需要一個提前編譯好的、帶一個包含了探針描述的 notes 段的 ELF 文件, 著對于以 JIT(just-in-time)方式編譯的 Java 代碼來說是不存在的.

動態(tài) USDT 以如下方式解決該問題:

  • 預(yù)編譯一個共享庫, 帶著想要內(nèi)置在函數(shù)中的 USDT 探針.這個共享庫可以使用C/C++ 語言編寫, 其中有一個針對 USDT 探針的 ELF notes 區(qū)域.它可以像其它USDT 探針一樣被插樁.

  • 在需要時, 使用 dlopen(3) 加載該動態(tài)庫.

  • 針對目標(biāo)語言增加對該共享庫的調(diào)用.這些可以使用一個合適該語言的 API, 以便

隱藏底層的共享庫調(diào)用.

Matheus Marchini 已經(jīng)為 Node.jsPython 實(shí)現(xiàn)了一個叫做 libstapsdt 的庫(一個新的 libusdt 庫正在開發(fā)中), 一提供這些語言中定義和呼叫 USDT 探針的方法.對其他語言的支持可以通過封裝這個庫實(shí)現(xiàn).libstapsdt 會在運(yùn)行時自動創(chuàng)建包含 USDT 探針和 ELF notes 區(qū)域的共享庫, 而且它會將這些區(qū)域映射到運(yùn)行著的程序的地址空間.

BPF raw tracepoint

BPF raw tracepoint 是一種新的 tracepoint, 相比于內(nèi)核的 tracepoint,BPF raw tra- cepoint 接口向 tracepoint 線路原始參數(shù), 這樣可以避免需要創(chuàng)建穩(wěn)定的 tracepoint 參數(shù)從而導(dǎo)致的開銷, 因?yàn)檫@些參數(shù)可能壓根不被使用.

PMC

性能監(jiān)控計(jì)數(shù)器(Performance monitoring counter, PMC)還有其它的一些名字, 比如性能觀測計(jì)數(shù)器(Performance instrumentation counter, PIC), 性能監(jiān)控單元事件(Per- formance monitoring unit event,PMU event).這些名字指的都是同一個東西: 處理器上的硬件可編程計(jì)數(shù)器.

PMC 數(shù)量眾多,Intel 從中選擇了 7 個作為“架構(gòu)集合”, 這些 PMC 會對一些核心功能提供全局預(yù)覽.

f6918964-711a-11ed-8abf-dac502259ad0.png

圖 1.4. Intel 架構(gòu)上的 PMC

PMC 是性能分析領(lǐng)域的至關(guān)重要的資源.只有通過 PMC 才能測量 CPU 指令執(zhí)行的效率、CPU 緩存的命中率、內(nèi)存/數(shù)據(jù)互聯(lián)和設(shè)備總線的利用率, 以及阻塞的指令周期等.盡管有數(shù)百個可用的 PMC 可用, 但在任一時刻, 在 CPU 中只允許固定數(shù)量(可能只有 6 個)的寄存器進(jìn)行讀?。趯?shí)現(xiàn)中需要選擇通過這 6 個寄存器來讀取哪些 PMC, 過著可以以循環(huán)采樣的方式覆蓋多個 PMC 集合(Linux perf(1) 工具可以自動支持這種循環(huán)采樣).

PMC 可以工作在下面兩種模式中.

  • 計(jì)數(shù): 在此模式下,PMC 能跟蹤事件發(fā)生的頻率, 只要內(nèi)核有需要, 就可以隨時讀取,

比如每秒讀取一次.這種模式的開銷幾乎為零.

  • 溢出采樣: 此模式下,PMC 在所監(jiān)控的事件發(fā)生一定次數(shù)時通知內(nèi)核, 這樣內(nèi)核可以獲取額外的狀態(tài).監(jiān)控的事件可能會以每秒百萬、億級別的頻率發(fā)生, 如果每次事件都進(jìn)行中斷會導(dǎo)致系統(tǒng)性能下降到不可用.解決方案是利用一個也可編程的計(jì)數(shù)器進(jìn)行采樣, 具體來說是當(dāng)計(jì)數(shù)器溢出時就像內(nèi)核發(fā)信號

由于存在中斷延遲或者亂序執(zhí)行, 溢出采樣可能不能正確地記錄觸發(fā)事件發(fā)生時的指令指針.對于 CPU 周期性能分析來說, 這類“打滑”可能不是什么問題, 但是對于測量另外一些事件, 比如緩存未命中率, 這些采樣的指令指針就必須是精確的.

Intel 開發(fā)了一種解決方案, 叫做精確事件采樣(precise event-based sampling, PEBS).PEBS 使用硬件緩沖區(qū)來記錄 PMC 事件發(fā)生時正確的指令指針.Linux 的 perf events 機(jī)制支持 PEBS.

perf_events

perf_events 是 perf(1) 命令所以來的采樣和跟蹤機(jī)制.BPF 跟蹤工具可以調(diào)用 perf_events來使用它的特性.BCC 和 bpftrace 先是使用 perf_events 作為它們的喚醒緩沖區(qū), 然后
又增加了對 PMC 的支持, 現(xiàn)在又通過 perf_event_open() 來對所有的 perf_events 事件進(jìn)行觀測.同時 perf(1) 也開發(fā)了一個使用 BPF 的接口, 這讓 perf(1) 成為了一個 BPF前端也是唯一內(nèi)置在 linux 中的 BPF 前端.perf(1) 的 BPF 功能還在開發(fā)中, 目前在使用上還有一些不方便的地方.

1.3.2 跟蹤框架

Ftrace

f6a29682-711a-11ed-8abf-dac502259ad0.png

圖 1.5. Ftrace 跟蹤框架

Ftrace 是內(nèi)核 hacker 的最佳搭檔, 它是內(nèi)核內(nèi)置的, 支持內(nèi)核 tracepoint,kprobe,uprobe事件.提供事件跟蹤, 可選的過濾器和參數(shù), 事件計(jì)數(shù)和定時, 在內(nèi)核空間中的數(shù)據(jù)匯總.它通過/sys 文件系統(tǒng)訪問, 是專門針對 root 用戶的.有一個專門的 Ftrace 前端 trace-cmd.不足之處是 Ftrace 不是可編程的, 用戶只能從內(nèi)核緩沖區(qū)讀取事件原始數(shù)據(jù)然后在用戶態(tài)進(jìn)行后續(xù)處理.
perf_event

f6c34b34-711a-11ed-8abf-dac502259ad0.png

圖 1.6. perf-event 跟蹤框架

perf_event 是 Linux 用戶使用的主要跟蹤框架, 其源代碼位于內(nèi)核源代碼中, 其跟蹤器前端 perf(1)Linux 用戶最常用的性能分析工具.Ftrace 能做的事情 perf_ 幾乎都能做到.同時 perf_event 具有更強(qiáng)的安全檢查, 這也使得 perf_event 不容易被 hack.它可以用于采樣,CPU 性能計(jì)數(shù)器, 用戶態(tài)回棧.它還支持多用戶的并發(fā)使用.

perf_event 支持的事件源

f6da1986-711a-11ed-8abf-dac502259ad0.png

圖 1.7. perf_event 支持的事件源

SystemTap

SystemTap 是最強(qiáng)大的跟蹤器.它幾乎無所不能: 采樣(剖析),tracepoints,kprobes,uprobes,USDT,可編程等.它通過把程序編譯成模塊加載進(jìn)內(nèi)核來支持可編程的事件跟蹤, 這是一種極其
安全的可編程事件跟蹤實(shí)現(xiàn)方案.以使用 SystemTap 跟蹤一個 kprobe 事件為例, 其基本步驟如下:

  • 決定要跟蹤的事件類型:kprobe 事件

  • 編寫“systemtap 程序”并編譯成內(nèi)核模塊

  • 內(nèi)核模塊被插入內(nèi)核以后會創(chuàng)建 kprobe 探針, 當(dāng)事件觸發(fā)時內(nèi)核調(diào)用模塊* 內(nèi)核模塊使用 relayfs 或者其它的方式將結(jié)果打印到用戶空間

f75fd2d8-711a-11ed-8abf-dac502259ad0.png

圖 1.8. systemtap 原理

1.3.3 跟蹤前端

跟蹤器前端往往不與跟蹤框架一一對應(yīng), 一個跟蹤器前端可能會使用多個跟蹤框架的接口, 常用的跟蹤其前端有:perf、trace-cmd、perf-tools 等

第二章 BPF

2.1 基本概念

BPF是 Berkeley Packet Filter 的縮寫, 這項(xiàng)技術(shù)誕生與 1992 年, 其作用是提升網(wǎng)絡(luò)包過濾工具的性能.2013 年,Alexei Starrovoitov 向 Linux 社區(qū)提交了重新實(shí)現(xiàn) BPF 的內(nèi)核補(bǔ)丁, 經(jīng)過他和 Daniel Borkmann 的共同完善, 相關(guān)工作在 2014 年正式并入 Linux 內(nèi)核主線, 此舉將 BPF 變成了一個更通用的執(zhí)行引擎.拓展后的 BPF 通??s寫為 eBPF,但官方的縮寫仍是 BPF, 事實(shí)上內(nèi)核只有一個執(zhí)行引擎, 即 BPF(拓展后的 BPF), 它同時支持“經(jīng)典”的 BPF 程序也支持拓展后的 BPF, 若無特殊說明, 我們所說的 BPF 就是指內(nèi)核中的 BPF 執(zhí)行引擎.

BPF 目標(biāo)文件本質(zhì)上就是 ELF 文件, 據(jù)我所知目前只有使用 LLVM 指定 target 為BPF 可以編譯出 BPF 目標(biāo)文件.它與通常的目標(biāo)文件的差別在于編譯后的目標(biāo)文件是BPF 虛擬機(jī)的字節(jié)碼程序, 同時它還包含了以 BTF 格式保存的調(diào)試信息以及重定位信息等.

BTF(BPF Type Format) 是編譯成 BPF 目標(biāo)文件的 BPF 程序中用記錄 BPF 程序的調(diào)試、鏈接、重定位以及 BPF 映射表信息的元數(shù)據(jù)格式.其記錄的信息的作用類似于ELF 文件中的 DWARF 段、notes 段和重定位相關(guān)的段.通常一個編譯成 BPF 目標(biāo)文件的 BPF 程序中會有多個以.btf 為前綴命名的段用于記錄以上信息.有關(guān) BTF 的詳細(xì)內(nèi)容請參考BTF 文檔.

BPF 程序通常是指用戶編寫的要被注入內(nèi)核的跟蹤器程序.內(nèi)核態(tài)程序直接在內(nèi)核態(tài)對內(nèi)核收集的事件信息進(jìn)行匯總統(tǒng)計(jì)等處理之后保存在 BPF 映射表,BPF 用戶態(tài)程序直接讀取 BPF 映射表中已經(jīng)處理好的數(shù)據(jù)就可以直接或者稍加處理之后進(jìn)行展示.有時BPF 程序也指 BPF 內(nèi)核態(tài)程序和用戶態(tài)程序, 把二者視為一個整體.用戶態(tài)程序負(fù)責(zé)將內(nèi)核態(tài)程序加載鏈接到內(nèi)核并附著(“attach”)到指定的事件上, 讀取內(nèi)核態(tài)程序處理好保存在 BPF 映射表中的數(shù)據(jù)并進(jìn)行展示之類的上層操作以及退出前的清理操作.BPF內(nèi)核態(tài)程序通常由多個函數(shù)定義和 BPF 映射表定義構(gòu)成.每一個函數(shù)屬于一個特定的BPF 程序類型,BPF 程序類型是 BPF 函數(shù)和該函數(shù)所關(guān)聯(lián)的事件類型之間的接口.也就是說函數(shù)的程序類型和它所關(guān)聯(lián)的事件類型之間有一種對應(yīng)關(guān)系, 不過這種對應(yīng)關(guān)系并不非常嚴(yán)格, 一種程序類型可以附著(“attach”)到多種事件之上, 一種事件也可以被多種不同 BPF 程序類型的函數(shù)附著.同時 BPF 程序類型也限制了函數(shù)能夠使用的 BPF虛擬機(jī)字節(jié)碼指令集, 這有利于將函數(shù)功能限制在其類型所定義的功能范圍之內(nèi), 提高安全性.關(guān)于 BPF 程序類型的詳細(xì)介紹請參考Linux 手冊.

BPF 映射表 (BPF Map)BPF 映射表是 BPF 程序使用的內(nèi)核緩沖區(qū), 用于保存事件數(shù)據(jù), 類似于 MySQL 的表.BPF 映射表的數(shù)據(jù)類型有很多種, 實(shí)現(xiàn)對用戶都是是透明的.描述 BPF 映射表的信息記錄在 BTF 格式的元數(shù)據(jù)中.BPF 程序只能使用 BPF 執(zhí)行引擎提供的內(nèi)核接口(這些內(nèi)核接口是 BPF 系統(tǒng)調(diào)用的一部分)來創(chuàng)建、訪問、修改和刪除 BPF 映射表.關(guān)于 BPF 映射表的詳細(xì)介紹請參考Linux 手冊

BPF 系統(tǒng)調(diào)用是 BPF 執(zhí)行引擎提供的一套內(nèi)核接口, 用于 BPF 用戶態(tài)程序與 BPF執(zhí)行引擎之間的交互以及創(chuàng)建、訪問、修改和刪除 BPF 映射表.關(guān)于 BPF 系統(tǒng)調(diào)用的詳細(xì)介紹請參考Linux 手冊和Linux 內(nèi)核文檔.

BPF 幫助函數(shù)(BPF helpers)也是一套內(nèi)核接口, 大部分 BPF 幫助函數(shù)作用和BPF 系統(tǒng)調(diào)用相同,BPF 幫助函數(shù)和 BPF 系統(tǒng)調(diào)用有很多同名且功能也完全相同的接口.區(qū)別在于參數(shù)不同, 而且 BPF 幫助函數(shù)僅由 BPF 內(nèi)核態(tài)程序調(diào)用.有關(guān) BPF 幫助函數(shù)的詳細(xì)介紹請參考Linux 手冊

2.2 是什么, 以及為什么

如圖 1.2所示:BPF 是一種最新的 Linux 跟蹤技術(shù), 可用于網(wǎng)絡(luò)、可觀測性和安全三個領(lǐng)域, 我們將主要關(guān)心其在可觀測性領(lǐng)域的運(yùn)用.

如圖 2.1所示, 作為可觀測性工具,BPF 支持上一章所述的全部事件源, 并且它是可編程的.

f76ffe42-711a-11ed-8abf-dac502259ad0.png

圖 2.1. BPF 支持的事件源

BPF 跟蹤可以在整個軟件范圍內(nèi)提供能見度, 允許我們隨時根據(jù)需要開發(fā)新的工具和監(jiān)測功能.在生產(chǎn)環(huán)境中可以立刻部署 BPF 跟蹤程序, 不需要重啟系統(tǒng), 也不需要以特殊方式重啟應(yīng)用軟件.圖 2.2展示了一個通用的系統(tǒng)軟件棧的各部分及相應(yīng)的 BPF 跟蹤工具.

f788e902-711a-11ed-8abf-dac502259ad0.png

圖 2.2. BPF 跟蹤工具提供的能見度

表 2.1列出了傳統(tǒng)工具, 同時也列出了 BPF 工具是否支持對這些組件進(jìn)行監(jiān)測.傳統(tǒng)工具提供的信息可以作為性能分析的起點(diǎn), 后續(xù)則可以通過 BPF 跟蹤工具做更見深入的調(diào)查.

表 2.1. 傳統(tǒng)分析工具 VS BPF 工具

f7c60832-711a-11ed-8abf-dac502259ad0.png

圖 2.3是 perf_event 和 BPF 采樣流程的對比.可以看到相比 perf_event,BPF 大大精簡了流程, 避免了內(nèi)核與用戶空間之間大量不必要的數(shù)據(jù)拷貝以及磁盤 IO.

f7e675ae-711a-11ed-8abf-dac502259ad0.png

圖 2.3BPF vsperf_event

2.3 BPF 虛擬機(jī)和運(yùn)行時

簡單來說 BPF 提供了一種在各種內(nèi)核事件和應(yīng)用程序事件發(fā)生時運(yùn)行一段小程序的機(jī)制.類似 JavaScript 允許網(wǎng)站在瀏覽器中發(fā)生某事件時運(yùn)行一段小程序,BPF 則允許內(nèi)核在系統(tǒng)和應(yīng)用程序事件(如磁盤 IO 事件)發(fā)生時運(yùn)行一段小程序(后面我們將看到這是如何實(shí)現(xiàn)的), 這就催生了新的編程技術(shù), 該技術(shù)將內(nèi)核變得可編程, 允許用戶(包括非專業(yè)內(nèi)核開發(fā)人員)定制和控制他們的系統(tǒng), 以解決現(xiàn)實(shí)問題.

BPF 是一項(xiàng)靈活為高效的技術(shù), 由指令集(有時也稱 BPF 字節(jié)碼)、存儲對象和輔助函數(shù)等及部分組成.由于它采用了虛擬指令集規(guī)范, 因此也可以將它視作一種虛擬機(jī)實(shí)現(xiàn).

BPF 指令由 Linux 內(nèi)核的 BPF 運(yùn)行時模塊執(zhí)行, 具體來說, 該運(yùn)行時模塊提供兩種執(zhí)行機(jī)制: 一個解釋器和一個將 BPF 指令動態(tài)轉(zhuǎn)換為本地化指令的即時編譯器(JIT,just- in-time).注意只有當(dāng) BPF 程序通過解釋器執(zhí)行時其實(shí)現(xiàn)才是一種虛擬機(jī).當(dāng) JIT 啟用之后 BPF 指令將通過 JIT 編譯后像任何其它本地內(nèi)核代碼一樣, 直接在處理器上運(yùn).如果沒有啟用 JIT 則經(jīng)由解釋器執(zhí)行.相比于解釋器執(zhí)行,JIT 執(zhí)行的性能更能好, 一些發(fā)行版在 x86 架構(gòu)上 JIT 是默認(rèn)開啟的, 完全移除了內(nèi)核中解釋器的實(shí)現(xiàn).

這里暫停一下繼續(xù)啰嗦幾句, 對編譯原理不是很了解的讀者可能不能快速地理解上一段闡述.無論是解釋器解釋執(zhí)行還是編譯執(zhí)行, 源代碼都需要被翻譯成機(jī)器代碼然后由CPU 執(zhí)行, 二者地區(qū)別在于:1)從用戶地角度看, 解釋執(zhí)行把用戶輸入和源代碼作為解釋器的輸入, 解釋器解釋運(yùn)行之后直接給出在用戶的輸入下, 源代碼的執(zhí)行結(jié)果.請注意解釋器的輸入是源代碼本身和源代碼的輸入.而輸出則是源代碼的輸出.而編譯執(zhí)行則至少分為兩個階段, 第一個階段編譯的輸入是源代碼, 輸出是可執(zhí)行文件, 可執(zhí)行文件是可以保存在磁盤中被重復(fù)讀取執(zhí)行的文件.只要源代碼沒有修改, 第一階段就只需要執(zhí)行一次.第二階段才是運(yùn)行可執(zhí)行程序.2)解釋器在解釋執(zhí)行源代碼是也需要編譯源代碼得到機(jī)器代碼, 但是其編譯結(jié)果是作為以一種中間數(shù)據(jù)保存在內(nèi)存中, 這意味著每次解釋執(zhí)行一個源代碼, 解釋器都要編譯一次源代碼, 因?yàn)榻忉屍鞑惠敵鰴C(jī)器代碼到磁盤.回到上一段,JIT 會把加載進(jìn)內(nèi)核的 BPF 字節(jié)碼程序編譯成機(jī)器代碼并保存下來, 每一次事件觸發(fā)運(yùn)行 BPF 程序時直接運(yùn)行機(jī)器代碼即可, 而 BPF 解釋器保存的則是 BPF 字節(jié)碼程序, 每次事件觸發(fā)都需要先將字節(jié)碼編譯成機(jī)器代碼然后再運(yùn)行, 因此 JIT 比解釋器性能更好.了解 Java 的讀者或許已經(jīng)想到 BPF 運(yùn)行時和 Java 運(yùn)行時的原理完全相同,Java同樣存在解釋執(zhí)行和 JIT 編譯執(zhí)行兩種執(zhí)行方式.

在實(shí)際執(zhí)行之前,BPF 指令必須先通過驗(yàn)證器的安全性檢查, 以確保 BPF 程序自身不會崩潰或者損壞內(nèi)核.Linux BPF 運(yùn)行時的各模塊的建構(gòu)如圖 2.4所示, 它展示了 BPF指令如果通過 BPF 驗(yàn)證器驗(yàn)證, 再有 BPF 虛擬機(jī)執(zhí)行.BPF 虛擬機(jī)的實(shí)現(xiàn)既包括一個解釋器又包括一個 JIT 編譯器:JIT 編譯器負(fù)責(zé)生成處理器可直接執(zhí)行的機(jī)器指令.驗(yàn)證其會拒絕那些不安全的操作, 這包括對無界循環(huán)的檢查:BPF 必須在有限的時間內(nèi)完成.同時 BPF 程序還有大小限制, 最初的 BPF 總指令數(shù)限制是 4096, 不過 Linux 5.2 內(nèi)核極大地提升了這個值的上限, 因此在 Linux 5.2 以上的內(nèi)核中這不再是一個需要關(guān)心的問題.

f7fc1d0a-711a-11ed-8abf-dac502259ad0.png

圖 2.4. BPF 運(yùn)行時的內(nèi)部結(jié)構(gòu)

BPF 可以利用輔助函數(shù)獲取內(nèi)核狀態(tài), 利用 BPF 映射表進(jìn)行存儲.BPF 程序在特定事件發(fā)生時執(zhí)行, 包括 kprobes、uprobes、內(nèi)核跟蹤點(diǎn)(tracepoint)、PMC 和 perf events事件.

BPF 程序的主要運(yùn)行過程如下:

  1. BPF 用戶態(tài)程序調(diào)用 BPF_PROG_LOAD 系統(tǒng)調(diào)用加載已經(jīng)編譯成 BPF 字節(jié)碼的 BPF 內(nèi)核態(tài)程序.根據(jù) BPF 程序中的 BPF 映射表定義為 BPF 映射表分配內(nèi)存

  2. BPF 用戶態(tài)程序調(diào)用 BPF_PROG_ATTACH 系統(tǒng)調(diào)用將 BPF 內(nèi)核態(tài)程序和事件關(guān)聯(lián)起來并向內(nèi)核注冊事件

  3. 事件被觸發(fā), 內(nèi)核收集“事件上下文”傳遞給關(guān)聯(lián)到該事件的 BPF 內(nèi)核態(tài)程序并執(zhí)行該 BPF 程序.BPF 內(nèi)核態(tài)程序處理內(nèi)核收集的數(shù)據(jù)將處理結(jié)果保存在 BPF 映射表

  4. 用戶態(tài)程序調(diào)用 BPF 系統(tǒng)調(diào)用讀取映射表中的數(shù)據(jù)并做進(jìn)一步的處理.

  5. 用戶提程序邏輯結(jié)束以后卸載 BPF 內(nèi)核態(tài)程序、注銷事件并釋放 BPF 映射表內(nèi)存.

    f8093756-711a-11ed-8abf-dac502259ad0.png

圖 2.5. BPF 程序執(zhí)行流程

2.4 CO-RE

BPF 程序的可移植性是指在某個內(nèi)核版本中可以成功加載、驗(yàn)證、編譯、執(zhí)行的BPF 程序也能夠在不加修改的前提下在其它的內(nèi)核版本中成功地加載、驗(yàn)證、編譯和執(zhí)行.BPF 程序運(yùn)行于內(nèi)核態(tài)可以直接訪問內(nèi)核的內(nèi)存和內(nèi)核數(shù)據(jù)結(jié)構(gòu)使得 BPF 成為一個強(qiáng)大而靈活的工具, 但同時它也導(dǎo)致了 BPF 與生俱來的可移植問題.因?yàn)椴煌姹镜膬?nèi)核, 內(nèi)核數(shù)據(jù)結(jié)構(gòu)的定義是發(fā)展變化的, 成員順序變化、重命名、從一個結(jié)構(gòu)體移動到另一個結(jié)構(gòu)體等均會導(dǎo)致內(nèi)存訪問出錯, 這種錯誤是致命的.舉一個例子假設(shè)我們在 BPF內(nèi)核態(tài)程序中訪問了 struct task_struct 的成員 pid, 目標(biāo)平臺的內(nèi)核版本在 pid 成員前面插入了一個新的成員, 當(dāng)我們編譯 BPF 內(nèi)核態(tài)程序后, 對 pid 的成員訪問將被翻譯成task_struct 結(jié)構(gòu)體的起始地址加上成員偏移量.而目標(biāo)平臺在 pid 之前插入了新的成員因此目標(biāo)平臺的 pid 成員偏移量發(fā)生了變化, 很顯然如果次程序在目標(biāo)平臺運(yùn)行將導(dǎo)致內(nèi)存訪問出錯.

解決 BPF 程序的可移植性問題的一種解決方案是依賴 BCC(BPF Compiler Collec- tion).在 BCC 中,BPF 內(nèi)核態(tài)源代碼被當(dāng)作一個字符串, 這個字符串被 BPF 用戶態(tài)程序當(dāng)作普通的數(shù)據(jù)保存.當(dāng) BPF 用戶態(tài)程序被編譯之后在目標(biāo)平臺上被 BCC 運(yùn)行時,BCC調(diào)用目標(biāo)平臺的 Clang/LLVM 來編譯 BPF 內(nèi)核態(tài)程序源代碼, 因?yàn)?BPF 內(nèi)核態(tài)程序源代碼是在目標(biāo)平臺上編譯成字節(jié)碼的, 因此 BPF 內(nèi)核態(tài)程序可以正確地訪問內(nèi)核數(shù)據(jù)結(jié)構(gòu).這個方案的本質(zhì)是延遲 BPF 內(nèi)核態(tài)程序的編譯, 直到內(nèi)核相關(guān)的信息全部已知之后才開始編譯.不過這種解決方案不夠好,Clang/LLVM 是非常龐大地庫, 而且也需要大量地系統(tǒng)資源, 同時還需要目標(biāo)平臺安裝了內(nèi)核頭文件, 而且使用這種方式開發(fā)的 BPF 程序的調(diào)試和開發(fā)迭代過程也是十分的繁瑣.尤其是在嵌入式這種資源有限的系統(tǒng), 幾乎不會有軟件是在目標(biāo)平臺上直接編譯的.

一種優(yōu)美的解決方式是 CO-RE(Code Once,Run Everywhere).BPF CO-RE 依賴于以下組件之間的協(xié)作:

  • BTF 記錄了內(nèi)核、BPF 程序類型和源代碼重定位的關(guān)鍵信息.

  • 編譯器(Clang/LLVM)提供了生成和記錄 BPF C 源代碼的重定位信息的方法

  • BPF 加載器(libbpf)將內(nèi)核的 BTF 信息和 BPF 程序捆綁在一起對 BPF 字節(jié)碼

程序進(jìn)行重定位以適配目標(biāo)主機(jī)的內(nèi)核版本

簡單來說 CO-RE 是使用 BTF 記錄源代碼的重定位信息和內(nèi)核數(shù)據(jù)結(jié)構(gòu)的相關(guān)信息, 由libbpf 在加載時進(jìn)行重定位來解決 BPF 程序的可移植問題的.仍然以前面 task_struct結(jié)構(gòu)體的 pid 成員訪問為例, 在 CO-RE 的解決方案中,BPF 字節(jié)碼中對 pid 的訪問被記錄為一個重定位點(diǎn).當(dāng) libbpf 加載 BPF 字節(jié)碼程序時, 會根據(jù)目標(biāo)平臺的內(nèi)核數(shù)據(jù)結(jié)構(gòu)信息進(jìn)行重定位.

更多關(guān)于 CO-RE 的知識可以參考博客

2.5 bpftool

BPF 目標(biāo)文件時 BPF 字節(jié)碼程序,BPF 字節(jié)碼是 BPF 虛擬機(jī)的“機(jī)器指令”.類似于 GCC 編譯工具鏈中的 objdump,addr2line 和 readelf 等處理真是硬件平臺的 ELF 文件的工具,BPF 目標(biāo)文件這種 ELF 文件也有功能與 objdump,addr2line 和 readelf 相同的工具, 這個工具就是 bpftool, 它位于 Linux 源代碼的 tools/bpf/bpftool 中.它可以用來查看可操作 BPF 對象, 即 BPF 程序和 BPF 映射表.bpftool 是一個很強(qiáng)大的工具, 也是唯一可以用查看和操作 BPF 程序的工具, 關(guān)于其用法讀者可參考內(nèi)核文檔, 工具自身的幫助文檔以及網(wǎng)絡(luò)上其它的學(xué)習(xí)資料

第三章 開發(fā) BPF 跟蹤工具

前面我們已經(jīng)概述了 BPF 相關(guān)的基本概念, 工作原理以及它能做什么.現(xiàn)在我們來看看具體怎么開發(fā) BPF 程序.開發(fā) BPF 程序的方案有很多種, 可使用的編程語言原則上也沒有限制, 只要有相應(yīng)的編譯器能將源代碼編譯成 BPF 目標(biāo)文件(BPF 虛擬機(jī)上的字節(jié)碼指令可執(zhí)行 ELF 文件)就可以了, 其它的與一般的軟件開發(fā)并沒有什么不同.如果你對 BPF 字節(jié)碼和虛擬機(jī)足夠了解, 甚至可以直接使用 BPF 字節(jié)碼來開發(fā) BPF 程序, 這無異于使用機(jī)器代碼來開發(fā)軟件.

BPF 的開發(fā)者提供了 BCC 和 bpftrace 兩個 BPF 開發(fā)前端, 它們都提供了把高級語言編譯成 BPF 字節(jié)碼并自動加載的功能.BCC 支持 C/C++、python、lua 等高級語言,bpftrace 則自己提供了一種腳本語言.不過這兩個前端都不適合用于開發(fā)嵌入式 BPF程序,BCC 的運(yùn)行時依賴過重,bpftrace 只適合開發(fā)非常簡單的, 短小精悍的 BPF 程序.另外 GoLang 也有不少可用的 BPF 開發(fā)庫, 不過它們大多都依賴于 BCC.這里就不過多贅述

本文主要介紹一下基于 libbpf 的 BPF 程序開發(fā).libbpf 是一個 C 語言庫, 它封裝BPF 系統(tǒng)調(diào)用, 并提供了大量的使用的函數(shù), 把 BPF 虛擬機(jī)比作運(yùn)行 BPF 程序的操作系統(tǒng)的話,libbpf 就好比是 libc.

libbpf 依賴于 zlib 和 libelf, 在正式開發(fā)之前需要先安裝好這三個庫.下面以我自己寫的用 fp 回棧獲取特定系統(tǒng)調(diào)用發(fā)生時的調(diào)用鏈的 BPF 程序?yàn)槔? 講述 BPF 程序的代碼結(jié)構(gòu)和開發(fā)流程.

編碼

內(nèi)核態(tài)程序

#include//linux內(nèi)核版本頭文件
#include11linux/vmlinux.h11//linux內(nèi)核數(shù)據(jù)結(jié)構(gòu)頭文件
//用到的libbpf頭文件
#include"bpf_helpers.h"//BPF幫助函數(shù)
#include"bpf_tracing.h"
#includenbpf_core_read?h"
#include"common.h”
struct
{
__uint(type,BPF_MAP_TYPE_STACK_TRACE);
__uint(key_size,sizeof(u32));
__uint(value_size,MAX_STACK_DEPTH*sizeof(u64));
__uint(max_entries,1000);
}kstack_mapSEC(".maps");
struct
{
__uint(type,BPF_MAP_TYPE_STACK_TRACE);
__uint(key_size,sizeof(u32));
__uint(value_size,MAX_STACK_DEPTH*sizeof(u64));
__uint(max_entries,1000);
}ustack_mapSEC(".maps");

#defineKERN_STACKID_FLAGS(0IBPF_F_FAST_STACK_CMP)
#defineUSER_STACKID_FLAGS(0IBPF_F_FAST_STACK_CMPIBPF_F_USER_STACK)

SEC("kprobe/do_sys_open")
intbpg_open(structpt_regs*ctx)
{
constchartarget_comm[]="static_demo";
charcurr_comm[MAX_COMM_LEN]="";
longret=bpf_get_current_comm(curr_comm,sizeof(curr_comm));
if(ret==0)
{
boolis_target=false;
for(size_tj=0;j<=?sizeof(target_conim);++j)
{
if(j==sizeof(target_comm))
{
charfmt[]="currentcommis%s";
bpf_trace_printk(fmt,sizeof(fmt),curr_comm);
is_target=true;
break;
}
if(target_comm[j]!=curr_comm[j])
{
break;
}
}
if(is_target)
{
longkstack_id=bpf_get_stackid(ctx,&kstack_map,KERN_STACKID_FLAGS);
if(kstack_id0)
{
charfmt[]="getkernstackidfailedwithkstack_id=%ld";

bpf_trace_printk(fmt,sizeof(fmt),kstack_id);
return0;
}
else
{
charfmt[]="getkernstackidsuccesswithkstack_id=Xld";
bpf_trace_printk(fmt,sizeof(fmt),kstack_id);
}
longustack_id=bpf_get_stackid(ctx,&ustack_map,USER_STACKID_FLAGS);
if(ustack_id0)
{
charfmt[]="getuserstackidfailedwithustack_id=%ld";
bpf_trace_printk(fmt,sizeof(fmt),ustack_id);
return0;
}
else
{
charfmt[]="getuserstackidsuccesswithustack_id=%ld";
bpf_trace_printk(fmt,sizeof(fmt),ustack_id);
}
charfilename[64];
bpf_core_read(filename,sizeof(filename),(void*)PT_REGS_PARM2(ctx));
charmsg[]="file%sisopened";
bpf_trace_printk(msg,sizeof(msg),filename);
}
}
return0;
}

char_license[]SECClicense")="GPL";
u32versionSEC("version")=LINUXVERSIONCODE;

內(nèi)核版本頭文件和 Linux 數(shù)據(jù)結(jié)構(gòu)頭文件總是必須的.其中 vmlinux.h 需要手動從目標(biāo)平臺的內(nèi)核生成.也可不使用 vmlinux.h, 直接手動添加內(nèi)核頭文件(同樣必須是目標(biāo)平臺的內(nèi)核頭文件), 不過這樣的話需要開發(fā)者自己管理內(nèi)核頭文件依賴, 個人覺得這種方式容易出錯, 推薦使用 vmlinux.h.之所以內(nèi)核態(tài)程序需要依賴 Linux 內(nèi)核頭文件, 是因?yàn)閮?nèi)核態(tài)程序往往需要訪問內(nèi)核的數(shù)據(jù)結(jié)構(gòu).接下來的四個頭文件都是 libbpf 的頭文件, 是代碼中用到的接口所依賴的頭文件, 這四個頭文件是最常用的, 大部分情況下都需要.

kstack_map 和 ustack_map 是兩個 BPF 映射表的定義, 每個 BPF 映射表的定義方式都是一樣的: 需要指定映射表類型, 鍵的大小, 值的大小, 鍵值對的個數(shù).

第 26 行則定義了一個 BPF 程序, 其關(guān)聯(lián)的是一個 kprobe 探針, 探針位于 do_sys_open內(nèi)核函數(shù)的入口.該 BPF 程序首先獲取了當(dāng)前進(jìn)程的運(yùn)行命令(bpf_get_current_comm()), 將當(dāng)前進(jìn)程的運(yùn)行命令與我們感興趣的進(jìn)程命令進(jìn)行比較, 如果一致說明當(dāng)前運(yùn)行的是我們感興趣的進(jìn)程, 于是我們首先獲取其內(nèi)核堆棧并保存于 kstack_map(bpf_get_stackid()使用 FP 回棧獲取堆棧), 然后獲取其用戶態(tài)堆棧并保存于 ustack_map.其中 bpf_trace_printk()用于打印內(nèi)核調(diào)試信息, 因?yàn)?BPF 程序運(yùn)行于內(nèi)核態(tài), 這可能是我們唯一可用的調(diào)試方式了.注意一個源文件可以定義多個 BPF 程序.

最后兩行是指定內(nèi)核證書和版本,LINUX_VERION_CODE 是一個定義在 vmlinux.h中的宏.這兩行在任何 BPF 程序中都是一樣的.

用戶態(tài)程序

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include"bpf.h"
#include"libbpf.h"
#include"perf-sys.h"
#include"uapi/linux/trace_helpers.h"
#include"common.h"
//declarevariables
staticinterr=0;
staticstructbpf_object*obj=NULL;
staticconstintNR_BPF_PROGS=1;
staticconstchar*bpf_prog_names[NR_BPF_PROGS]={
"bpg_open",
};
staticstructbpf_program*bpf_progs[NR_BPF_PROGS]={};

staticstructbpf_link*bpf_prog_links[NR_BPF_PROGS]={};

staticconstintNR_MAPS=2;
staticconstchar*map_names[NR_MAPS]={
"kstack_map",
"ustack_map"};
staticintmap_fds[NR_MAPS]={};

voidprint_ksym(__u64addr)
{
structksym*sym;
if(!addr)
return;
sym=ksym_search(addr);
if(!sym)
{
printf("ksymnotfound.Iskallsymsloaded?
");
return;
}
printf("ip=%11usym=%s;
",addr,sym->name);
}

longget_base_addr()
{
size_tstart,offset;
charbuf[256];
FILE*f;
f=fopen("/proc/self/maps",
"r");
if(!f)
return-errno;
while(fscanf(f,"%zx-%*x%s%zx%*[^
]
",&start,buf,&offset)==3)
{
if(strcmp(buf,"r-xp")==0)
{
fclose(f);
returnstart-offset;
}
}
fclose(f);
return-1;
}

intsetupprobes(constchar*filename)
{
//setuplibbpfdeubuglevel
libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
//setRLIMITMEMLOCK
structrlimitr={RLIM_INFINITY,RLIMINFINITY};
setrlimit(RLIMIT_MEMLOCK,&r);

//openkernobjectfile
obj=bpf_object__open_file(filename,NULL);
err=0;
err=libbpf_get_error(obj);
if(err)
{
fprintf(stderr,"openingBPFobjectfile%sfialed:%s
",filename,strerror(err));
return-1;
}
//findbpfprogs
for(intj=0;j0;
err=libbpf_get_error(bpf_progs[j]);
if(err)
{
fprintf(stderr,"findingBPFprog%sfailed:%s
",bpf_prog_names[j],strerror(err));
return-1;
}

//loadbpfprograms
if(bpf_objectload(obj))
{
fprintf(stderr,"loadingBPFobjectfile%sfailed:%s
",filename,
strerror(errno));
return-1;
}

//attachbpfprogs
for(intj=0;j0;
err=libbpf_get_error(bpf_prog_links[j]);
if(err)
{
fprintf(stderr,"attachingBPFprogram%sfailed:%s",bpf_prog_names[j],strerror(err));
return-1;
}

//findmapfds
for(intj=0;jif(map_fds[j]0)
{
printf("findmapfdbyname%sfailed
",map_names[j]);
return-1;
}
return0;
}

voidcleanup_probes()
{
for(intj=0;jintmain(intargc,char*argv[])
{
charfilename[256]={};
snprintf(filename,sizeof(filename),"%s_kern.o",argv[0]);
if(setup_probes(filename))
{
printf("ER0OR:setup_probesfailed
");
cleanup_probes();
return0;
}
printf("setup_probes()done,pressanykeytocontinue
");
charstr[32]="";
scanf("%s",str);
printf("%sreceived,continueprocessingdata
",str);
//processdata
load_kallsyms();
__u32key=0;
__u32next_key=0;
__u64ips[MAX_STACK_DEPTH]={};
printf("---------kernelstacks---------
");
while(bpf_map_get_next_key(map_fds[0],&key,&next_key)==0)
{
if(bpf_map_lookup_elem(map_fds[0],&next_key,ips)==0)
{
printf("kernstackswithkstack_id=%u:
",next_key);
for(intj=0;j0;
next_key=0;
printf("---------userstacks---------
");
while(bpf_map_get_next_key(map_fds[1],&key,&next_key)==0)
{
if(bpf_map_lookup_elem(map_fds[1],&next_key,ips)==0)
{
printf("userstackswithustack_id=%u:
",next_key);
for(intj=0;jif(ips[j]==0)
{
break;
}
printf("ip=%l1x
",ips[j]);
}
}
key=next_key;
}
cleanup_probes();
return0;
}

用戶態(tài)程序的流程分為三部分: 準(zhǔn)備階段(在 setup_probes() 函數(shù)), 業(yè)務(wù)處理(在main() 函數(shù)), 清理 BPF 程序和映射表(在 cleanup_probes() 函數(shù)).準(zhǔn)備階段的邏輯分為: 設(shè)置 libbpf 調(diào)試級別(可選), 設(shè)置 RLIMIT_MEMLOCK(大部分情況下都需要做此設(shè)置, 否則 BPF 內(nèi)核態(tài)程序太小限制太小將導(dǎo)致內(nèi)核態(tài)程序加載失?。? 打開 BPF 目標(biāo)文件, 獲取 BPF 程序?qū)ο缶浔? 加載 BPF 程序, 附著(attach)BPF 程序, 獲取 BPF映射表對象句柄.這些步驟在任何用戶態(tài)程序中都是固定不變的.

用戶態(tài)程序的業(yè)務(wù)邏輯都在 main() 函數(shù)中, 它首先調(diào)用了 setup_probes() 函數(shù)設(shè)置好 BPF 程序和映射表, 然后分別讀取 BPF 映射表 kstack_map 和 ustack_map, 在內(nèi)核態(tài)程序中, 這兩個映射表被寫入了目標(biāo)進(jìn)程調(diào)用鏈的 ip 指針.讀取內(nèi)核和用戶態(tài)調(diào)用鏈的 ip 值以后將它們打印了出來.

編譯和運(yùn)行

內(nèi)核態(tài)程序必須使用 Clang/LLVM 編譯并指定“-target bpf”選項(xiàng), 這樣我們將得到一個 BPF 目標(biāo)文件, 用戶態(tài)程序則像通常的程序一樣交叉編譯即可.在上述例子中, 我將用戶態(tài)程序命名為 fs_user.c, 用戶態(tài)程序打開 BPF 目標(biāo)文件是假設(shè)了目標(biāo)文件的名字是fs_kern.o, 運(yùn)行時只需將編譯好用戶態(tài)可執(zhí)行文件和內(nèi)核態(tài) BPF 目標(biāo)文件放在同一目錄下, 然后執(zhí)行用戶態(tài)程序, 用戶態(tài)的準(zhǔn)備階段就會讀取 BPF 目標(biāo)文件跟 BPF 目標(biāo)文件的內(nèi)容加載并附著 BPF 程序并為 BPF 映射表分配內(nèi)存, 這些都成功以后每次事件觸發(fā), 相關(guān)聯(lián)的 BPF 程序就會被自動執(zhí)行.

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • Linux
    +關(guān)注

    關(guān)注

    87

    文章

    11123

    瀏覽量

    207895
  • 跟蹤系統(tǒng)
    +關(guān)注

    關(guān)注

    0

    文章

    83

    瀏覽量

    18596
  • BPF
    BPF
    +關(guān)注

    關(guān)注

    0

    文章

    24

    瀏覽量

    3926

原文標(biāo)題:內(nèi)核觀測性方法

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    光學(xué)跟蹤測量系統(tǒng)如何工作的

    光學(xué)跟蹤測量系統(tǒng)是一種高精度的測量技術(shù),廣泛應(yīng)用于航空航天、軍事、工業(yè)制造等領(lǐng)域。 一、光學(xué)跟蹤測量系統(tǒng)的工作原理 光學(xué)跟蹤測量
    的頭像 發(fā)表于 08-29 17:26 ?253次閱讀

    有什么模塊或系統(tǒng)可以實(shí)現(xiàn)頻率的自動跟蹤?

    對壓電諧振器的自動頻率檢測. 目前設(shè)計(jì)了基于PLL的閉環(huán)電路,但是不能自動跟蹤諧振器諧振頻率的變化。 求問有什么模塊或系統(tǒng)可以實(shí)現(xiàn)頻率的自動跟蹤
    發(fā)表于 08-19 06:40

    創(chuàng)想智控激光焊縫跟蹤系統(tǒng)在地磅秤自適應(yīng)焊接的應(yīng)用

    焊接的一致性和精度,無法滿足現(xiàn)代制造業(yè)的高標(biāo)準(zhǔn)需求。針對這些難題,創(chuàng)想智控激光焊縫跟蹤系統(tǒng)在地磅秤自適應(yīng)焊接的應(yīng)用。 ?? 激光焊縫跟蹤系統(tǒng)原理 ??激光焊縫
    的頭像 發(fā)表于 07-28 16:32 ?533次閱讀
    創(chuàng)想智控激光焊縫<b class='flag-5'>跟蹤</b><b class='flag-5'>系統(tǒng)</b>在地磅秤自適應(yīng)焊接的應(yīng)用

    如何集成激光焊縫跟蹤系統(tǒng)與現(xiàn)有焊接設(shè)備

    工業(yè)技術(shù)不斷發(fā)展,焊接自動化遍及各行各業(yè),激光焊縫跟蹤系統(tǒng)作為一種先進(jìn)的焊接輔助設(shè)備,能夠顯著提高焊接精度和效率,減少人工干預(yù),降低生產(chǎn)成本。今天跟隨創(chuàng)想智控小編一起了解如何有效的集成激光焊縫跟蹤
    的頭像 發(fā)表于 07-18 15:30 ?232次閱讀
    如何集成激光焊縫<b class='flag-5'>跟蹤</b><b class='flag-5'>系統(tǒng)</b>與現(xiàn)有焊接設(shè)備

    焊接專機(jī)加裝激光跟蹤系統(tǒng)的作用

    藝的革新和提升帶來了革命性的變化。本文將探討焊接專機(jī)加裝激光跟蹤的作用,并分析其在提高焊接質(zhì)量、提高生產(chǎn)效率以及降低成本等方面的積極影響。 首先,焊接專機(jī)加裝激光跟蹤的作用之一是提高焊接質(zhì)量。激光跟蹤
    的頭像 發(fā)表于 03-12 11:47 ?224次閱讀
    焊接專機(jī)加裝激光<b class='flag-5'>跟蹤</b><b class='flag-5'>系統(tǒng)</b>的作用

    視覺焊縫跟蹤系統(tǒng)的發(fā)展趨勢與挑戰(zhàn)

    在當(dāng)今制造業(yè)中,焊接技術(shù)一直扮演著至關(guān)重要的角色。為了提高焊接質(zhì)量和效率,視覺焊縫跟蹤系統(tǒng)應(yīng)運(yùn)而生。這些系統(tǒng)利用計(jì)算機(jī)視覺技術(shù),實(shí)時監(jiān)測焊接過程中的焊縫位置,從而實(shí)現(xiàn)自動化控制和跟蹤。
    的頭像 發(fā)表于 03-05 16:30 ?294次閱讀
    視覺焊縫<b class='flag-5'>跟蹤</b><b class='flag-5'>系統(tǒng)</b>的發(fā)展趨勢與挑戰(zhàn)

    創(chuàng)想焊縫跟蹤系統(tǒng)適配大牛機(jī)器人進(jìn)行智能尋位跟蹤的應(yīng)用案例

    在自動化焊接領(lǐng)域,先進(jìn)技術(shù)技術(shù)的不斷涌現(xiàn)正為制造業(yè)帶來深刻的變革。其中,焊縫跟蹤技術(shù)的創(chuàng)想成果為提升焊接精度和效率貢獻(xiàn)巨大。該系統(tǒng)通過實(shí)時檢測和智能調(diào)整,確保焊接過程始終精準(zhǔn)無誤。今天創(chuàng)想焊縫跟蹤
    的頭像 發(fā)表于 12-23 16:00 ?582次閱讀
    創(chuàng)想焊縫<b class='flag-5'>跟蹤</b><b class='flag-5'>系統(tǒng)</b>適配大牛機(jī)器人進(jìn)行智能尋位<b class='flag-5'>跟蹤</b>的應(yīng)用案例

    螺旋管焊縫自動跟蹤系統(tǒng):內(nèi)外焊精準(zhǔn)跟蹤控制,應(yīng)用案例豐富

    隨著工業(yè)技術(shù)的不斷發(fā)展,螺旋管在輸送液體、氣體和固體等方面的應(yīng)用日益廣泛。在螺旋管制造過程中,焊接是至關(guān)重要的步驟,而焊縫的質(zhì)量直接關(guān)系到整體管道的性能。為了提高焊接質(zhì)量、效率和安全性,創(chuàng)想公司推出了螺旋管焊縫自動跟蹤系統(tǒng),實(shí)現(xiàn)
    的頭像 發(fā)表于 12-16 11:37 ?532次閱讀

    如何通過Tracealyzer實(shí)現(xiàn)Linux系統(tǒng)跟蹤

    Tracealyzer是Percepio 公司開發(fā)的一款可視化跟蹤工具, 目前它提供了30多種相互關(guān)聯(lián)的運(yùn)行時行為視圖,支持裸機(jī)、FreeRTOS、μC/OS-III、Zephyr、ThreadX、VxWorks、Linux系統(tǒng)
    的頭像 發(fā)表于 12-08 14:08 ?1089次閱讀
    如何通過Tracealyzer實(shí)現(xiàn)<b class='flag-5'>Linux</b><b class='flag-5'>系統(tǒng)</b>的<b class='flag-5'>跟蹤</b>?

    linux系統(tǒng)基礎(chǔ)入門教程

    Linux是一種開源的操作系統(tǒng),它被廣泛應(yīng)用于服務(wù)器、嵌入式系統(tǒng)以及個人電腦上。本篇文章將帶領(lǐng)讀者從入門的角度,詳細(xì)介紹Linux系統(tǒng)的基礎(chǔ)
    的頭像 發(fā)表于 11-16 16:45 ?863次閱讀

    內(nèi)核觀測技術(shù)BPF詳解

    補(bǔ)丁和不斷完善代碼,BPF程序變成了一個更通用的執(zhí)行引擎,可以完成多種任務(wù)。簡單來說,BPF提供了一種在各種內(nèi)核時間和應(yīng)用程序事件發(fā)生時運(yùn)行一小段程序的機(jī)制。其允許內(nèi)核在系統(tǒng)和應(yīng)用程序事件發(fā)生時運(yùn)行一小段程序,這樣就將內(nèi)核變得完
    的頭像 發(fā)表于 11-10 10:34 ?1036次閱讀

    linux屬于什么操作系統(tǒng)

    Linux屬于一種類UNIX操作系統(tǒng)。Linux,全稱GNU/Linux,是一套免費(fèi)使用和自由傳播的類Unix操作系統(tǒng),是一個基于POSIX
    的頭像 發(fā)表于 11-08 11:01 ?3425次閱讀

    思看科技新品TrackProbe 跟蹤式光筆測量系統(tǒng)正式發(fā)布!

    2023年11月3日,思看科技(SCANTECH)正式發(fā)布TrackProbe跟蹤式光筆測量系統(tǒng)。TrackProbe跟蹤式光筆測量系統(tǒng),實(shí)力進(jìn)階,超越想象,以無畏探索之勢,洞見測量邊
    的頭像 發(fā)表于 11-06 16:17 ?518次閱讀
    思看科技新品TrackProbe <b class='flag-5'>跟蹤</b>式光筆測量<b class='flag-5'>系統(tǒng)</b>正式發(fā)布!

    認(rèn)知扭曲類別

    可以看出認(rèn)知扭曲本身雖然往往和負(fù)面情緒相關(guān),但其更多是強(qiáng)調(diào)不合理的負(fù)面情緒,這些負(fù)面情緒的形成和加強(qiáng)都和認(rèn)知扭曲相關(guān)。認(rèn)知扭曲更是不合理的負(fù)面情緒的放大器和加重者。盡管以往的研究更多關(guān)注負(fù)面情緒,但我們的C2D2數(shù)據(jù)集旨在關(guān)注和
    的頭像 發(fā)表于 11-03 16:53 ?619次閱讀
    <b class='flag-5'>認(rèn)知</b>扭曲類別

    基于ARM的Linux系統(tǒng)移植

    電子發(fā)燒友網(wǎng)站提供《基于ARM的Linux系統(tǒng)移植.pdf》資料免費(fèi)下載
    發(fā)表于 10-11 10:57 ?1次下載
    基于ARM的<b class='flag-5'>Linux</b><b class='flag-5'>系統(tǒng)</b>移植