今回やること
前回はページフォルトハンドラの中心となっている__do_page_fault()の前半部分を読んでみました。
今回は引き続き__do_page_fault()を読みます。
さて、早くも足止め?
以下のソースから始まります。
コメントから想像するに、割り込みハンドラ内でページフォルト例外が発生したケースを捌く処理のようです。
/*
* If we're in an interrupt, have no user context or are running
* in an atomic region then we must not take the fault:
*/
if (unlikely(in_atomic() || !mm)) {
bad_area_nosemaphore(regs, error_code, address);
return;
}
in_atomic()については、Appendixを見てください。(というより、Appendixがメインだったりもします...。)
単純なようでなかなか面白い実装が見えます。こういう横道に逸れていくのがカーネルソースリーディングの面白いところです。
ここでは、bad_area_nosemaphore()を見てみましょう。
static noinline void
bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
__bad_area_nosemaphore(regs, error_code, address, SEGV_MAPERR);
}
続く関数を読んでみましょう。
static void
__bad_area_nosemaphore(struct pt_regs *regs, unsigned long error_code,
unsigned long address, int si_code)
{
struct task_struct *tsk = current;
/* User mode accesses just cause a SIGSEGV */
if (error_code & PF_USER) {
/* SEGVを投げる処理。略 */
return;
}
if (is_f00f_bug(regs, address))
return;
no_context(regs, error_code, address, SIGSEGV, si_code);
}
error_codeは前回紹介したdo_page_fault()の第二引数で渡されてくる値です。
実は、error_codeは例外発生時にスタックに積まれる値の一つです。
Intel SDM Vol3.「6.15 EXCEPTION AND INTERRUPT REFERENCE」のページフォルトの説明のところに以下の図が載っています。
error_codeのbit2を見れば、ユーザモードで実行していたときかカーネルモードで実行していたときかわかりますね。
※「Intel SDM Vol3 6.13 ERROR CODE」に載っているerror_codeはこの場合当てはまりません。ご注意を。
The format of the error code is different for page-fault exceptions (#PF). See the “Interrupt 14—Page-Fault Exception (#PF)” section in this chapter.
次に呼ばれている関数名に含まれる「f00f_bug」というのは、Intel CPU(Pentium)のerrataです。詳細はIntelのページを参考にしてみてください。
この関数は今回の対象外としますが、興味のある方はarch/x86/mm/fault.cと先の参考資料のWorkaroundをもとに読み解いてみるのも良いかもしれません。
no_context()は以下の実装です。
static noinline void
no_context(struct pt_regs *regs, unsigned long error_code,
unsigned long address)
{
/* Are we prepared to handle this kernel fault? */
if (fixup_exception(regs))
return;
if (handle_trapped_io(regs, address))
return;
/*
* Oops. The kernel tried to access some bad page. We'll have to
* terminate things with extreme prejudice.
*/
bust_spinlocks(1);
show_fault_oops(regs, address);
die("Oops", regs, error_code);
bust_spinlocks(0);
do_exit(SIGKILL);
}
Oops!というやつです。
(fixup_exception()も追ってみたいですが、細かくなりすぎるのでここは飛ばします。)
割り込みハンドラとページフォルト
コメントを信じるなら、割り込みハンドラ内でのページフォルトのケースの処理では、原則としてOopsになります。
それは、割り込みハンドラ内では、ページフォルトを引き起こすような危険なメモリ空間へのアクセスはしてはいけないためです。
なぜでしょうか?
それは、割り込み内でsleepすることが許されないためです。
ページフォルトが起きて、バッキングストアへのアクセスが起きるケースを想定しましょう。バッキングストアは大抵の場合、二次記憶装置です。
すると、I/O待ちのためにsleepでCPUの実行権を放棄する可能性が極めて高いのです。
割り込みはどのプロセスにも属さないコンテキストで動くので、「プロセスのコンテキストをスイッチして他のプロセスを実行状態にする」ということができません。。
そもそも、割り込みは現在実行中の処理に割り込んでいるので、少しでも早く処理を完了させるのが筋というものです。なので、ページフォルトが起きる可能性のあるメモリへアクセスすること自体があまりよろしくありません。
/*
* It's safe to allow irq's after cr2 has been saved and the
* vmalloc fault has been handled.
*
* User-mode registers count as a user access even for any
* potential system fault or CPU buglet:
*/
if (user_mode_vm(regs)) {
local_irq_enable();
error_code |= PF_USER;
flags |= FAULT_FLAG_USER;
} else {
if (regs->flags & X86_EFLAGS_IF)
local_irq_enable();
}
user_mode_vm()は以下の実装となっています。
static inline int user_mode_vm(struct pt_regs *regs)
{
#ifdef CONFIG_X86_32
return ((regs->cs & SEGMENT_RPL_MASK) | (regs->flags & X86_VM_MASK)) >=
USER_RPL;
#else
return user_mode(regs);
#endif
}
RPL(Requested Privilege Level)を見ているようです。Privilege Levelはいわゆる以下のリングで説明されることが多いやつですね。(Intel SDMより抜粋)
error_codeでなくわざわざPrivilege Levelを見ているのは何か理由があるのでしょうか?(ここは謎なので、理由はもう少し調べてみたいです。しかし、今はわからないので先に進みます。)
perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, address);
if (error_code & PF_WRITE)
flags |= FAULT_FLAG_WRITE;
書き込みでフォルトが起きた場合、書き込みアクセスを示すフラグを立てて先に進みます。
まだ先は長い...。今回はここまでにします。
次回の予定
次回はまた続きから読んでいきます。
Appendix
in_atomic?
/*
* Are we running in atomic context? WARNING: this macro cannot
* always detect atomic context; in particular, it cannot know about
* held spinlocks in non-preemptible kernels. Thus it should not be
* used in the general case to determine whether sleeping is possible.
* Do not use in_atomic() in driver code.
*/
#define in_atomic() ((preempt_count() & ~PREEMPT_ACTIVE) != 0)
このコメントだけを読むと、処理がプリエンプト(実行権の横取り)された場合にtrueになるように思われます。
preempt_count()は以下のとおりです。
DECLARE_PER_CPU(int, __preempt_count);
/* 略 */
/*
* We mask the PREEMPT_NEED_RESCHED bit so as not to confuse all current users
* that think a non-zero value indicates we cannot preempt.
*/
static __always_inline int preempt_count(void)
{
return raw_cpu_read_4(__preempt_count) & ~PREEMPT_NEED_RESCHED;
}
個人的にはraw_cpu_read_4()の実装、なかなかおもしろいと思いました。
それは措いて、__preempt_countの扱い方を見ましょう。
__preempt_count
詳しくは以下のヘッダを読まれるとよいと思います。
arch/x86/include/asm/preempt.h
include/linux/preempt.h
しかし、メモも兼ねて記録を残すと...
__preempt_countのbit31 ... need reschedule flag。
このビットが0の場合、再スケジュールを 必要とします。1だと必要としないという意味です。(一見直感的ではありません。)
need reschedule flagとは「次に再スケジュールする機会があったら、スケジューリングを試みる旨を依頼する」ためのフラグです。
そして、残りのビットは「カーネルプリエンプトされた回数」を表します。
これを手がかりに上記ヘッダを読んでみると良いと思います。
カーネルプリエンプション
一昔前の多くのOSでは「カーネルモードで処理中にはsleep()などによって明示的にCPU実行権を手放さない限り、実行権を取得したまま」となります。
これは、排他などを簡潔にすることなどが目的なのですが、時間のかかるシステムコールなどを実行するとレスポンスが悪くなったりします。
このあたりのページがプリエンプションについて簡潔に書いてくれています。
Linuxではpreempt_disable()しない限り、プリエンプトされる可能性があるということです。
プリエンプトが起きる可能性があるのは以下の箇所です。(Linux Kerkel Development 3rd ed P64より)
Kernel preemption can occur
- When an interrupt handler exits, before returning to kernel-space
- When kernel code becomes preemptive again
- If a task in the kernel explicitly calls schedule()
- If a task in the kernel blocks(Which results in a call to schedule()
あらかじめneed reschedule flagが立っていて、上記のイベントが起きたとき、再スケジューリングされ、プリエンプションされる可能性があります。