精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

Linux跟蹤系統和BPF的整體認知

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

第一章 Linux 跟蹤技術

1.1 前言

本文目的是給大家建立一個對 Linux 跟蹤系統和 BPF 的整體認知.

1.2 跟蹤系統

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

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

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

1.2.1 概覽

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

圖 1.1. 跟蹤系統框架

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

1.2.2 術語

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

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

采樣 (sampling) 工具通過獲取全部觀測量的子集來描繪目標的大致圖像; 這也被稱作生成性能剖析樣本或 profiling.有一個 BPF 工具就叫 profile(8), 它基于計時器來對運行中的代碼定時采樣.采樣工具的一個優點是, 其性能開銷比跟蹤工具小, 因為值對大量事件的一部分進行測量.采樣的缺點是, 它只提供了一個大致的畫像, 或遺漏事件.

探針 (probe) 軟件或者硬件中的探測點, 它產生一個事件, 該事件將導致內核中的一段程序被運行.

靜態跟蹤 (static tracing) 跟蹤的事件由硬編碼的探針產生, 這類探針的位置在編譯時就已經寫死比如 tracepoint.因為這些探針是固定的, 所以它們的 API 比較穩定, 可以對它們編寫文檔, 但同時這也意味著這類事件源是不靈活的.

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

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

1.2.3 Linux 跟蹤技術的發展時間線

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

圖 1.2. Linux 跟蹤技術發展時間線

1.3 Linux 跟蹤技術棧

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

圖 1.3. Linux 跟蹤技術棧

1.3.1 事件源

kernel tracepoint

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

tracepoint 出于不啟用狀態時, 性能開銷要盡可能小, 這是為了避免對不使用的東西“交性能稅”, 為此 Linux 使用了一項叫做“靜態跳轉補丁(static jump patching)”的技
術.其工作原理如下:

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

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

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

a. 在 tracepoint 回調函數數組中插入一條新的 tracepoint 回調函數, 以 RCU 的形式進行同步更新

b. 如果 tracepoint 之前出于禁用狀態,nop 指令的地址會被重寫為跳轉到蹦床函數的指令.

  1. 當跟蹤器禁用某個 tracepoint 時:

a. 在 tracepoint 回調函數數組中刪除該跟蹤器的回調函數, 以 RCU 的形式進行

同步更新

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

這樣可以最小化出于禁用狀態的 tracepoint 的性能開銷, 幾乎可以忽略不計.

tracepoint 有以下兩個接口:

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

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

kprobes

kprobes 提供了針對內核的動態插樁支持.kprobes 可以對任何內核函數進行插樁, 它還可以對函數內部的指令進行插樁.它可以實時在生產環境系統中啟用, 不需要重啟系統,也不需要以特殊方式重啟內核.這是一項令人驚嘆的能力, 這意味著我們可以對 Linux 中數以萬計的內核函數任意插樁.根據需要生成指標.

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

使用 kprobes 對內核進行動態插樁的過程如下:

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

  1. 把要插樁的目標地址中的字節內容復制并保存(為的是給單步斷點指令騰出

位置).

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

  2. 當指令流執行到斷點時, 斷點處理函數會檢查這個斷點是否是由 kprobes 注冊的, 如果是, 就會執行 kprobes 處理函數.

  3. kprobes 處理函數執行完后原始的指令會接著執行, 指令流繼續.

  4. 當不再需要(deactivate)kprobes 時, 原始的字節內容會被復制回目標地址上,

這樣這些指令就回到了它們的原始狀態.

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

  1. 將一個 Ftrace kprobe 處理函數注冊為對應函數的 Ftrace 處理器

  2. 當在函數起始處執行內建入口函數時 (在 x86 架構 gcc 4.6 下是 fentry),
    該函數會調用 Ftrace,Ftrace 接下來會調用 kprobe 處理函數.

  3. 當 kprobe 不在會被調用時, 從 Ftrace 中移除 Ftrace-kprobe 處理函數.

C. 如果是一個 kretprobe:

  1. 對函數入口進行 kprobe 插樁.

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

  3. 當函數最終返回時,CPU 將控制權交給蹦床函數處理.

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

  5. 當不再需要 kretprobe 時, 函數入口的 krobe 和 kretprobe 處理函數就被移除了.

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

kprobe 在某些 ARM 64 位系統上不能正常工作, 出于安全性的考慮, 這些平臺上的內核代碼區不允許被修改.

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

  • kprobe API: 如 register_kprobe() 等

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

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

uprobe

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

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

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

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

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

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

USDT

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

當編譯應用程序時, 在 USDT 探針的地址放置了一個 nop 指令.當 USDT 探針被激活時, 這個地址會由內核使用 uprobe 動態地將器修改位一個斷點指令.當該斷點被觸發時, 內核會執行相應的 BPF 程序.BPF 跟蹤器前端 BCC 和 bpftrace 均支持 USDT, 如果不使用這些前端, 則不能直接使用 USDT, 需要前端自己實現 USDT 支持.

動態 USDT

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

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

動態 USDT 以如下方式解決該問題:

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

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

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

隱藏底層的共享庫調用.

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

BPF raw tracepoint

BPF raw tracepoint 是一種新的 tracepoint, 相比于內核的 tracepoint,BPF raw tra- cepoint 接口向 tracepoint 線路原始參數, 這樣可以避免需要創建穩定的 tracepoint 參數從而導致的開銷, 因為這些參數可能壓根不被使用.

PMC

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

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

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

圖 1.4. Intel 架構上的 PMC

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

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

  • 計數: 在此模式下,PMC 能跟蹤事件發生的頻率, 只要內核有需要, 就可以隨時讀取,

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

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

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

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

perf_events

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

1.3.2 跟蹤框架

Ftrace

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

圖 1.5. Ftrace 跟蹤框架

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

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

圖 1.6. perf-event 跟蹤框架

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

perf_event 支持的事件源

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

圖 1.7. perf_event 支持的事件源

SystemTap

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

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

  • 編寫“systemtap 程序”并編譯成內核模塊

  • 內核模塊被插入內核以后會創建 kprobe 探針, 當事件觸發時內核調用模塊* 內核模塊使用 relayfs 或者其它的方式將結果打印到用戶空間

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

圖 1.8. systemtap 原理

1.3.3 跟蹤前端

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

第二章 BPF

2.1 基本概念

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

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

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

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

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

BPF 系統調用是 BPF 執行引擎提供的一套內核接口, 用于 BPF 用戶態程序與 BPF執行引擎之間的交互以及創建、訪問、修改和刪除 BPF 映射表.關于 BPF 系統調用的詳細介紹請參考Linux 手冊和Linux 內核文檔.

BPF 幫助函數(BPF helpers)也是一套內核接口, 大部分 BPF 幫助函數作用和BPF 系統調用相同,BPF 幫助函數和 BPF 系統調用有很多同名且功能也完全相同的接口.區別在于參數不同, 而且 BPF 幫助函數僅由 BPF 內核態程序調用.有關 BPF 幫助函數的詳細介紹請參考Linux 手冊

2.2 是什么, 以及為什么

如圖 1.2所示:BPF 是一種最新的 Linux 跟蹤技術, 可用于網絡、可觀測性和安全三個領域, 我們將主要關心其在可觀測性領域的運用.

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

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

圖 2.1. BPF 支持的事件源

BPF 跟蹤可以在整個軟件范圍內提供能見度, 允許我們隨時根據需要開發新的工具和監測功能.在生產環境中可以立刻部署 BPF 跟蹤程序, 不需要重啟系統, 也不需要以特殊方式重啟應用軟件.圖 2.2展示了一個通用的系統軟件棧的各部分及相應的 BPF 跟蹤工具.

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

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

表 2.1列出了傳統工具, 同時也列出了 BPF 工具是否支持對這些組件進行監測.傳統工具提供的信息可以作為性能分析的起點, 后續則可以通過 BPF 跟蹤工具做更見深入的調查.

表 2.1. 傳統分析工具 VS BPF 工具

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

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

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

圖 2.3BPF vsperf_event

2.3 BPF 虛擬機和運行時

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

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

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

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

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

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

圖 2.4. BPF 運行時的內部結構

BPF 可以利用輔助函數獲取內核狀態, 利用 BPF 映射表進行存儲.BPF 程序在特定事件發生時執行, 包括 kprobes、uprobes、內核跟蹤點(tracepoint)、PMC 和 perf events事件.

BPF 程序的主要運行過程如下:

  1. BPF 用戶態程序調用 BPF_PROG_LOAD 系統調用加載已經編譯成 BPF 字節碼的 BPF 內核態程序.根據 BPF 程序中的 BPF 映射表定義為 BPF 映射表分配內存

  2. BPF 用戶態程序調用 BPF_PROG_ATTACH 系統調用將 BPF 內核態程序和事件關聯起來并向內核注冊事件

  3. 事件被觸發, 內核收集“事件上下文”傳遞給關聯到該事件的 BPF 內核態程序并執行該 BPF 程序.BPF 內核態程序處理內核收集的數據將處理結果保存在 BPF 映射表

  4. 用戶態程序調用 BPF 系統調用讀取映射表中的數據并做進一步的處理.

  5. 用戶提程序邏輯結束以后卸載 BPF 內核態程序、注銷事件并釋放 BPF 映射表內存.

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

圖 2.5. BPF 程序執行流程

2.4 CO-RE

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

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

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

  • BTF 記錄了內核、BPF 程序類型和源代碼重定位的關鍵信息.

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

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

程序進行重定位以適配目標主機的內核版本

簡單來說 CO-RE 是使用 BTF 記錄源代碼的重定位信息和內核數據結構的相關信息, 由libbpf 在加載時進行重定位來解決 BPF 程序的可移植問題的.仍然以前面 task_struct結構體的 pid 成員訪問為例, 在 CO-RE 的解決方案中,BPF 字節碼中對 pid 的訪問被記錄為一個重定位點.當 libbpf 加載 BPF 字節碼程序時, 會根據目標平臺的內核數據結構信息進行重定位.

更多關于 CO-RE 的知識可以參考博客

2.5 bpftool

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

第三章 開發 BPF 跟蹤工具

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

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

本文主要介紹一下基于 libbpf 的 BPF 程序開發.libbpf 是一個 C 語言庫, 它封裝BPF 系統調用, 并提供了大量的使用的函數, 把 BPF 虛擬機比作運行 BPF 程序的操作系統的話,libbpf 就好比是 libc.

libbpf 依賴于 zlib 和 libelf, 在正式開發之前需要先安裝好這三個庫.下面以我自己寫的用 fp 回棧獲取特定系統調用發生時的調用鏈的 BPF 程序為例, 講述 BPF 程序的代碼結構和開發流程.

編碼

內核態程序

#include//linux內核版本頭文件
#include11linux/vmlinux.h11//linux內核數據結構頭文件
//用到的libbpf頭文件
#include"bpf_helpers.h"//BPF幫助函數
#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;

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

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

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

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

用戶態程序

#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;
}

用戶態程序的流程分為三部分: 準備階段(在 setup_probes() 函數), 業務處理(在main() 函數), 清理 BPF 程序和映射表(在 cleanup_probes() 函數).準備階段的邏輯分為: 設置 libbpf 調試級別(可選), 設置 RLIMIT_MEMLOCK(大部分情況下都需要做此設置, 否則 BPF 內核態程序太小限制太小將導致內核態程序加載失敗), 打開 BPF 目標文件, 獲取 BPF 程序對象句柄, 加載 BPF 程序, 附著(attach)BPF 程序, 獲取 BPF映射表對象句柄.這些步驟在任何用戶態程序中都是固定不變的.

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

編譯和運行

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

審核編輯 :李倩


聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • Linux
    +關注

    關注

    87

    文章

    11232

    瀏覽量

    208940
  • 跟蹤系統
    +關注

    關注

    0

    文章

    84

    瀏覽量

    18627
  • BPF
    BPF
    +關注

    關注

    0

    文章

    24

    瀏覽量

    3979

原文標題:內核觀測性方法

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

收藏 人收藏

    評論

    相關推薦

    天合跟蹤跟蹤支架系統集成創新價值分析

    近日,第二十屆中國太陽級硅及光伏發電研討會(20th CSPV)在中國深圳隆重舉行。天合跟蹤智控產品開發負責人廖格兵在“N型TOPCon時代 光儲氫系統創新升級應用”分論壇上發表了《跟蹤支架
    的頭像 發表于 11-27 15:19 ?146次閱讀

    Linux根文件系統的掛載過程

    Linux根文件系統(rootfs)是Linux系統中所有其他文件系統和目錄的起點,它是內核啟動時掛載的第一個文件
    的頭像 發表于 10-05 16:50 ?282次閱讀

    如何構建Linux根文件系統

    構建Linux根文件系統是一個涉及多個步驟和概念的過程,它對于Linux系統的啟動和運行至關重要。
    的頭像 發表于 10-05 16:47 ?243次閱讀

    高抗噪性 電壓跟蹤

    電壓跟蹤
    jf_30741036
    發布于 :2024年09月29日 19:26:44

    光學跟蹤測量系統如何工作的

    光學跟蹤測量系統是一種高精度的測量技術,廣泛應用于航空航天、軍事、工業制造等領域。 一、光學跟蹤測量系統的工作原理 光學跟蹤測量
    的頭像 發表于 08-29 17:26 ?567次閱讀

    有什么模塊或系統可以實現頻率的自動跟蹤

    對壓電諧振器的自動頻率檢測. 目前設計了基于PLL的閉環電路,但是不能自動跟蹤諧振器諧振頻率的變化。 求問有什么模塊或系統可以實現頻率的自動跟蹤
    發表于 08-19 06:40

    創想智控激光焊縫跟蹤系統在地磅秤自適應焊接的應用

    焊接的一致性和精度,無法滿足現代制造業的高標準需求。針對這些難題,創想智控激光焊縫跟蹤系統在地磅秤自適應焊接的應用。 ?? 激光焊縫跟蹤系統原理 ??激光焊縫
    的頭像 發表于 07-28 16:32 ?605次閱讀
    創想智控激光焊縫<b class='flag-5'>跟蹤</b><b class='flag-5'>系統</b>在地磅秤自適應焊接的應用

    如何集成激光焊縫跟蹤系統與現有焊接設備

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

    焊接專機加裝激光跟蹤系統的作用

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

    視覺焊縫跟蹤系統的發展趨勢與挑戰

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

    3562-Linux系統啟動卡制作及系統固化

    ](基于 RK3562_LINUX_SDK_RELEASE_V1.1.0_20231220) 評估板支持通過 Linux 系統啟動卡(下文稱為“SD 啟動卡”)和板載 eMMC 設備兩 種方式啟動。本文檔主要演示 SD 啟動
    的頭像 發表于 03-05 15:58 ?284次閱讀
    3562-<b class='flag-5'>Linux</b><b class='flag-5'>系統</b>啟動卡制作及<b class='flag-5'>系統</b>固化

    linux和windows的區別 linux系統一般用來干嘛

    Linux和Windows是兩種不同的操作系統,有著不同的設計理念和用途。本文將對Linux和Windows的區別進行詳細分析,并介紹Linux系統
    的頭像 發表于 02-05 14:06 ?881次閱讀

    創想焊縫跟蹤系統適配大牛機器人進行智能尋位跟蹤的應用案例

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

    螺旋管焊縫自動跟蹤系統:內外焊精準跟蹤控制,應用案例豐富

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

    如何通過Tracealyzer實現Linux系統跟蹤

    Tracealyzer是Percepio 公司開發的一款可視化跟蹤工具, 目前它提供了30多種相互關聯的運行時行為視圖,支持裸機、FreeRTOS、μC/OS-III、Zephyr、ThreadX、VxWorks、Linux系統
    的頭像 發表于 12-08 14:08 ?1215次閱讀
    如何通過Tracealyzer實現<b class='flag-5'>Linux</b><b class='flag-5'>系統</b>的<b class='flag-5'>跟蹤</b>?