Save 37% off PRO during our Black Friday Sale! »

[試して理解] Linuxのプロセススケジューラのしくみ

[試して理解] Linuxのプロセススケジューラのしくみ

842515eaf8fbb2dfcc75197e7797dc15?s=128

Satoru Takeuchi

November 10, 2018
Tweet

Transcript

  1. [試して理解] Linuxのプロセススケジューラのしくみ Nov. 10, 2018 Satoru Takeuchi <satoru.takeuchi@gmail.com> Twitter: @satoru_takeuchi

    1
  2. はじめに • Linuxのスケジューラのしくみを紹介 ◦ カーネルソースを一切見ずに図解と実験によって確認 • 用語 ◦ タスク: カーネルのスケジューリング単位。プロセスまたはスレッド

    ◦ LCPU: カーネルがCPUとして認識するもの(物理CPU or コア or スレッド) • 実験環境 ◦ ハードウェア ▪ CPU: Ryzen 1800x (8 core, 16 thread => 16LCPU) ◦ ソフトウェア ▪ Ubuntu 18.04 ▪ Kernel v4.15.0-34-generic 2
  3. ランキュー • 実行可能タスク(ここではt0, t1, t2)をつないでおくキュー 3 LCPU t0 t1 t2

  4. ラウンドロビンスケジューリング • ランキューにつながるタスクに順番にCPU時間を与える 4 LCPU t0 t1 t2 LCPU t1

    t2 t0 LCPU t2 t0 t1 current
  5. ラウンドロビンスケジューリング • 時刻経過に伴うcurrentの変化 5 t0 t1 t2 t0 t1 t2

    時間 current
  6. 前振り: 詳解Linuxカーネル第三版曰く… • ランキューはactiveキューとinactiveキューの二本 • タスクの優先度は動的に変化する • タスクごとのタイムスライスは100ms 6

  7. 前振り: 詳解Linuxカーネル第三版曰く… • ランキューはactiveキューとinactiveキューの二本 • タスクの優先度は動的に変化する • タスクごとのタイムスライスは100ms 7 •

    今は全然違う • 約11年前にスケジューラは殆ど全て書き直されている • O(1)スケジューラ -> Completely Fair Scheduler(CFS)
  8. もくじ • スケジューラ • ロードバランサ • Fair Share Scheduler •

    CFS Bandwidth Controller 8
  9. まずは1LCPUから • 16個のLCPUのうち15個をオフラインにする • CPUホットプラグという機能を利用 • これで論理的に「1コア1スレッド」のシステムになる 9 # grep

    -c processor /proc/cpuinfo 16 # for ((i=1;i<16;i++)) ; do > echo 0 >/sys/devices/system/cpu/cpu${i}/online > done # grep -c processor /proc/cpuinfo 1 #
  10. CFSのスケジューリングのしくみ • ラウンドロビンスケジューリング • 各タスクはレイテンシターゲットと呼ばれる期間に一度CPU時間を得られる ◦ 数ms~数十ms。カーネルバージョンによって変わる ◦ sysctlのkernel.sched_latency_nsパラメタ(ns単位) •

    タイムスライス = レイテンシターゲット/実行可能タスク数 10 $ sysctl kernel.sched_latency_ns kernel.sched_latency_ns = 6000000 # 6ms $
  11. LCPU上で動くタスク 11 タスク数2 タスク数3 タスク数4 時間 t0 t1 t0 t1

    t0 t1 t2 t0 t1 t2 t0 t1 t2 t3 t0 t1 t2 t3 レイテンシターゲット レイテンシターゲット
  12. スケジューラの挙動: 初期状態 • 仮定 ◦ レイテンシターゲット : 6 [ms] •

    タイムスライス=6/(runnableタスク数=3) [ms] 12 0 0 0 t0 t1 t2 vruntime(簡単のため簡略化している ) LCPU ランキュー(vruntime順にソートされた赤黒木 )
  13. ランキュー先頭のタスクをcurrentに 13 0 0 t1 t2 LCPU 0 t0

  14. タイムスライス切れまで動かす • 6/3=2 [ms]動くとvruntimeが1.0増えるよう計算 14 0 0 t1 t2 LCPU

    1.0 t0
  15. currentをランキューに再挿入 15 0 0 t1 t2 LCPU 1.0 t0

  16. 全タスクのvruntime=1.0になるまで繰り返し 16 1.0 1.0 t0 t1 LCPU 1.0 t2

  17. 実験プログラム • 実行方法 ◦ ./sched <nproc> <total> <granularity> • 引数の意味

    ◦ Nproc: 同時実行プロセス(タスク)数 ◦ Total: 各プロセスの実行時間 (ms単位) ◦ granularity: 測定粒度(ms単位) • 説明 ◦ Nproc個のタスクを同時に動かす ◦ 各タスクはtotal[ms]だけ無限ループする。このとき granularity[ms]に一回進捗を記録する ◦ 全タスク終了時に、各タスクの ID, 経過時間(us単位), 進捗(%単位)を出力 17
  18. 実験プログラム実行例 • 1つのタスクを1000ms実行。1msごとにデータをとる 18 $ ./sched 1 1000 1 0

    180 0 ... 0 10613 1 # 1%時点でだいたい1ms … 0 20384 2 # 2%時点でだいたい2ms … 0 979192 100 # 100%時点でだいたい1000ms $
  19. パラメタ • nproc=1, 2, 3 • total=1000 • granularity=1 19

  20. 実験結果(nproc=1) 20

  21. 実験結果(nproc=2) 21 拡大 3msくらい

  22. 実験結果(nproc=3) 22 拡大 2msくらい

  23. タイムスライスは無限に小さくなるのか? • 例: レイテンシターゲットが6msでnproc=1000 ◦ 各タスクのタイムスライスはたったの 6us? 23

  24. タイムスライスは無限に小さくなるのか? • 例: レイテンシターゲットが6msでnproc=1000 ◦ 各タスクのタイムスライスはたったの 6us? • タイムスライスの最低保証値がある ◦

    目的: コンテキストスイッチのコストを増やしすぎないようにするため ◦ sysctlのkernel.sched_min_granularity_ns 24 $ sysctl kernel.sched_min_granularity_ns kernel.sched_min_granularity_ns = 750000 # 0.75ms $
  25. nice値の意味 • Niceの変化によってタイムスライスの比率が変わる ◦ nice値が低い(高優先度): 比率が上がる ◦ nice値が低い(低優先度): 比率が下がる 25

    T0 0 T1 -1 T0 0 T1 0 T0 0 T1 1 時間 T0 0 T1 0 nice値 T0のnice値 > t1のnice値 T0のnice値 == t1のnice値 T0のnice値 < t1のnice値 T0 0 T1 -1 T0 0 T1 1
  26. nice値によるタイムスライスの変化量 • Nice値が1低いとタイムスライスが1.25倍になる • nice値を考慮したタイムスライスの計算式 ◦ Weight = 1.25 ^

    (-nice値) ◦ Slice = latency_target * (weight / Σweight) • 例: 2つのタスクt0, t1がnice値0, -1が存在する ◦ T0のweight = 1.25 ^ 0 = 1 ◦ T1のweight = 1.25 ^ 1 = 1.25 ◦ Σweight = 1 + 1.25 = 2.25 ◦ T0のタイムスライス = 6 * 1 / 2.25 ◦ T1のタイムスライス = 6 * 1.25 / 2.25 26
  27. 実験プログラム • 実行方法 ◦ ./sched <nvalue> <total> <resolution> • 引数の意味

    ◦ nvalue: nice値 ◦ Total: 各プロセスの実行時間 (ms単位) ◦ granularity: 測定粒度(ms単位) • 説明 ◦ 2個のタスクt0, t1を同時に動かす ◦ T0のnice値は0, t1のnice値はnvalue ◦ 2つのタスクはtotal[us]だけ無限ループする。このとき granularity[ms]に一回進捗を記録する ◦ 2つのタスク終了時に、各タスクの ID, 経過時間(us単位), 進捗(%単位)を出力 27
  28. 実行結果 • nvalue= 1, total=1000, granularity=1 28

  29. もくじ • スケジューラ • ロードバランサ • Fair Share Scheduler •

    CFS Bandwidth Controller 29
  30. ここから複数LCPUの話 • LCPUの数を2にする 30 # echo 1 >/sys/devices/system/cpu/cpu8/online # …(*1)

    # grep -c processor /proc/cpuinfo 2 # *1) LCPU8はLCPU0と一番共有するリソースが少ないので実験が楽。詳細は割愛
  31. ロードバランサ • ランキューはLCPUごとに存在 • ロードバランサ: 負荷の高いLCPUから負荷の低いLCPUにタスクを移動 ◦ 基本: 負荷 ==

    ランキュー長 ◦ 正確にはnice値やスケジューリングポリシーを考慮して重み付け (説明は省略) • 動作契機 ◦ 新規タスクの生成時 ◦ LCPUがアイドルになった時 ◦ スリープしていたタスクの起床時 ◦ 所定期間の経過時 31
  32. ロードバランサ: 初期状態 • ランキュー長が偏る 32 LCPU0 LCPU1 ランキュー長=4 忙しいんですけど ランキュー長=0

  33. バランス後 • ランキュー長が偏る 33 LCPU0 LCPU1 ランキュー長=2 ありがとう ランキュー長=2 しょうがねえなあ

    移動
  34. LCPU数とレイテンシターゲットの関係 • LCPU数が増えるほどレイテンシターゲットは長くなる • 計算式 ◦ レイテンシターゲット = LCPUが1のときのレイテンシターゲット *

    (1+floor(lg(LCPU数))) • 理由 ◦ スループット向上のためにレイテンシターゲットは長くしたい ◦ しかし応答性向上のためにはレイテンシターゲットを短くしたい ▪ 1LCPUに閉じると、スリープ状態から起床したタスクが CPU時間を得るまでの長さはレイテン シターゲットの長さに比例 ◦ LCPU数が多ければレイテンシターゲットが長くてもスリープしていたタスクはアイドルな別 LCPU上 で起床後即座に実行開始できる可能性が高いはず 34
  35. 実験 • LCPU数の増加しながらレイテンシターゲットの値を確認 35 # grep -c processor /proc/cpuinfo 1

    # sysctl kernel.sched_latency_ns kernel.sched_latency_ns = 6000000 # echo 1 >/sys/devices/system/cpu/cpu1/online # LCPU数 -> 2 # sysctl kernel.sched_latency_ns kernel.sched_latency_ns = 12000000 # 6000000 * 2 # echo 1 >/sys/devices/system/cpu/cpu2/online # LCPU数 -> 3 # sysctl kernel.sched_latency_ns kernel.sched_latency_ns = 12000000 # 6000000 * 2 # echo 1 >/sys/devices/system/cpu/cpu3/online # LCPU数 -> 4 # sysctl kernel.sched_latency_ns kernel.sched_latency_ns = 18000000 # 6000000 * 3 #
  36. 実験: schedプログラム再び • nproc= 2 • Total = 1000 •

    Granularity = 1 36
  37. CPU affinity • タスクを動作させるLCPUの集合を決められる ◦ 用途: 全CPUで1つづつ動かしたいハートビート処理 , VMやコンテナが動ける CPUの制限

    ◦ sched_setaffinity()システムコールやtasksetコマンドによって設定 37 T0 Affinity: 0, 1 T1 Affinity: 0 T2 Affinity: 1 LCPU0 LCPU1 × 移動可 〇 移動不可
  38. 実験 • 2LCPU環境でnproc=2で1coreにbind ◦ Taskset --cpu-list 0 ./sched 2 1000

    1 38
  39. cpuset cgroup • グループに属するタスクを特定LCPUの集合(cpuset)でしか動作させなくする ◦ CPU affinityの発展版 39

  40. ロードバランサの階層構造 • ハードウェア構成によってロードバランサは階層構造を持つ • 階層の例 ◦ NUMAノード間 ◦ NUMAノード内のソケット間 ◦

    ソケット内のコア間 ◦ コア内のスレッド間 • 動作の概略 ◦ 最上位階層でバランス -> 1つ下の階層でバランス -> … -> 最下階層でバランス 40
  41. ロードバランサの挙動: 初期状態 • 2ノード×2CPU(合計4LCPU)構成 41 t0 t1 t5 t4 t3

    LCPU0 LCPU1 LCPU2 LCPU3 t2 node0 node1
  42. 一番busyなnodeと暇なnodeを見つける • 42 t0 t1 t5 t4 t3 LCPU0 LCPU1

    LCPU2 LCPU3 t2 node0 node1 タスク数4 タスク数2
  43. 一番busyなnode内の一番忙しいLCPUを選ぶ • 43 t0 t1 t5 t4 t3 LCPU0 LCPU1

    LCPU2 LCPU3 t2 node0 node1 タスク数3 タスク数1
  44. 一番暇なnode内の一番暇なLCPUを選ぶ • 44 t0 t1 t5 t4 t3 LCPU0 LCPU1

    LCPU2 LCPU3 t2 node0 node1 タスク数0 タスク数2
  45. 一番busyなLCPUから一番暇なLCPUにタスクを移動 • 45 t0 t1 t5 t4 t3 LCPU0 LCPU1

    LCPU2 LCPU3 t2 node0 node1 t2 移動
  46. 古き良きコンシューマ向けCPU • 1物理CPU, 2コア、2スレッド => 4LCPU 1. 物理CPU内コア間のバランス 2. コア内スレッド間のバランス

    46 コア 物理CPU コア スレッド スレッド
  47. • 1物理CPU, 2CCX(*1), 4コア, 2スレッド => 16LCPU 1. 物理CPU内CCX間のバランス 2.

    CCX内コア間のバランス 3. コア内スレッド間のバランス CCX 複雑なアーキテクチャ(AMD Ryzen 1800X) 47 *1) L3キャッシュを共有するコアの塊 CCX コア スレッド スレッド CCX コア コア コア 物理CPU
  48. 階層構造の確認方法 • /proc/schedstat • 実験環境の場合 48 # cat /proc/schedstat …

    Cpu0 ... domain0 0003 ... domain1 00ff ... domain2 ffff ... ... cpu2 ... domain0 000c ... domain1 00ff ... domain2 ffff … … # コア内スレッド間のバランス CCX内コア間のバランス 物理CPU内CCX間のバランス
  49. 実験環境におけるCPUの物理構成とLCPU番号 49 LCPU番号 CCX コア スレッド 0 0 0 0

    1 1 2 1 2 3 3 4 2 4 5 5 6 3 6 7 7 LCPU番号 CCX コア スレッド 8 1 4 8 9 9 10 5 10 11 11 12 6 12 13 13 14 7 14 15 15
  50. ロードバランスの例: 無限ループプログラムの同時実行 • n=1 50 0 1 0 0 1

    2 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 4 5 6 7 CCX core thread p0
  51. ロードバランスの例: 無限ループプログラムの同時実行 • n=2: 各CCXに均等配置 51 0 1 0 0

    1 2 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 4 5 6 7 CCX core thread p0 p1
  52. ロードバランスの例: 無限ループプログラムの同時実行 • n=8: 各コアに均等配置 52 0 1 0 0

    1 2 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 4 5 6 7 CCX core thread p0 p1 p2 p3 p4 p5 p6 p7
  53. ロードバランスの例: 無限ループプログラムの同時実行 • n=16: 各スレッドに均等配置 53 0 1 0 0

    1 2 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 4 5 6 7 CCX core thread p0 p2 p4 p6 p8 p10 p12 p14 p1 p3 p5 p7 p9 p11 p13 p15
  54. おまけ: 変態アーキテクチャ(AMD EPYC7601フル構成) • 2物理CPU, 4ダイ(*1), 2CCX, 4コア, 2スレッド =>

    128LCPU 1. 物理CPU間のバランス 2. 物理CPU内ダイ間のバランス 3. ダイ内CCX間のバランス 4. CCX内コア間のバランス 5. コア内スレッド間のバランス 54 *1) CCXを2つ繋いだもの 物理CPU 物理CPU ダイ ダイ ダイ ダイ CCX CCX コア コア コア コア スレッド スレッド
  55. もくじ • スケジューラ • ロードバランサ • Fair Share Scheduler •

    CFS Bandwidth Controller 55
  56. 1LCPUに戻す • ここからの話は複数LCPUあると非常ややこしくなるので1LCPUで考える 56 # for ((i=0;i<16;i++)) ; do >

    echo 0 >/sys/devices/system/cpu/cpu${i}/online > done # grep -c processor /proc/cpuinfo 1 #
  57. fair group scheduling • Cpu cgroup 1. グループ間でCPUを均等配分 2. グループ内でさらに均等配分

    • Cpu cgroupのcpu.sharesパラメタによって重み付け • ネストも可能 57 Cpu cgroup “Foo” Shaers = 1024 Cpu cgroup “Bar” Shares = 1024 t0 t1 t2 1. Foo, bar間でCPU 時間を均等配分 1. Foo: T0, t1間で均等配分 2. Bar: t2に全部配分
  58. LCPU上で動くタスク 58 時間 t0 t1 t2 t0 t1 t2 cgroupなし

    Cgroupあり グループfoo: t0, t1 グループbar: t2 t0 t1 t2 t0 t1 t2 1 1/3 1/3 1/3 1 1/4 1/4 1/2 1/2
  59. LCPU上で動くタスク: 重み付けあり 59 時間 t0 t1 t0 t1 Cgroupあり  グループfoo:

    t0   share: 1024  グループbar: t1   share: 1024 Cgroupあり グループfoo: t0 Share: 1024 グループbar: t1 Share: 2048 1 1/2 1/2 t0 t1 1 1/3 2/3 t0 t1
  60. Usecase: ユーザ間でCPU資源を均等配分 • 例: ユーザAがt0, ユーザBがt1-t3を動かしているとする 60 Fair share scheduler不使用

    ユーザごとにcpu cgroup作成 時間 T0 (ユーザA) T1 (ユーザB) T2 (ユーザB) T3 (ユーザC) 時間 T0 (ユーザA) T1 (ユーザB) T2 (ユーザB) T3 (ユーザB) タスク同士が公平 ユーザ同士は不公平 ユーザ同士が公平
  61. 実験プログラム 1. 2つのcpu cgroup, fooとbarを作る 2. fooにbarの二倍のCPU時間を与える 3. Fooとbar上でそれぞれloopプログラムを動かす 4.

    topで%cpuを監視 61 # mkdir /sys/fs/cgroup/cpu/foo # mkdir /sys/fs/cgroup/cpu/bar # echo 2048 > /sys/fs/cgroup/cpu/foo/cpu.shares # echo 1024 > /sys/fs/cgroup/cpu/bar/cpu.shares # # ./loop & [1] 25688 # ./loop & [2] 25689 # echo 25688 >/sys/fs/cgroup/cpu/foo/tasks # echo 25689 >/sys/fs/cgroup/cpu/bar/tasks # top -b -d 1 -p 25688 -p 25689
  62. 実験結果 62 # top -b -d 1 -p 25688 -p

    25689 … PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 25688 root 20 0 4376 800 736 R 66.0 0.0 1:11.55 loop 25689 root 20 0 4376 744 684 R 33.0 0.0 1:08.45 loop … PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 25688 root 20 0 4376 800 736 R 66.3 0.0 1:12.22 loop 25689 root 20 0 4376 744 684 R 32.7 0.0 1:08.78 loop Fooに属するloopプログラムがbarに 属するそれの1.5倍CPU時間を得ている 時間 Loop (foo) Loop (bar) current Loop (foo) Loop (bar) レイテンシターゲット (lt) lt*(⅔) lt*(⅓)
  63. もくじ • スケジューラ • ロードバランサ • Fair Share Scheduler •

    CFS Bandwidth Controller 63
  64. Fair group schedulingの課題: 上限設定ができない • 例: 2ユーザA, Bが同居するマルチテナントシステム ◦ やりたいこと:

    個々のユーザにCPU資源を”最大”1/2与えたい ▪ ユーザ間の不平等、過剰なサービスの提供を避けたい ◦ Fair group schedulingにできること: ユーザごとにグループ作成 64 時間 Aだけがタスクを動かしている状態 (あるべき姿) Aのタスク Aだけがタスクを動かしている状態 (現実) Aのタスク idle Aのタスク idle
  65. CFS bandwidth controller • あるcpu cgroupが所定期間内に動ける時間を制限 ◦ cpu.Cfs_period_us: 期間([us]単位) ◦

    Cpu.cfs_quota_us: 動ける時間([us]単位) • ネストも可能 • 主な用途: VMやコンテナのリソース制限 65 時間 Aのタスク idle Aのタスク idle quota period quota切れ quotaチャージ
  66. 実験プログラム 1. fooという名前のCpu cgroupを作る 2. Fooに属するタスクはCPU資源の半分しか使えないようにする 3. Loopプログラムをバックグラウンドで動かしてfooに属させる 4. Topで%cpuを監視

    66 # Mkdir /sys/fs/cgroup/cpu/foo Echo 100000 > /sys/fs/cgroup/cpu/foo/cpu.cfs_period_us Echo 50000 > /sys/fs/cgroup/cpu/foo/cpu.cfs_quota_us # ./loop & [1] 25676 # echo 25676 > /sys/fs/cgroup/cpu/foo/tasks # Top -b 1 -d 1 -p 25676
  67. 実験結果 67 # top -b -d 1 -p 25576 ...

    PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 25676 root 20 0 4376 856 796 R 50.0 0.0 2:56.33 loop ... PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 25676 root 20 0 4376 856 796 R 51.0 0.0 2:56.84 loop ... LCPU上でこのプロセスしか動いていないのに 50%しかCPU資源を使えていない 時間 loop idle current loop idle loop idle quota period loopのquota切れ loopのquotaチャージ
  68. おわりに • より深く知りたければ… ◦ 実験プログラムのパラメタを色々変えながら実行 ◦ ソースを読む(Linuxカーネルソースのkernel/sched/以下) ◦ 実験プログラムを作る ◦

    Ftraceなどのトレーサを使う • 漫然とソースを読むより実験しながらのほうが理解しやすいよ! ◦ これはどのソフトウェアでも同じ 68
  69. リンク集 • 実験プログラムのソース ◦ https://github.com/satoru-takeuchi/kernelvm_hokuriku_part4 • 「Linuxのプロセススケジューラの歴史」 ◦ https://speakerdeck.com/sat/linux-sched-history •

    「タスクスケジューラ用のsysctlパラメタ」 ◦ https://qiita.com/satoru_takeuchi/items/7830f28c88e08ff4c720 69