为了更深入的理解PCB结构体task_struct,通过一个案例来进行
实现一个可应用于工程实践中的负载分析方法,load_monitor负载监视模块
系统负载
通过top
或者uptime
这样的命令来查看系统的负载
会分别动态和静态的显示出系统的负载情况,其中load average字段分别表示系统1,5,10分钟的负载情况。
wa表示iowait
负载和CPU核心数
- 单核CPU,就是单个处理单元,任务串行工作,相当于进程在一条车道上通行。
- 多核CPU,就是一个物理CPU有多个单独的核进行工作,任务可以并行工作,相当于进程可以在多条车道上通行。
- 多CPU,就是一个计算机系统中集成了多个物理CPU。
通过nproc
或者lscpu
命令可以查看CPU的核心数。
负载就是指CPU的负载情况,以负载值0.5,1,1.7和2.3举例
在单核的环境下,0.5表示负载良好,1表示负载正好满了,1.7表示还有0.7的进程在等待,2.3表示还有1.3的进程在等待
在2核的环境下,0.5,1,1.7都表示负载良好。2.3表示负载满了,但依然有0.3的进程在等待。
需求分析
该模块的功能是持续的监视系统的负载,当系统负载超过某一阈值时,打印出系统内所有线程的调用栈。
通过调用栈的信息,进一步分析负载异常。
为了实现这样的功能,内核模块需要完成3项工作
- 获得系统负载值
- 定时判断当前系统负载值是否超过某一阈值
- 打印线程的调用栈
代码实例
#include<linux/module.h>
#include<linux/init.h>
#include<linux/kernel.h>#include<linux/kallsyms.h>
#include<linux/sched.h>
#include<linux/hrtimer.h>
#include<linux/stacktrace.h>#define FSHIFT 11
#define FIXED_1 (1 << FSHIFT)
#define LOAD_INT(x) ((x) >> FSHIFT) // 高位整数
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1 - 1)) * 100)
#define BACKTRACE_DEPTH 20 // 保存的栈深struct hrtimer timer; // 高精度定时器
static unsigned long *ptr_avenrun; // 系统负载保存的地址static void print_all_task_stack(void){struct task_struct *g, *p;unsigned long backtrace[BACKTRACE_DEPTH]; // 每个调用具体的信息struct stack_trace trace; // 保存进程调用栈信息memset(&trace, 0, sizeof(trace));memset(backtrace, 0, BACKTRACE_DEPTH * sizeof(unsigned long));trace.max_entries = BACKTRACE_DEPTH; // 初始化 保存 栈深trace.entries = backtrace; // 定义保存信息数组printk("======================================\n");printk("\tLoad: %lu.%02lu, %lu.%02lu, %lu.%02lu\n",LOAD_INT(ptr_avenrun[0]), LOAD_INT(ptr_avenrun[0]),LOAD_INT(ptr_avenrun[1]), LOAD_INT(ptr_avenrun[1]),LOAD_INT(ptr_avenrun[2]), LOAD_INT(ptr_avenrun[2]));printk("dump all task: ...\n");// 接下来就是打印线程链表了, 需要先锁一下链表, 防止出现并发问题rcu_read_lock();printk("dump running task.\n");do_each_thread(g, p){if(p->state == TASK_RUNNING){ // 打印正在运行的进程printk("running task, comm: %s, pid %d\n", p->comm, p->pid);memset(&trace, 0, sizeof(trace));memset(backtrace, 0, BACKTRACE_DEPTH * sizeof(unsigned long));trace.max_entries = BACKTRACE_DEPTH;trace.entries = backtrace;save_stack_trace_tsk(p, &trace);print_stack_trace(&trace, 0);}}while_each_thread(g, p);printk("dump uninterrupted task.\n");do_each_thread(g, p){if(p->state & TASK_UNMAPPED_BASE){ // 打印不可中断的进程printk("running task, comm: %s, pid %d\n", p->comm, p->pid);memset(&trace, 0, sizeof(trace));memset(backtrace, 0, BACKTRACE_DEPTH * sizeof(unsigned long));trace.max_entries = BACKTRACE_DEPTH;trace.entries = backtrace;save_stack_trace_tsk(p, &trace);print_stack_trace(&trace, 0);}}while_each_thread(g, p);rcu_read_unlock();
}
static void check_load(void){static ktime_t last;__u64 ms;int load = LOAD_INT(ptr_avenrun[0]);if(load < 3) return ; // 判断阈值ms = ktime_to_ms(ktime_sub(ktime_get(), last));if(ms < 20 * 1000) 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();// 定时器到期时间推迟10mshrtimer_forward_now(hrtimer, ms_to_ktime(10));// 返回定时器重启信号return ret;
}
static void start_timer(void){// 初始化定时器hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_PINNED);// 指定回调函数timer.function = monitor_handler(); // 指定定时器重启的到期时间(10ms)hrtimer_start_range_ns(&timer, ms_to_ktime(10), 0, HRTIMER_MODE_REL_PINNED);
}static int load_monitor_init(void){printk("load-monitor loaded in kernel...\n");// 获得保存系统负载变量 所在的地址, 之后所有判断都会先读取一下系统负载ptr_avenrun = (void *)kallsyms_lookup_name("averrun");i(!ptr_avenrun) return -EINVAL;start_timer();return 0;
}
static void load_monitor_exit(void){printk("load-monitor exit...\n");
}
module_init(load_monitor_init);
module_exit(load_monitor_exit);
MODULE_LICENSE("GPL v2");
Makefile
OS_VER := UNKNOWN
UNAME := $(shell uname -r)
ifneq ($(findstring 4.15.0-39-generic,$(UNAME)),)OS_VER := UBUNTU_1604
endififneq ($(KERNELRELEASE),)obj-m += $(MODNAME).o$(MODNAME)-y := main.occflags-y := -I$(PWD)/
elseexport PWD=`pwd`ifeq ($(KERNEL_BUILD_PATH),)KERNEL_BUILD_PATH := /lib/modules/`uname -r`/build
endif
ifeq ($(MODNAME),)export MODNAME=load_monitor
endifall:make CFLAGS_MODULE=-D$(OS_VER) -C /lib/modules/`uname -r`/build M=`pwd` modules
clean:make -C $(KERNEL_BUILD_PATH) M=$(PWD) clean
endif
make
编译,insmod
加载模块,dmesg
查看日志,remod
卸载模块。
细节介绍
这里主要新的东西是用到了rcu锁和定时器. 打印进程信息其实没什么要过多介绍的, 只需要留意一下遍历进程用到的宏即可.
- rcu锁(read copy update), 顾名思义, 读, 拷贝, 更新锁. 读者不需要获取任何锁即可访问, 写者需要拷贝一个副本, 在副本上修改,在所有读操作结束之后通过一个callback回调函数将原来数据的指针指向被修改的数据. 开销更少, 适用于读多写少的情况.
- hrtimer定时器: 区别于低精度定时器依赖系统定期产生的tick中断的定时器, hrtimer直接由系统硬件高精度定时器触发, 目前系统中由3个定时器的hrtimer到期时间分别为10ns, 100ns和1000ns.
- 内核中的浮点数: Linux内核中对浮点数的支持不够, 因此一般使用long来保存浮点数. 代码中使用 unsigned long *ptr_avenrun 数组来保存三个时间的浮点数的, 低11位为小数部分, 高位为整数部分. 我们常用的
top
,uptime
等命令读取的系统负载都是通过这种方式读取的. - 遍历进程, do_each_thread和while_each_thread两个宏定义在sched.h下, 点进去就能看到其实就是for和while.
运行实况
通过压力测试工具fio模拟io密集型任务.
参考资料
- Linux内核分析与应用:https://next.xuetangx.com/learn/XIYOU08091001441/XIYOU08091001441/14767915/video/30179772
- 谢宝友:Load高故障分析:https://heapdump.cn/article/1678795