Linux 进程管理(五)进程调度的调试

原图

概述

Linux 内核为开发者提供了丰富的进程调度信息,这些调度信息都需要在内核配置时打开 CONFIG_SCHED_DEBUG 选项。

查看与进程相关的调度信息

有时候我们需要查看进程相关的调度信息,如进程的 nice 值、优先级、调度策略、vruntime 及量化计算能力等信息。在 Linux 的 proc 目录中,为每个进程提供一个独立的目录,该目录包含了与进程相关的信息,执行如下命令:

1
vooxle@liushuai:~# cat /proc/1/sched

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
systemd (1, #threads: 1)
-------------------------------------------------------------------
se.exec_start : 5438471989.820758
se.vruntime : 3446.301246
se.sum_exec_runtime : 66272.041453
se.nr_migrations : 4998
nr_switches : 172620
nr_voluntary_switches : 169241
nr_involuntary_switches : 3379
se.load.weight : 1048576
se.runnable_weight : 1048576
se.avg.load_sum : 172
se.avg.runnable_load_sum : 172
se.avg.util_sum : 176128
se.avg.load_avg : 0
se.avg.runnable_load_avg : 0
se.avg.util_avg : 0
se.avg.last_update_time : 5438471989820416
se.avg.util_est.ewma : 10
se.avg.util_est.enqueued : 0
policy : 0
prio : 120
clock-delta : 103
mm->numa_scan_seq : 0
numa_pages_migrated : 0
numa_preferred_nid : 0
total_numa_faults : 806
current_node=0, numa_group_id=0
numa_faults node=0 task_private=734 task_shared=72 group_private=0 group_shared=0
numa_faults node=1 task_private=0 task_shared=0 group_private=0 group_shared=0

上面就是 ID 为 1 的进程的 proc 目录,在进程 proc 目录里,看到和进程调度相关的节点为 sched,读者可以使用 cat 命令来读取这个节点的信息,我们可以得到很多有用的信息,这些信息是在 proc_sched_show_task() 函数中输出的,该函数实现在 kerel/sched/debug.c 文件中。这些信息如下:

  • 进程的名称为 systemd,ID 是 1,线程有 1 个
  • 进程的优先级为 120
  • 进程的调度策略是 SCHED_NORMAL,使用的调度类是 CFS 调度类(fair_sched_class)
  • 当前进程的虚拟时间(vruntime)是 3446.301246ms。总运行时间为 66272.041453ms
  • 进程发生过 4998 次迁移,172620 次进程上下文切换,其中主动调度有 169241 次,被抢占调度有 3379 次
  • 进程的权重 se.load.weight 和 se.runnable.weight 相等,都是 1048576。注意,这两个值均为原本的权重值乘以 1024。进程优先级为 120,nice 值为 0,它原本的权重值为 1024
  • 当前时刻,进程的 se.avg.load_sum 值和 se.avg.runnable_load_sum 相等,都是 172
  • 进程的 se.ave.load_avg 和 se.avg.runnable_load_avg 是相等的都是 0。对于该进和来说,它的量化负载的最大值就等于它的权重值,即 1024,这是在 100%占用 CPU 的情况下得到的
  • 进程的量化计算能力(se.avg.util_avg)为 0

Linux 内核还提供一个与调度信息相关的数据结构 sched_statistics,其中包含了非常多和调度相关的统计信息。要查看这些统计信息,需要打开 CONFIG_SCHEDSTATS 配置选项。另外,还需要打开 sched_schedstats 节点。

1
echo 1 > /proc/sys/kernel/sched_schedstats

重新查看 ID 为 1 的进程调度信息,我们会发现里面多了很多统计信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
root@liushuai:/proc/1# cat sched
systemd (1, #threads: 1)
-------------------------------------------------------------------
se.exec_start : 5439456327.802850
se.vruntime : 3452.764216
se.sum_exec_runtime : 66278.504423
se.nr_migrations : 4998
se.statistics.sum_sleep_runtime : 0.000000
se.statistics.wait_start : 0.000000
se.statistics.sleep_start : 0.000000
se.statistics.block_start : 0.000000
se.statistics.sleep_max : 0.000000
se.statistics.block_max : 0.000000
se.statistics.exec_max : 0.000000
se.statistics.slice_max : 0.000000
se.statistics.wait_max : 0.000000
se.statistics.wait_sum : 0.000000
se.statistics.wait_count : 0
se.statistics.iowait_sum : 0.000000
se.statistics.iowait_count : 0
se.statistics.nr_migrations_cold : 0
se.statistics.nr_failed_migrations_affine : 0
se.statistics.nr_failed_migrations_running : 0
se.statistics.nr_failed_migrations_hot : 0
se.statistics.nr_forced_migrations : 0
se.statistics.nr_wakeups : 0
se.statistics.nr_wakeups_sync : 0
se.statistics.nr_wakeups_migrate : 0
se.statistics.nr_wakeups_local : 0
se.statistics.nr_wakeups_remote : 0
se.statistics.nr_wakeups_affine : 0
se.statistics.nr_wakeups_affine_attempts : 0
se.statistics.nr_wakeups_passive : 0
se.statistics.nr_wakeups_idle : 0
avg_atom : 0.383900
avg_per_cpu : 13.261005
nr_switches : 172645
nr_voluntary_switches : 169266
nr_involuntary_switches : 3379
se.load.weight : 1048576
se.runnable_weight : 1048576
se.avg.load_sum : 286
se.avg.runnable_load_sum : 286
se.avg.util_sum : 293807
se.avg.load_avg : 6
se.avg.runnable_load_avg : 6
se.avg.util_avg : 6
se.avg.last_update_time : 5439456327801856
se.avg.util_est.ewma : 10
se.avg.util_est.enqueued : 0
policy : 0
prio : 120
clock-delta : 56
mm->numa_scan_seq : 0
numa_pages_migrated : 0
numa_preferred_nid : 0
total_numa_faults : 806
current_node=0, numa_group_id=0
numa_faults node=0 task_private=734 task_shared=72 group_private=0 group_shared=0
numa_faults node=1 task_private=0 task_shared=0 group_private=0 group_shared=0

查看 CFS 的信息

Linux 内核在调度方面实现了一些调试接口,为开发者提供窥探调度器内部信息的接口。 在 proc 目录下面有一个 sched_debug 节点,其信息如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
root@liushuai:/proc# cat sched_debug
Sched Debug Version: v0.11, 5.4.0-81-generic #91-Ubuntu
ktime : 5439613876.757507
sched_clk : 5439566740.450647
cpu_clk : 5439566596.040965
jiffies : 5654795694
sched_clock_stable() : 1

sysctl_sched
.sysctl_sched_latency : 24.000000
.sysctl_sched_min_granularity : 3.000000
.sysctl_sched_wakeup_granularity : 4.000000
.sysctl_sched_child_runs_first : 0
.sysctl_sched_features : 2059067
.sysctl_sched_tunable_scaling : 1 (logarithmic)
...
rt_rq[0]:
.rt_nr_running : 0
.rt_nr_migratory : 0
.rt_throttled : 0
.rt_time : 0.000000
.rt_runtime : 950.000000

dl_rq[0]:
.dl_nr_running : 0
.dl_nr_migratory : 0
.dl_bw->bw : 996147
.dl_bw->total_bw : 0
  • Sched 调试信息的版本号:v0.11
  • Linux 内核版本号:5.4.0
  • 内核时间(ktime)的值 5439613876.757507
  • sched_clock=5439566740.450647 和 cpu_clk=5439566596.040965 的值
  • 当前 jiffies 的值 5654795694
  • 和调度相关的 sysctl_sched 的值
    • 调度周期 sysctl_sched_latency 为 24.000000ms
    • 调度最小粒度为 3.000000ms
    • 唤醒的最小粒度为 4.000000ms
    • fork 调用完成之后禁止子进程先运行
    • 调度支持的特性

cpu 相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cpu#0, 2500.140 MHz
.nr_running : 0
.nr_switches : 57306500
.nr_load_updates : 0
.nr_uninterruptible : -2994
.next_balance : 5654.795676
.curr->pid : 0
.clock : 5439566591.813321
.clock_task : 5439566591.813321
.avg_idle : 1000000
.max_idle_balance_cost : 500000
.yld_count : 1
.sched_count : 8189
.sched_goidle : 4044
.ttwu_count : 1273
.ttwu_local : 321
  • nr_running:有 0 个进程在就绪队列里
  • nr_switches:就绪队列发生进程切换的次数 57306500 次
  • nr_load_updates:就绪队列的 cpu_load[] 平滑负载更新的次数 0 次
  • next_balance:下一次做负载均衡的时间 5654.795676
  • curr->pid:正在运行状态的 PID
  • clock 和 clock_task:当前系统采样的时刻

cfs 就绪队列相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cfs_rq[1]:/user.slice
.exec_clock : 12.379262
.MIN_vruntime : 0.000001
.min_vruntime : 2306434.555699
.max_vruntime : 0.000001
.spread : 0.000000
.spread0 : -8049717.704115
.nr_spread_over : 0
.nr_running : 1
.load : 1048576
.runnable_weight : 1048576
.load_avg : 3
.runnable_load_avg : 3
.util_avg : 3
.util_est_enqueued : 0
.removed.load_avg : 0
.removed.util_avg : 0
.removed.runnable_sum : 0
.tg_load_avg_contrib : 3
.tg_load_avg : 1037
.throttled : 0
.throttle_count : 0
.se->exec_start : 5439566596.839807
.se->vruntime : 7544926.691548
.se->sum_exec_runtime : 2240863.841914
.se->statistics.wait_start : 0.000000
.se->statistics.sleep_start : 0.000000
.se->statistics.block_start : 0.000000
.se->statistics.sleep_max : 0.000000
.se->statistics.block_max : 0.000000
.se->statistics.exec_max : 0.303114
.se->statistics.slice_max : 0.000000
.se->statistics.wait_max : 0.000000
.se->statistics.wait_sum : 0.000000
.se->statistics.wait_count : 143
.se->load.weight : 521740
.se->runnable_weight : 521740
.se->avg.load_avg : 2
.se->avg.util_avg : 3
.se->avg.runnable_load_avg : 2
  • exec_clock:CFS 就绪队列的总运行时间
  • MIN_vruntime:表示 CFS 就绪队列的红黑树中最左边节点的 vruntime 的值
  • min_vruntime:CFS 就绪队列里 min_vruntime 的值
  • max_vruntime: CFS 就绪队列的红黑树中最右边节点的 vruntime 的值
  • nr_running:CFS 就绪队列中的进程数
  • load:CFS 就绪队列的总权重
  • runnable_weight.CFS 就绪队列中可运行状态的进程总权重
  • load_avg: 调度队列中总的量化负载,这是 CFS 就绪队列中所有进程的量化负载之和
  • runnable_load_avg:CFS 就绪队列里所有可运行状态下的进程总量化负载,可以看到它的值和 load_avg 相等
  • util_avg:CFS 就绪队列当前的量化计算能力

所有进程相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
runnable tasks:
S task PID tree-key switches prio wait-time sum-exec sum-sleep
-----------------------------------------------------------------------------------------------------------
I rcu_gp 3 13.968356 2 100 0.000000 0.003216 0.000000 0 0 /
I rcu_par_gp 4 15.968978 2 100 0.000000 0.002189 0.000000 0 0 /
I kworker/0:0H 6 4826.431210 4 100 0.000000 0.020363 0.000000 0 0 /
I mm_percpu_wq 10 24.040288 2 100 0.000000 0.002587 0.000000 0 0 /
S ksoftirqd/0 11 10356126.783742 181278 120 0.077741 3101.302842 18658.038816 0 0 /
S migration/0 13 0.000000 1360506 0 0.000000 14628.664708 0.000000 0 0 /
S idle_inject/0 14 0.000000 3 49 0.000000 0.005710 0.000000 0 0 /
S cpuhp/0 15 298446.415933 21 120 0.000000 1.152329 0.000000 0 0 /
S watchdogd 369 11.999994 2 0 0.000000 0.000000 0.000000 0 0 /
Secryptfs-kthrea 418 3181.658188 2 120 0.000000 0.007012 0.000000 0 0 /
I kworker/0:1H 771 10354055.277526 23104 100 0.000000 515.468619 0.000000 0 0 /
S rsyslogd 1230 100.894491 9129 120 0.000000 325.720995 0.000000 0 1230 /system.slice/rsyslog.service
S dockerd 598258 31439.037574 27303 120 0.000000 28928.757364 0.000000 0 597670 /system.slice/snap.docker.dockerd.service
S containerd 597865 31427.350714 113110 120 0.000000 13081.711696 0.030330 0 598167 /system.slice/snap.docker.dockerd.service
I kworker/0:2 646991 10279557.062806 122739 120 0.000000 3114.131510 0.000000 0 0 /
I kworker/0:0 649432 10356141.706616 369586 120 1.089386 9517.253921 103008.678000 0 0 /
S smbd 651779 1641474.345475 460050 120 11.539434 246732.864126 64471.275817 0 651779 /system.slice/smbd.service
S node 652105 3528667.050423 1354 120 0.000000 2331.451911 0.000000 0 652102 /user.slice
S cpptools 652144 3527016.422624 6 120 0.000000 0.203793 0.000000 0 0 /user.slice
S cpptools 652162 3527016.384935 6 120 0.000000 0.146937 0.000000 0 0 /user.slice
S cpptools 652182 3527016.373736 6 120 0.000000 0.115269 0.000000 0 0 /user.slice
S cpptools 652296 3527040.380268 1 120 0.000000 0.047959 0.000000 0 0 /user.slice
S cpptools-srv 652246 3527028.332316 1 120 0.000000 0.076723 0.000000 0 0 /user.slice
  • 第 1 列显示进程的状态。R 表示可运行状态,也就是在 CFS 就绪队列中的进程;S 表示睡眠状态的进程
  • 第 2 列显示进程的名称
  • 第 3 列显示 PID
  • 第 4 列显示进程在 CFS 就绪队列的红黑树中的键值
  • 第 5 列显示进程切换的次数
  • 第 6 列显示进程的优先级
  • 第 7 列显示进程等待时间
  • 第 8 列显示进程运行的总时间
  • 第 9 列显示进程休眠的总时间

查看调度域信息

在理解 SMP 负载均衡机制的过程中,CPU 的拓扑关系是一个难点。Linux 内核新增了一个调试节点来帮助开发者理解(详见 kernel/sched/topology.c 文件)。在编译内核时,不仅需要打开 CONFIG_SCHED_DEBUG 选项,还需要在内核启动参数里传递 sched_debug 参数。 在内核启动之后,可以通过 dmesg 命令来得到 CPU 拓扑关系,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
[0.224258] CPUO attaching sched-domain(s)
[0.224422] domain-0: span=0-3 level=MC
[0.224780] groups: O:{ span=0 },1:{span=1},2:{span=2},3:{span=3}
[0.227045] CPU1 attaching sched-domain(s):
[0.227111] domain-0: span=0-3 level=MC
[0.227179] groups: 1:{span=1},2:{span=2 },3:{span=3 }, 0:{ span=0}
[0.227347] CPU2 attaching sched-domain(s):
[0.227407] domain-0: span=0-3 level=MC
[0.227471] groups: 2:{span=2},3:{span=3},0:{span=0},1:{span=1}
[0.228416] CPU3 attaching sched- domain(s):
[0.228485] domain-0: span=0-3 level=MC
[02285530] groups: 3:{span=3 }, 0:{span=0 }, 1:{span=1},2:{span=2}
[0.228757] root domain span: O-3 (max cpucapacity = 1024)

可以看出,该系统只有 MC 层级,从 CPU0 角度看,它对应的调度域是 domain-0,管辖的范围是 CPU[0~3],调度层级是 MC。同理,可以得出 CPU1、CPU2 以及 CPU3 的 CPU 拓扑关系。

与调度相关的调试节点

和调度相关的调试节点在/proc/sys/kernel 目录下,下面来简单介绍一下。

  • sched_cfs_bandwidth_slice_us:用于设置 CFS 的带宽限制,默认值为 5ms
  • sched_child_runs_first:通过 fork 调用创建进程之后,sched_child_runs_first 可以用于控制是父进程还是子进程先运行;若该值为 1,表示子进程先运行;默认值为 0
  • sched_domain:与调度域相关的目录
  • sched_latency_ns:设置 CFS 就绪队列调度的总时间片 sysctl_sched_latency,默认值为 6ms。 sched_latency_ns 表示一个运行队列中所有进程运行一次的时间片,它与运行队列的进程数有关。如果进程数超过 sched_nr_latency(默认是 8),那么调度周期就是 sched_min_granularity_ns 乘以运行队列里的进程数量;否则,就是 sysctl_sched_latency
  • sched_migration_cost_ns:判断一个进程是否可以利用高速缓存的热度。如果进程的运行时间(now-p->se.exec_start)小于它,那么内核认为它的数据还在高速缓存里,所以该进程可以利用高速缓存的热度,在迁移的时候就不会考虑它
  • sched_min_granularity_ns:设置 CPU 密集型进程最小的调度时间片,也就是进程最短运行时间,用于防止频繁切换。默认值为 0.75ms
  • sched_nr_migrate:设置在 SMP 负载均衡机制里每次最多可以从目标 CPU 迁移多少个进程到源 CPU 里。在迁移的过程中关闭了中断,包括软中断机制,因此增大该值会导致中断延迟,同时也增大了实时进程的延迟。该值默认设置为 32
  • sched_schedstats:用于打开调度统计信息
  • sched_wakeup_granularity_ns:待唤醒进程会检查是否需要抢占当前进程,若待唤醒进程的睡眠时间短于 sched_wakeup_granularity_ns,那么不会抢占当前进程。增加该值会减小待唤醒进程的抢占概率。若减小该值,那么发生抢占的概率就越大。默认值为 1ms

另外,在/sys/kermel/debug/目录下也有几个和调度相关的节点。

  • sched_features:表示调度器支持的特性,如 START_DEBIT(新进程尽量早调度),WAKEUP_PREEMPT(唤醒的进程是否可以抢占当前运行的进程)等。所有的特性详见 kerel/sched/sech_features.h 文件的定义
  • osched_debug:调度器的调试信息的开关。注意,该值仅仅控制 sched_debug_enabled,并不会控制/proc/sched_debug 节点 关于 sched_debug_enabled 的示例代码如下,
1
2
3
4
5
6
static int __init sched_debug_setup(char *str)
{
sched_debug_enabled = true;
return 0;
}
early_param("sched_debug", sched_debug_setup);

参考文献

《奔跑吧 Linux 内核》
《Linux 内核设计与实现》