高级OS(六) - Load高故障分析
一.题目
阅读load高故障分析一文以及3.5节的视频,回答作者在文中提出的问题
- 为什么要定义OS_VER这个变量?
- load.h里面什么都没有,这有什么用?
- 这么久了,还没有切入正题。作者是不是忘记了是在解Load高的问题?
- 某些版本的Linux内核不能直接调用kallsyms_lookup_name,有哪些办法获得内核函数/变量的地址?
- 模块代码还有哪些值得改进的地方?
- 你还有其他方法跟踪Load高问题吗?不同的方法什么优缺点?
二.解答
1.为什么要定义OS_VER这个变量?
根据内核的版本去设置OS_VER的值,需要对应。刚开始初始为UNKNOWN,当查到内核版本不为4.15.0-39-generic时,将OS_VER改为UBUNTU_1604。
2. load.h里面什么都没有,这有什么用?
可能linux系统一直在更新维护,有可能是为了之后的拓展预留一个头文件,方便以后的拓展维护。也可能是防止一些错误的发生。
3.这么久了,还没有切入正题。作者是不是忘记了是在解Load高的问题?
不是,在解Load高的问题之前,需要先对load指标概念等做了解,再根据一些测试用例进一步了解,通过对基本的前提的了解后,就会对解Load高问题更清晰了。一步一步的增加代码,能看到其变化,从而了解解Load高问题。
通过一步步的引出问题,让我们自己去查阅资料,去探索load高的问题,然后自己动手去操作的话肯定会有更加透彻的理解。
4.某些版本的Linux内核不能直接调用kallsyms_lookup_name,有哪些办法获得内核函数/变量的地址?
(1)已知内核符号地址,获取内核符号名
1.1 使用sprint_symbol内核函数
#include <linux/kallsyms.h>
int sprint_symbol(char *buffer, unsigned long address)
这个方法是根据一个内存中的地址address查找一个内核符号,然后会把这个符号的基本信息,如符号名name,它在内核符号表中的偏移offset和大小size ,所属的模块名的一些信息连接成字符串赋值给文本缓冲区buffer。其中所查找的内核符号可以是原本就存在于内核中的符号,也可以是位于动态插入的模块中的符号。
输入参数说明:
buffer:文本缓冲区,它用来记录内核符号的信息,它是一个输出型参数。
address:内核符号中的某一地址,为输入型参数。
返回参数说明:返回值是一个int型,它表示内核符号基本信息串的长度,即buffer所表示的字符串的长度。
(2)已知内核符号,获取内核符号地址
2.1 使用kallsyms_lookup_name()
这个方法在kernel/kallsyms.c文件中定义的,要使用它必须启用CONFIG_KALLSYMS编译内核。
kallsyms_lookup_name()接受一个字符串格式内核函数名,返回那个内核函数的地址。
kallsyms_lookup_name(“函数名”);
(3)通用
3.1 利用System.map
$ grep “函数名或地址” /usr/src/linux/System.map
3.2 使用nm 命令:
$ nm vmlinuz | grep “函数名或地址”
3.3 利用 /proc/kallsyms
$ cat /proc/kallsyms | grep “函数名或地址”
5.模块代码还有哪些值得改进的地方?
两人水平有限,暂时没有想到需要改进的地方。
6.你还有其他方法跟踪Load高问题吗?不同的方法什么优缺点?
(1)从cpu层面排查
1.1排查哪些进程cpu占用率高。通过top -c命令显示进程运行信息列表
执行 jstack 2346 > loop.txt
1.2查看对应java进程的每个线程的CPU占用率。命令:top -Hp 2346
1.3追踪线程内部,查看load过高原因。将十进制转为十六进制
1.4打开loop.txt文件,搜得到的十六进制
(2)查看运行中的队列R,不可中断的睡眠进程D
进程五种状态:
系统有很高的负载但是CPU使用率却很低,或者负载很低而CPU利用率很高,这两者没有直接关系,
在 load 比较高的时候,有大量的 nginx 处于 R 或者 D 状态,他们才是造成 load 上 升的元凶。
排查僵尸进程:
2.1 执行top命令,查看zombie进程
2.2 ps -A -o stat,ppid,pid,cmd|grep -e ‘^ [Zz]’ 查看进程状态为Z的进程
(3)OS系统层面,检查系统IO
执行iostat
(4)系统负载问题定位
//查看负载情况
除了uptime、top(比uptime更详细)
还有:
vmstat
strace -c -p pid //统计系统调用耗时情况
strace -T -e epoll_wait -p pid //跟踪指定的系统操作例如epoll_wait
dmesg //查看内核日志信息
调试其中的代码,给出截图,说明调试过程遇到的问题,解决方法,心得体会。
1.观察load指标
top:
uptime:
load average:表示系统在最近1分钟、5分钟、15分钟内的平均负载情况。
Cpu利用率:
前三个指标分别代表程序运行在用户态、内核态、以及运行在nice值调整期间的时间。
若系统中I/O密集型的程序在运行,那么iowait指标会偏高(第五个wa)
nproc查看cpu核心数。
2.构造测试用例
启动四个后台运行的CPU密集型任务,占用四个CPU。
启动fio,运行IO密集型任务:
wa是指当CPU空闲且磁盘IO阻塞的时间占比。这里只统计磁盘IO(包含NFS类型的磁盘),不包含网络IO。I/O 等待时间只是 idle 时间的子项,本质上 CPU 是空闲的,Linux 内核当然可以把 CPU 交给第二个任务使用。原本用于等待 I/O 完成的 CPU 时间,现在用于处理第二个任务了。此时通过 top 命令查看 wa,自然得到接近 0 的结果。
3.找到load高问题的元凶
//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
//main.c
/**
* Baoyou Xie's load monitor module
*
* Copyright (C) 2018 Baoyou Xie.
*
* Author: Baoyou Xie <baoyou.xie@gmail.com>
*
* License terms: GNU General Public License (GPL) version 2
*/
#include <linux/version.h>
#include <linux/module.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/tracepoint.h>
#include <linux/stacktrace.h>
#include <linux/sched/task.h> /*init_task 头文件*/
#include <linux/sched/signal.h> /*do_each_thread 头文件*/
#include "load.h"
struct hrtimer timer;
static unsigned long *ptr_avenrun; //存放指向avenrun数组的指针
//avenrun数组,对它进行地址转换的
#define FSHIFT 11 /* nr of bits of precision */
#define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */
#define LOAD_INT(x) ((x) >> FSHIFT)
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)
#define BACKTRACE_DEPTH 20 //backtrace深度,也就是数组的能够存放的调用栈的数目
/*#if defined(UBUNTU_1604)
extern struct task_struct init_task;
#define next_task(p) \
list_entry_rcu((p)->tasks.next, struct task_struct, tasks)
#define do_each_thread(g, t) \
for (g = t = &init_task ; (g = t = next_task(g)) != &init_task ; ) do
#define while_each_thread(g, t) \
while ((t = next_thread(t)) != g)
static inline struct task_struct *next_thread(const struct task_struct *p)
{
return list_entry_rcu(p->thread_group.next,
struct task_struct, thread_group);
}
#endif*/
static void print_all_task_stack(void)
{
struct task_struct *g, *p;
unsigned long backtrace[BACKTRACE_DEPTH]; //backtrace数组,存放每一个调用具体的信息
struct stack_trace trace; //stack_trace专门保存进程调用栈信息
//memset将两个数据结构进行初始化
memset(&trace, 0, sizeof(trace));
memset(backtrace, 0, BACKTRACE_DEPTH * sizeof(unsigned long));
trace.max_entries = BACKTRACE_DEPTH; //设置保存的最大的调用栈深度是20
trace.entries = backtrace; //存放调用信息的数组我们将他设置为backtrace这样我们定义的数组
printk("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n");
printk("\tLoad: %lu.%02lu, %lu.%02lu, %lu.%02lu\n",
LOAD_INT(ptr_avenrun[0]), LOAD_FRAC(ptr_avenrun[0]),
LOAD_INT(ptr_avenrun[1]), LOAD_FRAC(ptr_avenrun[1]),
LOAD_INT(ptr_avenrun[2]), LOAD_FRAC(ptr_avenrun[2]));
printk("dump all task: balabala\n");
//1.锁很关键,因为我们在打印线程时需要遍历系统内的进程链表,若在这过程中有某些进程死亡或者产生一些变化,
//对链表产生了修改,就会对遍历链表产生影响。所以遍历需要加锁
//2.rcu锁全称read-copy update(读-拷贝修改),对于一个被rcu保护的共享数据结构,读者不需要获得任何锁就可以访问它;
//写者访问时,首先拷贝一个副本,然后对副本进行修改,最后再使用一个callback回调机制,在适当时机把指向原来数据的指针重新指向新的被修改的数据
//这个回调的时机就是所有引用该数据的cpu都退出对共享数据的操作的时候
//3.锁的好处:比起传统的读写锁、自旋锁,rcu锁开销更小,适用于读多写少的情况
rcu_read_lock(); //遍历线程之前,将rcu_read的这个锁打开
printk("dump running task.\n");
//遍历系统中所有的线程,linux系统中线程并没有一个独立的结构,它是用进程模拟的
//将0号进程的task_struct的地址赋给g和p
do_each_thread(g, p) {
//满足某一状态的进程,我们保存它的堆栈追踪信息在trace中
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("uninterrupted 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; //设置ktime_t的静态局部变量last,ktime_t是hrtimer保存时间的一个数据结构
u64 ms;
int load = LOAD_INT(ptr_avenrun[0]); /* 最近1分钟的Load值 */
if (load < 3)
return;
/**
* 如果上次打印时间与当前时间相差不到20秒,就直接退出
*/
ms = ktime_to_ms(ktime_sub(ktime_get(), last)); //ktime_get()获取当前的系统时间,last保存的是上一次打印时的系统时间,ktime_sub完成两个值的相减
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); //hrtimer定时器,分三种10ns、100ns、1000ns
timer.function = monitor_handler;
hrtimer_start_range_ns(&timer, ms_to_ktime(10), 0, HRTIMER_MODE_REL_PINNED);
}
static int load_monitor_init(void)
{
ptr_avenrun = (void *)kallsyms_lookup_name("avenrun"); //avenrun数组有三个元素,存放着无符号长整型,低11位存放负载的小数部分,高位存放整数部分
if (!ptr_avenrun)
return -EINVAL;
start_timer(); //定时判断系统的负载值,此函数启动定时器
printk("load-monitor loaded.\n");
return 0;
}
static void load_monitor_exit(void)
{
hrtimer_cancel(&timer);
printk("load-monitor unloaded.\n");
}
module_init(load_monitor_init)
module_exit(load_monitor_exit)
MODULE_DESCRIPTION("Baoyou Xie's load monitor module");
MODULE_AUTHOR("Baoyou Xie <baoyou.xie@gmail.com>");
MODULE_LICENSE("GPL v2");
//load.h
/**
* Boyou Xie's load monitor module
*
* Copyright (C) 2018 Baoyou Xie.
*
* Author: Baoyou Xie <baoyou.xie@gmail.com>
*
* License terms: GNU General Public License (GPL) version 2
*/
增加定时器后:
为了防止某些版本的内核没有通过EXPORT_SYMBOL导出这个变量,我们使用如下语句获得这个变量的地址:
遇见问题和解决方法:
1.fio not found:
是因为fio未安装导致,通过’sudo apt install fio’命令安装即可解决。
2.make编译的时候报错 Makefile:20:***缺少分隔符(你的意思是TAB而不是8个空格?)。停止
解决:是因为Makefile中的有些地方并不是tab,而是空格导致的,重新用tab栅格化代码即可。
参考文献: