概述
本程序在謝寶友老師[1]所提供的高負載處理模塊的代碼[2]基礎上,根據5.15版內核的變化,修改出的。本程序是一個內核模塊,用于監控系統負載,在平均負載超過4時,打印所有進程的調用棧。
本程序分為三個文件:main.c、load.h、Makefile。其中,main.c是本內核模塊的主程序;load.h中是該內核模塊的擴展代碼,這里放了一個獲取內核中未被導出符號(變量或函數)的一個函數;Makefile用來編譯該內核模塊。完整代碼在文章的最下面。
模塊的主要實現方式為:設置一個定時器,以固定的間隔訪問系統給出的1分鐘內平均負載,如果超過負載閾值,則輸出運行隊列全部進程棧信息,并使程序休眠一段較長的時間。流程圖如下:
定時器
本模塊采用了hrtimer——高精度定時器,由linux/hrtimer.h引入,可精確到ns級。
平均負載
這里有所改動,原文中是通過kallsyms_lookup_name函數獲取的,但我在瀏覽頭文件時發現了linux/sched/loadavg.h頭文件,里面已經定義好了一些有關平均負載——loadavg的宏,并導出了avenrun——平均負載數組——1、5、15分鐘內的平均負載,所以我這里直接引用了該頭文件、直接使用了相關符號
輸出進程棧
這里改動很大,在5.15版中,沒有了save_stack_trace_tsk,通過查看linux/stacktrace.h文件,發現這個函數被用于未配置CONFIG_ARCH_STACKWALK的系統,而配置了CONFIG_ARCH_STACKWALK的系統中,有新的函數:unsigned int stack_trace_save_tsk(struct task_struct *task, unsigned long *store, unsigned int size, unsigned int skipnr),定義于kernel/stacktrace.c中,與舊函數相比變化很大,好在在源代碼中有詳細的接口說明,根據這我成功的修改了棧的輸出部分。
與此同時我發現了功能類似的另一個函數show_stack,定義于arch/x86/kernel/dumpstack.c
然而,這兩個函數的符號都沒有導出,也就無法通過引入相關頭文件來使用,原文章來獲取內核中未導出符號的kallsyms_lookup_name函數也未被導出,這就要求我尋找一種新的方法來獲取未導出符號,我找到了kprobe技術。
kprobes技術[3]是內核開發者們專門為了便于跟蹤內核函數執行狀態所設計的一種輕量級內核調試技術。利用kprobes技術,內核開發人員可以在內核的絕大多數指定函數中動態的插入探測點來收集所需的調試狀態信息而基本不影響內核原有的執行流程。我們可以通過注冊一個指定了函數名的kprobe來獲取函數的地址。
main.c
#include/* for stack_trace_print */ #include /* for module_*, MODULE_*, printk */ #include /* for hrtimer_*, ktime_* */ #include /* for avenrun, LOAD_* */ #include /* for struct task_struct */ #include /* for do_each_thread, while_each_thread */ #include "load.h" /* for find_kallsyms_lookup_name */ #define BACKTRACE_DEPTH 20 /* 最大棧深度 */ void (*ShowStack)(struct task_struct *task, unsigned long *sp, const char *loglvl); /* 將要指向stack_show函數,可以直接輸出進程控制塊的調用棧 */ unsigned int (*StackTraceSaveTask)(struct task_struct *tsk, unsigned long *store, unsigned int size, unsigned int skipnr); /* 將要指向stack_trace_save_tsk */ static void print_all_task_stack(void) { /* 打印全部進程調用棧 */ struct task_struct *g, *p; /* 用于遍歷進程 */ unsigned long backtrace[BACKTRACE_DEPTH]; /* 用于存儲調用棧的函數地址 */ unsigned int nr_bt; /* 用于存儲調用棧的層數 */ printk("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! "); printk("Load: %lu.%02lu, %lu.%02lu, %lu.%02lu ", /* 輸出近期平均負載 */ LOAD_INT(avenrun[0]), LOAD_FRAC(avenrun[0]), LOAD_INT(avenrun[1]), LOAD_FRAC(avenrun[1]), LOAD_INT(avenrun[2]), LOAD_FRAC(avenrun[2])); rcu_read_lock(); /* 為運行隊列上鎖 */ printk("dump running task. "); do_each_thread(g, p) { /* 遍歷運行隊列 */ if(p->__state == TASK_RUNNING) { printk("running task, comm: %s, pid %d ", p->comm, p->pid); // show_stack(p, NULL, ""); /* 可以取代下面兩個語句 */ nr_bt = StackTraceSaveTask(p, backtrace, BACKTRACE_DEPTH, 0); /* 保存棧 */ // 和下面一個語句一起可以取代上面一條語句 stack_trace_print(backtrace, nr_bt, 0); /* 打印棧 */ } } while_each_thread(g, p); printk("dump uninterrupted task. "); do_each_thread(g, p) { /* 和上面的遍歷類似 */ if(p->__state & TASK_UNINTERRUPTIBLE) { printk("uninterrupted task, comm: %s, pid %d ", p->comm, p->pid); // show_stack(p, NULL, ""); /* 可以取代下面兩個語句 */ nr_bt = StackTraceSaveTask(p, backtrace, BACKTRACE_DEPTH, 0); /* 保存棧 */ // 和下面一個語句一起可以取代上面一條語句 stack_trace_print(backtrace, nr_bt, 0); /* 打印棧 */ } } while_each_thread(g, p); rcu_read_unlock(); /* 為運行隊列解鎖 */ } struct hrtimer timer; /* 創建一個計時器 */ static void check_load(void) { /* 主要的計時器觸發后的程序 */ static ktime_t last; /* 默認值是0 */ u64 ms; int load = LOAD_INT(avenrun[0]); if(load < 4) /* 近1分鐘內平均負載不超過4,沒問題 */ return; ms = ktime_to_ms(ktime_sub(ktime_get(), last)); /* 計算打印棧時間間隔 */ if(ms < 20*1000) /* 打印棧的時間間隔小于20s,不打印 */ return; last = ktime_get(); /* 獲取當前時間 */ print_all_task_stack(); /* 打印全部進程調用棧 */ } static enum hrtimer_restart monitor_handler(struct hrtimer *hrtimer) { /* 計時器到期后調用的程序 */ enum hrtimer_restart ret = HRTIMER_RESTART; check_load(); hrtimer_forward_now(hrtimer, ms_to_ktime(10)); /* 延期10ms后到期 */ return ret; } static void start_timer(void) { hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_PINNED); /* 初始化計時器為綁定cpu的自開機以來的恒定時鐘 */ timer.function = monitor_handler; /* 設定回調函數 */ hrtimer_start_range_ns(&timer, ms_to_ktime(10), 0, HRTIMER_MODE_REL_PINNED); /* 啟動計時器并設定計時模式為綁定cpu的相對時間,計時10ms,松弛范圍為0 */ } static int load_monitor_init(void) { /* 模塊初始化 */ // ShowStack = find_kallsyms_lookup_name("show_stack"); /* 使用show_stack時將此三行取消注釋 */ // if(!ShowStack) // return -EINVAL; StackTraceSaveTask = find_kallsyms_lookup_name("stack_trace_save_tsk"); /* 使用stack_trace_save_tsk時將此三行取消注釋 */ if(!StackTraceSaveTask) return -EINVAL; start_timer(); printk("load-monitor loaded. "); return 0; } static void load_monitor_exit(void) { /* 模塊退出 */ hrtimer_cancel(&timer); /* 取消計時器 */ printk("load-monitor unloaded. "); } module_init(load_monitor_init); module_exit(load_monitor_exit); MODULE_DESCRIPTION("load monitor module"); MODULE_AUTHOR("Baoyou Xie "); MODULE_LICENSE("GPL");
load.h
#include/* for *kprobe* */ /* 調用kprobe找到kallsyms_lookup_name的地址位置 */ int noop_pre(struct kprobe *p, struct pt_regs *regs) { return 0; } /* 定義探針前置程序 */ void *find_kallsyms_lookup_name(char *sym) { /* 通過kprobe找到函數入口地址 */ int ret; void *p; /* 用于保存要返回的函數入口地址 */ struct kprobe kp = { /* 初始化探針 */ .symbol_name = sym, /* 設置要跟蹤的內核函數名 */ .pre_handler = noop_pre /* 放置前置程序 */ }; if ((ret = register_kprobe(&kp)) < 0) { /* 探針注冊失敗就報告錯誤信息并返回空指針 */ printk(KERN_INFO "register_kprobe failed, error ", ret); return NULL; } /* 保存探針跟蹤地址,即函數入口;輸出注冊成功信息,注銷探針,返回地址 */ p = kp.addr; printk(KERN_INFO "%s addr: %lx ", sym, (unsigned long)p); unregister_kprobe(&kp); return p; }
Makefile
OS_VER := UNKOWN UNAME := $(shell uname -r) ifneq ($(findstring 4.15.0-39-generic,$(UNAME)),) OS_VER := UBUNTU_1604 endif ifneq ($(KERNELRELEASE),) obj-m += $(MODNAME).o $(MODNAME)-y := main.o ccflags-y := -I$(PWD)/ else export PWD=`pwd` endif ifeq ($(KERNEL_BUILD_PATH),) KERNEL_BUILD_PATH := /lib/modules/`uname -r`/build endif ifeq ($(MODNAME),) export MODNAME=load_monitor endif all: make CFLAGS_MODULE=-D$(OS_VER) -C /lib/modules/`uname -r`/build M=`pwd` modules clean: make -C $(KERNEL_BUILD_PATH) M=$(PWD) clean
運行結果
將三個文件放入一個單獨的文件夾中,運行make命令,編譯出可插入內核的程序。編譯好后,運行sudo insmod load_monitor.ko命令將其插入內核。
接下來是測試,運行stress -c 8命令(stress需要另外安裝),使平均負載快速到達4以上,這里可以在新的虛擬終端通過top命令實時觀測負載。當負載到達4之后,在運行著stress命令的窗口中按下ctrl+c終止程序,運行sudo dmesg命令就可以查看到內核棧的輸出信息。
-
內核
+關注
關注
3文章
1366瀏覽量
40236 -
定時器
+關注
關注
23文章
3241瀏覽量
114516 -
高負載
+關注
關注
0文章
4瀏覽量
5943
原文標題:概述
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論