「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。 それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。 世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。
本稿では、旧版第3章で解説されていたソフト割り込み処理について、カーネルv6.8/arm64のコードをベースに解説します。
執筆者 : 高倉 遼
※ 「新Linuxカーネル解読室」連載記事一覧はこちら
はじめに
本稿では、ハード割り込みが発生してからソフト割り込み処理が実行されるまでの流れを、実行コンテキストの切り替わりを追いかけながら紹介します。
解説にあたり前提とする実行コンテキストは以下となります*1。
プロセスコンテキスト
- ユーザープロセス
- カーネルスレッド
割り込みコンテキスト
- ソフト割り込みコンテキスト
- ハード割り込みコンテキスト
- NMIコンテキスト
実行コンテキストは、スケジューラによって管理されるプロセスコンテキストとHWのイベントに応じてスケジューラに関わらず実行される割り込みコンテキストに分けられます。
プロセスコンテキスト下で実行される処理は、スケジューラで管理されているため、優先度を設定することで実行時間を優先的に確保するなどの調整ができます。カーネルスレッドは、ユーザーが優先度を設定したプロセスができる限り設定通りの優先度で動作するように、一部を除いて高い優先度で動作しないよう設計されています。
一方で、割り込みコンテキストで動作する処理は、スケジューラを介さずに割り込みが発生したタイミングで実行されます。しかし、実行コンテキストによっては、特定の割り込みコンテキストでの実行が禁止されており、割り込みが発生したタイミングと割り込み処理が実行されるタイミングが異なる場合があります。以下は、それぞれの実行コンテキストにおいて、許可されている処理 (○) と禁止されている処理(×) を一覧にしたものです。禁止されている処理は、実行中の割り込み処理が終わった後に実行されます。
プロセスコンテキスト | カーネルスレッド | ソフト割り込みコンテキスト | ハード割り込みコンテキスト | NMIコンテキスト | |
---|---|---|---|---|---|
ソフト割り込み(SoftIRQ) | ○ | ○ | × | × | × |
ハード割り込み(IRQ) | ○ | ○ | ○ | × | × |
NMI | ○ | ○ | ○ | ○ | × |
以下は、それぞれの実行コンテキストに対して割り込み処理を行った場合のイメージ図になります。
実線の左側に記載されている関数は、割り込みが発生した際に実行コンテキストの切り替えを行っています。実線の右側に記載されている関数については、ソフト割り込み実行に関わる主要な関数となりますが、それぞれ呼ばれる実行コンテキストが異なります。(ソフト割り込みコンテキスト: 薄灰色、ハード割り込みコンテキスト: 薄紫、NMIコンテキスト: 灰色)
それぞれの関数は後述するソフト割り込み処理の要所で登場するので、以下の図も参考にしながら読んでみてください。
割り込み禁止区間と応答性について
話を始めるにあたって、まず割り込み禁止区間と応答性の関係について具体例を交えて紹介したいと思います。 具体例として、perfのサンプリングに用いられるNMIを紹介します。
perfにおけるNMI割り込みから見る応答性
perfをサンプリング周期を指定して実行した場合、Performance Monitoring Unit(PMU)から指定したサンプリング周期で割り込みが発生します。この際に発生する割り込みにはNMIを利用しています。
perfにNMIを使用する理由として、サンプリング対象の処理が割り込み禁止を実行している場合でも割り込んでサンプリングを行えるため、取得できるサンプルの精度が上がる点が挙げられます。
しかし、どのような実行コンテキストにも割り込めるNMIをperfに使うことには、以下のデメリットがトレードオフとして挙げられます。
- 割り込まれた処理の完了が遅れる
- NMIコンテキスト下のperf処理中は、いずれの実行コンテキストの処理も行えない
1点目については、割り込まれた処理の完了が、perfのサンプリング処理実行時間分だけ遅れます。2点目については、もしperf処理中にもっと重要な処理にかかわるハード割り込みが入ったとしても、perfのサンプリング処理の完了を待つ必要があります。
いずれの場合においても、システムの応答性をperfによって落とさないためには、perfのサンプリング処理がNMIコンテキストにいる時間をできる限り短くすることが課題となります。
実際にperfは、NMIコンテキストにおけるサンプリング処理時間を抑えるため、必要最小限のサンプリング処理を終えた後は、残処理をハード割り込みコンテキストに任せるという仕組みとなっています*2。
ソフト割り込み処理
ソフト割り込み処理の考え方
perfの例のように、処理が適切な実行コンテキストで行われない場合には、システムの応答性を下げる原因となります。
より一般的なハード割り込みについても課題は同じです。応答性を維持するための仕組みも、NMIコンテキストにおけるperf処理と同様に、ハード割り込みコンテキストにおける処理は最小限に抑えて、他コンテキストに残処理を任せるというアプローチになります。このハード割り込みコンテキストの残処理を他コンテキストに引き渡す仕組みがソフト割り込み処理となります。
本節では、まずハード割り込みからソフト割り込み処理が実行されるまでの流れをスタックから確認します。その後、ソフト割り込み処理が実行されるまでに登場する主な関数を紹介していきます。
ソフト割り込み実行までの流れ
現在のLinuxには、10種類のソフト割り込み種別が用意されています。種別は用途に応じて用意されており、ハード割り込み処理から対応する種別を指定してソフト割り込み要求を行うことで、種別ごとに事前に登録してあるハンドラがソフト割り込みコンテキストで実行されます。以下の表は、ソフト割り込み処理を実行するための主な関数と種別ごとに実行される処理の内容となります。
関数名 | 説明 |
---|---|
open_softirq | 種別ごとにハンドラを登録 |
raise_softirq | ソフト割り込みを種別ごとに要求 |
__do_softirq | ソフト割り込み処理の実行 |
ソフト割り込み種別 | 説明 |
---|---|
HI_SOFTIRQ | taskletを実行 |
TIMER_SOFTIRQ | タイマーの実行 |
NET_TX_SOFTIRQ | ネットワーク送信完了処理を実行 |
NET_RX_SOFTIRQ | ネットワーク受信処理を実行 |
BLOCK_SOFTIRQ | ブロックI/O処理を実行 |
IRQ_POLL_SOFTIRQ | ポーリングモードでのブロックI/O処理を実行 |
TASKLET_SOFTIRQ | taskletを実行 |
SCHED_SOFTIRQ | スケジューラの負荷バランス処理を実行 |
HRTIMER_SOFTIRQ | ハイレゾタイマーの実行 |
RCU_SOFTIRQ | RCUの後処理を行なうコールバック実行 |
以下で取り挙げるスタックは、HI_SOFTIRQに対しての要求があった場合のものとなります。なお、スタックは、arm64環境でSysRqを使って取得しました。
まず、割り込みごとに登録されているハード割り込み処理がel1h_64_irq_handler()
で実行(#25)されます。ソフト割り込み要求をこのハード割り込み処理中に行うことで、ハード割り込み処理終了後にソフト割り込み処理が実行(#18)されます。以下のスタックの場合、ハード割り込み処理中にHI_SOFTIRQに対するソフト割り込み要求があったこと(#17)がわかります。実際にハード割り込みからソフト割り込み要求が行われるまでの流れについては、RCU_SOFTIRQを具体例に後述します。
#16 [ffffffc008003ed0] tasklet_action_common at ffffffd83cc94098 #17 [ffffffc008003f10] tasklet_hi_action at ffffffd83cc9415c // ソフト割り込み種別ごとのハンドラ #18 [ffffffc008003f30] __do_softirq at ffffffd83cc104e4 // ソフト割り込み処理 #19 [ffffffc008003ff0] ____do_softirq at ffffffd83cc16f74 --- <IRQ stack> --- #20 [ffffffd83df93b70] call_on_irq_stack at ffffffd83cc16f2c #21 [ffffffd83df93b80] do_softirq_own_stack at ffffffd83cc16fa4 #22 [ffffffd83df93b90] __irq_exit_rcu at ffffffd83cc939e8 // ソフト割り込み要求の有無を判定 #23 [ffffffd83df93bb0] irq_exit_rcu at ffffffd83cc93d84 #24 [ffffffd83df93bc0] el1_interrupt at ffffffd83d7ab124 #25 [ffffffd83df93be0] el1h_64_irq_handler at ffffffd83d7ab938 // 割り込みハンドラ処理・ソフト割り込み要求 #26 [ffffffd83df93d20] el1h_64_irq at ffffffd83cc11ae4 #27 [ffffffd83df93d40] arch_cpu_idle at ffffffd83d7ac468 #28 [ffffffd83df93d50] default_idle_call at ffffffd83d7b74d0 #29 [ffffffd83df93d80] do_idle at ffffffd83cce2f24 #30 [ffffffd83df93de0] cpu_startup_entry at ffffffd83cce3178 #31 [ffffffd83df93e00] rest_init at ffffffd83d7ad0f4 #32 [ffffffd83df93e20] arch_call_rest_init at ffffffd83db80888 #33 [ffffffd83df93e50] start_kernel at ffffffd83db80fe4
なお、ソフト割り込み処理(#18)はソフト割り込み要求がある場合に実行されますが、ソフト割り込み要求があったかどうかの確認が行われるのがハード割り込み処理終了後に呼ばれる__irq_exit_rcu()
(#23)となります。
__irq_exit_rcu()
は、ソフト割り込み要求があるかどうかの確認に加えて、実行コンテキストの切り替えも行っています。実行コンテキストの切り替えはL.630で行われており、L.630以降の実行コンテキストは割り込まれた処理の実行コンテキストに戻ります。
そのため、ソフト割り込み処理が実行されるのは、割り込まれた処理の実行コンテキストがプロセスコンテキストであることに加えて、ソフト割り込み要求があった場合(L.631)となります。
622 static inline void __irq_exit_rcu(void) 623 { 624 #ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED 625 local_irq_disable(); 626 #else 627 lockdep_assert_irqs_disabled(); 628 #endif 629 account_hardirq_exit(current); 630 preempt_count_sub(HARDIRQ_OFFSET); 631 if (!in_interrupt() && local_softirq_pending()) 632 invoke_softirq(); 633 634 tick_irq_exit(); 635 }
ソフト割り込み要求(RCU_SOFTIRQの場合)
ハード割り込みからソフト割り込み要求が行われるまでの流れをRCU_SOFTIRQを例に確認します。なお、説明を単純化するためにTiny RCUの場合について紹介します。
RCUでは、同期の完了が確認できると保護されていたデータを削除するためのコールバックが実行されますが、同期が完了しているかどうかの判定にはハード割り込み(タイマー割り込み)を利用しています。
同期が完了していた場合(L.73)に呼ばれるコールバックは、タイマー割り込み処理の実行時間が延びて応答性が悪くなることを防ぐため、タイマー割り込み処理中にはソフト割り込み要求だけ行い(L.59)、実際のコールバックの実行はタイマー割り込み処理後のソフト割り込みコンテキストで行われています*3。
71 void rcu_sched_clock_irq(int user) 72 { 73 if (user) { 74 rcu_qs(); 75 } else if (rcu_ctrlblk.donetail != rcu_ctrlblk.curtail) { 76 set_tsk_need_resched(current); 77 set_preempt_need_resched(); 78 } 79 }
52 void rcu_qs(void) 53 { 54 unsigned long flags; 55 56 local_irq_save(flags); 57 if (rcu_ctrlblk.donetail != rcu_ctrlblk.curtail) { 58 rcu_ctrlblk.donetail = rcu_ctrlblk.curtail; 59 raise_softirq_irqoff(RCU_SOFTIRQ); 60 } 61 WRITE_ONCE(rcu_ctrlblk.gp_seq, rcu_ctrlblk.gp_seq + 2); 62 local_irq_restore(flags); 63 }
ソフト割り込み処理の実行コンテキスト
ソフト割り込み処理はinvoke_softirq()
を契機に実行されますが、ソフト割り込み処理が実行されるコンテキストは、カーネルのコンフィグレーションやブートパラメータによって異なります。
本節では、ソフト割り込み処理が実行されるコンテキストを、invoke_softirq()
の実装から確認します。
通常のカーネルの場合、invoke_softirq()
の実装はL.418となっています。ソフト割り込み処理は、明示的な設定をしなければ、ソフト割り込みコンテキストで行われます(L.420)。ソフト割り込み処理をカーネルスレッドで実行したい場合(L.436)には、CONFIG_IRQ_FORCED_THREADINGを有効化したカーネルにthreadirqsをブートパラメータとして渡してあげることで、ソフト割り込み処理がカーネルスレッドで実行されるようになります。
なお、いずれの実行コンテキストにおいても、最終的に呼び出されるのはソフトウェア割り込み処理の本体である__do_softirq()
となります。
418 static inline void invoke_softirq(void) 419 { 420 if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) { 421 #ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK 422 /* 423 * We can safely execute softirq on the current stack if 424 * it is the irq stack, because it should be near empty 425 * at this stage. 426 */ 427 __do_softirq(); 428 #else 429 /* 430 * Otherwise, irq_exit() is called on the task stack that can 431 * be potentially deep already. So call softirq in its own stack 432 * to prevent from any overrun. 433 */ 434 do_softirq_own_stack(); 435 #endif 436 } else { 437 wakeup_softirqd(); 438 } 439 }
ちなみに、応答性を重視したCONFIG_PREEMPT_RTを有効にした場合(RTカーネル)だと*4、ソフト割り込み処理は全てカーネルスレッドで実行されます。
/kernel/softirq.c
276 static inline void invoke_softirq(void) 277 { 278 if (should_wake_ksoftirqd()) 279 wakeup_softirqd(); 280 }
ソフト割り込み処理の実行
ソフト割り込み処理がソフト割り込みコンテキストもしくはカーネルスレッドで実行されることをinvoke_softirq()
で確認しましたが、ソフト割り込み処理自体は__do_softirq()
で実行されます。
本節では、__do_softirq()
内における実行コンテキストの切り替わりを確認します。
510 asmlinkage __visible void __softirq_entry __do_softirq(void) 511 { 512 unsigned long end = jiffies + MAX_SOFTIRQ_TIME; 513 unsigned long old_flags = current->flags; 514 int max_restart = MAX_SOFTIRQ_RESTART; 515 struct softirq_action *h; 516 bool in_hardirq; 517 __u32 pending; 518 int softirq_bit; 519 520 /* 521 * Mask out PF_MEMALLOC as the current task context is borrowed for the 522 * softirq. A softirq handled, such as network RX, might set PF_MEMALLOC 523 * again if the socket is related to swapping. 524 */ 525 current->flags &= ~PF_MEMALLOC; 526 527 pending = local_softirq_pending(); 528 529 softirq_handle_begin(); 530 in_hardirq = lockdep_softirq_start(); 531 account_softirq_enter(current); 532 533 restart: 534 /* Reset the pending bitmask before enabling irqs */ 535 set_softirq_pending(0); 536 537 local_irq_enable(); 538 539 h = softirq_vec; 540 541 while ((softirq_bit = ffs(pending))) { 542 unsigned int vec_nr; 543 int prev_count; 544 545 h += softirq_bit - 1; 546 547 vec_nr = h - softirq_vec; 548 prev_count = preempt_count(); 549 550 kstat_incr_softirqs_this_cpu(vec_nr); 551 552 trace_softirq_entry(vec_nr); 553 h->action(h); 554 trace_softirq_exit(vec_nr); 555 if (unlikely(prev_count != preempt_count())) { 556 pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n", 557 vec_nr, softirq_to_name[vec_nr], h->action, 558 prev_count, preempt_count()); 559 preempt_count_set(prev_count); 560 } 561 h++; 562 pending >>= softirq_bit; 563 } 564 565 if (!IS_ENABLED(CONFIG_PREEMPT_RT) && 566 __this_cpu_read(ksoftirqd) == current) 567 rcu_softirq_qs(); 568 569 local_irq_disable(); 570 571 pending = local_softirq_pending(); 572 if (pending) { 573 if (time_before(jiffies, end) && !need_resched() && 574 --max_restart) 575 goto restart; 576 577 wakeup_softirqd(); 578 } 579 580 account_softirq_exit(current); 581 lockdep_softirq_end(in_hardirq); 582 softirq_handle_end(); 583 current_restore_flags(old_flags, PF_MEMALLOC); 584 }
ソフト割り込み処理入口
前節で確認したように、ソフト割り込み処理の実行コンテキストは、ソフト割り込みコンテキストもしくはカーネルスレッドとなっており、ソフト割り込み処理中もハード割り込みを受け付けています。しかし、ソフト割り込みの入り口区間(~L.537)については、ハード割り込みが禁止されています*5。
__do_softirq()
では、__do_softirq()
が呼ばれるまでに要求(pending状態)があったソフト割り込み種別のハンドラを実行(L.553)しますが、実行対象となるソフト割り込み種別の取得はL.527で行われます。ソフト割り込み種別ごとのpending状態については、ソフト割り込み種別のハンドラを実行する前にリセットされます(L.535)が、実行対象となるのはL.527で取得したソフト割り込み種別のみです。
そのため、ハード割り込み禁止は、pending状態にあるソフト割り込み種別の取得からリセットまでの区間(L.527~L.535)に新たなソフト割り込み要求が行われた場合の取りこぼし防ぐために設けられています。なお、pending状態にあるソフト割り込み種別の確認はソフト割り込み種別のハンドラを実行後にも行われ(L.571)、ソフト割り込み処理中にソフト割り込み要求があった場合にも同様に、pending状態の取得からリセットまでの区間(L.571~L.535)がハード割り込み禁止となっています。
ソフト割り込みコンテキスト区間(softirq_handle_begin/end)
__irq_exit_rcu()
において、ハード割り込み処理から呼び出されるinvoke_softirq()
は、割り込まれた処理の実行コンテキストがプロセスコンテキストだった場合であることを確認しました。実行コンテキストの確認に用いられているin_interrupt()
は、割り込みコンテキストにいるかどうかの判定を行うために用いられますが、判定の対象となる割り込みコンテキストはハード・ソフト・NMI割り込みとなっています。そのため、ハード割り込み処理からinvoke_softirq()
が呼ばれたタイミングでは、ハード割り込みコンテキストが終了しただけの状態であり、ソフト割り込みコンテキストには切り替わっていません。
softirq_handle_begin()
は、実際にソフト割り込みコンテキストへの切り替わりを行っている関数となります。そのため、実行コンテキストとしてのソフト割り込み処理は、softirq_handle_begin()
からsoftirq_handle_end()
までの区間(L.529~L.582)となります。
RTカーネルにおけるsoftirq_handle_begin/end(余談)
invoke_softirq()
の実装で確認したとおり、RTカーネルにおいてソフト割り込み処理は、ソフト割り込みコンテキストではなくカーネルスレッドで行われます。この実行コンテキストの違いから、通常のカーネルとRTカーネルではsoftirq_handle_begin/end()
の実装が異なります。
通常のカーネルの場合
/kernel/softirq.c
392 static inline void softirq_handle_begin(void) 393 { 394 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); 395 } 396 397 static inline void softirq_handle_end(void) 398 { 399 __local_bh_enable(SOFTIRQ_OFFSET); 400 WARN_ON_ONCE(in_interrupt()); 401 }
RTカーネルの場合
/kernel/softirq.c
268 static inline void softirq_handle_begin(void) { } 269 static inline void softirq_handle_end(void) { }
それぞれを比較してわかる通り、RTカーネルの場合にはsoftirq_handle_begin/end()
で行われる処理はありません。
これは、RTカーネルでは、ソフト割り込み処理が常にスケジューラを介したカーネルスレッドで行われるため、ソフト割り込み処理中にソフト割り込みコンテキストのソフト割り込み処理がネストして実行されることを考慮する必要がないためです。通常のカーネルでは、local_bh_disable/enable()
がソフト割り込み禁止の区間を設定することで、ソフト割り込み処理がネストして実行されることを防いでいます。
本稿では、実行コンテキストの流れについてのみ紹介したため、実行コンテキストを管理しているデータ構造や関数の実装については触れませんでした。次稿では、本稿で触れなかったそれら実行コンテキストに関わる仕組みについて紹介したいです。
*1:preempt_countで管理される実行コンテキストとなります。そのため、本稿で取り上げる実行コンテキストの判定や切り替えは、preempt_countの状態に対する操作です。ただし、割り込み禁止区間については、preempt_countの状態に基づくものではなくHWで実現しています。
*2:NMIからハード割り込みコンテキストへの遅延処理の仕組みであるirq_workを使っています
*3:RCUの仕組みの詳細は、別途ブログ記事にするのでお待ちください
*4:RTカーネルでは、応答性を上げるために、本稿で触れた割り込み処理以外でも様々な工夫がされています。それらについて、別稿で紹介する機会もあるかと思います
*5:arm64では、割り込みの発生と同時にHWにより割り込みが禁止されます