load average含义

命令uptime或者top中,都有load average的显示,那么load average到底什么含义

load average真正的出处是 /proc/loadavg

比如可以查看下这个值

$ cat /proc/loadavg 
1.17 0.79 0.61 2/1413 9334

下面分析这个值的获取原理

loadavg_proc_show是显示值的函数,文件位置: fs/proc/loadavg.c

static int loadavg_proc_show(struct seq_file *m, void *v)
{
	unsigned long avnrun[3];

	get_avenrun(avnrun, FIXED_1/200, 0);

	seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %u/%d %d\n",
		LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]),
		LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]),
		LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]),
		nr_running(), nr_threads,
		idr_get_cursor(&task_active_pid_ns(current)->idr) - 1);
	return 0;
}

static int __init proc_loadavg_init(void)
{
	struct proc_dir_entry *pde;

	pde = proc_create_single("loadavg", 0, NULL, loadavg_proc_show);
	pde_make_permanent(pde);
	return 0;
}
fs_initcall(proc_loadavg_init);

通过proc_create_single创建 proc 文件节点 loadavg,并指定show 函数指针为loadavg_proc_show()。

get_avenrun 用于取值avnrun,FIXED_1/200的作用是小数点后第三位 4舍5入,函数如下

/**
 * get_avenrun - get the load average array
 * @loads:	pointer to dest load array
 * @offset:	offset to add
 * @shift:	shift count to shift the result left
 *
 * These values are estimates at best, so no need for locking.
 */
void get_avenrun(unsigned long *loads, unsigned long offset, int shift)
{
	loads[0] = (avenrun[0] + offset) << shift;
	loads[1] = (avenrun[1] + offset) << shift;
	loads[2] = (avenrun[2] + offset) << shift;
}

LOAD_INT作用是取整数部分,LOAD_FRAC作用是取小数部分

#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)

avnrun 保存的值,高11位是整数部分,低11位是小数部分。

整数部分直接右移11位就获取到了。

小数部分先((x) & (FIXED_1-1)),这样将高11位与掉,只保留了低11位,然后乘以100,这样是为了 执行LOAD_INT(x)后,保留的是小数点后两位

nr_running 是获取有多少个运行线程,函数如下

/*
 * nr_running and nr_context_switches:
 *
 * externally visible scheduler statistics: current number of runnable
 * threads, total number of context switches performed since bootup.
 */
unsigned int nr_running(void)
{
	unsigned int i, sum = 0;

	for_each_online_cpu(i)
		sum += cpu_rq(i)->nr_running;

	return sum;
}

nr_threads 是获取总的线程数量

idr_get_cursor(&task_active_pid_ns(current)->idr) - 1)  

idr_get_cursor(&task_active_pid_ns(current)->idr) - 1) 是为了获取到最新分配的线程pid值

真正的数据在avnrun,我们下面分析

calc_global_load 函数是更新avenrun值的地方,文件位置kernel/sched/loadavg.c

/*
 * calc_load - update the avenrun load estimates 10 ticks after the
 * CPUs have updated calc_load_tasks.
 *
 * Called from the global timer code.
 */
void calc_global_load(void)
{
	unsigned long sample_window;
	long active, delta;

	sample_window = READ_ONCE(calc_load_update);
	if (time_before(jiffies, sample_window + 10))
		return;

	/*
	 * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.
	 */
	delta = calc_load_nohz_read();
	if (delta)
		atomic_long_add(delta, &calc_load_tasks);

	active = atomic_long_read(&calc_load_tasks);
	active = active > 0 ? active * FIXED_1 : 0;

	avenrun[0] = calc_load(avenrun[0], EXP_1, active);
	avenrun[1] = calc_load(avenrun[1], EXP_5, active);
	avenrun[2] = calc_load(avenrun[2], EXP_15, active);

	WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);

	/*
	 * In case we went to NO_HZ for multiple LOAD_FREQ intervals
	 * catch up in bulk.
	 */
	calc_global_nohz();
}

calc_load_tasks的值表示负载更新的周期LOAD_FREQ,具体位置kernel/sched/core.c

void __init sched_init(void)
{
...
calc_load_update = jiffies + LOAD_FREQ;
...
}

#define LOAD_FREQ	(5*HZ+1)	/* 5 sec intervals */

if (time_before(jiffies, sample_window + 10)) 说明当调用calc_global_load速度小于5秒,不更新新值

active = atomic_long_read(&calc_load_tasks);

这个函数获取到当前running状态的进程数 + uninterrupt状态的进程数

calc_load是计算负载的函数,函数位置include/linux/sched/loadavg.h

#define EXP_1		1884		/* 1/exp(5sec/1min) as fixed-point */
#define EXP_5		2014		/* 1/exp(5sec/5min) */
#define EXP_15		2037		/* 1/exp(5sec/15min) */

/*
 * a1 = a0 * e + a * (1 - e)
 */
static inline unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{
	unsigned long newload;

	newload = load * exp + active * (FIXED_1 - exp);
	if (active >= load)
		newload += FIXED_1-1;

	return newload / FIXED_1;
}

参数说明:

load: 上一个周期负载值

exp: EXP_N的值

active 当前running状态的进程数 + uninterrupt状态的进程数

这些值什么含义?

这里用到了指数移动平均线 (EMA)的概念

EMA(Exponential moving average) 本来用的最多的是股市中计算MACD的值

其基本计算公式为:
EMA(n) = α × Cn+ (1-α) × EMA(n-1)

这个反应的是股市中收盘价的走势

计算机中也有不少地方用到了EMA的概念,比如我们这里的负载计算,不过我们的公式稍微发生了一些变化,原理类似

公式如下

avenrun(t) = avenrun(t -1)  * exp_n + nr_active (1 - exp_n)

avenrun(t) 是我们要计算的t时刻的负载

avenrun(t -1) 是上一时刻的负载

nr_active t 时刻active的线程数量

exp_n 是可调节参数

我们回到calc_load这个函数中的三个参数

load: 上一个周期负载值,这里就是avenrun(t -1)

exp: EXP_N的值,这里就是exp_n,我们这里1分钟,5分钟,15分钟时,三个值不一样

active: 当前running状态的进程数 + uninterrupt状态的进程数,这里是nr_active

avenrun[n] = avenrun[0] * exp_n + nr_active * (1 - exp_n)
nr_active += cpu_of(cpu)->nr_running + cpu_of(cpu)->nr_uninterruptible;

使用EMV的优点最明显的如下两个

  1. 不需要保存前面所有时刻的实际数值,在计算的过程中是逐步覆盖的,因此可以减少内存的占用

  2. 在有些场景下,其实更符合实际情况的,例如股票价格,天气等,最新的值对下一个值影响最大,远一点的次之,越远的值影响越小

最终转换后,公式如下

load(t)= load(t-1) * EXP_N / FIXED_1 + nr_active * (1 - EXP_N/FIXED_1)

其中用到的几个值

#define FSHIFT		11		/* nr of bits of precision */
#define FIXED_1		(1<<FSHIFT)	/* 1.0 as fixed-point */
#define LOAD_FREQ	(5*HZ+1)	/* 5 sec intervals */
#define EXP_1		1884		/* 1/exp(5sec/1min) as fixed-point */
#define EXP_5		2014		/* 1/exp(5sec/5min) */
#define EXP_15		2037		/* 1/exp(5sec/15min) */

EXP_N就是EXP_1,EXP_5,EXP_15三个值

到此,我们就介绍完了loadavg这个值如何获取


版权声明:本文为lizhijun_buaa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。