linuxプロセススケジューリング関数の概要(3.16-rc 4ベース)

38108 ワード

プロセススケジューリングはschedule()関数を使用して行われることはよく知られています.次に、この関数の解析から始めます.コードは以下の通りです(kernel/sched/core.c).
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システム呼び出しを分析すると、見えます.