「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。
本稿では、タスクの切り替えに伴うレジスタの切り替え処理の内、前編では解説しきれなかった部分について解説します。
執筆者 : 西村 大助
※ 「新Linuxカーネル解読室」連載記事一覧はこちら
1. 前編の振り返り
1.1 全体の処理フロー
プロセスディスパッチャの処理の内、本稿で説明している処理全体のフローを抜粋すると以下のようになっています。
__schedule() └── context_switch() └── switch_to() == __switch_to_asm() └── __switch_to()
1.2 __switch_to_asm()
171 /* 172 * %rdi: prev task 173 * %rsi: next task 174 */ 175 .pushsection .text, "ax" 176 SYM_FUNC_START(__switch_to_asm) 177 /* 178 * Save callee-saved registers 179 * This must match the order in inactive_task_frame 180 */ 181 pushq %rbp 182 pushq %rbx 183 pushq %r12 184 pushq %r13 185 pushq %r14 186 pushq %r15 187 188 /* switch stack */ 189 movq %rsp, TASK_threadsp(%rdi) 190 movq TASK_threadsp(%rsi), %rsp ... 206 /* restore callee-saved registers */ 207 popq %r15 208 popq %r14 209 popq %r13 210 popq %r12 211 popq %rbx 212 popq %rbp 213 214 jmp __switch_to 215 SYM_FUNC_END(__switch_to_asm)
上記の通り、最低限のレジスタの退避・復元とカーネルスタックの切り替えを行い、__switch_to()
にジャンプします。
詳細は、前編の「3.1 通常のタスク切り替え」を参照してください。
2. 生成されたばかりのタスクへの切り替え
本章では、前編の「3.2 生成されたばかりのタスクへの切り替え」で頭出しした通り、next
が生成されたばかりのタスクの場合について、詳細に見ていきたいと思います。
タスクが生成される入り口としては、fork(2)に限らずいくつかありますが、いずれにしても、copy_process()
という関数が呼ばれます。copy_process()
は、その名の通り、task_struct構造体*1をはじめとした、タスク管理に関わる様々なデータ構造を、生成されるタスクのために準備する(大抵の物は、親からコピーする)ための関数で様々な処理を行いますが、プロセスディスパッチャ観点では以下の処理が重要となります。
copy_process() ├── dup_task_struct() : └── alloc_thread_stack_node() └── copy_thread()
まず、alloc_thread_stack_node()
についてです。
スタック領域の割当てのオーバヘッドをなるべく回避する為に、CPU単位で同領域のキャッシュを融通するような工夫はあるものの、
基本的には、「カーネルスタック用のメモリを確保し、それをtask_struct構造体のstack
で指すようにする」処理*2となります。
鍵となるのは、copy_thread()
の最初の方の処理です。
159 int copy_thread(struct task_struct *p, const struct kernel_clone_args *args) 160 { ... 164 struct inactive_task_frame *frame; 165 struct fork_frame *fork_frame; 166 struct pt_regs *childregs; ... 170 childregs = task_pt_regs(p); 171 fork_frame = container_of(childregs, struct fork_frame, regs); 172 frame = &fork_frame->frame; 173 174 frame->bp = encode_frame_pointer(childregs); 175 frame->ret_addr = (unsigned long)ret_from_fork_asm; 176 p->thread.sp = (unsigned long) fork_frame; 177 p->thread.io_bitmap = NULL; 178 p->thread.iopl_warn = 0; 179 memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps)); ...
この処理を理解するために、inactive_task_frame構造体とfork_frame構造体の定義も見てみましょう。
(arch/x86/include/asm/switch_to.h)
19 /* 20 * This is the structure pointed to by thread.sp for an inactive task. The 21 * order of the fields must match the code in __switch_to_asm(). 22 */ 23 struct inactive_task_frame { 24 #ifdef CONFIG_X86_64 25 unsigned long r15; 26 unsigned long r14; 27 unsigned long r13; 28 unsigned long r12; 29 #else 30 unsigned long flags; 31 unsigned long si; 32 unsigned long di; 33 #endif 34 unsigned long bx; 35 36 /* 37 * These two fields must be together. They form a stack frame header, 38 * needed by get_frame_pointer(). 39 */ 40 unsigned long bp; 41 unsigned long ret_addr; 42 }; 43 44 struct fork_frame { 45 struct inactive_task_frame frame; 46 struct pt_regs regs; 47 };
これらの定義を見つつ、copy_thread()
の処理を見てみると、この処理により生成されたタスクのthread_struct構造体やカーネルスタックの状態が、以下のようになっている事がわかります。
以上の事から、タスク切り替え時に切り替え先が生成されたばかりのタスクだった場合、thread_struct.spがfork_frameを指しているので、前述の__switch_to_asm()
の後半(L190以降)の処理は、ret_from_fork_asm()
へのアドレスがスタックの先頭に積まれた状態で__switch_to()
にジャンプする、という動きになります。なので*3、__switch_to()
がreturn(RET命令)すると、ret_from_fork_asm()
に制御が移ります。
以降の処理は詳細には解説しませんが、ret_from_fork_asm()
からret_from_fork()
が呼ばれユーザ空間に処理が戻ります。
3. __switch_to()
本章では、前編では解説しきれなかった、(__switch_to_asm()
のジャンプ先である)__switch_to()
について解説したいと思います。
ただし、本稿においても前編同様、タスクのメモリ管理に関する内容は扱いません。
3.1 FPUの切り替え
__switch_to()
の役割の1つに、FPUの切り替えがあります。
厳密にいえば、__switch_to()
が行うのは古い(切り替え前のタスクの)FPUレジスタをthread_struct構造体内のfpu構造体に保存する事だけで、fpu構造体に保存されたFPUレジスタをCPUへ復元する処理は、ユーザ空間に戻る際に行われます。
3.1.1 基本的な流れ
ここでは、まず、この基本的な保存・復元処理の流れについて解説したいと思います。
3.1.1.1 FPUレジスタの保存
前述の通り、FPUレジスタの保存はコンテキストスイッチの際に、__switch_to()
により行われます。具体的には、__switch_to()
の先頭の処理の延長から呼ばれる、save_fpregs_to_fpstate()
で行われます。
__switch_to() └── switch_fpu_prepare() └── save_fpregs_to_fpstate()
ソースとしては以下のようになっています。
(arch/x86/kernel/process_64.c)
559 __no_kmsan_checks 560 __visible __notrace_funcgraph struct task_struct * 561 __switch_to(struct task_struct *prev_p, struct task_struct *next_p) 562 { ... 571 if (!test_thread_flag(TIF_NEED_FPU_LOAD)) 572 switch_fpu_prepare(prev_fpu, cpu); ... 626 switch_fpu_finish();
(arch/x86/include/asm/fpu/sched.h)
18 /* 19 * FPU state switching for scheduling. 20 * 21 * This is a two-stage process: 22 * 23 * - switch_fpu_prepare() saves the old state. 24 * This is done within the context of the old process. 25 * 26 * - switch_fpu_finish() sets TIF_NEED_FPU_LOAD; the floating point state 27 * will get loaded on return to userspace, or when the kernel needs it. 28 * 29 * If TIF_NEED_FPU_LOAD is cleared then the CPU's FPU registers 30 * are saved in the current thread's FPU register state. 31 * 32 * If TIF_NEED_FPU_LOAD is set then CPU's FPU registers may not 33 * hold current()'s FPU registers. It is required to load the 34 * registers before returning to userland or using the content 35 * otherwise. 36 * 37 * The FPU context is only stored/restored for a user task and 38 * PF_KTHREAD is used to distinguish between kernel and user threads. 39 */ 40 static inline void switch_fpu_prepare(struct fpu *old_fpu, int cpu) 41 { 42 if (cpu_feature_enabled(X86_FEATURE_FPU) && 43 !(current->flags & (PF_KTHREAD | PF_USER_WORKER))) { 44 save_fpregs_to_fpstate(old_fpu); 45 /* 46 * The save operation preserved register state, so the 47 * fpu_fpregs_owner_ctx is still @old_fpu. Store the 48 * current CPU number in @old_fpu, so the next return 49 * to user space can avoid the FPU register restore 50 * when is returns on the same CPU and still owns the 51 * context. 52 */ 53 old_fpu->last_cpu = cpu; 54 55 trace_x86_fpu_regs_deactivated(old_fpu); 56 } 57 }
save_fpregs_to_fpstate()
自身は、FPUを初めとしたCPUの拡張状態(Extended States)を保存するための専用命令である、XSAVE系の命令*4で実装されています。
3.1.1.2 FPUレジスタの復元
次に、保存されたFPUレジスタをCPUに復元する処理ですが、細かい処理の流れは割愛しますが*5、カーネル空間からユーザ空間に処理が戻る際に実行されるfpregs_restore_userregs()
という関数内の、restore_fpregs_from_fpstate()
により行われます。
(arch/x86/kernel/fpu/context.h)
53 /* Internal helper for switch_fpu_return() and signal frame setup */ 54 static inline void fpregs_restore_userregs(void) 55 { 56 struct fpu *fpu = ¤t->thread.fpu; 57 int cpu = smp_processor_id(); 58 59 if (WARN_ON_ONCE(current->flags & (PF_KTHREAD | PF_USER_WORKER))) 60 return; 61 62 if (!fpregs_state_valid(fpu, cpu)) { 63 /* 64 * This restores _all_ xstate which has not been 65 * established yet. 66 * 67 * If PKRU is enabled, then the PKRU value is already 68 * correct because it was either set in switch_to() or in 69 * flush_thread(). So it is excluded because it might be 70 * not up to date in current->thread.fpu.xsave state. 71 * 72 * XFD state is handled in restore_fpregs_from_fpstate(). 73 */ 74 restore_fpregs_from_fpstate(fpu->fpstate, XFEATURE_MASK_FPSTATE); 75 76 fpregs_activate(fpu); 77 fpu->last_cpu = cpu; 78 } 79 clear_thread_flag(TIF_NEED_FPU_LOAD); 80 }
restore_fpregs_from_fpstate()
自身も、前述のsave_fpregs_to_fpstate()
と同じように、CPUの拡張状態を復元するための専用命令である、XRSTOR系*6の命令実装されています。
3.1.2 効率化のための工夫
ここまでの説明では、実際にFPUの保存・復元する処理のみに焦点を当てるため、その処理が行われる条件については触れていませんでした。端的に言うと、
- FPUレジスタの保存はコンテキストスイッチの際に行われる。
- FPUレジスタの復元はユーザ空間に戻る際に行われる。
という事になるのですが、FPUの保存・復元は、それなりに重い処理なため、「可能な限り避ける」ような工夫がなされています。
ここではその工夫についていくつか解説したいと思います。
3.1.2.1 TIF_NEED_FPU_LOAD
前述の__switch_to()
のコードを見てもわかる通り、TIF_NEED_FPU_LOADが立っていない場合に限りFPUの保存が行われます。
このフラグは、commit:383c252545edcc708128e2028a2318b05c45ede4で導入されたフラグで、
commit 383c252545edcc708128e2028a2318b05c45ede4 Author: Sebastian Andrzej Siewior <bigeasy@linutronix.de> Date: Wed Apr 3 18:41:45 2019 +0200 x86/entry: Add TIF_NEED_FPU_LOAD Add TIF_NEED_FPU_LOAD. This flag is used for loading the FPU registers before returning to userland. It must not be set on systems without a FPU. If this flag is cleared, the CPU's FPU registers hold the latest, up-to-date content of the current task's (current()) FPU registers. The in-memory copy (union fpregs_state) is not valid. If this flag is set, then all of CPU's FPU registers may hold a random value (except for PKRU) and it is required to load the content of the FPU registers on return to userland. Introduce it now as a preparatory change before adding the main feature.
commitメッセージの通り、CPUのFPUレジスタとメモリ上に保存されているFPUレジスタの情報の妥当性に関するフラグで、
- clear状態
- メモリに保存されている情報が古く、(コンテキストスイッチにより当該タスクがCPUを明け渡す際に)CPUのFPUレジスタを保存する必要がある。
- set状態
- メモリに保存されている情報が最新で、(当該タスクがユーザ空間の処理に戻る際に)CPUのFPUレジスタに復元する必要がある。
という意味を持ちます。
このフラグがどのようなタイミングでset/clearされるかですが、基本的にコンテキストスイッチの際、切り替え後のタスクに必ずsetされます。具体的には、前述の__switch_to()
の後半(L626)で呼ばれる、switch_fpu_finish()
でsetされます。
(arch/x86/include/asm/fpu/sched.h)
59 /* 60 * Delay loading of the complete FPU state until the return to userland. 61 * PKRU is handled separately. 62 */ 63 static inline void switch_fpu_finish(void) 64 { 65 if (cpu_feature_enabled(X86_FEATURE_FPU)) 66 set_thread_flag(TIF_NEED_FPU_LOAD); 67 }
一方、clearされるタイミングですが、前述のCPUのFPUレジスタを復元する際に実行される、fpregs_restore_userregs()
の末尾(L79)でclearされる事になるため、ユーザプロセスだとclearされている状態、カーネルスレッドなどユーザ空間に戻らないタスクはsetされている状態となります。
これはつまり、先程、「TIF_NEED_FPU_LOADが立っていない場合に限りFPUの保存が行われる」と説明しましたが、FPUを使用するかどうかカーネルがあずかり知れないユーザプロセスからコンテキストスイッチする際はFPUレジスタの状態を保存するが、カーネル空間のみで動作しFPUを使用する事のない*7カーネルスレッドからコンテキストスイッチする際はFPUレジスタの状態を保存せず(その必要がない)、無駄な保存は行わない事を意味します。
また、このフラグが立っていない場合はユーザ空間に戻る際でもfpregs_restore_userregs()
が呼ばれないように制御されているため、あるユーザプロセスが、割り込みやシステムコールのために一時的にカーネル空間に入り、コンテキストスイッチせずにそのままユーザ空間に戻る場合は、FPUレジスタの復元は行われません(ユーザ空間に戻るからと言って必ず復元が行われるわけではありません)。
3.1.2.2 FPUオーナーコンテキスト
ここまでで、
- コンテキストスイッチの際に、切り替え前タスクのFPUレジスタは保存される。
- ただし、カーネルスレッドの場合は保存されない。
- コンテキストスイッチの後、切り替え後のタスクがユーザプロセスなら、ユーザ空間に戻るタイミングでFPUレジスタの復元が行われる。
- コンテキストスイッチの発生していない場合(システムコール等のために一時的にカーネル空間で処理が行われていた場合)は、ユーザ空間に戻るタイミングでもFPUレジスタの復元は行われない。
事を説明しました。
これだけだと、例えば、(同一CPUにて)ユーザプロセスA⇒カーネルスレッド⇒ユーザプロセスAとコンテキストスイッチが行われた(且つ、プロセスAが他のCPUで1度も実行されていない)場合、この後半の「カーネルスレッド⇒ユーザプロセスA」におけるFPUレジスタの復元は不要(カーネルスレッドでFPUが更新される事はないため、CPUのFPUレジスタの状態はプロセスAが実行されていた状態と一致しているはず)にもかかわらず、復元されてしまう事になりそうです。
このようなケースの無駄な復元を行わないようにするため、FPUオーナーコンテキスト*8という概念が導入されています。
カーネル空間からユーザ空間に処理が戻る際に、FPUレジスタを復元するために実行される関数、fpregs_restore_userregs()
のコードを改めて抜粋します。
(arch/x86/kernel/fpu/context.h)
62 if (!fpregs_state_valid(fpu, cpu)) { ... 74 restore_fpregs_from_fpstate(fpu->fpstate, XFEATURE_MASK_FPSTATE); 75 76 fpregs_activate(fpu); 77 fpu->last_cpu = cpu; 78 }
FPUオーナーコンテキストの確認は、L62のfpregs_state_valid()
という関数で行われ、
(arch/x86/kernel/fpu/context.h)
36 static inline int fpregs_state_valid(struct fpu *fpu, unsigned int cpu) 37 { 38 return fpu == this_cpu_read(fpu_fpregs_owner_ctx) && cpu == fpu->last_cpu; 39 }
関数の定義を見ればわかる通り、FPUレジスタの復元が行われるのは、
- per cpu変数である、
fpu_fpregs_owner_ctx
が、(ユーザ空間に戻ろうとしているタスクの)fpu構造体である事 - (ユーザ空間に戻ろうとしているタスクの)fpu構造体の
cpu
メンバが、今動作しているCPUと同じである事
のどちらかの条件を満たさない事が条件となります。
逆に言えば、この条件を両方満たす場合はFPUレジスタの復元は行われないわけですが、この条件を満たすように各データを更新するのが、L76のfpregs_activate()
とL77の処理となっていて、前述の「(同一CPUにて)ユーザプロセスA⇒カーネルスレッド⇒ユーザプロセスAとコンテキストスイッチが行われた(且つ、プロセスAが他のCPUで1度も実行されていない)場合の、この後半の「カーネルスレッド⇒ユーザプロセスA」におけるFPUレジスタの復元」が行われないようになっているわけです。
3.1.2.3 CPUによる最適化
先に、FPUレジスタの保存には、XSAVE系の専用命令が使用されていると言及しましたが、このXSAVE系の命令自体でもCPUによる最適化が行われます*9。
具体的には以下の2つです。
- 対応するレジスタが初期状態(0)である場合、保存しない。
- 対応するレジスタが、直近の復元(XRSTOR系命令の実行)以降変更されていない場合、保存しない。
詳細については、Intel SDM(SoftwareDevelopperManual)の「Vol.1 13.6 PROCESSOR TRACKING OF XSAVE-MANAGED STATE」を参照してください。
3.2 pcpu_hot
__switch_to()
のもう1つの役割として、pcpu_hotデータの切り替えがあります。
pcpu_hotは、頻繁にアクセスされるデータを1つの構造体にまとめてpercpu変数として定義する事で、キャッシュ効率を上げるために導入されました。
(arch/x86/include/asm/current.h)
15 struct pcpu_hot { 16 union { 17 struct { 18 struct task_struct *current_task; 19 int preempt_count; 20 int cpu_number; 21 #ifdef CONFIG_CALL_DEPTH_TRACKING 22 u64 call_depth; 23 #endif 24 unsigned long top_of_stack; 25 void *hardirq_stack_ptr; 26 u16 softirq_pending; 27 #ifdef CONFIG_X86_64 28 bool hardirq_stack_inuse; 29 #else 30 void *softirq_stack_ptr; 31 #endif 32 }; 33 u8 pad[64]; 34 }; 35 }; 36 static_assert(sizeof(struct pcpu_hot) == 64); 37 38 DECLARE_PER_CPU_ALIGNED(struct pcpu_hot, pcpu_hot);
このうち、__switch_to()
ではタスクに紐づくデータの更新を行います。
(arch/x86/kernel/process_64.c)
623 raw_cpu_write(pcpu_hot.current_task, next_p); 624 raw_cpu_write(pcpu_hot.top_of_stack, task_top_of_stack(next_p));
current_task
:current
マクロで参照される、実行中のタスクのtask_struct構造体へのポインタtop_of_stack
: システムコール等でカーネル空間での処理を行うための、カーネルスタックの位置
3. 最後に
前後編にわたりプロセスディスパッチャの処理(タスクの切り替え処理)について、カーネルv6.8のコードベースで解説しました。
非常にプリミティブな機能でアーキテクチャに依存する部分が多いため、x86_64に限定して解説しましたが、他のアーキテクチャについて調べてみるのも面白いかもしれません。
また、これらの記事ではタスクのメモリ空間の切り替えなど、メモリに関する話題はスキップしましたが、タスクのメモリ管理に関する話題は、Linuxにおけるメモリ管理全般(旧版のPartⅢ相当)の一部として、別途、まとめられればと思っています。
参考資料
*1:task_struct構造体については、前編の「1.2 task_struct構造体」も参照してください。
*2:中身もゼロクリアされます。
*3:この辺りのロジックは、前編の「3.1 通常のタスク切り替え」と同様です。
*4:XSAVES/XSAVEC/XSAVEOPT/XSAVEの内、プロセッサがサポートしてる中で最も高機能な物(基本的に左の方が高機能)が使用されます。
*5:ユーザ空間⇔カーネル空間のentry/exitに関しても、別途、まとめる予定です。
*6:XRSTORS/XRSTORの内、プロセッサがサポートしてる中で最も高機能な物(左の方が高機能)が使用されます。
*7:カーネル空間の処理でも、crypto API等でFPUを使用していますが、そこでTIF_NEED_FPU_LOADがどのように扱われているかは本稿では触れません。
*8:一般的な用語ではなく、本稿での説明の都合上、筆者がそう呼んでいるだけです。
*9:実行される命令やハードウェアのサポート状況によりその有無は変わります。