当前课程知识点:Linux 内核分析与应用 > 第3章 进程管理 > 3.5工程实践-基于内核模块的负载监控 > Video
大家好 今天给大家介绍的是谢宝友老师的一篇文章load高故障分析
在文章中 谢老师分享了一个可应用于工程实践中的负载分析方法
load_monitor负载监视模块 今天就让我们一同来学习一下
首先我们要能够正确理解系统负载的含义
在linux 中 我们一般使用
top 或者 uptime 这样的命令来查看系统负载
它会返回load average的3个数字
分别表示的是系统在1分钟 5分钟 15分钟内的平均负载情况
那么 如何通过这些数值形象的理解系统负载呢
这里 我们需要做一个简单的类比
首先 假设一个最简单的情况 你的电脑只有一个CPU核心
所有的运算都必须由这个CPU来完成
那么CPU就可以被看成是一条
单车道高速公路
系统负载为0 表示这条路上没有车辆通行
系统负载为0.5 则表示着这条路上只有一半的路段有车辆通行
而系统负载为1.0 则表示着整条路上
已经被车辆占满 而系统负载为1.7则表示着
除了已经被占满的这条路 还有一些车辆等待着上高速公路
而这些等待上路的车辆的数量占已上路车辆数量的70%
接下来我们需要理解平均负载和CPU核心数的关系
这里我们来看两个概念 一个是多处理器 一个是多核处理器
多处理器表示一个计算机系统中
集成了两个或多个物理CPU 也就是说我们的主板上 插了
可能两个或多个CPU芯片
而多核处理器则表示一个物理CPU也就是一个
CPU芯片 它内部可能有两个或者多个单独的核在并行工作
双核意味着有两个处理单元 四核那就是有四个处理单元了
在linux中我们可以通过
nproc或者lscpu这样的命令来查看cpu核心数
在lscpu
这个命令中它可能看到的信息比较多它会告诉你
系统中有几个物理核心 有几个逻辑核心
注意我们这里需要看的是逻辑核的个数
多个核心也就意味我们的系统可以在同一时间处理多个进程
放在我们刚提到的高速公路的例子中呢 多核心就意味着多车道
比如在双cpu核心的系统中 如果系统负载为2的话
也就意味着我们
高速公路的两条车道被车辆占满
但是呢此时并没有等待上高速公路的车辆
好了了解了这些让我们来一同看看源码
该内核模块的功能是持续的监视系统负载
当系统负载值超过某一阈值时 打印出系统内所有线程的调用栈
有了这些调用栈的信息 我们就可以作进一步的负载异常分析了
为了实现该功能 内核模块需要完成3项工作
1获得系统负载值 2定时判断当前系统负载值是否超过某一阈值
3打印线程的调用栈
我们就这三方面将源码分为三部分 接下来我给大家来作一一介绍
首先是如何获取系统负载值 我们先找到模块初始化函数
在这里Linux 内核定义一个
无符号的长整形的数组unsigned long
该数组有三个元素 因为内核不能很好的支持浮点数
所以在这个数组中存放的无符号长整型呢
将它的低 11位用来存放负载的
小数部分,高位用于存放整数部分
我们使用topuptime等命令读取的系统
负载 load average 就是通过这个数组来获取的
我们使用topuptime等命令读取的系统
负载 load average 就是通过这个数组来获取的
虽然我们可以直接在模块中读取这个数组的值
但是为了防止某些版本的内核没有通过EXPORT_SYMBOL的
导出这个变量 我们使用这样一个函数
来获得这个变量的地址 将他赋给我们事先定义的一个指针
并且 因为我们要将其转化为方便阅读的十进制的小数
所以还需要定义一些宏 来实现进制的转换
在我们的源码的前面 这些 有了这些宏
我们就可以随时获取系统中的的lode average了
我们再回到刚才的模块初始化函数
这里给大家留一个思考题 在某些版本的linux内核中
不能直接调用kallsyms_lookup_name这个函数
那么还有哪些办法可以获得内核函数或者变量的地址呢
这第二项工作 我们需要能够定时的判断系统负载值
这里我们需要设置一个定时器
start_timer函数就是用来启动定时器的 我们来看看它的内容
在这里
负载异常变化的情况 有可能只是
进程执行过程的某一个瞬间
所以为了能够精确的监视系统负载的变化情况
并且及时的打印线程的调用栈 我们需要一个精度高
且触发有保证的定时器
hrtimer就是我们的选择
不同于低精度定时器
是依赖系统定期产生的 tick 中断的
在高精度时钟模式下 定时器直接由
高精度定时器硬件产生的中断触发
目前系统中有 3 个 hrtimer
其到期时间分别为 10ns 100ns 和 1000ns
有关hrtimer的更多内容 在第五章第四讲中将会提到
我们这里只介绍其使用方法
在我们使用hrtimer时 首先需要定义一个
hrtimer结构的实例
我们在这里将其命名为timer
它的定义在源码的前面
然后用hrtimer_init函数
对它进行初始化
这里是已定义的hrtimer实例的地址
还有一些有关于定时器的标志位传入函数
接着需要指定回调函数 也就是定时器到期时
我们需要执行的函数
我们将该函数赋给timer的function字段 就完成了设置
最后一步我们要指定
定时器重启的到期范围
也就是这样一个函数hrtimer_start_range
该函数指定了定时器到期的时间范围是10ms
接着我们再来看看monitor_handler
也就是定时器到期后用来处理
监视内容的这样一个函数 在上面
我们可以看到该函数的返回值是一个枚举类型
hrtimer_restart 这个枚举类型实际上有两个值
一个是restart 一个是norestar
顾名思义就表示我们的定时器是否需要重启
首先我们将返回值设置为restart
接着是check_load函数
也就是检查当前系统负载值
检查完后我们需要执行这样一个hrtimer_forward_now的函数
将定时器的到期时间从现在开始起推迟10ms
最后是返回定时器重启的信号
完成这样的设置之后 我们就可以每隔10ms
检查一下我们的系统负载值
然后我们再来看看check_load函数中的内容
在这里 首先是设置一个ktime_t类型的
静态局部变量last
这个ktime_t呢它是
hrtimer保存时间的一个数据结构
我们在这里取得
最近一分钟内的平均负载值 将其转化为10进制
我们与事先设置的阈值进行比较
这对于我这台单cpu核心的系统来说已经是一个很高的负载值了
也意味着至少有两倍于cpu处理能力的进程正在等待
接着呢 我们用这样一个函数ktime_get获取当前的系统时间
如果当前的系统时间和这个last
我们刚才定义的last 这个last保存的是
上一次打印时的系统时间
我们用ktime_sub函数完成这两个数值的相减
求出两个时间的差值
如果差值不到20秒的话那么就直接退出
如果两次打印的时间超过20秒
那么我们就继续打印 首先呢用ktime_get
获取当前的系统时间存放在last中
最后执行这个print_all_task_stack函数 打印出所有的线程栈
最后一项工作 我们就是
需要一个能够打印出线程的调用栈函数 也就是
这里的print_all_task_stack 我们来看一看它的内容
这里使用了一个叫做
stack_trace的结构体
它是用来专门保存进程调用栈信息的
另外定义了一个名为backtrace的数组
它来存放每一个调用具体的信息
首先呢我们用函数memset
将这两个数据结构进行初始化
并且呢将
stack_trace这个结构体
它再进行初始化 也就是
设置它的保存的最大的
登录栈的深度是20 这个是我们在前面设置的一个宏 它的值是20
然后用来存放调用信息的数组我们就将它设置为backtrace这样我们定义的数组
完成了这些之后 我们就可以进行遍历线程了
这里我们使用是这样两个宏do_each_thread(g, p)
还有while_each_thread(g, p)这两个宏
来遍历系统中所有的线程
因为在Linux中线程并没有一个独立的结构
它是用进程模拟的 也称之为轻量级进程
所以线程也会有其pid 但是除此之外
线程还有其tgid即thread group id
表示该线程组的id 其实也就是所属主线程的pid
在do_each_thread这个宏
在这个宏之中呢它会将0号进程
task_struct的地址赋给g和p 这个g和p呢
是我们在上面定义的一个
task_struct机构的这样一个指针
将0号进程task_struct的地址赋给g和p之后呢
它就会从0号进程开始 按照进程之间的父子关系
遍历系统中所有的进程 也就是遍历系统中所有的线程
对于满足某一状态的进程呢 我们保存
它的堆栈追踪信息在trace中
并且用print_stack_trace()将其中的信息打印出来
注意这里呢 我们每一次
打印都需要初始化stack_trace结构体和
我们的这个backtrace数组
否则会打印出非当前进程的调用栈
最后我们可以看到在第一个循环中 打印的是TASK
RUNNING状态的进程
在第二个循环中,打印的是TASK_UNINTERRUPTIBLE
进程 由于我们得平均负载
它的定义是指在单位时间内 系统处于可运行状态
不可中断状态的平均进程数
所以在这里将这两种状态的进程都打印了出来
对于第一种处于TASK_RUNNING状态的进程很好理解
它指的是正在使用 CPU 或者正在等待 CPU 的进程 而后者
不可中断状态的进程
则是正处于内核态关键流程中的进程
并且这些流程是不可打断的
最常见的是等待硬件设备的 I/O 响应的进程
所以 我们需要将这两种状态的进程都分别打印出来
这里呢 我们可以看到最后这样一个点就是
我们在遍历线程之前 我们将rcu_read的这个锁
打开了 然后在遍历完线程后将这个锁解开了 这个锁呢
很关键
因为我们在打印线程栈时需要遍历系统内的进程链表
如果在这一过程中有某些进程死亡或者产生了一些变化
对链表产生了修改 就会对遍历链表产生影响
所以在遍历的过程中需要加锁
这个RCU锁的全称是(Read-Copy Update)
顾名思义就是读-拷贝修改
它是基于其原理命名的
对于一个被RCU保护的共享数据结构
读者不需要获得任何锁就可以访问它
但是对于写者在访问它时
首先拷贝一个副本 然后对副本进行修改 最后再使用一个
callback回调机制
在适当的时机把指向原来数据的指针重新指向新的被修改的数据
个回调的时机就是所有引用该数据的CPU
都退出对共享数据的操作的时候
这个锁的好处是 比起传统的读写锁 自旋锁
而rcu锁开销更小 适用于读多写少的情况
也就是我们在这里对于
对于进程链表的遍历 这种情况我们使用RCU锁是再合适不过了
好了有了这些 我们的内核模块就可以顺利的完成那三个任务了
实现实时监视系统负载
并在系统负载超过阈值时打印出线程的调用栈了
最后我们再来看看源码前面对于一些宏的定义
这里是backtrace的深度 也就是数组的
能够存放的调用栈的数目
这里我们设置是20
这里呢是我们刚才看到的avenrun数组
对它进行进制转换的
这里我们定义了一个hrtimer结构的实例 timer
这里是一个无符号长整型的一个指针
ptr_avenrun,用来存放
指向avenrun数组的指针
好了 这就是内核模块的全部内容
我们注意到在我们的内核源码中它加入了一个load.h的头文件
打开这个.h的头文件之后发现里边是空的 只有一些注释
那么这里给大家留一个思考题 这个空的load.h头文件有什么作用
这是内核模块的Makefile文件 这里给大家留一个思考题
为什么要定义OS_VER这个变量
在make之后将.ko文件插入 我们的负载监视器就开始工作了
为了检验其运行情况 我们需要模拟高系统负载的状况
这里我们使用这样一个循环来模拟cpu密集型任务
FIO是很好的用来测试
每秒IO操作的工具 一般是用来对硬件进行压力测试和验证的
在这里用以模拟I/O密集型的任务
执行这样一个命令后呢
系统就开始频繁的进行IO操作
我这里已经事先运行过这些任务了 我们在top中可以看到
我的系统在15分钟内系统负载都保持在一个比较高的水平
只是在1分钟内系统负载已经达到了3.4 这已经是非常高了
现在我们就可以检验一下我们的
负载监视器有没有正常工作 我们现在使用dmesg-c
命令来查看打印的log
我们可以看到线程的调用栈被依次打印了出来
首先打印出的是处在RUNNING状态的进程
下面是处在UNINTERRUPTIBLE状态的进程
看到 这个进程的
名称 这是它的进程号
这个进程在执行的过程中呢 会从下
到上依次调用这些函数
这里每一条信息表示的是
该指令执行的地址 前面是函数名
加上来的呢这个是
该指令相对于函数首地址偏移地址
最后这个数值是这个函数总的长度
这样我们就完成了实时监视系统负载值
并可以在高负载时将系统调用栈打印出来
有了这些信息 我们就可以对这些产生异常的
进程作进一步分析了
最后再给大家留几个思考题 在刚刚我们用top命令
打印出来的系统参数中 有一个指标叫做wa——iowait
只要系统中有I/O密集型的程序运行时 这个iowait指标就会上升
但是呢我们注意到我们在使用了fio 用来一个模拟
I/O密集型的程序的操作之后呢我们的
wa的这个指标值它一直是保持为0的
那么大家思考一下 这是为什么呢
接下来的思考题是 我们的模块代码还有哪些值得修改的地方
您还有其他的方法用来追踪load高的问题吗
这些不同的方法有什么优缺点呢
好了 今天的分享就是这些 谢谢大家
-1.1 Linux操作系统概述
-1.2 Linux内核结构以及内核模块编程
--Video
-1.3 Linux内核源码中的双链表结构
--Video
-1.4 源码分析-内核中的哈希表
--Video
-1.5 动手实践-Linux内核模块的插入和删除
--Video
-第1章 概述--章节测验
-2.1 内存管理之内存寻址
--Video
-2.2 段机制
--Video
-2.3分页机制
--Video
-2.4 动手实践-把虚拟地址转换成物理地址
--Video
-第2章 内存寻址--章节测验
-3.1 进程概述
--Video
-3.2 Linux进程创建
--Video
-3.3 Linux进程调度
--Video
-3.4 动手实践-打印进程描述符task_struct中的字段
--Video
-3.5工程实践-基于内核模块的负载监控
--Video
-第3章 进程管理--章节测验
-4.1 Linux内存管理机制
--Video
-4.2 进程用户空间管理机制
--Video
-4.3 物理内存分配与回收机制(上)
--Video
-4.4 物理内存分配与回收机制(下)
--Video
-4.5 动手实践-Linux内存映射基础(上)
--Video
-4.6 动手实践-Linux内存映射实现(中)
--Video
-4.7 动手实践-Linux内存映射测试(下)
--Video
-4.8 初学者对内存管理的常见疑惑
-第4章 内存管理--章节测验
-5.1 中断机制概述
--Video
-5.2 中断处理机制
--Video
-5.3 中断下半部处理机制
--Video
-5.4 时钟中断机制
--Video
-5.5 动手实践-中断上半部的代码分析及应用
--Video
-5.6 动手实践-中断下半部的代码分析及应用
--Video
-第5章 中断--章节测验
-6.1 Linux中的各种API
--Video
-6.2 系统调用机制
--Video
-6.3 动手实践-添加系统调用(系统调用日志收集系统)
--Video
-第6章 系统调用--章节测验
-7.1 内核同步概述
--Video
-7.2 内核同步机制
--Video
-7.3 动手实践-内核多任务并发实例(上)
--Video
-7.4 动手实践-内核多任务并发实例(下)
--Video
-第7章 内核同步--章节测验
-8.1 虚拟文件系统的引入
--Video
-8.2 虚拟文件系统的主要数据结构
--Video
-8.3 文件系统中的各种缓存
--Video
-8.4 页高速缓存机制以及读写
--Video
-8.5 动手实践-编写一个文件系统(上)
--Video
-8.6 动手实践-编写一个文件系统(中)
--Video
-8.7 动手实践-编写一个文件系统(下)
--Video
-第8章 文件系统--章节测验
-9.1 设备驱动概述
--Video
-9.2 I/O空间管理
--Video
-9.3 设备驱动模型
--Video
-9.4 字符设备驱动程序简介
--Video
-9.5 块设备驱动程序简介
--Video
-9.6 动手实践-编写字符设备驱动程序
--Video
-9.7工程实践-编写块设备驱动的基础(上)
--Video
-9.8 工程实践-块设备驱动程序分析(中)
--Video
-9.9 工程实践-块设备驱动程序实现(下)
--Video
-第9章 设备驱动--章节测验
-致谢与说明
--Video