0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

高负载处理及上下文切换

Linux阅码场 来源:Linux阅码场 作者:Linux阅码场 2022-11-10 09:37 次阅读

概述

本程序在谢宝友老师[1]所提供的高负载处理模块的代码[2]基础上,根据5.15版内核的变化,修改出的。本程序是一个内核模块,用于监控系统负载,在平均负载超过4时,打印所有进程的调用栈。

本程序分为三个文件:main.c、load.h、Makefile。其中,main.c是本内核模块的主程序;load.h中是该内核模块的扩展代码,这里放了一个获取内核中未被导出符号(变量或函数)的一个函数;Makefile用来编译该内核模块。完整代码在文章的最下面。

模块的主要实现方式为:设置一个定时器,以固定的间隔访问系统给出的1分钟内平均负载,如果超过负载阈值,则输出运行队列全部进程栈信息,并使程序休眠一段较长的时间。流程图如下:

0451803c-608f-11ed-8abf-dac502259ad0.png

定时器

本模块采用了hrtimer——高精度定时器,由linux/hrtimer.h引入,可精确到ns级。

平均负载

这里有所改动,原文中是通过kallsyms_lookup_name函数获取的,但我在浏览头文件时发现了linux/sched/loadavg.h头文件,里面已经定义好了一些有关平均负载——loadavg的宏,并导出了avenrun——平均负载数组——1、5、15分钟内的平均负载,所以我这里直接引用了该头文件、直接使用了相关符号

输出进程栈

这里改动很大,在5.15版中,没有了save_stack_trace_tsk,通过查看linux/stacktrace.h文件,发现这个函数被用于未配置CONFIG_ARCH_STACKWALK的系统,而配置了CONFIG_ARCH_STACKWALK的系统中,有新的函数:unsigned int stack_trace_save_tsk(struct task_struct *task, unsigned long *store, unsigned int size, unsigned int skipnr),定义于kernel/stacktrace.c中,与旧函数相比变化很大,好在在源代码中有详细的接口说明,根据这我成功的修改了栈的输出部分。

与此同时我发现了功能类似的另一个函数show_stack,定义于arch/x86/kernel/dumpstack.c

然而,这两个函数的符号都没有导出,也就无法通过引入相关头文件来使用,原文章来获取内核中未导出符号的kallsyms_lookup_name函数也未被导出,这就要求我寻找一种新的方法来获取未导出符号,我找到了kprobe技术。

kprobes技术[3]是内核开发者们专门为了便于跟踪内核函数执行状态所设计的一种轻量级内核调试技术。利用kprobes技术,内核开发人员可以在内核的绝大多数指定函数中动态的插入探测点来收集所需的调试状态信息而基本不影响内核原有的执行流程。我们可以通过注册一个指定了函数名的kprobe来获取函数的地址。

main.c

#include  /* for stack_trace_print */
#include  /* for module_*, MODULE_*, printk */
#include  /* for hrtimer_*, ktime_* */
#include  /* for avenrun, LOAD_* */
#include  /* for struct task_struct */
#include  /* for do_each_thread, while_each_thread */
#include "load.h" /* for find_kallsyms_lookup_name */
#define BACKTRACE_DEPTH 20 /* 最大栈深度 */
void (*ShowStack)(struct task_struct *task, unsigned long *sp, const char *loglvl); /* 将要指向stack_show函数,可以直接输出进程控制块的调用栈 */
unsigned int (*StackTraceSaveTask)(struct task_struct *tsk, unsigned long *store, unsigned int size, unsigned int skipnr); /* 将要指向stack_trace_save_tsk */
static void print_all_task_stack(void) { /* 打印全部进程调用栈 */
    struct task_struct *g, *p; /* 用于遍历进程 */
    unsigned long backtrace[BACKTRACE_DEPTH]; /* 用于存储调用栈的函数地址 */
    unsigned int nr_bt; /* 用于存储调用栈的层数 */
    printk("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
");
    printk("Load: %lu.%02lu, %lu.%02lu, %lu.%02lu
", /* 输出近期平均负载 */
        LOAD_INT(avenrun[0]), LOAD_FRAC(avenrun[0]),
        LOAD_INT(avenrun[1]), LOAD_FRAC(avenrun[1]),
        LOAD_INT(avenrun[2]), LOAD_FRAC(avenrun[2]));
    rcu_read_lock(); /* 为运行队列上锁 */
    printk("dump running task.
");
    do_each_thread(g, p) { /* 遍历运行队列 */
        if(p->__state == TASK_RUNNING) {
            printk("running task, comm: %s, pid %d
", p->comm, p->pid);
            // show_stack(p, NULL, ""); /* 可以取代下面两个语句 */
            nr_bt = StackTraceSaveTask(p, backtrace, BACKTRACE_DEPTH, 0); /* 保存栈 */ // 和下面一个语句一起可以取代上面一条语句
            stack_trace_print(backtrace, nr_bt, 0); /* 打印栈 */
        }
    } while_each_thread(g, p);
    printk("dump uninterrupted task.
");
    do_each_thread(g, p) { /* 和上面的遍历类似 */
        if(p->__state & TASK_UNINTERRUPTIBLE) {
            printk("uninterrupted task, comm: %s, pid %d
", p->comm, p->pid);
            // show_stack(p, NULL, ""); /* 可以取代下面两个语句 */
            nr_bt = StackTraceSaveTask(p, backtrace, BACKTRACE_DEPTH, 0); /* 保存栈 */ // 和下面一个语句一起可以取代上面一条语句
            stack_trace_print(backtrace, nr_bt, 0); /* 打印栈 */
        }
    } while_each_thread(g, p);
    rcu_read_unlock(); /* 为运行队列解锁 */
}
struct hrtimer timer; /* 创建一个计时器 */
static void check_load(void) { /* 主要的计时器触发后的程序 */
    static ktime_t last; /* 默认值是0 */
    u64 ms;
    int load = LOAD_INT(avenrun[0]);
    if(load < 4) /* 近1分钟内平均负载不超过4,没问题 */
        return;
    ms = ktime_to_ms(ktime_sub(ktime_get(), last)); /* 计算打印栈时间间隔 */
    if(ms < 20*1000) /* 打印栈的时间间隔小于20s,不打印 */
        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();
    hrtimer_forward_now(hrtimer, ms_to_ktime(10)); /* 延期10ms后到期 */
    return ret;
}
static void start_timer(void) {
    hrtimer_init(&timer, CLOCK_MONOTONIC, HRTIMER_MODE_PINNED); /* 初始化计时器为绑定cpu的自开机以来的恒定时钟 */
    timer.function = monitor_handler; /* 设定回调函数 */
    hrtimer_start_range_ns(&timer, ms_to_ktime(10), 0, HRTIMER_MODE_REL_PINNED); /* 启动计时器并设定计时模式为绑定cpu的相对时间,计时10ms,松弛范围为0 */
}
static int load_monitor_init(void) { /* 模块初始化 */
    // ShowStack = find_kallsyms_lookup_name("show_stack"); /* 使用show_stack时将此三行取消注释 */
    // if(!ShowStack)
    //     return -EINVAL;
    StackTraceSaveTask = find_kallsyms_lookup_name("stack_trace_save_tsk"); /* 使用stack_trace_save_tsk时将此三行取消注释 */
    if(!StackTraceSaveTask)
        return -EINVAL;
    start_timer();
    printk("load-monitor loaded.
");
    return 0;
}
static void load_monitor_exit(void) { /* 模块退出 */
    hrtimer_cancel(&timer); /* 取消计时器 */
    printk("load-monitor unloaded.
");
}
module_init(load_monitor_init);
module_exit(load_monitor_exit);
MODULE_DESCRIPTION("load monitor module");
MODULE_AUTHOR("Baoyou Xie ");
MODULE_LICENSE("GPL");

load.h

#include  /* for *kprobe* */
/* 调用kprobe找到kallsyms_lookup_name的地址位置 */
int noop_pre(struct kprobe *p, struct pt_regs *regs) { return 0; } /* 定义探针前置程序 */
void *find_kallsyms_lookup_name(char *sym) { /* 通过kprobe找到函数入口地址 */
    int ret;
    void *p; /* 用于保存要返回的函数入口地址 */
    struct kprobe kp = { /* 初始化探针 */
        .symbol_name = sym, /* 设置要跟踪的内核函数名 */
        .pre_handler = noop_pre /* 放置前置程序 */
    };
    if ((ret = register_kprobe(&kp)) < 0) { /* 探针注册失败就报告错误信息并返回空指针 */
        printk(KERN_INFO "register_kprobe failed, error
", ret);
        return NULL;
    }
    /* 保存探针跟踪地址,即函数入口;输出注册成功信息,注销探针,返回地址 */
    p = kp.addr;
    printk(KERN_INFO "%s addr: %lx
", sym, (unsigned long)p);
    unregister_kprobe(&kp);
    return p;
}

Makefile

OS_VER := UNKOWN
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`
endif


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

运行结果

将三个文件放入一个单独的文件夹中,运行make命令,编译出可插入内核的程序。编译好后,运行sudo insmod load_monitor.ko命令将其插入内核。

接下来是测试,运行stress -c 8命令(stress需要另外安装),使平均负载快速到达4以上,这里可以在新的虚拟终端通过top命令实时观测负载。当负载到达4之后,在运行着stress命令的窗口中按下ctrl+c终止程序,运行sudo dmesg命令就可以查看到内核栈的输出信息。

04656c78-608f-11ed-8abf-dac502259ad0.png

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1309

    浏览量

    39846
  • 定时器
    +关注

    关注

    23

    文章

    3147

    浏览量

    112037
  • 高负载
    +关注

    关注

    0

    文章

    4

    浏览量

    5920

原文标题:概述

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    关于进程上下文、中断上下文及原子上下文的一些概念理解

    : 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈。 当发生进程调度时,进行进程切换就是上下文切换(context switch
    发表于 09-06 09:58

    进程上下文与中断上下文的理解

    )进程下文:其是指切换到内核态后执行的程序,即进程运行在内核空间的部分。2.中断上下文:(1)中断上文:硬件通过中断触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的
    发表于 12-11 19:45

    BT堆栈上下文切换

    100ms就会产生一个上下文切换上下文切换每秒似乎有点。我想我真的不能抱怨10个开关,但是有什么东西吗?在BT协议中真的需要这个吗?不能处理中断驱动吗?(这是BT是可连接的,但既没
    发表于 12-17 16:30

    多线程如何实现上下文切换

    处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成
    发表于 08-02 08:21

    上下文切换简介

    处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成
    发表于 08-06 08:08

    上下文切换的情况发生

    处理系统中,CPU需要处理所有程序的操作,当用户来回切换它们时,需要记录这些程序执行到哪里。上下文切换就是这样一个过程,他允许CPU记录并恢复各种正在运行程序的状态,使它能够完成
    发表于 08-07 08:38

    ucos上下文该怎么切换

    的值,那么在pendSV处理结束的时刻我们看到有条 BXLR 指令, 那岂不是又回到了任务A的这个地方呀,怎么能切换到别的任务上去啊??????---->问题2:参见附件:图示中上下文切换为什么会回到任务B呀?不是IRQ一直悬起
    发表于 08-26 03:21

    基于cortex-m3的rt-thread系统如何实现线程上下文切换

    PendSV 异常处理函数里完成上下文切换 LDRr0, =NVIC_INT_CTRL LDRr1, =NVIC_PENDSVSET STRr1, [r0] BXLR流程图如下
    发表于 05-05 15:00

    讨论ARM mbed OS(RTX) 的上下文切换

    ;#125;4.2 第一次上下文切换在本文 3.2 节对系统调用处理函数的解析中,我们有意忽略了一个极其重要的点。因为在 3.2 节中,我们聚焦的是系统调用的实现。本节中,我们补充被忽略的事实:mbed 会在
    发表于 02-16 14:26

    rt-thread上下文切换函数的意义在哪?

    Cortex-M3内核上下文切换函数rt_hw_context_switch()/ rt_hw_context_switch_interrupt()中有个判断rt_thread_switch_interrupt_flag的地方,不知道意义在哪?
    发表于 03-10 11:28

    中断中的上下文切换详解

    的,那么根据上文分析,在出systick中断处理函数时,也是需要调用tos_knl_irq_leave来使得此任务得到一次上下文切换的机会。  总结一下,对于可能会触发某些任务进入ready状态的中断处理
    发表于 03-23 17:18

    CPU上下文切换的详细资料讲解

    当UCOS-III转向执行另一项新任务的时候,他保存了当前任务的CPU寄存器到堆栈,并从新任务的堆栈CPU寄存器载入CPU,这个过程叫做上下文切换
    发表于 08-16 17:31 2次下载
    CPU<b class='flag-5'>上下文切换</b>的详细资料讲解

    如何分析Linux CPU上下文切换问题

    在我的上一篇文章:《探讨 Linux CPU 的上下文切换》中,我谈到了 CPU 上下文切换的工作原理。快速回顾一下,CPU 上下文切换是保证 Linux 系统正常运行的核心功能。可分为进程
    的头像 发表于 05-05 20:11 1629次阅读

    Linux技术:什么是cpu上下文切换

    过多的上下文切换会消耗 CPU 的时间来保存和恢复寄存器、程序计数器、内核栈和虚拟内存等数据,从而导致系统性能显着下降。 既然上下文切换对系统性能的影响如此之大,那么我们如何检查它呢?好了,你可以使用 vmstat 工具来查询你系统的
    发表于 09-01 09:31 233次阅读
    Linux技术:什么是cpu<b class='flag-5'>上下文切换</b>

    FreeRTOS系列技术文章:上下文切换

    嵌入式实时操作系统(RTOS)中的上下文切换是指保存和恢复任务的状态,以使调度程序能够切换到另一个任务,从而促进多任务处理
    的头像 发表于 11-21 15:48 411次阅读