linuxプロセススケジューリング関数の概要(3.16-rc 4ベース)
38108 ワード
プロセススケジューリングはschedule()関数を使用して行われることはよく知られています.次に、この関数の解析から始めます.コードは以下の通りです(kernel/sched/core.c).
3行目は、現在のプロセス記述子ポインタを取得し、ローカル変数tskに格納します.6行目の呼び出し_schedule()で、コードは以下の通りです(kernel/sched/core.c).
9行目はカーネルプリエンプトを禁止します.10行目は現在のcpu番号を取得します.11行目は、現在のcpuのプロセス実行キューを取得します.13行目は、現在のプロセスの記述子ポインタをprev変数に保存します.55行目は、次のスケジューリングされたプロセス記述子ポインタをnext変数に格納する.56行目は、現在のプロセスのカーネルプリエンプトタグを消去します.60行目は、現在のプロセスと次のスケジューリングが同じプロセスであるか否かを判断し、そうでなければスケジューリングを行う.65行目で、現在のプロセスと次のプロセスのコンテキストを切り替えます(スケジューリングする前にコンテキストを切り替えます).この関数(kernel/sched/core.c)を見てみましょう.
コンテキスト切り替えは一般的に2つに分けられますが、一つはハードウェアコンテキスト切替(cpuレジスタを指し、現在のプロセスで使用されているレジスタ内容を保存し、次のプログラムのレジスタ内容を復元する)であり、もう一つはプロセスのアドレス空間(つまりプログラムコード)を切り替える.プロセスのアドレス空間である.(プログラムコード)主にプロセス記述子のstruct mm_struct構造体に保存されるため、この関数は主にこの構造体を操作する.17行目にスケジューリングされた次のプロセスアドレス空間mmが空であれば、次のプロセスがスレッドであり、独立したアドレス空間がなく、所属プロセスのアドレス空間を共用することを示すため、18行目は前のプロセスで使用されたアドレス空間をactive_mmポインタは次のプロセスのドメインに割り当てられ,次のプロセスもこのアドレス空間を用いる.22行目、次のプロセスのアドレス空間が空でない場合、次のプロセスに独自のアドレス空間があることを示し、switch_を実行するmmプロセスページ表を切り替えます.40行目はプロセスのハードウェアコンテキストを切り替えます.switch_to関数コードは次のとおりです(arch/x 86/include/asm/switch_to.h):
この関数では、プロセスのハードウェアコンテキスト切り替えを完了するためにインラインアセンブリが使用されます.12〜13行目は、プロセスが再び切り替えられた後、この2つのレジスタの値が使用されるため、eflagsおよびebpレジスタの値をスタックに圧縮する.14行目現在のプロセスのスタックトップポインタをプロセスのthread_に保存info.sp中.15行目は次のプロセスのthread_info.spの値はespレジスタに復元され、次のプロセスのカーネルスタックに切り替わり、プロセス切替が完了し(プロセスカーネルスタックの切替はプロセス切替のフラグ)、後のコードの実行は新しいプロセスで行われる.16行目は符号1で表されるアドレスを前のプロセスのthread_info.ipに格納し、以降前のプロセスに切り替えるとthread_info.ipが指すコードから実行する(実際には、前のプロセスが再び切り替えられたときにどのコマンドから実行されるかを考えて、そのコマンドのアドレスを前のプロセスのthread_info.ipに保存します.プロセスのフィールド保護と関数呼び出し時のフィールド保護には違いがあります.関数呼び出しのフィールド保護はレジスタの値をスタックに圧します.(結局スタックは切り替えていない)、その後現場に復帰した時にレジスタの値を弾き出す;プロセス切替の現場保護はレジスタの値をプロセスのthread_info構造に格納し、切替されたプロセスが再び実行されるとthread_info構造から現場を回復し、結局プロセス切替はカーネルスタックまで一緒に交換したので、必ずプロセスのリソースを保証するプロセスに関連するデータ構造が存在してこそ、失われず、リカバリが容易になります).17行目現在のプロセスのthread_info.ipはカーネルスタックに圧入され、このipが指す命令から実行される.19行目_にジャンプswitch_to関数にあります.下を見てみようswitch_to関数コード(arch/x 86/kernel/process_32.c):
この関数は主に切り替えたばかりの新しいプロセスをさらに初期化します.例えば、第34のプロセスで使用されるスレッドのローカルストレージセグメント(TLS)ローカルcpuのグローバル記述子テーブルを読み込みます.84行目の戻り文は、戻り値prev_pをeaxレジスタに保存する2つのアセンブリ命令にコンパイルされます.もう1つはret命令で、カーネルスタックの上部の要素をeipレジスタにポップアップし、このeipポインタから実行されます.つまり、前の関数の17行目に押し込まれたポインタです.一般的に、押し込まれたポインタは、前の関数の20行目の番号1で表されるアドレスであり、switch_to関数が戻ると、ラベル1から運転が開始されます.
なお、既にスケジューリングされているプロセスについては、_switch_to関数が戻った後、ラベル1から運転を開始します.ただし、fork()、clone()などの関数で作成されたばかりの新しいプロセス(スケジューリングされていません)では、do_fork()関数は、プロセスを作成した後、プロセスにthread_を与えます.info.ip付与ret_from_fork関数のアドレスは、1のアドレスではなく、ret_にジャンプします.from_fork関数.後でforkシステム呼び出しを分析すると、見えます.
1 asmlinkage __visible void __sched schedule(void)
2 {
3 struct task_struct *tsk = current;
4
5 sched_submit_work(tsk);
6 __schedule();
7 }
8 EXPORT_SYMBOL(schedule);
3行目は、現在のプロセス記述子ポインタを取得し、ローカル変数tskに格納します.6行目の呼び出し_schedule()で、コードは以下の通りです(kernel/sched/core.c).
1 static void __sched __schedule(void)
2 {
3 struct task_struct *prev, *next;
4 unsigned long *switch_count;
5 struct rq *rq;
6 int cpu;
7
8 need_resched:
9 preempt_disable();
10 cpu = smp_processor_id();
11 rq = cpu_rq(cpu);
12 rcu_note_context_switch(cpu);
13 prev = rq->curr;
14
15 schedule_debug(prev);
16
17 if (sched_feat(HRTICK))
18 hrtick_clear(rq);
19
20 /*
21 * Make sure that signal_pending_state()->signal_pending() below
22 * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
23 * done by the caller to avoid the race with signal_wake_up().
24 */
25 smp_mb__before_spinlock();
26 raw_spin_lock_irq(&rq->lock);
27
28 switch_count = &prev->nivcsw;
29 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
30 if (unlikely(signal_pending_state(prev->state, prev))) {
31 prev->state = TASK_RUNNING;
32 } else {
33 deactivate_task(rq, prev, DEQUEUE_SLEEP);
34 prev->on_rq = 0;
35
36 /*
37 * If a worker went to sleep, notify and ask workqueue
38 * whether it wants to wake up a task to maintain
39 * concurrency.
40 */
41 if (prev->flags & PF_WQ_WORKER) {
42 struct task_struct *to_wakeup;
43
44 to_wakeup = wq_worker_sleeping(prev, cpu);
45 if (to_wakeup)
46 try_to_wake_up_local(to_wakeup);
47 }
48 }
49 switch_count = &prev->nvcsw;
50 }
51
52 if (prev->on_rq || rq->skip_clock_update < 0)
53 update_rq_clock(rq);
54
55 next = pick_next_task(rq, prev);
56 clear_tsk_need_resched(prev);
57 clear_preempt_need_resched();
58 rq->skip_clock_update = 0;
59
60 if (likely(prev != next)) {
61 rq->nr_switches++;
62 rq->curr = next;
63 ++*switch_count;
64
65 context_switch(rq, prev, next); /* unlocks the rq */
66 /*
67 * The context switch have flipped the stack from under us
68 * and restored the local variables which were saved when
69 * this task called schedule() in the past. prev == current
70 * is still correct, but it can be moved to another cpu/rq.
71 */
72 cpu = smp_processor_id();
73 rq = cpu_rq(cpu);
74 } else
75 raw_spin_unlock_irq(&rq->lock);
76
77 post_schedule(rq);
78
79 sched_preempt_enable_no_resched();
80 if (need_resched())
81 goto need_resched;
82 }
9行目はカーネルプリエンプトを禁止します.10行目は現在のcpu番号を取得します.11行目は、現在のcpuのプロセス実行キューを取得します.13行目は、現在のプロセスの記述子ポインタをprev変数に保存します.55行目は、次のスケジューリングされたプロセス記述子ポインタをnext変数に格納する.56行目は、現在のプロセスのカーネルプリエンプトタグを消去します.60行目は、現在のプロセスと次のスケジューリングが同じプロセスであるか否かを判断し、そうでなければスケジューリングを行う.65行目で、現在のプロセスと次のプロセスのコンテキストを切り替えます(スケジューリングする前にコンテキストを切り替えます).この関数(kernel/sched/core.c)を見てみましょう.
1 context_switch(struct rq *rq, struct task_struct *prev,
2 struct task_struct *next)
3 {
4 struct mm_struct *mm, *oldmm;
5
6 prepare_task_switch(rq, prev, next);
7
8 mm = next->mm;
9 oldmm = prev->active_mm;
10 /*
11 * For paravirt, this is coupled with an exit in switch_to to
12 * combine the page table reload and the switch backend into
13 * one hypercall.
14 */
15 arch_start_context_switch(prev);
16
17 if (!mm) {
18 next->active_mm = oldmm;
19 atomic_inc(&oldmm->mm_count);
20 enter_lazy_tlb(oldmm, next);
21 } else
22 switch_mm(oldmm, mm, next);
23
24 if (!prev->mm) {
25 prev->active_mm = NULL;
26 rq->prev_mm = oldmm;
27 }
28 /*
29 * Since the runqueue lock will be released by the next
30 * task (which is an invalid locking op but in the case
31 * of the scheduler it's an obvious special-case), so we
32 * do an early lockdep release here:
33 */
34 #ifndef __ARCH_WANT_UNLOCKED_CTXSW
35 spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
36 #endif
37
38 context_tracking_task_switch(prev, next);
39 /* Here we just switch the register state and the stack. */
40 switch_to(prev, next, prev);
41
42 barrier();
43 /*
44 * this_rq must be evaluated again because prev may have moved
45 * CPUs since it called schedule(), thus the 'rq' on its stack
46 * frame will be invalid.
47 */
48 finish_task_switch(this_rq(), prev);
49 }
コンテキスト切り替えは一般的に2つに分けられますが、一つはハードウェアコンテキスト切替(cpuレジスタを指し、現在のプロセスで使用されているレジスタ内容を保存し、次のプログラムのレジスタ内容を復元する)であり、もう一つはプロセスのアドレス空間(つまりプログラムコード)を切り替える.プロセスのアドレス空間である.(プログラムコード)主にプロセス記述子のstruct mm_struct構造体に保存されるため、この関数は主にこの構造体を操作する.17行目にスケジューリングされた次のプロセスアドレス空間mmが空であれば、次のプロセスがスレッドであり、独立したアドレス空間がなく、所属プロセスのアドレス空間を共用することを示すため、18行目は前のプロセスで使用されたアドレス空間をactive_mmポインタは次のプロセスのドメインに割り当てられ,次のプロセスもこのアドレス空間を用いる.22行目、次のプロセスのアドレス空間が空でない場合、次のプロセスに独自のアドレス空間があることを示し、switch_を実行するmmプロセスページ表を切り替えます.40行目はプロセスのハードウェアコンテキストを切り替えます.switch_to関数コードは次のとおりです(arch/x 86/include/asm/switch_to.h):
1 #define switch_to(prev, next, last) \
2 do { \
3 /* \
4 * Context-switching clobbers all registers, so we clobber \
5 * them explicitly, via unused output variables. \
6 * (EAX and EBP is not listed because EBP is saved/restored \
7 * explicitly for wchan access and EAX is the return value of \
8 * __switch_to()) \
9 */ \
10 unsigned long ebx, ecx, edx, esi, edi; \
11 \
12 asm volatile("pushfl
\t" /* save flags */ \
13 "pushl %%ebp
\t" /* save EBP */ \
14 "movl %%esp,%[prev_sp]
\t" /* save ESP */ \
15 "movl %[next_sp],%%esp
\t" /* restore ESP */ \
16 "movl $1f,%[prev_ip]
\t" /* save EIP */ \
17 "pushl %[next_ip]
\t" /* restore EIP */ \
18 __switch_canary \
19 "jmp __switch_to
" /* regparm call */ \
20 "1:\t" \
21 "popl %%ebp
\t" /* restore EBP */ \
22 "popfl
" /* restore flags */ \
23 \
24 /* output parameters */ \
25 : [prev_sp] "=m" (prev->thread.sp), \
26 [prev_ip] "=m" (prev->thread.ip), \
27 "=a" (last), \
28 \
29 /* clobbered output registers: */ \
30 "=b" (ebx), "=c" (ecx), "=d" (edx), \
31 "=S" (esi), "=D" (edi) \
32 \
33 __switch_canary_oparam \
34 \
35 /* input parameters: */ \
36 : [next_sp] "m" (next->thread.sp), \
37 [next_ip] "m" (next->thread.ip), \
38 \
39 /* regparm parameters for __switch_to(): */ \
40 [prev] "a" (prev), \
41 [next] "d" (next) \
42 \
43 __switch_canary_iparam \
44 \
45 : /* reloaded segment registers */ \
46 "memory"); \
47 } while (0)
この関数では、プロセスのハードウェアコンテキスト切り替えを完了するためにインラインアセンブリが使用されます.12〜13行目は、プロセスが再び切り替えられた後、この2つのレジスタの値が使用されるため、eflagsおよびebpレジスタの値をスタックに圧縮する.14行目現在のプロセスのスタックトップポインタをプロセスのthread_に保存info.sp中.15行目は次のプロセスのthread_info.spの値はespレジスタに復元され、次のプロセスのカーネルスタックに切り替わり、プロセス切替が完了し(プロセスカーネルスタックの切替はプロセス切替のフラグ)、後のコードの実行は新しいプロセスで行われる.16行目は符号1で表されるアドレスを前のプロセスのthread_info.ipに格納し、以降前のプロセスに切り替えるとthread_info.ipが指すコードから実行する(実際には、前のプロセスが再び切り替えられたときにどのコマンドから実行されるかを考えて、そのコマンドのアドレスを前のプロセスのthread_info.ipに保存します.プロセスのフィールド保護と関数呼び出し時のフィールド保護には違いがあります.関数呼び出しのフィールド保護はレジスタの値をスタックに圧します.(結局スタックは切り替えていない)、その後現場に復帰した時にレジスタの値を弾き出す;プロセス切替の現場保護はレジスタの値をプロセスのthread_info構造に格納し、切替されたプロセスが再び実行されるとthread_info構造から現場を回復し、結局プロセス切替はカーネルスタックまで一緒に交換したので、必ずプロセスのリソースを保証するプロセスに関連するデータ構造が存在してこそ、失われず、リカバリが容易になります).17行目現在のプロセスのthread_info.ipはカーネルスタックに圧入され、このipが指す命令から実行される.19行目_にジャンプswitch_to関数にあります.下を見てみようswitch_to関数コード(arch/x 86/kernel/process_32.c):
1 __visible __notrace_funcgraph struct task_struct *
2 __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
3 {
4 struct thread_struct *prev = &prev_p->thread,
5 *next = &next_p->thread;
6 int cpu = smp_processor_id();
7 struct tss_struct *tss = &per_cpu(init_tss, cpu);
8 fpu_switch_t fpu;
9
10 /* never put a printk in __switch_to... printk() calls wake_up*() indirectly */
11
12 fpu = switch_fpu_prepare(prev_p, next_p, cpu);
13
14 /*
15 * Reload esp0.
16 */
17 load_sp0(tss, next);
18
19 /*
20 * Save away %gs. No need to save %fs, as it was saved on the
21 * stack on entry. No need to save %es and %ds, as those are
22 * always kernel segments while inside the kernel. Doing this
23 * before setting the new TLS descriptors avoids the situation
24 * where we temporarily have non-reloadable segments in %fs
25 * and %gs. This could be an issue if the NMI handler ever
26 * used %fs or %gs (it does not today), or if the kernel is
27 * running inside of a hypervisor layer.
28 */
29 lazy_save_gs(prev->gs);
30
31 /*
32 * Load the per-thread Thread-Local Storage descriptor.
33 */
34 load_TLS(next, cpu);
35
36 /*
37 * Restore IOPL if needed. In normal use, the flags restore
38 * in the switch assembly will handle this. But if the kernel
39 * is running virtualized at a non-zero CPL, the popf will
40 * not restore flags, so it must be done in a separate step.
41 */
42 if (get_kernel_rpl() && unlikely(prev->iopl != next->iopl))
43 set_iopl_mask(next->iopl);
44
45 /*
46 * If it were not for PREEMPT_ACTIVE we could guarantee that the
47 * preempt_count of all tasks was equal here and this would not be
48 * needed.
49 */
50 task_thread_info(prev_p)->saved_preempt_count = this_cpu_read(__preempt_count);
51 this_cpu_write(__preempt_count, task_thread_info(next_p)->saved_preempt_count);
52
53 /*
54 * Now maybe handle debug registers and/or IO bitmaps
55 */
56 if (unlikely(task_thread_info(prev_p)->flags & _TIF_WORK_CTXSW_PREV ||
57 task_thread_info(next_p)->flags & _TIF_WORK_CTXSW_NEXT))
58 __switch_to_xtra(prev_p, next_p, tss);
59
60 /*
61 * Leave lazy mode, flushing any hypercalls made here.
62 * This must be done before restoring TLS segments so
63 * the GDT and LDT are properly updated, and must be
64 * done before math_state_restore, so the TS bit is up
65 * to date.
66 */
67 arch_end_context_switch(next_p);
68
69 this_cpu_write(kernel_stack,
70 (unsigned long)task_stack_page(next_p) +
71 THREAD_SIZE - KERNEL_STACK_OFFSET);
72
73 /*
74 * Restore %gs if needed (which is common)
75 */
76 if (prev->gs | next->gs)
77 lazy_load_gs(next->gs);
78
79 switch_fpu_finish(next_p, fpu);
80
81 this_cpu_write(current_task, next_p);
82
83 return prev_p;
84 }
この関数は主に切り替えたばかりの新しいプロセスをさらに初期化します.例えば、第34のプロセスで使用されるスレッドのローカルストレージセグメント(TLS)ローカルcpuのグローバル記述子テーブルを読み込みます.84行目の戻り文は、戻り値prev_pをeaxレジスタに保存する2つのアセンブリ命令にコンパイルされます.もう1つはret命令で、カーネルスタックの上部の要素をeipレジスタにポップアップし、このeipポインタから実行されます.つまり、前の関数の17行目に押し込まれたポインタです.一般的に、押し込まれたポインタは、前の関数の20行目の番号1で表されるアドレスであり、switch_to関数が戻ると、ラベル1から運転が開始されます.
なお、既にスケジューリングされているプロセスについては、_switch_to関数が戻った後、ラベル1から運転を開始します.ただし、fork()、clone()などの関数で作成されたばかりの新しいプロセス(スケジューリングされていません)では、do_fork()関数は、プロセスを作成した後、プロセスにthread_を与えます.info.ip付与ret_from_fork関数のアドレスは、1のアドレスではなく、ret_にジャンプします.from_fork関数.後でforkシステム呼び出しを分析すると、見えます.