reference
http://caspar.hazymoon.jp/OpenBSD/annex/gcc_inline_asm.html オペランド制約の部分が非常によくまとまっている
概要
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 を参照のこと。
上記をふまえたうえでの流れは以下の通り:
- %eflags, %ebp, %espをメモリ上に退避
- %espをnext processのものに復元 -> 以下、next processのstack上で作業することになる。
- %eipをnext processのスタックに積む
jmp __switch_to
実行__switch_to
が終了したので、3で積んでいた値を%eip registerにpopする- 次のinstrcutionは%eipの指示す先で、これは次なるプロセスの文脈のアドレス(
"1:\t"
のラベルのアドレス)になる。
__switch_to
を説明していないが、これはnext
processのstackを基準にいろんなregisterが復元される。例えば、load_TSS
のばあい、
tss->esp0 = next->esp0;
でtssのesp0にnext processのesp0を保存している。tss
はstruct 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_to
のlast
はどの場面で使われるのか?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において、後者の場合は、forkret
で https://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
-
TSS(Task State Segment). TSS自体については、http://cstmize.hatenablog.jp/entry/2019/03/14/GDT%E3%81%A8IDT%E5%91%A8%E8%BE%BA%E3%81%AE%E7%90%86%E8%A7%A3%28xv6%E3%82%92%E4%BE%8B%E3%81%AB%29 のGlosaryの節を参照のこと。↩