进程管理实践:load_monitor负载监控模块(笔记)

为了更深入的理解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项工作

  1. 获得系统负载值
  2. 定时判断当前系统负载值是否超过某一阈值
  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锁和定时器. 打印进程信息其实没什么要过多介绍的, 只需要留意一下遍历进程用到的宏即可.

  • 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.
  • 获取系统负载:有些版本可以通过(void *)kallsyms_lookup_name(“averrun”);方法将averrun获取出来,但我使用的版本(3.10.0)不行(会一直返回0),因此我是通过读取文件的方式将/proc/loadavg文件中的系统负载信息读取出来。
    #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密集型任务.

参考资料

  • Linux内核分析与应用:https://next.xuetangx.com/learn/XIYOU08091001441/XIYOU08091001441/14767915/video/30179772
  • 谢宝友:Load高故障分析:https://heapdump.cn/article/1678795

你可能感兴趣的:(Linux内核编程,笔记)