为了更深入的理解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_UNINTERRUPTIBLE){ // 打印不可中断的进程
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();
// 定时器到期时间推迟10ms
hrtimer_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");
// printk("ptr_avenrun = %lx\n", ptr_avenrun);
if(!ptr_avenrun) return -EINVAL;
start_timer();
return 0;
}
static void load_monitor_exit(void){
hrtimer_cancel(&timer);
printk("load-monitor exit...\n");
}
module_init(load_monitor_init);
module_exit(load_monitor_exit);
MODULE_LICENSE("GPL v2");
Makefile
obj-m:= load_monitor.o
ccflags-y:= -std=gnu99
current_path:= $(shell pwd)
linux_kernel_path:= /usr/src/kernels/$(shell uname -r)/
all:
make -C $(linux_kernel_path) M=$(current_path) modules
clean:
make -C $(linux_kernel_path) M=$(current_path) clean
make
编译,insmod
加载模块,dmesg
查看日志,rmmod
卸载模块。
细节介绍
这里主要新的东西是用到了rcu锁和定时器. 打印进程信息其实没什么要过多介绍的, 只需要留意一下遍历进程用到的宏即可.
- rcu锁(read copy update), 顾名思义, 读, 拷贝, 更新锁. 读者不需要获取任何锁即可访问, 写者需要拷贝一个副本, 在副本上修改,在所有读操作结束之后通过一个callback回调函数将原来数据的指针指向被修改的数据. 开销更少, 适用于读多写少的情况.
- hrtimer定时器: 区别于低精度定时器依赖系统定期产生的tick中断的定时器, hrtimer直接由系统硬件高精度定时器触发, 目前系统中由3个定时器的hrtimer到期时间分别为10ns, 100ns和1000ns.
- 定时器的设置和释放是需要成对的, hrtimer_init初始化一个hrtimer就要在模块结束的时候hrtimer_cancel释放掉, 否则会导致系统崩溃.
- 内核中的浮点数: Linux内核中对浮点数的支持不够, 因此一般使用long来保存浮点数. 代码中使用 unsigned long *ptr_avenrun 数组来保存三个时间的浮点数的, 低11位为小数部分, 高位为整数部分. 我们常用的
top
,uptime
等命令读取的系统负载都是通过这种方式读取的. - 遍历进程, do_each_thread和while_each_thread两个宏定义在sched.h下, 点进去就能看到其实就是for和while.
- 获取系统负载:有些版本可以通过(void *)kallsyms_lookup_name(“averrun”);方法将averrun获取出来,有些版本版本(3.10.0)不行(会一直返回0),因此我是通过读取文件的方式将/proc/loadavg文件中的系统负载信息读取出来。
#include<linux/fs.h> #include<linux/uaccess.h> static long extract_float(int pos){ pos = pos >= 2 ? 2 : pos; // 边界检查 int ans[2]; memset(ans, 0, sizeof(ans));// float ans[0].ans[1] int i = 0, j = 0, k = 0; for(; buf[i] != '\0' && j <= pos && i <= 25; ++i){ if(buf[i] == ' '){ if(j == pos) return ans; ans[0] = ans[1] = 0; k = 0; ++j; continue; } // printk("%d: %c\n", i, buf[i]); if(buf[i] == '.'){ k ^= 1; continue; } ans[k] = ans[k] * 10 + buf[i] - '0'; // printk("ans[%d] = %d\n", k, ans[k]); }return ans; } static void cpu_avenrun(void){ struct file *fp; mm_segment_t fs; char buf[256]; fp = filp_open("/proc/loadavg", O_RDONLY, 0); if(IS_ERR(fp)){ printk("Failed to open file\n"); return ; } // 更改内核对内存地址的检查处理方式 fs = get_fs(); set_fs(get_ds()); memset(buf, '\0', sizeof(buf)); vfs_read(fp, buf, sizeof(buf), &fp->f_pos); printk("read: %s\n", buf); for(int i = 0; i <= 2; ++i){ int *load = extract_float(i); printk("Load(%d): %d.%d\n", i, load[0], load[1]); } filp_close(fp, NULL); set_fs(fs); }
运行实况
通过压力测试工具fio模拟io密集型任务.
参考资料
- Linux内核分析与应用:https://next.xuetangx.com/learn/XIYOU08091001441/XIYOU08091001441/14767915/video/30179772
- 谢宝友:Load高故障分析:https://heapdump.cn/article/1678795