高级OS(七) -Load高故障分析

一.题目

阅读load高故障分析一文以及3.5节的视频,回答作者在文中提出的问题

  1. 为什么要定义OS_VER这个变量?
  2. load.h里面什么都没有,这有什么用?
  3. 这么久了,还没有切入正题。作者是不是忘记了是在解Load高的问题?
  4. 某些版本的Linux内核不能直接调用kallsyms_lookup_name,有哪些办法获得内核函数/变量的地址?
  5. 模块代码还有哪些值得改进的地方?
  6. 你还有其他方法跟踪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栅格化代码即可。

参考文献:

  1. cpu高 thread vm_谢宝友:Load高故障分析_行有恒堂的博客-CSDN博客
  2. top命令中的wa指标
  3. 获得内核函数地址的四种方法
  4. Load高问题排查
  5. Linux 问题故障定位
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值