学习文档
https://www.cnblogs.com/liulianzhen99/articles/17638178.html
TOP
问题 1:top 输出的利用率信息是如何计算出来的,它精确吗?
- top 命令访问 /proc/stat 获取各项 cpu 利用率使用值
- 内核调用 stat_open 函数来处理对 /proc/stat 的访问
- 内核访问的数据来源于 kernel_cpustat 数组,并汇总
- 打印输出给用户态
strace 跟踪 top 命令的系统调用
# strace top
...
openat(AT_FDCWD, "/proc/stat", O_RDONLY) = 4
openat(AT_FDCWD, "/proc/2351514/stat", O_RDONLY) = 8
openat(AT_FDCWD, "/proc/2393539/stat", O_RDONLY) = 8
...
除了 /proc/stat 外,还有各个进程细分的 /proc/{pid}/stat,是用来计算各个进程的 cpu 利用率时使用。
proc/stat伪文件
内核为各个伪文件都定义了处理函数,/proc/stat 文件的处理方法是 proc_stat_operations
//file:fs/proc/stat.c
static int __init proc_stat_init(void)
{
proc_create("stat", 0, NULL, &proc_stat_operations);
return 0;
}
static const struct file_operations proc_stat_operations = {
.open = stat_open,
...
};
proc_stat_operations 中包含了操作该文件时对应的操作方法,当打开 /proc/stat 文件时,stat_open 就会被调用
stat_open 依次调用 single_open_size->show_stat 来输出数据内容:
//file:fs/proc/stat.c
static int show_stat(struct seq_file *p, void *v)
{
u64 user, nice, system, idle, iowait, irq, softirq, steal;
for_each_possible_cpu(i) {
struct kernel_cpustat *kcs = &kcpustat_cpu(i);
user += kcs->cpustat[CPUTIME_USER];
nice += kcs->cpustat[CPUTIME_NICE];
system += kcs->cpustat[CPUTIME_SYSTEM];
idle += get_idle_time(kcs, i);
iowait += get_iowait_time(kcs, i);
irq += kcs->cpustat[CPUTIME_IRQ];
softirq += kcs->cpustat[CPUTIME_SOFTIRQ];
...
}
//转换成节拍数并打印出来
seq_put_decimal_ull(p, "cpu ", nsec_to_clock_t(user));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(nice));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(system));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(idle));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(iowait));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(irq));
seq_put_decimal_ull(p, " ", nsec_to_clock_t(softirq));
...
}
for_each_possible_cpu 是在遍历存储着 cpu 使用率数据的 kcpustat_cpu 变量
该变量是一个 percpu 变量,它为每一个逻辑核都准备了一个数组元素
里面存储着当前核所对应各种事件,包括 user、nice、system、idel、iowait、irq、softirq 等
在这个循环中,将每一个核的每种使用率都加起来,最后通过 seq_put_decimal_ull 将这些数据输出
注意
在内核中实际每个时间记录的是纳秒数,但是在输出时统一转化成节拍单位
/proc/stat 的输出是从 kernel_cpustat 这个 percpu 变量中读取出来
统计数据怎么来的
内核是以采样的方式来统计 cpu 使用率,这个采样周期依赖的是 Linux 时间子系统中的定时器:
Linux 内核每隔固定周期会发出 timer interrupt (IRQ 0),这有点像乐谱中的节拍。
每隔一段时间,就打出一个拍子,Linux 响应并处理一些事情。
一个节拍的长度是多长时间,是通过 CONFIG_HZ 来定义。它定义的方式是每一秒有几次 timer interrupts。
不同的系统中这个节拍的大小可能不同,通常在 1 ms 到 10 ms 之间。
每次当时间中断到来时,调用 update_process_times 来更新系统时间,更新后的时间存储在 percpu 变量 kcpustat_cpu 中。
问题 2:ni 这一列是 nice,它输出的是 cpu 在处理啥时的开销?
问题 3:wa 代表的是 io wait,那么这段时间中 cpu 到底是忙碌还是空闲?
CPU的idle和iowait的区别
idle 状态:
表示 CPU 处于空闲状态,即没有任何任务正在被执行
这意味着 CPU 此时没有工作可做,可以随时接受新的任务分配
例如,当系统处于待机状态,用户没有进行操作,且后台也没有运行任务时,CPU 可能处于 idle 状态
iowait 状态:
指 CPU 等待 I/O 操作完成所花费的时间
当系统正在进行磁盘读写、网络通信等 I/O 操作时,如果这些操作还未完成,CPU 就会处于 iowait 状态
比如,当从硬盘读取大量数据或者向网络发送大量数据时,可能会导致 CPU 进入 iowait 状态
总结:
idle 是 CPU 真正的空闲,没有任务需要处理
iowait 是 CPU 因为等待 I/O 操作而暂时无法执行计算任务
过高的 idle 可能表示系统资源未被充分利用
过高的 iowait 可能暗示 I/O 子系统存在性能瓶颈,需要优化 I/O 设备或相关的操作流程
如果一个数据库服务器经常出现高 iowait,可能需要考虑升级存储设备、优化数据库的读写操作、增加缓存,来减少 I/O 操作次数
update_process_times 简介
普通定时器: arch_timer_handler_phys->tick_handle_periodic->update_process_times
高精度定时器: tick_sched_handle->update_process_times
linux 进程可以同时执行,是因为采用时间片轮转方案。
每个进程都会分得相应的时间片,当前进程时间片用完,CPU就会停止执行当前进程,选择其他合适进程。
什么时候判断当前进程的时间片是否用完?
依赖于系统timer,timer周期性产生时钟中断,在中断处理函数中,会更新当前进程时间等统计信息
并判断当前进程的时间片是否用完,是否需要切换到其他进程执行。
这个工作由update_process_times函数来实现。
(引入采用周期的概念,定时每 1 毫秒采样一次。如果采样的瞬时,cpu 在运行,就将这 1 ms 记录为使用,这时会得出一个瞬时的 cpu 使用率,把它都存起来。当统计 3 秒内的 cpu 使用率时,比如 t1 ~ t2 这段时间范围。那就把这段时间内所有瞬时值全加,取个平均值,统计相对准确,避免瞬时值剧烈震荡且粒度过粗问题了)
update_process_times函数不会做实际的进程切换动作,只会设置是否需要做进程切换的标记,真正的切换在schedule函数中实现。
linux每个时钟中断(又称tick中断)处理中都会更新进程时间,即update_process_times。
void update_process_times(int user_tick)
{
struct task_struct *p=current;
/* 找到多核中的cpu id */
int cpu = smp_processor_id();
/*
// user_tick 根据 cpu 模式判断是用户态还是内核态
//
// linux统计时间的方式:
// 1 基于整个cpu的统计
// user_tick 表示 cpu 在用户态、内核态、中断状态
// 此处把一个 tick 的时间累加到 kstat_cpu(i).cpustat.xx
// /proc/stat的统计值是在此处统计的
// 表示cpu在用户态、内核态中断中各占用多少时间
// 对应 stat.c(fs/proc):static int __init proc_stat_init(void)
//
// 2 基于进程的统计
// linux还有一种统计时间的方法更细化
// 统计的是调度实体上的时间 sum_exec_runtime
// 它在 sched_clock_cpu 函数中基于 timer 计算
// /proc/pid/stat、/proc/pid/task/tid/stat 中的时间是在此处统计
// 它统计了一个进程/线程占用 cpu 的时间,对应 do_task_stat 实现
*/
account_process_tick(p, user_tick);
/*
// 此处负责系统中的定时器到期操作,并未真正处理,只是实现 raise_softirq(TIMER_SOFTIRQ)
// 当这个 tick 中断退出 irq_exit 时,会处理 TIMER_SOFTIRQ 软中断
// TIMER_SOFTIRQ 软中断处理函数 run_timer_softirq() 负责处理到期的定时器
*/
run_locl_timers();
/* 与进程和调度用过的时间参数 */
scheduler_tick();
}
根据 Tick 产生是在用户态/内核态,以及idle进程的上下文等信息,选择不同函数进行处理
这里是把 TICK_NSEC ,也就是每 Tick 对应的 ns 加到对应的 cpustat 数组中
即 cpustat 数组中的数据,虽然是 ns 级,但精度是按照 Tick 的频率而定,不算准确
//file:kernel/sched/cputime.c
void account_process_tick(struct task_struct *p, int user_tick)
{
cputime = TICK_NSEC;
...
if (user_tick)
//3.1 统计用户态时间
account_user_time(p, cputime);
else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
//3.2 统计内核态时间
account_system_time(p, HARDIRQ_OFFSET, cputime);
else
//3.3 统计空闲时间
account_idle_time(cputime);
}
首先设置 cputime = TICK_NSEC
一个 TICK_NSEC 的定义是一个节拍所占的纳秒数。
接下来根据判断结果分别执行 account_user_time、account_system_time 和 account_idle_time 来统计用户态、内核态和空闲时间
用户态时间统计
//file:kernel/sched/cputime.c
void account_user_time(struct task_struct *p, u64 cputime)
{
//分两种种情况统计用户态 CPU 的使用情况
int index;
index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER;
//将时间累积到 /proc/stat 中
task_group_account_field(p, index, cputime);
......
}
account_user_time 函数主要分两种情况统计:
- 如果进程的 nice 值大于 0,那么将会增加到 CPU 统计结构的 nice 字段中
- 如果进程的 nice 值小于等于 0,那么增加到 CPU 统计结构的 user 字段中
其实用户态的时间不只是 user 字段,nice 也是
之所以要把 nice 分出来,是为了让 Linux 用户更一目了然地看到调过 nice 的进程所占的 cpu 周期有多少
如果想要观察系统的用户态消耗的时间,应将 top 中输出的 user + nice ,不是只看 user
接着调用 task_group_account_field 来把时间加到前面提到的 kernel_cpustat 内核变量中。
//file:kernel/sched/cputime.c
static inline void task_group_account_field(struct task_struct *p, int index, u64 tmp)
{
__this_cpu_add(kernel_cpustat.cpustat[index], tmp);
...
}
内核态时间统计
//file:kernel/sched/cputime.c
void account_system_time(struct task_struct *p, int hardirq_offset, u64 cputime)
{
if (hardirq_count() - hardirq_offset)
index = CPUTIME_IRQ;
else if (in_serving_softirq())
index = CPUTIME_SOFTIRQ;
else
index = CPUTIME_SYSTEM;
account_system_index_time(p, cputime, index);
}
内核态的时间主要分 3 种情况进行统计
- 如果当前处于硬中断执行上下文,那么统计到 irq 字段中
- 如果当前处于软中断执行上下文,那么统计到 softirq 字段中
- 否则统计到 system 字段中
判断好要加到哪个统计项中后,依次调用 account_system_index_time、task_group_account_field
来将这段时间加到内核变量 kernel_cpustat 中
空闲时间的累积
在内核变量 kernel_cpustat 中不仅仅是统计了各种用户态、内核态的使用统计,空闲也一并统计起来。
如果在采样的瞬间,cpu 既不在内核态也不在用户态的话,就将当前节拍的时间都累加到 idle 中。
//file:kernel/sched/cputime.c
void account_idle_time(u64 cputime)
{
u64 *cpustat = kcpustat_this_cpu->cpustat;
struct rq *rq = this_rq();
if (atomic_read(&rq->nr_iowait) > 0)
cpustat[CPUTIME_IOWAIT] += cputime;
else
cpustat[CPUTIME_IDLE] += cputime;
}
在 cpu 空闲的情况下,进一步判断当前是不是在等待 IO(例如磁盘 IO):
- 是的话,这段空闲时间会加到 iowait 中
- 否则,加到 idle 中
iowait 其实是 cpu 的空闲时间,只不过是在等待 IO 完成而已
io wait 其实是 cpu 在空闲状态的一项统计,只不过这种状态和 idle 的区别是 cpu 是因为等待 io 而空闲
总结
实际查看一下服务器的 cpu 利用率的demo
https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/cpu/test06/cpu_stat.sh
TOP 中输出的 cpu 时间项目其实大致可以分为三类:
第一类:用户态消耗时间,包括 user 和 nice;如果想看用户态的消耗,要将 user 和 nice 加起来
第二类:内核态消耗时间,包括 irq、softirq 和 system
第三类:空闲时间,包括 io_wait 和 idle;其中 io_wait 也是 cpu 的空闲状态,只不过是在等 io 完成而已;如果只是想看 cpu 到底有多闲,应该把 io_wait 和 idle 加起来。
其他
当前程序使用的堆栈一个是内核堆栈,一个是用户态堆栈
普通的内核态可以视为是系统调用进入
是否在 hardirq 环境以及 softirq 环境,可以通过 preempt_count(thread_info 的一个成员)进行判断
idle状态,只需要判断当前进程是否是idle进程就可以
iowait :在 idle 进程上下文,但 rq->nr_iowait 仍大于 0 的情况