基於eBPF的CPU利用率精準計算小工具開發

語言: CN / TW / HK

作者簡介: 張玉哲,西安郵電大學 研二在讀,Linux核心之旅社群成員。

陳莉君老師寫在前面 :玉哲分享的 使用eBPF技術編寫小工具 ,穿越核心態和使用者態,涉及很多知識,把這些知識揉碎後重新進行了排列組合,估計你讀三遍後有所領悟,希望你依次受啟發,啟用自己,從而做出自己的一些有意思的 工具,首屆eBPF大會將給予展示。

傳統工具介紹

首先,要介紹的關於CPU Time相關的一些工具。在使用eBPF技術之前,先來了解一下傳統的常用於統計CPU Time的工具TOP。

上圖是TOP工具的使用介面,可以看到TOP工具將CPU Time分為user、system、nice、idle、iowait、hardirq、softirq以及steal幾個部分。那麼TOP工具展示的資料是從哪裡來的呢?

其實是proc檔案系統,我們輸出/proc/stat,會看到這樣格式的資料:

而通過分析proc檔案系統的程式碼show_stat,可以看到除了idle和iowait有些特殊外,其餘的時間資訊資料皆是來自於cpustat陣列。

for_each_online_cpu(i) {

struct kernel_cpustat kcpustat;

u64 *cpustat = kcpustat.cpustat;



kcpustat_cpu_fetch(&kcpustat, i);



/* Copy values here to work around gcc-2.95.3, gcc-2.96 */

user = cpustat[CPUTIME_USER];

nice = cpustat[CPUTIME_NICE];

system = cpustat[CPUTIME_SYSTEM];

idle = get_idle_time(&kcpustat, i);

iowait = get_iowait_time(&kcpustat, i);

irq = cpustat[CPUTIME_IRQ];

softirq = cpustat[CPUTIME_SOFTIRQ];

steal = cpustat[CPUTIME_STEAL];

guest = cpustat[CPUTIME_GUEST];

guest_nice = cpustat[CPUTIME_GUEST_NICE];

seq_printf(p, "cpu%d", i);

seq_put_decimal_ull(p, " ", nsec_to_clock_t(user));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(nice));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(system));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(idle));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(iowait));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(irq));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(softirq));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(steal));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(guest));

seq_put_decimal_ull(p, " ", nsec_to_clock_t(guest_nice));

seq_putc(p, '\n');

}

那麼,cpu_stat的資料是什麼時候更新的呢?

一般來說,是在每次Tick的時候,在其對應的時鐘中斷處理函式中會呼叫一個函式update_process_times,該函式順序執行到account_process_tick。

以下是account_process_tick的部分程式碼:

cputime = TICK_NSEC;



if (user_tick)

account_user_time(p, cputime);

else if ((p != this_rq()->idle) || (irq_count() != HARDIRQ_OFFSET))

account_system_time(p, HARDIRQ_OFFSET, cputime);

else

account_idle_time(cputime);

根據Tick產生時是在使用者態還是核心態以及idle程序的上下文等資訊,選擇不同的函式進行處理,這裡是把這個TICK_NSEC,也就是每Tick對應的ns加到對應的cpustat陣列中。

也就是說cpustat陣列中的資料雖然是ns級的,但其實精度是要按照Tick的頻率而定的,這就emmmm,不算很準確吧。

這裡簡單說一下選擇cpustat的下標的問題,首先根據使用者態和核心態是很容易區分的,最簡單來說,當前程式使用的堆疊一個是核心堆疊,一個是使用者態堆疊。而普通的核心態可以視為是系統呼叫進入,而是否在hardirq環境以及softirq環境可以通過preempt_count(thread_info的一個成員)進行判斷。而idle狀態,只需要判斷當前程序是否是idle程序就可以了。

而至於前面說到的idle和iowait比較特殊,先清楚,iowait是在idle程序上下文中,但是rq->nr_iowait仍然大於0的情況。

這裡直接分析get_idle_time的實現:

static u64 get_idle_time(struct kernel_cpustat *kcs, int cpu)

{

u64 idle, idle_usecs = -1ULL;



if (cpu_online(cpu))

idle_usecs = get_cpu_idle_time_us(cpu, NULL);



if (idle_usecs == -1ULL)

/* !NO_HZ or cpu offline so we can rely on cpustat.idle */

idle = kcs->cpustat[CPUTIME_IDLE];

else

idle = idle_usecs * NSEC_PER_USEC;



return idle;

}

可以看到,這裡主要是受NOHZ的影響,導致這裡的計算有兩條路徑。

一般系統在高精度定時器開啟時是會開啟NOHZ的,所謂的NOHZ,俗稱動態時鐘,就是在idle程序執行idle任務(HLT指令或WFI指令等的時候)會關閉Tick,因為該CPU可能會空閒超出一個Tick,這時候關閉Tick,也就避免了每Tick的電能損耗。

而在進入實際的idle狀態之前,每CPU的tick_sched會記錄下進入idle的時間(該時間為高精度),而在proc檔案系統計算的時候,會使用當前時間減去進入idle的時間,獲得準確的時間,但遺憾的是,由get_cpu_idle_time_us可以看出,這裡的最大精度是us。

通過對TOP工具資料來原始碼的分析,可以看出傳統工具的問題是依賴於核心資料結構的更新,而許多資料的粒度是與Tick的頻率相關的,即便是最精度最高的idle時間,也只是us級。

而且以Tick為單位進行更新,也會將一些上下文的時間判斷錯誤。如在兩次Tick之間,從使用者態進入系統態,在系統態觸發Tick,根據上下文環境,就會將這段時間都視為是系統態時間。

那麼,使用eBPF編寫CPU Time相關的工具最直觀的感受是什麼呢?沒錯,就是直接可以獲得ns級的時間資訊,並且可以不出現這種以取樣方式進行統計帶來的誤差。

程序CPU時間統計

來看第一個工具,直接選擇一種最暴力的思想,統計出每一個程序的CPU Time,將idle程序和其它程序區分開來,這樣得到總時間和idle時間,並以此算出CPU利用率。

我們知道一個程序在CPU上執行的開始和結束都是以排程為標誌的。統計每一個程序的CPU Time,可以關注排程的關鍵掛載點。這裡可以選擇使用kprobe掛載到finish_task_switch函式上,也可以使用tracepoint掛載到sched/sched_switch上。

排程主要涉及到地址空間的切換和核心棧的切換,在以上兩者切換完成之後,就會執行到finish_task_switch函式,做一些後續的清理工作,此時current巨集已經發生了變化。

kprobe技術是需要依靠核心符號表查詢地址,一般來說符號表中的函式基本都是可以掛載的,當然kprobe自身實現的一些和一些顯示標註notrace的函式除外。我們可以通過/proc/kallsyms來查詢符號表中是否有對應的核心函式。

Tracepoint可以使用的點可以進入/sys/kernel/tracing/events進行檢視:

在這裡,我們選擇使用kprobe到finish_task_switch函式上。

b = BPF(text=bpf_text)

b.attach_kprobe(event="finish_task_switch", fn_name="pick_start")

當執行到finish_task_switch函式的時候,會轉去執行我們自己定義的pick_start函式。

BPF_HASH(start, struct key_t);

BPF_HASH(dist, u32, struct time_t);



int pick_start(struct pt_regs *ctx, struct task_struct *prev)

{

u64 ts = bpf_ktime_get_ns();

u64 pid_tgid = bpf_get_current_pid_tgid();

struct key_t key;

struct time_t cpu_time, *time_prev;

u32 cpu, pid;

u64 *value, delta;



cpu = key.cpu = bpf_get_smp_processor_id();

key.pid = pid_tgid;

key.tgid = pid_tgid >> 32;



start.update(&key, &ts);

pid = key.pid = prev->pid;

key.tgid = prev->tgid;



value = start.lookup(&key);



if (value == 0) {

return 0;

}

delta = ts - *value;

start.delete(&key);


time_prev = dist.lookup(&cpu);

if (time_prev == 0) {

cpu_time.total = 0;

cpu_time.idle = 0;

}else {

cpu_time = *time_prev;

}

cpu_time.total += delta;

if (pid == 0) {

cpu_time.idle += delta;

}

dist.update(&cpu, &cpu_time);

return 0;

}

這個函式的邏輯就是,在每次排程完成之後,記錄下將要執行的程序的上處理器時間,並且查找出剛被換下的程序的上處理器時間,用現在的時間減去上處理器時間就可以得到本次程序執行的時間,將時間加到CPU總時間裡面,如果剛被換下的程序是idle程序,也就是pid為0,那麼把這部分時間算作idle時間。

可以看到,上述程式碼中用到了兩個Map,Map在eBPF中可以作為核心態程式碼和使用者態程式碼進行資料交流的一個橋樑。後面在使用者態可以通過查詢Map來獲得CPU時間的一個統計資訊。

dist = b.get_table("dist")



print("%-5s%-12s%-12s"%("CPU","IDLE(%)","CPU_USAGE(%)"))

while (1):

sleep(1)

for k, v in dist.items():

idle = (v.idle / v.total) * 100

print("%-5d%-12.2f%-12.2f"%(k.value,idle,100-idle))

dist.clear()

以下是該工具的演示結果:

這裡只是簡單的按照是否為idle程序為判斷標準,計算了一下CPU的利用率。當然也可以對這個工具做進一步的升級,比如要像TOP那樣對每個程序的CPU利用率進行排序,我們可以以pid為key統計出所有程序的CPU時間。還比如,我們要對system時間進行程序的排序,可以只關注系統呼叫的入口和出口函式,同樣以pid為key統計出所有程序的system時間(當然,這裡要考慮中斷搶佔系統呼叫的情況)。

idleState情況統計

其實,如果說我們單純想要獲得idle時間,可以有開銷更小的一些方法,比如說只關注idle程序。

每個CPU都有一個單獨idle程序,當該CPU的runqueue沒有任務可以執行的時候,就轉去執行idle程序。idle程序執行do_idle函式,當要被排程的之前,會呼叫schedule_idle函式去執行排程主函式__schedule。

也就是說,我們可以只掛載do_idle和schedule_idle,就可以實現idle時間的追蹤,與finish_task_switch的頻繁呼叫相比,這樣明顯觸發次數更少,帶來的開銷也更小。

但我要演示的第二個工具不是簡單的獲取idle時間以及CPU利用率,而是要深入去分析idle程序具體做了什麼。

我們先來看下do_idle的部分程式碼:

while (!need_resched()) {

rmb();



local_irq_disable();



if (cpu_is_offline(cpu)) {

tick_nohz_idle_stop_tick();

cpuhp_report_idle_dead();

arch_cpu_idle_dead();

}



arch_cpu_idle_enter();



if (cpu_idle_force_poll || tick_check_broadcast_expired()) {

tick_nohz_idle_restart_tick();

cpu_idle_poll();

} else {

cpuidle_idle_call();

}

arch_cpu_idle_exit();

}

只要在CPU沒有被熱插拔(hotplug)以及支援cpuidle模組的情況下,cpuidle是會去執行cpuidle_idle_call函式的,在這個函式中會選擇一個cpuidle_state,然後讓CPU進入該狀態。

此時CPU其實一直在執行所謂的省電指令(X86:HLT;ARM:WFI),根據選擇的cpuidle_state,會關閉CPU的一些子部件。進入的cpuidle_state越深,關閉的子部件越多,在該狀態下越省電,同時,也意味著從該狀態退出帶來的延遲和電能損耗越高。

我們可以看下cpuidle_state的結構定義:

struct cpuidle_state {

char name[CPUIDLE_NAME_LEN];

char desc[CPUIDLE_DESC_LEN];



u64 exit_latency_ns;

u64 target_residency_ns;

unsigned int flags;

unsigned int exit_latency; /* in US */

int power_usage; /* in mW */

unsigned int target_residency; /* in US */



int (*enter) (struct cpuidle_device *dev,

struct cpuidle_driver *drv,

int index);



int (*enter_dead) (struct cpuidle_device *dev, int index);



int (*enter_s2idle)(struct cpuidle_device *dev,

struct cpuidle_driver *drv,

int index);

};

我們重點關注其中的exit_latency_ns、target_residency_ns和power_usage這幾個成員,exit_latency_ns是退出該狀態需要花費的時間,target_residency_ns是期望在該狀態停留的最低時間,如果實際在該狀態的時間小於targe_residency_ns,則說明本次選擇該idle_state的決定是錯誤的,不但沒有節約,反倒增加了功耗。

我們來看一下我們選擇掛載的函式:

sched_idle_set_state(target_state);



trace_cpu_idle(index, dev->cpu);

time_start = ns_to_ktime(local_clock());



stop_critical_timings();

if (!(target_state->flags & CPUIDLE_FLAG_RCU_IDLE))

rcu_idle_enter();

entered_state = target_state->enter(dev, drv, index);

if (!(target_state->flags & CPUIDLE_FLAG_RCU_IDLE))

rcu_idle_exit();

start_critical_timings();



sched_clock_idle_wakeup_event();

time_end = ns_to_ktime(local_clock());

trace_cpu_idle(PWR_EVENT_EXIT, dev->cpu);



/* The cpu is no longer idle or about to enter idle. */

sched_idle_set_state(NULL);

在呼叫具體的進入該state的enter回撥函式之前,會先呼叫sched_idle_set_state函式修改該CPU的執行佇列的成員idle_state,而在離開該idle_state之後,也會呼叫sched_idle_set_state函式將該CPU的執行佇列的成員idle_state置為NULL。

也就是說,通過掛載sched_idle_set_state函式,對其引數進行判斷就可以知道當前是進入一個idle狀態還是退出一個idle狀態,並對進入和退出的時間進行記錄,就可以知道本次是否滿足最小停留時間。

該部分程式碼如下:

BPF_HASH(idle_start, u32, cpuidle_key_t);

BPF_HASH(idle_account, cpuidle_key_t, cpuidle_info_t);



// kernel_function : sched_idle_set_state

int do_idle_start(struct pt_regs *ctx, struct cpuidle_state *target_state) {

cpuidle_key_t key = {}, *key_p;

cpuidle_info_t info = {}, *info_p;



u32 cpu = bpf_get_smp_processor_id();

u64 delta, ts = bpf_ktime_get_ns();



if (target_state == NULL) {



key_p = idle_start.lookup(&cpu);

if (key_p == 0) {

return 0;

}



key.cpu = key_p->cpu;

key.exit_latency_ns = key_p->exit_latency_ns;



info_p = idle_account.lookup(&key);

if (info_p) {

delta = ts - info_p->start;



info_p->total += delta;



if (delta > (info_p->exit_latency_ns + info_p->target_residency_ns))

info_p->more++;

else

info_p->less++;

}



return 0;



};



key.cpu = cpu;

key.exit_latency_ns = target_state->exit_latency_ns;



idle_start.update(&cpu, &key);



info_p = idle_account.lookup(&key);

if (info_p) {

info_p->start = ts;

} else {

info.cpu = cpu;

bpf_probe_read_kernel(&(info.name), sizeof(info.name), target_state->name);



info.exit_latency_ns = target_state->exit_latency_ns;

info.target_residency_ns = target_state->target_residency_ns;



info.start = ts;

info.total = 0;

info.less = info.more = 0;



idle_account.update(&key, &info);

}



return 0;

}

該工具演示結果如下:

從左到右依次輸出為:CPU、idle_state的名字、退出延遲、期望停留時間、在該狀態停留的總時間、沒有滿足節約功耗的次數、滿足的次數。

通過提取cpuidle模組的這部分資料,對後續優化該部分選擇cpuidle狀態的演算法應該是比較有意義的。

使用CPU_CYCLES計算CPU利用率

第三個工具是計算CPU利用率的,不過該工具有點特殊,它藉助於硬體資訊進行CPU利用率的計算。

在談第三個工具之前,我們先來了解一下perf,perf是Linux中一個特別常用的效能工具,其本質的實現基於一個函式perf_event_open,以下是man手冊中perf_event_open的描述。

通過perf_event_open可以對定義的perf事件進行計數,通過read返回的檔案描述符可以讀取這些perf事件的計數資訊。

perf支援的事件包括硬體、軟體、cache以及tracepoint等等。

這裡我們關注硬體中的一個事件PERF_COUNT_HW_CPU_CYCLES。

以上是man手冊中對PERF_COUNT_HW_CPU_CYCLES的描述,可以看到該計數值是會受到CPU頻率變換的影響,那麼我們先把頻率固定,然後看一下CPU_CYCLS和CPU頻率有什麼樣的關係呢?

我們先將CPU頻率定為2200000KHZ。

接下來依靠perf_event_open註冊CPU_CYLES和CPU_CLOCK事件:

struct perf_event_attr attr_cpu_clk;

struct perf_event_attr attr_cpu_cyr;


memset(&attr_cpu_clk, 0, sizeof(struct perf_event_attr));

memset(&attr_cpu_cyr, 0, sizeof(struct perf_event_attr));




attr_cpu_clk.type = PERF_TYPE_SOFTWARE;

attr_cpu_clk.size = sizeof(struct perf_event_attr);

attr_cpu_clk.config = PERF_COUNT_SW_CPU_CLOCK;

attr_cpu_clk.sample_period = SAMPLE_PERIOD;

attr_cpu_clk.disabled = 1;



attr_cpu_cyr.type = PERF_TYPE_HARDWARE;

attr_cpu_cyr.size = sizeof(struct perf_event_attr);

attr_cpu_cyr.config = PERF_COUNT_HW_CPU_CYCLES;

attr_cpu_cyr.sample_period = SAMPLE_PERIOD;

attr_cpu_cyr.disabled = 1;


for (i = 0; i < nr_cpus; i++) {

// cpu cycle

fd_cpu_clk[i] = perf_event_open(&attr_cpu_clk, -1,

i, -1, 0);


if (fd_cpu_clk[i] == -1)

err_exit("PERF_COUNT_HW_CPU_CYCLES");


ioctl(fd_cpu_clk[i], PERF_EVENT_IOC_RESET, 0);

ioctl(fd_cpu_clk[i], PERF_EVENT_IOC_ENABLE, 0);


// cpu ref cycle

fd_cpu_cyr[i] = perf_event_open(&attr_cpu_cyr, -1,

i, -1, 0);


if (fd_cpu_cyr[i] == -1)

err_exit("PERF_COUNT_SW_CPU_CLOCK");


ioctl(fd_cpu_cyr[i], PERF_EVENT_IOC_RESET, 0);

ioctl(fd_cpu_cyr[i], PERF_EVENT_IOC_ENABLE, 0);


}

在讀取到CPU_CYCLES和CPU_CLOCK的計數值之後,將其與頻率值進行如下運算,並與根據/proc/stat計算得到的CPU利用率進行對比。

read(fd_cpu_clk[i], &data_cpu_clk[i], sizeof(data_cpu_clk[i]));

read(fd_cpu_cyr[i], &data_cpu_cyr[i], sizeof(data_cpu_cyr[i]));


cpu_clk = data_cpu_clk[i] - prev_cpu_clk[i];

cpu_cyr = data_cpu_cyr[i] - prev_cpu_cyr[i];


double new_rate = cpu_cyr * 1000.0 / (22 * cpu_clk);



printf("CPU:%d CPU_CLOCK:%lld CPU_CYCLES:%lld RATE: %16.5f %16.5f\n",

i,

cpu_clk,

cpu_cyr,

new_rate,

cpu_rate);

將CPU_CYCLES的計數值與CPU_CLOCKS的計數值與頻率的乘積做一個除法,可以得到這樣的結果:

可以看到該公式計算的結果與根據/proc/stat中資料計算的結果基本上保持趨勢的一致。為什麼會這樣呢?

之前說到cpuidle會去執行一些省電指令,其實在執行這些省電指令的時候,CPU_CYCLES會停止計數;而在執行其餘指令時,CPU_CYCLES的計數值的增長速度是與當前CPU的頻率保持一致的。

這就是說,只要我們在每次頻率變化的時候讀取一下CPU_CYCLES的計數值,然後做一個簡單的運算,就可以知道CPU在上一個頻率下的CPU利用率。這時候,就有兩個問題要解決,如何獲得頻率的變化資訊以及如何在ebpf的核心程式碼中讀取perf事件的計數值。

首先,先來看下掛載的函式cpufreq_freq_transition_end的使用場景:

cpufreq_freq_transition_begin(policy, freqs);

ret = cpufreq_driver->target_intermediate(policy, index);

cpufreq_freq_transition_end(policy, freqs, ret);

在執行具體的頻率變化鉤子函式之前,會先呼叫cpu_freq_transition_begin,而在具體的頻率變化鉤子函式之後,會呼叫cpufreq_freq_transition_end,我們可以根據其引數ret和freqs->new和freqs->old兩個成員是否一致來判斷是否進行了頻率切換以及具體切換函式是否執行成功。

以下是獲得頻率變化的部分程式:

int do_cpufreq_ts(struct pt_regs *ctx, struct cpufreq_policy *policy, struct cpufreq_freqs *freqs, int transition_failed)

{

if (transition_failed) {

return 0;

}



u32 cpu = bpf_get_smp_processor_id();

u32 freq_new = freqs->new;

u32 freq_old = freqs->old;



if (freq_new == freq_old)

return 0;



bpf_trace_printk("CPU: %d OLD: %d ---> NEW: %d\\n", cpu, freq_old, freq_new);



// freq_cpu.update(&cpu, &freq_new);



return 0;

}

該指令碼的演示結果如下:

現在可以獲得CPU的頻率變化資訊,那麼怎麼在ebpf核心態程式碼讀取到perf事件的計數值呢?

這裡需要藉助一個特殊的Map,eBPF提供了許多種Map,有單純的雜湊表,佇列,棧這些,還有一些Map是用於存取特殊的資料結構的,例如現在要使用的BPF_MAP_TYPE_PERF_EVENT_ARRAY。

我們需要建立該型別的Map,然後在使用者態註冊perf事件,然後將該perf事件的檔案描述符更新到該Map中,在核心態程式中可以依靠讀取該Map來獲得perf事件的計數值。

部分實現程式碼如下:

// kern.c

struct {

__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);

__uint(key_size, sizeof(int));

__uint(value_size, sizeof(u32));

__uint(max_entries, 64);

} pmu_cyl SEC(".maps");



u64 cpu = bpf_get_smp_processor_id();



u64 cyl = bpf_perf_event_read(&pmu_cyl, cpu);

char fmt[] = "CPU: %u CYL: %u \n";



bpf_trace_printk(fmt, sizeof(fmt), cpu, cyl);

// user.c

struct perf_event_attr attr_cycles = {

.freq = 0,

.sample_period = SAMPLE_PERIOD,

.inherit = 0,

.type = PERF_TYPE_HARDWARE,

.read_format = 0,

.sample_type = 0,

.config = PERF_COUNT_HW_CPU_CYCLES,

};



static void register_perf(int cpu, struct perf_event_attr *attr) {



pmufd_cyl[cpu] = sys_perf_event_open(attr, -1, cpu, -1, 0);



ioctl(pmufd_cyl[cpu], PERF_EVENT_IOC_RESET, 0);

ioctl(pmufd_cyl[cpu], PERF_EVENT_IOC_ENABLE, 0);



bpf_map_update_elem(map_fd[0], &cpu, &(pmufd_cyl[cpu]), BPF_ANY);

}

該指令碼演示結果如下:

可以看到,在ebpf的核心態程式碼也是可以讀取到perf事件的計數值。

如果說之前使用eBPF計數都只是單純的對以往技術的封裝升級,如kprobe掛載函式之類的,到這裡就可以看到eBPF的優勢,那就是可以很輕鬆地實現多種技術的聯動,在這裡就將kprobe和perf聯動起來了。

列印核心函式呼叫關係

最後,我們再來看一個小工具,這個工具通過eBPF技術可以打印出來核心函式的呼叫關係。一般來說,如果要顯示函式呼叫關係,最常使用的是dump_stack函式。

但是dump_stack函式的使用是有一定的侷限的,一般都是在驅動程式碼中使用,而如果我們想要去列印某個核心函式的函式呼叫關係,去該函式內部中新增dump_stack函式,再重新編譯一遍核心,這樣明顯不太現實。

而eBPF提供了一種比較特殊的Map,可以用於列印核心棧的函式呼叫關係,這就是BPF_MAP_TYPE_STACK_TRACE。

核心態函式可以通過bpf_get_stackid()獲得觸發eBPF程式時的核心態堆疊或者使用者態堆疊的id號,而在使用者態程式中可以通過該id號打印出觸發時的堆疊資訊,也就是函式呼叫關係。

下面我們看下這部分程式碼:

// kern.c

struct {

__uint(type, BPF_MAP_TYPE_STACK_TRACE);

__uint(key_size, sizeof(u32));

__uint(value_size, PERF_MAX_STACK_DEPTH * sizeof(u64));

__uint(max_entries, 10000);

} stackmap SEC(".maps");



#define STACKID_FLAGS (0 | BPF_F_FAST_STACK_CMP)



SEC("kprobe/finish_task_switch")

int bpf_prog1(struct pt_regs *ctx, struct task_struct *prev)

{

struct key_t key = {};

struct val_t val = {};



val.stack_id = bpf_get_stackid(ctx, &stackmap, STACKID_FLAGS);

bpf_get_current_comm(&val.comm, sizeof(val.comm));



key.pid = bpf_get_current_pid_tgid();

key.cpu = bpf_get_smp_processor_id();



bpf_map_update_elem(&task_info, &key, &val, BPF_ANY);



return 0;

}



這時候需要兩個Map,一個Map儲存stackid,另外一個Map就是 BPF_MAP_TYPE_STACK _TRACE。我們在使用者態總是在第一個Map中獲得stackid,然後再獲得堆疊資訊。

while (bpf_map_get_next_key(map_fd[0], &key, &next_key) == 0) {

bpf_map_lookup_elem(map_fd[0], &next_key, &val);

print_stack_info(&next_key, &val);

key = next_key;

}



static void print_stack_info(struct key_t *key, struct val_t *val) {

__u64 ip[PERF_MAX_STACK_DEPTH] = {};

int i;



printf("CPU: %d PID: %d COMM: %s\n", key->cpu, key->pid, val->comm);

printf("function call:\n");



if (bpf_map_lookup_elem(map_fd[1], &(val->stack_id), ip) != 0) {

printf("---NONE---\n");

} else {

for (i = PERF_MAX_STACK_DEPTH - 1; i >= 0; i--)

print_ksym(ip[i]);

printf("=========\n");

}

}



至於根據堆疊中的地址打印出函式名稱的部分,需要藉助於核心符號表的幫助,這部分的實現在C和python中都有對應的介面API。

下面是該工具的演示結果,本例中掛載函式為finish_task_switch:

這裡可以看到,同樣是finish_task_switch,對於普通程序可能是schedule函式執行到這裡的,而對於idle程序來說,則是從schedule_idle一步一步執行下來的。

目前這裡還是使用C語言書寫的,其實完全可以使用python去實現一下,這樣會避免掉一個編譯的過程,可以實現在命令列輸入然後動態顯示核心呼叫關係的功能。

直播回放

首屆eBPF大會創新通道開啟-徵集主題和專案

陳莉君老師:首屆eBPF大會之回望Linux核心之旅