新Linuxカーネル解読室 - リアルタイムカーネル

「Linuxカーネル2.6解読室」(以降、旧版)出版後、Linuxには多くの機能が追加され、エンタープライズ領域をはじめとする様々な場所で使われるようになりました。
それに伴いコードが肥大かつ複雑化し、多くのエンジニアにとって解読不能なブラックボックスとなっています。
世界中のトップエンジニア達の傑作であるLinuxカーネルにメスを入れ、ブラックボックスをこじ開けて、時に好奇心の赴くままにカーネルの世界を解読する「新Linuxカーネル解読室」プロジェクト。

執筆者 : 高倉 遼

はじめに

Linuxカーネルをリアルタイム化するための最後の拡張機能が、カーネル v6.12にて取り込まれました。リアルタイム(RT)カーネルには応答性をあげるための仕組みがいくつかありますが、それらは通常のカーネルにおいてもデフォルトで有効化されているものから、リアルタイム機能を有効化したカーネルでのみ機能するものまで様々な形で取り込まれています。本記事では、RTカーネルにおける応答性を向上させるための施策である割り込みのスレッド化と排他制御における優先度継承について確認した上で、最近筆者が楽しみながら読んでいるスピンロックを通してRTカーネルの実装を簡単に紹介できればと思います。

応答性に関わる機能

応答性とは

応答性は、実行可能な状態にある処理が実際に実行されるまでにかかる時間です。実際に処理が動き出すまでの時間は、実行可能な状態になった時点で動作している処理をPreemptするまでにかかる時間となります。Linuxにおいては、どの程度Preemptが有効化されているかは、用途に応じて様々なオプションが用意されています。それぞれの違いは、カーネル内におけるPreemptが禁止されている区間の長さやスケジューラ呼び出しの契機の違いとなります。下の表はLinuxの主なPREEMPT関連のコンフィグとなります。PREEMPT_NONEからPREEMPT_RTにかけてカーネル内でPreemptが禁止されている区間の長さは短く、スケジューラ呼び出しの契機は多くなります。

CONFIG 用途 実装
PREEMPT_NONE サーバ向け、スループット重視 自発的なCPU明け渡しをベースとしたスケジューリング
PREEMPT_VOLUNTARY デスクトップ向け、スループット重視 同上、自発的なCPU明け渡しがPREEMPT_NONEと比較してより多く行われる
PREEMPT デスクトップ向け、応答性重視 タイマーなどの割り込みに伴いスケジューリングが行われるため、応答性が安定する
PREEMPT_LAZY デスクトップ向け、PREEMPTと比較してスループット重視 同上、SCHED_NORMALクラスのタスクに対するPreemptを遅延する
PREEMPT_RT リアルタイム用、応答性保証 割り込み処理・排他制御区間における処理をPreemptibleにすることで、応答性を保証する

カーネル内でPreemptが禁止される主な処理は、割り込みや排他制御が必要な区間の処理となります。そのため、RTカーネルの実装の特徴は、それらの処理において発生するPreemptするまでの遅延ができる限り短くなるようになっている点となります。 次節では、この割り込みや排他制御において発生する遅延とRTカーネルで実装されている対策について簡単に紹介したいと思います。

割り込み処理のスレッド化

割り込みは、スケジューラに関わらず実行されるためPreemptされることはなく、リアルタイム処理に割り込んだ場合には処理の完了を遅らせる原因となります。そのため、RTカーネルでは割り込みをできる限りスレッドとして処理します。
割り込みがスレッド化された場合には"irq/"で始まる名前のプロセスが登録されますが、RTカーネルを確認してみると、通常のカーネルに比べて割り込みスレッドが多いことがわかります。これらの割り込みスレッドの優先度は、リアルタイムクラスであるSCHED_FIFOポリシーの優先度51となっています。そのため、ユーザーがプロセスに対してSCHED_RRやSCHED_FIFOポリシーの優先度52以上を設定した場合には、指定されたプロセスはスレッド化された割り込み処理に対して優先して実行されることがわかります。以下は、ラズパイ4でRTカーネルを動かした場合です。

$ ps ax -o 'cls,priority,cmd' | grep "irq/"
 FF -51 [irq/14-fe00b880.mailbox]
 FF -51 [irq/26-PCIe PME]
 FF -51 [irq/26-aerdrv]
 FF -51 [irq/26-s-aerdrv]
 FF -51 [irq/26-PCIe bwctrl]
 FF -51 [irq/27-bcm2708_fb DMA]
 FF -51 [irq/30-xhci_hcd]
 FF -51 [irq/34-VCHIQ doorbell]
 FF -51 [irq/15-DMA IRQ]
 FF -51 [irq/41-mmc1]
 FF -51 [irq/41-mmc0]
 FF -51 [irq/41-s-mmc0]
 FF -51 [irq/42-feb10000.codec]
 FF -51 [irq/42-s-feb10000.codec]
 FF -51 [irq/39-uart-pl011]
 FF -51 [irq/28-eth0]
 FF -51 [irq/29-eth0]
 FF -51 [irq/40-ttyS0]

なおスレッド化は、割り込み処理に限らず、遅延処理の仕組みであるIRQ_WORKやソフト割り込みに対しても行われます。これらカーネルの処理をスレッド化するかどうかは、主にforce_irqthreads()によって決まります。RTカーネルの場合には、以下のようにforce_irqthreads()が無条件に有効化されるため、割り込みや遅延処理はスレッド化されます。

(include/linux/interrupt.h)

  516 #ifdef CONFIG_IRQ_FORCED_THREADING
  517 # ifdef CONFIG_PREEMPT_RT
  518 #  define force_irqthreads()    (true)
  519 # else
  520 DECLARE_STATIC_KEY_FALSE(force_irqthreads_key);
  521 #  define force_irqthreads()    (static_branch_unlikely(&force_irqthreads_key))
  522 # endif
  523 #else
  524 #define force_irqthreads()      (false)
  525 #endif

割り込み処理の場合には以下のように、デバイスドライバによる割り込み処理の登録(irq_setup_forced_threading())時、割り込み処理がスレッド化ができない属性(IRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOT)を持たなければスレッドとして動作します。

(kernel/irq/manage.c)

static int irq_setup_forced_threading(struct irqaction *new)
{
    if (!force_irqthreads())
        return 0;
    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
        return 0;
    ...

以下は、ラズパイ4のシリアルデバイス(amba-pl011)の割り込み登録処理ですが、先のpsコマンドの結果(irq/39-uart-pl011)にもある通りスレッド化されます。

(drivers/tty/serial/amba-pl011.c)

static int pl011_allocate_irq(struct uart_amba_port *uap)
{
    pl011_write(uap->im, uap, REG_IMSC);

    return request_irq(uap->port.irq, pl011_int, IRQF_SHARED, "uart-pl011", uap);
}

ちなみに、RTカーネルがメインラインに取り込まれるに至った背景も、応答性を下げる原因となっていたコンソールデバイスの出力処理(printk)のスレッド化でした。printkは、NMIやハード割り込みからも使用されることから様々な工夫がされています。カーネルのとても面白い機能だと思うので、気になる方は見てみてください。

排他制御における優先度継承

ミューテックス、スピンロックなどのロックに保護された排他制御区間は、競合した際には待ち合わせが発生したり、スピンロックの場合にはPreemptが禁止されていたりと遅延の原因となります。本節では、ミューテックスのようなPreemptが禁止されていないロックにおいて発生する遅延について、RTカーネルにおける対策を簡単に紹介したいと思います。  

優先度の異なるタスク間での排他制御において競合が発生した場合、優先度の低いタスクが保持しているロックに対して、より優先度の高いタスクの待ち合わせが発生します。仮にタスクAがタスクBを待ち合わせている間に、タスクBよりも優先度が高いタスクCが実行された場合には、待ち合わせているタスクAの完了が遅れる原因となります。このような場合に、実行されたタスクCがタスクAよりも優先度が高い場合には優先度通りに実行されたことになりますが、タスクAがタスクCよりも優先度が高かった場合には、タスクAには必要(ロック解放待ち)以上の待ち合わせが発生することになります。以下の図は、このような優先度が異なるタスク間で待ち合わせが発生した場合に、優先度が考慮されないスケジューリング(優先度逆転)が行われる場合を示した図です。

   Priority: A > C > B

   A --------~~~~~~~~~~~~~~~-----|─────►            
     A waiting for            Lock released by B.  
        the lock held by B.                       
   B ───────►               ────►                
     B running with a lock.                       
   C        ★──────────────►                     
         C preempts B as it gets ready.                                                      

優先度継承では、上記のような場合においてロックを保持しているタスク(タスクB)の優先度を、待ち合わせているタスク(タスクA)と同じ優先度に設定します。優先度継承を行うことにより、ロックを待ち合わせている間に行われるスケジューリングは待ち合わせているタスク(タスクA)の優先度を基に行われるようになるため、ロックを保持しているタスクに対してのPreempt(図中★印)は発生せず、必要(ロック解放待ち)以上の待ち合わせが発生しません。この優先度継承は、RTカーネルにおけるミューテックス(rt_mutex)において実装されています。

Preemptibleなスピンロック

前章では、ミューテックスのようなPreemptibleなロックの待ち合わせにおいて、優先度が逆転したスケジューリングが発生することを紹介しました。 もし先図のような競合がスピンロックのようなPreemptが禁止されているロックにおいて発生した場合には、★印におけるPreemptは発生せず、以下の図のようにタスクBはロックを解放するまで実行されます。そのため、最も優先度の高いタスクAは、タスクBによりロックが解放された際のスケジューリングにおいてタスクCに優先して実行されるため、ロック解放待ち以上の待ち合わせは発生しません。

  A -------------|──────────────────►       
        A gets to run                       
            as B releases the lock.           
  B ────────────►                           
    B running with a non-preemptible lock.  
  C        |-----------------------─►       
        C gets ready but can't preempt.     

しかし、RTカーネルにおいては、応答性を確保するためにスピンロック区間中もPreemptibleとします。そのため、スピンロックにおいても優先度逆転が発生してしまうため、RTカーネルではスピンロック(spinlock_t)をミューテックス(rt_mutex)に置き換える形で優先度継承が実装されています。なお、本来(通常のカーネルにおいて)Preempt、Sleepが行われないことを前提に使用されるスピンロックがPreemptible、Sleepableになることに伴い、RTカーネルでは一部の使用法やサブシステムによっては意図する排他制御が行われない場合があります。本章では最後に、それらの処理におけるRTカーネルの対策をいくつか紹介したいと思います。
なお、一部の処理においては、本来のPreempt、Sleepが行われないスピンロックが必要になる場合があります。具体的なユースケースについては後述しますが、そのような場合にはPreempt、Sleepが行われないraw_spinlock_tを使用する必要があります*1

割り込み禁止区間中のスピンロック取得

通常のカーネルにおけるスピンロックでは、ブロックした場合にもスリープすることはないため、割り込み禁止区間での使用に問題はありません*2。しかし、RTカーネルにおいてはスピンロックは、ミューテックス(rt_mutex)に置き換えられていることから、ブロックした際にはスリープします。そのため、通常のカーネルにおいては割り込み禁止区間中でも問題なく使用できるスピンロックですが、RTカーネルの場合には使用することはできません。
しかし、割り込みがスレッド化されるRTカーネルでは、スピンロックと割り込み禁止を併用する必要がある排他制御において、割り込みを禁止する必要がなくなります。これは、RTカーネルのスピンロック区間中(通常のカーネルスレッド)にスレッド化された割り込み処理がPreemptした場合でも、デッドロックせずにスレッド化された割込み処理がSleepするためです。割り込み処理におけるスピンロックを取得する本来(通常のカーネル)の目的は、マルチコアなシステムにおいて発生する割り込み処理を含めた様々な処理のコア間の排他を行うためですが、このようにRTカーネルにおいてはローカルコアにおける割り込み処理の排他制御においても有用になります。そのため、スピンロックの取得と割り込みの禁止を行うために用意されているspin_lock_irq()は、RTカーネルにおいて割り込みの禁止を行いません。
なお、スピンロックのみでの割り込み処理との排他制御は、割り込み処理がスレッド化されていない場合には適用できません。このようなRTカーネルにおいても本来のスピンロック・割り込み禁止の操作が必要な場合には、raw_spin_lock() / raw_spin_lock_irq()を使用する必要があります。そのため、タイマー割り込みなどスレッド化されない割り込み処理との排他制御にはraw_spin_lock_irq()が使用されます。以下は、settimeofdayシステムコールにおいて現在時刻を設定する処理とタイマー割り込みで現在時刻を更新する処理のそれぞれの抜粋となります。

settimeofdayシステムコール
(kernel/time/timekeeping.c)

int do_settimeofday64(const struct timespec64 *ts)
{
    ...
    raw_spin_lock_irqsave(&timekeeper_lock, flags);
    ...
    timekeeping_update(tk, TK_CLEAR_NTP | TK_MIRROR | TK_CLOCK_WAS_SET);

タイマー割り込み
(kernel/time/timekeeping.c)

static bool timekeeping_advance(enum timekeeping_adv_mode mode)
{
    ...
    raw_spin_lock_irqsave(&timekeeper_lock, flags);
    ...
    timekeeping_update(tk, clock_set);

RCU参照区間におけるスピンロック

スピンロックがSleepableになることにより影響を受けるもう一つのロジックとして、RCU参照区間中に取得されるスピンロックを紹介したいと思います。RCUは、更新処理がすべての参照処理の終了を待ち合わせることによって排他制御を実現しますが、この待ち合わせが適切に行われるための条件として参照区間中はSleepが行われないことが挙げられます( Preemptは行われます*3 )。そのため、通常のカーネルにおいてもRCU参照区間中にSleepするミューテックスなどを使用することは禁止されていますが、Sleepすることがないスピンロックを使用することには問題ありません。しかし、RTカーネルにおいてはスピンロックもSleepすることから、ミューテックス同様にRCU参照区間中に取得することが問題となります。 この問題に対してRCU参照区間では、前節で紹介した割り込み禁止区間のようにスピンロックの取得を禁止するのではなく、スピンロックの実装側においてワークアラウンドを用意しています。以下は、通常のカーネルにおいても取得が禁止されているミューテックスとスピンロックがSleepする際に呼ばれる関数ですが、ミューテックスによるSleepとスピンロックによるSleepは、スケジュールアウトのために呼び出す__schedule()に渡す引数を元に区別しています。__schedule()に渡される引数に伴うRCU参照処理の実装面の違いについては別途まとめたいと思いますが、スピンロックの場合においては__schedule_loop()に自発的なスケジュールアウトを意味するSM_NONEではなく、Preemptのような形でスケジュールアウトしたことを意味するSM_RTLOCK_WAITを渡すことで、RCUが問題なく動作するような仕組みとなっています。

ミューテックスの場合
(kernel/sched/core.c)

   7135 void rt_mutex_schedule(void)
   7136 {
   7137         lockdep_assert(current->sched_rt_mutex);
   7138         __schedule_loop(SM_NONE);
   7139 }

スピンロックの場合
(kernel/sched/core.c)

   6919 void __sched notrace schedule_rtlock(void)
   6920 {
   6921         __schedule_loop(SM_RTLOCK_WAIT);
   6922 }

Preemptibleなソフト割り込み禁止区間

このように様々な処理をPreemptibleにしながらも排他制御を実現しているRTカーネルですが、最後にPreemptibleなこと自体が問題となるケースについて紹介したいと思います。 RTカーネルにおけるソフト割り込み禁止区間は、ソフト割り込み処理がソフト割り込みコンテキストで実行されることがないため、スレッド化されたソフト割り込み処理とソフト割り込み禁止区間の排他を行うためのスピンロック*4を取得するのみの実装となっています。先述したとおり、RTカーネルのスピンロック(spinlock_t)はミューテックス(rt_mutex)に置き換えられているため、スピンロック区間中もPreemptは有効となっています。この違いによって、RTカーネルにおいてスピンロックによってのみ保護されるソフト割り込み禁止区間はPreemptが有効となります。この通常のカーネルの場合にはPreemptが禁止されているソフト割り込み禁止区間においてPreemptが有効となることが問題となるケースとして、コンテキストスイッチにおいて状態が保存されないFPUレジスタをカーネルタスクが使用していた場合があげられます。FPUコンテキストの保存が行われる・ない場合については プロセスディスパッチャ(後編) で解説させていただいているので本記事では詳細には触れませんが、カーネルタスクに対するコンテキストスイッチではFPUコンテキストの保存は行われません*5。もしカーネルタスクがFPUレジスタを使用している途中にPreemptされた場合には、使用中のFPUコンテキストが保存されず失われてしまいます。そのため、以下のようにカーネルタスクがFPUレジスタを使用するSIMD命令の実行区間では、RTカーネルの場合(CONFIG_PREEMPT_RT)にはソフト割り込みを禁止するためのlocal_bh_disable()ではなく、Preemptを禁止するためのpreempt_disable()によって保護されます*6

(arch/x86/include/asm/fpu/api.h)

   69 static inline void fpregs_lock(void)
   70 {
   71         if (!IS_ENABLED(CONFIG_PREEMPT_RT))
   72                 local_bh_disable();
   73         else
   74                 preempt_disable();
   75 }

余談

RTカーネルの意外な使い処として、低遅延な処理が求められるオーディオのサンプリングなどがあるそうです。筆者はボサノバなど音楽が好きなので、実際にRTカーネルをそれらオーディオ処理に使用した場合にどれほどの違いがあるのかについて、いちカーネル好きとしても、いち音楽好きとしても気になっています。もしまたRTカーネルの記事を書くことがあれば、リアルタイムアプリケーションをRTカーネルで動かす場合のスケジューリングクラスの設定やCPUのアイソレーションといった環境構築など、より使い方に焦点を当てた記事を書ければと思っています。

*1:通常のカーネルにおけるスピンロック( spinlock_t )は、 raw_spinlock_t に置き換えられています

*2:割り込み禁止区間においてPreemptは禁止されています

*3:CONFIG_PREEMPT_RCUが有効なカーネルにおいては、RCU参照区間中においてもPreemptが行われます

*4:RTカーネルにおいてソフト割り込み禁止区間を保護しているローカルロックはスピンロックに置き換えられています

*5:x86におけるコンテキストスイッチを想定しています

*6:RTカーネルではソフト割り込みはスレッドとして処理されるため、Preemptを禁止することでソフト割り込み処理も禁止することになります