switch_toの第三引数について
Rev.3を表示中。最新版はこちら。
タスクの切り替えとは、プロセスコンテキストの切り替えを言います。プロセスコンテキストとはCPUレベルでは各種レジスタ等で、カーネルレベルではメモリとか各種ディスクリプターテーブルとかです。それを行うのがcontext_switch関数から呼ばれるswitch_to関数です。switch_toマクロで実際のタスク切り替えが行われ、finish_task_switch関数で実行権を放棄したタスクの後処理を行います。
root/kernel/sched.c static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { ・・・・・ switch_to(prev, next, prev); ・・・・・ finish_task_switch(this_rq(), prev); }プロセスAがプロセスBにとタスク切り替えを行うといたします。switch_to(A,B,A)でマクロ展開された後、finish_task_switch(this_rq(), A)でタスクAの切り替え後の後処理を行おうとしていますが・・・。
各種レジスタを復帰するのは、スタック上に退避していたレジスタ群を復帰すればいいわけですが、各種ディスクリプターテーブルの復帰というのはどうすればいいのでしょうか?
実はスタックをそのプロセスのスタック切り替えればいいだけの話です。カーネルがカレントプロセスの資源参照する必要がある場合、currentマクロを使っていました。currentマクロはespレジスから取得し、そこからそのtask_struct構造体が取得できていました。task_struct構造体が取得できるというのは、そのプロセスのリソースが取得できるわけです。
ここでスタックというものですが、プロセスはユーザモードとカーネルモードでそれぞれ別のスタックを使います。ここで大切な事は、このカーネルモードでのスタックの扱いです。カーネルモードでのスタックは1つですべてのタスクで共通ではないと言う事です。カーネルスタックはプロセス毎に独自に有しているのです。
プロセスAがカーネルモードで使うカーネルスタックと、プロセスBがカーネルモードで使うスタックは別物なのです。このカーネルスタックはtask_struct->thread.spとする場所に確保されています。プロセスかカーネルモードに入ることは、ユーザプロセスとして使っていたスタック(vm_structで管理されているメモリー空間)から、task_struct->thread.spに切り替わるわけです。
従って、switch_toマクロ以前のスタックはプロセスAの、以降のスタックはプロセスBのスタックになってしまいます。そうなるとそのスタックは、prevはプロセスBの、nextはプロセスBから切り替えてプロセスのものになってしまい、finish_task_switch関数はプロセスBで呼ばれることになってしまいます(たぶん)。そこで登場するのが第三の引数prevです。
switch_toマクロは以下のように拡張インラインアセンブラ記述されています。
#define switch_to(prev, next, last) \ do { \ 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 */ \ "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) \ \ /* input parameters: */ \ : [next_sp] "m" (next->thread.sp), \ [next_ip] "m" (next->thread.ip), \ \ /* regparm parameters for __switch_to(): */ \ [prev] "a" (prev), \ [next] "d" (next)); \ } while (0)まず、プロセスAのスタックポイントをprev->thread.spへ、次回実行すべきアドレスをprev->thread.ipに保存し、next->thread.spをespにセットしています。この時点でカーネルリソースに関してプロセスが切り替わったということです。そしてnext->thread.ipをスタックにpushしてjmp __switch_toとしています。
プロセスを切り替えるには、EIPレジスタで次の実行する命令が決まるわけで、通常のCALL命令ではその次に命令のEIPレジスタもスタック上で退避されていて、復帰することで次のEIPレジスタもret後のcallを呼び出した後の命令にジャンプするわけですが、タスク切り替えではCALLの次から実行するというのでなく、切り替えタスクの切り替え後の命令から実行する必要があるからです。(switch_toではjmpの後から実行することになるのですが。)そこでswitch_toマクロではpush [next->thread.ip]し、__switch_to関数にjmpしているのです。そうすることでret命令ではnext->thread.ipから実行することになります。
そこで本題ですが、switch_to以降スタックが切り替わる故、切り替わる前のprev(スタックA)をaxレジスターにセットし、切り替わった後のプロセスBのスタック上のlast(prevと同じ)にaxをセットすることで、プロセスBのスタック上のprevにプロセスBでなく、プロセスAを再設定しているわけです。
後処理を行うfinish_task_switch関数では、実行権を放棄するプロセスが、カーネルスレッドなら(cpu変数のrp->prev_mmが0でないならカーネルスレッドということみたい。)、そのカーネルスレッドで使っていたmm_structの参照カウンターをデクリメントしています。もしユーザプロセスの場合それがexit等の終了処理の、TASK_DEAD状態ならプロセスディスクリプターの解放処理にかかる処理を行っているようです。