Upgrade to Pro — share decks privately, control downloads, hide ads and more …

スケジューラについての調査 - オペレーティングシステム授業発表

スケジューラについての調査 - オペレーティングシステム授業発表

Makoto Shimazu

January 29, 2013
Tweet

More Decks by Makoto Shimazu

Other Decks in Programming

Transcript

  1. スケジューラとは • 沢山仕事するときに、どれを実行するか選ぶための機構 – プロセス・スレッド => タスク • ハードウェアは決まっている –

    例えば、 CPU が 2 コア 4 スレッドとか • NG パターン – 飢餓状態 ( starvation ) • ずっとタスクが実行されないままになること – IO bound なものと CPU bound なものの区別 • 同等だとどういうことになるか?
  2. スケジューラの種類 • FIFO / RR • O(1) • CFS (

    Completely Fair Scheduler ) • BFS ( Brain Fuck Scheduler ) • RM ( Rate Monotonic ) • DM ( Deadline Monotonic ) • EDF ( Earliest Deadline First ) • ポリシー – システムコール sched_setscheduler で変更可 – man 見て
  3. O(1) • kernel 2.6 から • 本とかはめっちゃある – 詳解 Linux

    カーネルとか • タスクを優先度ごとのキュー (= runqueue,rq ) に並べる – active/expired • → 頭から一個取ってくるだけ O(1)
  4. O(1) • 静的優先順位 + 動的優先順位 (nice) • 優先順位に応じて、実行できる時間が決まってる • nice

    を決めるときには – IO bound と CPU bound – これまでの動きから予測する
  5. CFS - 概要 • kernel 2.6.23 から • O(1) はややこしいし予測にも失敗する

    • 単純に、「全てのタスクに同じ CPU 時間」を割り当てる • 実行時間: vruntime – 理想的には全てのタスクの vruntime は常に等しい
  6. CFS - 概要 • 単純にタイムスライスを切って分ける – cat /proc/sys/kernel/sched_latency_ns – (default:

    6ms * (1 + ilog(ncpus)), units: nanoseconds) @fair.c l.36 – この期間中に1回は動く
  7. CFS - 概要 • ただ、単純と言いつつ実はちょっと面倒なことをしている – 眠っていたときの処理 • vruntime を一番小さいやつに合わせる

    – すぐ追いついちゃったときの処理 • unsigned int sysctl_sched_wakeup_granularity = 1000000UL; – cat /proc/sys/kernel/sched_wakeup_granurarity_ns – next-buddy (preempt failure) / last-buddy (cache hot) ルール – スライスしすぎちゃったときの処理 • min_granularity_ns – cat /proc/sys/kernel/sched_min_granularity_ns – (default: 0.75 msec * (1 + ilog(ncpus)), units: nanoseconds) @fair.c l.65
  8. CFS – 実装 • 次のタスクを選ぶ関数 – pick_next_task – スケジューリングクラスごとにある static

    struct task_struct *pick_next_task_fair(struct rq *rq) { struct task_struct *p; struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se; if (!cfs_rq->nr_running) return NULL; do { se = pick_next_entity(cfs_rq); set_next_entity(cfs_rq, se); cfs_rq = group_cfs_rq(se); } while (cfs_rq); p = task_of(se); if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p; }
  9. CFS – 実装 • 次のタスクを選ぶ関数 – pick_next_task – スケジューリングクラスごとにある •

    すっきりしてるけど闇は深いよ? static struct task_struct *pick_next_task_fair(struct rq *rq) { struct task_struct *p; struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se; if (!cfs_rq->nr_running) return NULL; do { se = pick_next_entity(cfs_rq); set_next_entity(cfs_rq, se); cfs_rq = group_cfs_rq(se); } while (cfs_rq); p = task_of(se); if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p; }
  10. CFS - 実装 • rq とかいいつつ赤黒木 (O(logn)) • キーは vruntime

    → 左端が一番動いてないやつ • 赤黒木って? – 赤黒木は平衡二分木のひとつ – 黒の数が全ての経路で同じ , 赤は続かない – 深さは一番差があるときで 2 倍 – 回転はちょっと面倒
  11. 全体の実装 • スケジューラ全体の実装について • 重要な構造体は以下 – task_struct→ プロセスの情報管理 – sched_xx_entity

    • →xx = なし (CFS) / rt (FIFO/RR) / dl (DEADLINE) →xx に対応したスケジューラの情報管理 (vruntime とか ) – sched_class→ スケジューラのポリシー自体 • 関数ポインタ群
  12. 全体の実装 • task_struct – タスクに関する情報 – 4kB 近くあったり struct task_struct

    { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int prio, static_prio, normal_prio; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; struct sched_dl_entity dl; pid_t pid; pid_t tgid; struct timespec start_time; /* monotonic time */ struct timespec real_start_time; /* boot based time */ struct css_set __rcu *cgroups; …
  13. 全体の実装 • task_struct – タスクに関する情報 – 4kB 近くあったり struct task_struct

    { volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ void *stack; atomic_t usage; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; int prio, static_prio, normal_prio; const struct sched_class *sched_class; struct sched_entity se; struct sched_rt_entity rt; struct sched_dl_entity dl; pid_t pid; pid_t tgid; struct timespec start_time; /* monotonic time */ struct timespec real_start_time; /* boot based time */ struct css_set __rcu *cgroups; … int pdeath_signal; /* The signal sent when the parent dies */ unsigned int jobctl; /* JOBCTL_*, siglock protected */ /* ??? */ unsigned int personality; unsigned did_exec:1; unsigned in_execve:1; /* Tell the LSMs that the process is doing an * execve */
  14. 全体の実装 • sched_entity – 各種のスケジューリングクラスに 対応したデータを保持 struct sched_entity { struct

    load_weight load; /* for load-balancing */ struct rb_node run_node; struct list_head group_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; u64 prev_sum_exec_runtime; u64 nr_migrations; #ifdef CONFIG_SCHEDSTATS struct sched_statistics statistics; #endif #ifdef CONFIG_FAIR_GROUP_SCHED struct sched_entity *parent; /* rq on which this entity is (to be) queued: */ struct cfs_rq *cfs_rq; /* rq "owned" by this entity/group: */ struct cfs_rq *my_q; #endif };
  15. 全体の実装 • sched_xx_class – dl_sched_class const struct sched_class dl_sched_class =

    { .next = &rt_sched_class, .enqueue_task = enqueue_task_dl, .dequeue_task = dequeue_task_dl, .yield_task = yield_task_dl, .check_preempt_curr = check_preempt_curr_dl, .pick_next_task = pick_next_task_dl, .put_prev_task = put_prev_task_dl, #ifdef CONFIG_SMP .select_task_rq = select_task_rq_dl, .set_cpus_allowed = set_cpus_allowed_dl, .rq_online = rq_online_dl, .rq_offline = rq_offline_dl, .pre_schedule = pre_schedule_dl, .post_schedule = post_schedule_dl, .task_woken = task_woken_dl, #endif .set_curr_task = set_curr_task_dl, .task_tick = task_tick_dl, .task_fork = task_fork_dl, .task_dead = task_dead_dl, .prio_changed = prio_changed_dl, .switched_from = switched_from_dl, .switched_to = switched_to_dl, };
  16. 実装 • リスト構造 • pick_next_task あたりを見る と タスクをどう選ぶかわかりや すい •

    schedule() を呼ぶと次のタス クを選ぶ static inline struct task_struct * pick_next_task(struct rq *rq) { const struct sched_class *class; struct task_struct *p; /* * Optimization: we know that if all tasks are in * the fair class we can call that function directly: */ if (likely(rq->nr_running == rq->cfs.h_nr_running)) { p = fair_sched_class.pick_next_task(rq); if (likely(p)) return p; } for_each_class(class) { p = class->pick_next_task(rq); if (p) return p; } BUG(); /* the idle class will always have a runnable task */ }
  17. 全体の実装 • hrtick – high resolution tick – 全体の tick(

    .config で HZ として定義 ) と異なる – 10us 制限あり – 時間が来ると schedule を起こす static struct task_struct *pick_next_task_fair(struct rq *rq) { struct task_struct *p; struct cfs_rq *cfs_rq = &rq->cfs; struct sched_entity *se; if (!cfs_rq->nr_running) return NULL; do { se = pick_next_entity(cfs_rq); set_next_entity(cfs_rq, se); cfs_rq = group_cfs_rq(se); } while (cfs_rq); p = task_of(se); if (hrtick_enabled(rq)) hrtick_start_fair(rq, p); return p; }
  18. 実際に使ってみた - 方法 • 評価方法 – 右のようなプログラム (load.c) を用意する –

    これの優先順位を適当にいじったときの、タスクのスイッチングを確認する – 確認には trace-cmd を使う • trace-cmd record -e sched_switch • trace って ? – カーネルが提供するログ – mount -t debugfs nobody /proc/sys/debug – cat /proc/sys/debug/tracing/trace – kernelshark を用いて表示 • まず1コア1スレッドの仮想マシン (kvm) 上で動かす – 3.7.0-rc6-sched-deadline-mainline-dl #include <stdio.h> void main( void ) { volatile int i = 0; printf( "Load Program\n" ); printf( "Start!\n" ); while ( 1 ) i++; }
  19. – 実際に使ってみた CFS • CFS x3 nice が 5/10/15 –

    CFS における nice → vruntime の重み付け (1 につき 1.25 → 倍 1.25^5 = 3.05)
  20. CFS • CFS を大学生に例えると ...? • 状況  – OS の発表

    – と BDM ( ★ ☆ びっくり どっきり メカ ) – と実験レポ – と試験勉強をしないといけない
  21. CFS • 発表当日 進捗は・・・ • OS の準備 50% • BDM

    の準備 50% • 実験レポの準備 50% • 試験勉強 50%
  22. 実時間 (RealTime) • 発表に間に合ってない!!!! – けど試験勉強も進んでる • なぜ? – 締め切りの存在

    – 締め切りがあるようなタスクには不向き • ロボットの制御など – マップの更新 / モータの制御 (1ms ごと /10ms ごととか ) – 逆に画像を用いた補正などはゆっくりでいい
  23. 実時間 • ロボットだとどういう感じか – 10ms に一度 • センサ情報の更新  ( 例:速度

    v ) • 自分の位置の計算 (例: pos=∫vdt ) • モータ出力の更新 (例: V( 電圧) = A*dif ) – カメラからの画像がとれたら • 自己位置の補正 • モータ出力が遅れると? – 例:画像処理が遅くて固まる
  24. FIFO/RR • ダメな点 1 – 簡単に飢餓 • ダメな点 2 –

    じつは上手に終わらない例がある
  25. FIFO/RR • 例えば、 OS の発表の準備中に急にサークルの友人から電話がかかっ てくる – 「回路製作の締め切りが日曜になりました!よろしく!!!」 • でも自分はすでに

    OS の発表準備をしている よって無視 • ” ” 適切に優先順位を指定してやれば 大丈夫 – 予め全ての優先順位が決まってないと難しい • 同じなら – FIFO :とりあえず先にやってるものが終わるまでは次に移れない – RR :交代に進めるけど、時間は半々 ☆ 実際には後輩に任せました -( ゝ ω ・ ) b
  26. – 実際に使ってみた FIFO/RR • RR + CFS prio 1 +

    nice10 • /proc/sys/kernel/sched_rt_period_us → 1s /proc/sys/kernel/sched_rt_runtime_us → 0.95s
  27. – 実際に使ってみた FIFO/RR • 実際に RR を動かしてみよう • 0.05s しか割り当ててもらえない辛さ

    • コマンド – schedtool -R -p 1 -e ./load & – trace-cmd record -e sched_switch – killall -KILL load – kernelshark
  28. RM/DM • RM → 周期が短いものから実行 • DM → “ ”

    相対 デッドラインが短いものから実行 • 相対デッドライン ( relative deadline ) • 絶対デッドライン ( absolute deadline )
  29. RM/DM • 固定優先度と動的優先度 – fixed priority / dynamic priority •

    RM も DM も固定優先度 → 予め優先順位が決まる • RM は特に組み込みでよく使われる • けっこうデッドライン守れる – ほとんどの組み合わせで CPU 使用率 80% くらいはいけるらしい
  30. EDF - 概要 • EDF と RM の比較 • T1:period=4

    runtime=1 • T2:period=6 runtime=2 • T3:period=8 runtime=3 • CPU usage = 1/4+2/6+3/8=95.8%
  31. EDF - 概要 • メリット – CPU 使用率が 100% になるまではデッドラインを守れる

    • デメリット – CPU 使用率が 100% を超えた時にドミノ倒し現象が起こる ex. ) 明日締め切りなのに明後日までかかったら明後日締め切りのタスクは終わ らない – 動的に優先度を変えるために実装が (RM 比で ) かなり面倒 • あまり普及していない – マルチコアでは最適でなくなる
  32. EDF – 応答時間 • 1 CPU • CPU バウンド (IO

    待ちで寝たりしない ) • いつでもプリエンティブ • 任意の時刻にタスクが現れる • 相対デッドラインは、実行時間以上の任意の値を取る • 相対デッドラインは実行時間に比例 • タスクの到着間隔の平均・実行時間の平均は既知 • この条件で、到着間隔と実行時間が指数分布という仮定
  33. EDF – 実装 • とりあえず pick_next_task_dl を見てみる • すると、以下がキモっぽい dl_se

    = pick_next_dl_entity(rq, dl_rq); • これは右のようになっている • 要するに赤黒木 – キーは絶対デッドライン時刻 static struct sched_dl_entity *pick_next_dl_entity(struct rq *rq, struct dl_rq *dl_rq) { struct rb_node *left = dl_rq->rb_leftmost; if (!left) return NULL; return rb_entry(left, struct sched_dl_entity, rb_node); }
  34. 実際に使ってみた • sched_setscheduler2 とやらを呼べ って書いてある • → 呼んでみた • 呼んでみた・・・

    #include <sys/types.h> #include <unistd.h> #include <linux/sched.h> #include <sched.h> #include <stdlib.h> int main( int argc __attribute__((unused)), char *argv[] __attribute__((unused)) ) { pid_t mypid = getpid(); struct sched_param2 sched_status = { .sched_priority = 1 .sched_runtime = 10000, .sched_deadline = 100000, .sched_period = 100000 }; if ( sched_setscheduler2( mypid, SCHED_DEADLINE, &sched_status ) ) { perror( "sched_setscheduler2" ); return 1; } volatile int i=0; while( 1 ) i++; return 0;
  35. 実際に使ってみた • sched_setscheduler2 とやらを呼べ って書いてある • → 呼んでみた #include <sys/types.h>

    #include <unistd.h> #include <linux/sched.h> #include <sched.h> #include <stdlib.h> int main( int argc __attribute__((unused)), char *argv[] __attribute__((unused)) ) { pid_t mypid = getpid(); struct sched_param2 sched_status = { .sched_priority = 1 .sched_runtime = 10000, .sched_deadline = 100000, .sched_period = 100000 }; if ( sched_setscheduler2( mypid, SCHED_DEADLINE, &sched_status ) ) { perror( "sched_setscheduler2" ); return 1; } volatile int i=0; while( 1 ) i++; return 0; dame.c: In function ‘main’: dame.c:15: error: variable ‘sched_status’ has initializer but incomplete type dame.c:16: error: unknown field ‘__sched_priority’ specified in initializer dame.c:17: error: request for member ‘sched_flags’ in something not a structure or union dame.c:18: warning: excess elements in struct initializer dame.c:18: warning: (near initialization for ‘sched_status’) dame.c:15: error: storage size of ‘sched_status’ isn’t known  _人人人人人人人人_  > 突然のエラー <   ̄ Y^Y^Y^Y^Y^Y^Y  ̄
  36. 実際に使ってみた • でもソースには関数あるし・・・ • 動かない理由がわからない • わからない・・・ • そして溶けていった数日間 int

    sched_setscheduler2(struct task_struct *p, int policy, const struct sched_param2 *param2) { return __sched_setscheduler(p, policy, param2, true); } カーネルコンパイル周りでサークルの先輩にめっちゃ助けてもらいました・
  37. 実際に使ってみた • schedtool を使ったら動いた – 何で?システムコールが定義されてるって書いてあったけど・・・? • システムコールとは – じつは割り込み

    (0x80 番 ) – 引数はスタックではなくレジスタ – eax に種別をセット • exit なら 1, write なら 4 など • schedtool は右のようなマクロ を自分で定義していた #ifdef __i386__ #define __NR_sched_setparam2 350 #define __NR_sched_getparam2 351 #define __NR_sched_setscheduler2 352 #endif … #define sched_setscheduler2(pid, policy, param) \ syscall(__NR_sched_setscheduler2, pid, policy, param) #define sched_getparam2(pid, param) \ syscall(__NR_sched_getparam2, pid, param) #define sched_setparam2(pid, param) \ syscall(__NR_sched_setparam2, pid, param) tags/v4 だと ,349,350,351 と       1 少なくする必要あり
  38. 実際に使ってみれなかった • sched_class 的には dl_sched_class は rt より高い – つまり誰からも干渉を受けず、自分のしごとを淡々とこなすはず

    – 本来なら下図のようになるはず • でもどうも途中で切れてるように見える – trace-cmd のせい? ( うまく trace できてない可能性) – preemption 周りにバグ?(他のタスクが preempt したときにスルーできな い)
  39. 実際に使ってみれなかった • やりたかったこと – 1コアで • EDF + CFS •

    EDF + RR • EDF x2( 間に合う ) • EDF x2( 間に合わない ) • EDF x1( 1ms 周期 ) – 今度は2コア2スレッド • EDF x2 ( 間に合う ) • EDF x3 ( それぞれ CPU 時間 2/3 ) – マルチコアのときには CPU100% は保証できない • からどうしたらいいか? • という結論を導いて、もうすこし考察を深めたかった
  40. まとめ • CFS スケジューラは優秀 – 普通に使っている分には違和感を感じることはない • EDF スケジューラはリアルタイム性が欲しいときに有効な手段 –

    だけどうまく実装されている例があまりない – 動くようにしてみたいですね • 意外と idle している時間って多い • linux カーネルの広がる闇 – わからないことをしらべたらわからないからしらべたら ( 以下 n 回続く) – CONFIG_SMP( マルチコア対応部分 ) がかなりある – わからないことが多すぎて時間が食いつぶされてしまいました・・・
  41. おまけ - main • スケジューラの元ってどこ • nohz とか hrtick とか

    ,SMP とかそうじゃないとか , 同じような関数 でも違うやつがいろいろあって難しかった • → コンパイル時のオプション • ソースの感じでは hrtick として動くっぽい … …  いろいろたどる  • kernel の一番大元にいきついた – arch/x86/kernel/head32.c i386_start_kernel() とか – 疑問:これの上はどこ? amiq@ostest> grep NO_HZ /boot/config-3.7.0-rc6+ CONFIG_NO_HZ=y amiq@ostest> grep HRTICK /boot/config-3.7.0-rc6+ CONFIG_SCHED_HRTICK=y
  42. おまけ - main • i386_start_kernel • →start_kernel(init/main.c) – dmesg の最初はここから

    • → sched_init – init_hrtick->hrtimer_init • → tick_init – tick_notifier 構造体の登録 – → … … 数層の関数ジャンプ • → tick_periodic – do_timer → jiffies/xtime の変更とかしてる中枢
  43. おまけ - main • start_kernel → init_timers → open_softirq •

    run_timer_softirq → hrtimer_run_pending → hrtimer_switch_to_hres → tick_setup_sched_timer • ここで tick_sched_timer の準備をしてる /* * linux/init/main.c * * Copyright (C) 1991, 1992 Linus Torvalds * ... */ [ 0.000000] hpet clockevent registered [ 0.000000] tsc: Detected 2393.996 MHz processor [ 0.008000] Calibrating delay loop (skipped) preset value.. 4787.99 BogoMIPS (lpj=
  44. おまけ - main • tick_sched_timer か tick_periodic → update_process_times(user_mode(regs)); •

    からの scheduler_tick • ここでやっと scheduler に 戻ってきた • これが HZ ごとに実行されるらしい – HZ を使ってるところは 見つからなかった・・・ ?? /* * This function gets called by the timer code, with HZ frequency * We call it with interrupts disabled. */ void scheduler_tick(void) { int cpu = smp_processor_id(); struct rq *rq = cpu_rq(cpu); struct task_struct *curr = rq->curr; sched_clock_tick(); raw_spin_lock(&rq->lock); update_rq_clock(rq); update_cpu_load_active(rq); curr->sched_class->task_tick(rq, curr, 0); raw_spin_unlock(&rq->lock); perf_event_task_tick(); #ifdef CONFIG_SMP rq->idle_balance = idle_cpu(cpu);
  45. • flags/EBP/ESP を保存して、 データを引っ張ってくる • レジスタはスタックにある • __switch_to で切り替え •

    スタックポインタとかは 構造体のメンバとして保存 #define switch_to(prev, next, last) \ do { \ /* \ * Context-switching clobbers all registers, so we clobber \ * them explicitly, via unused output variables. \ * (EAX and EBP is not listed because EBP is saved/restored \ * explicitly for wchan access and EAX is the return value of \ * __switch_to()) \ */ \ unsigned long ebx, ecx, edx, esi, edi; \ \ asm volatile("pushfl\n\t" /* save flags */ \ "pushl %%ebp\n\t" /* save EBP */ \ "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ "pushl %[next_ip]\n\t" /* restore EIP */ \ __switch_canary \ "jmp __switch_to\n"/* regparm call */ \ "1:\t" \ "popl %%ebp\n\t" /* restore EBP */ \ "popfl\n" /* restore flags */ \ \ /* output parameters */ \ : [prev_sp] "=m" (prev->thread.sp), \ [prev_ip] "=m" (prev->thread.ip), \ "=a" (last), \ \ /* clobbered output registers: */ \ "=b" (ebx), "=c" (ecx), "=d" (edx), \ "=S" (esi), "=D" (edi) \ \ __switch_canary_oparam \ おまけ - タスクスイッチ