`switch_to`を読んでみる

reference

概要

linux kernel 2.6.11より

schedule() => context_switch(rq, prev, next); => switch_to(prev, next, prev); とつながる。

scheduleを実行する前段階としては、

  • proc->stateをWAIT状態にする
  • wait_queueのlistにこのproc(正確には、このprocをpointerにもつwait_queue_t型の変数)を登録する。
  • schedule()が実行される +終わったら、wait_queueのlistからこのprocを除く

というのがよくある流れっぽい。

switch_toの直前のプロセスをProcessAとする。 switch_toの実行途中(より正確にはjmp __switch_toが終了する直後)に別プロセス(ProcessB)に移行するので、今までRUNNINGしていたプロセスが眠りについて、別の(wait状態の)processがまたRUNNINGに移行する。

このように、次から次にprocessが移行していく。つまり、

ProcessA -> ProcessB -> ... ->

移行しているとは言っても、stack(%esp)とprogram counter(%eip)が変化するのであって、User landからKernel landに権限昇格しているわけではない。(Kernel領域で作業していることには変わらない。)

当然ProcessAはまだ完了しているわけでないので、しばらくすると、ProcessAに主導権が再び得られることになる。その時は、"jmp __switch_to\n"から処理がスタートしてProcessAで使っていたregisterたちを次々と復元していく。

雰囲気的にはこんな感じだが、実際のコードをみていく。assemblyなので慣れないとなかなかむずい。そこで、ひとつずつコードを紐解いていきたい。

code

// include/asm-i386/system.h
#define switch_to(prev,next,last) do {                   \
   unsigned long esi,edi;                        \
   asm volatile("pushfl\n\t"                    \
            "pushl %%ebp\n\t"                 \
            "movl %%esp,%0\n\t"   /* save ESP */      \
            "movl %5,%%esp\n\t"   /* restore ESP */   \
            "movl $1f,%1\n\t"      /* save EIP */      \
            "pushl %6\n\t"     /* restore EIP */   \
            "jmp __switch_to\n"                \
            "1:\t"                     \
            "popl %%ebp\n\t"                  \
            "popfl"                     \
            :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  \
             "=a" (last),"=S" (esi),"=D" (edi)            \
            :"m" (next->thread.esp),"m" (next->thread.eip),    \
             "2" (prev), "d" (next));                \
} while (0)

擬似コードはこんな感じかな:

  pushfl # push eflags register in the stack
  pushl %ebp # push ebp(base pointer) register in the stack
  movl %esp, [prev->thread.espのアドレス] # save %esp register to prev process
  movl [next->thread.espの中身],%%esp # restore %esp
  movl $1f, [prev->thread.eipのアドレス] # save %eip to prev process
  pushl [next<-thread.eipの中身] # `jmp __switch_to`が終了したときに`"1:\t"`のラベルのアドレスまで飛びたいので。
  jmp __switch_to # exec __switch_to
1:
  popl %%ebp # % restore %ebp(base pointer register)
  popfl # restore eflags register in the stack

jmp命令がとても重要で、この命令があるので、別プロセスへと移動できる。

jmp命令はret命令と異なり、戻った先のaddressをスタックに積まないのでpushl [next<-thread.eipの中身]にて手動でアドレス値をスタックに積んでいて、これが戻った先のaddressと解釈されて、別プロセスの関数へと移動できるわけだ。(単純にjumpするだけ)

対してret命令はC言語とかの関数の通り、step inして処理が終わればstep outする感じに似ている。詳しくは、 https://qiita.com/knknkn1162/items/9bd54e165c6c9edca49b#ret-instruction%E3%81%AE%E5%BF%9C%E7%94%A8 を参照のこと。

上記をふまえたうえでの流れは以下の通り:

  1. %eflags, %ebp, %espをメモリ上に退避
  2. %espをnext processのものに復元 -> 以下、next processのstack上で作業することになる。
  3. %eipをnext processのスタックに積む
  4. jmp __switch_to実行
  5. __switch_toが終了したので、3で積んでいた値を%eip registerにpopする
  6. 次のinstrcutionは%eipの指示す先で、これは次なるプロセスの文脈のアドレス("1:\t"のラベルのアドレス)になる。

__switch_toを説明していないが、これはnext processのstackを基準にいろんなregisterが復元される。例えば、load_TSSのばあい、

tss->esp0 = next->esp0;でtssのesp0にnext processのesp0を保存している。tssstruct tss_structでinclude/asm-i386/processor.hにDECLARE_PER_CPU(struct tss_struct, init_tss);とあるので、CPUごとに一つだけ存在する変数である。1


続いてコードを詳細に追っていく。popflまでがアセンブリ命令でそれ以外はオペランド制約というものに分かれている。まずはアセンブリ命令から。

  • movl %eax, bとあるのは、%eax -> bの方向に代入すると考えればわかりやすい。

  • jmp命令は上記の通り。ret命令との違いを認識できてるかどうか。

一方、オペランド制約わかんなすぎなので とてもありがたい記事 http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html を元にして一つづつばらしてゆく:

# ("movl %%esp,%0" and ) prev->thread.esp <= %0 ( save %esp of )
:"=m" (prev->thread.esp), \
# ("movl $1f,%1\n\t" and ) prev->thread.eip <= %1
"=m" (prev->thread.eip), \
# last <= %2(register) where `%2 == %eax`
"=a" (last), \
# esi <= %3(register) where `%3 == %esi`
"=S" (esi), \
# edi <= %4(register) where `%4 == %edi`
"=D" (edi), \
# %5 <= next->thread.esp (and  "movl %5,%%esp\n\t"(restore %esp))
:"m" (next->thread.esp), \
# %6 <= next<-thread.eip where "pushl %6\n\t"
"m" (next->thread.eip), \
# %2 <= prev (where %2 == %eax)
"2" (prev), \
# %edx <= next
"d" (next)); \
  • オペランド制約は並んでる順番は意味があって、1つ目のオペランド制約が%0に対応する。ただし、"2"とかあったら、%2を使う。

  • =は出力で、後続の変数に値を流し込んでいく。 例えば、"=a" (last) <=> movl %eax, [last変数のaddress]

  • 何もついてなければ入力で、後続の変数からregisterかメモリ上の指定されたアドレスに値を書き込んでいく。例えば、"2" (prev) <=> movl [prev変数のaddress], %2

  • "=m" はleft side valueがregisterではなくメモリ上のアドレスなので、mを指定している。


これまでで未だ不明の点は以下の通り:

  • __switch_tolastはどの場面で使われるのか?

  • next processがswitch_to終了したらどこに行くのか?

前者は This reference, however, turns out to be useful to complete the process switching (see Chapter 7 for more details).とあり、

後者はIf next_p was never suspended before because it is being executed for the first time, the function finds the starting address of the ret_from_fork( ) function

とあるので、次回に回す。

ちなみにxv6において、後者の場合は、forkrethttps://github.com/mit-pdos/xv6-public/blob/xv6-rev11/proc.c#L113 の部分で指定したものがswtch関数のret命令後に効いてくる。 https://qiita.com/knknkn1162/items/0bc9afc3ae304590e16c#switching%E5%AE%8C%E4%BA%86%E5%BE%8C らへんの図を参考に〜

追記) ここにもおんなじようなことをやってる人がいた .. http://d.hatena.ne.jp/naoya/20070924/1190653790