新Linuxカーネル解読室 - ソフト割り込み処理

「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)となります。

/kernel/softirq.c

  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

/kernel/rcu/tiny.c

   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 }

/kernel/rcu/tiny.c

   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()となります。

/kernel/softirq.c

  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()内における実行コンテキストの切り替わりを確認します。

/kernel/softirq.c

  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により割り込みが禁止されます