进程管理实践: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<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
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值