为了更深入的理解PCB结构体task_struct,通过一个案例来进行
实现一个可应用于工程实践中的负载分析方法,load_monitor负载监视模块
通过top
或者uptime
这样的命令来查看系统的负载
会分别动态和静态的显示出系统的负载情况,其中load average字段分别表示系统1,5,10分钟的负载情况。
wa表示iowait
通过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
#include
#include
#include
#include
#include
#include
#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){
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
endif
ifneq ($(KERNELRELEASE),)
obj-m += $(MODNAME).o
$(MODNAME)-y := main.o
ccflags-y := -I$(PWD)/
else
export PWD=`pwd`
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
endif
make
编译,insmod
加载模块,dmesg
查看日志,remod
卸载模块。
这里主要新的东西是用到了rcu锁和定时器. 打印进程信息其实没什么要过多介绍的, 只需要留意一下遍历进程用到的宏即可.
top
, uptime
等命令读取的系统负载都是通过这种方式读取的.#include
#include
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());
vfs_read(fp, buf, sizeof(buf), &fp->f_pos);
printk("read: %s\n", buf);
filp_close(fp, NULL);
set_fs(fs);
}
通过压力测试工具fio模拟io密集型任务.