LoginSignup
36
35

More than 3 years have passed since last update.

【読解入門】Linuxカーネル (スケジューラと割込みハンドラの関係)

Last updated at Posted at 2019-12-12

FUJITSU Advent Calendar 2019 13日目の記事です。

今回は、番外編としてLinuxカーネルにおけるスケジューラと割込みハンドラの関係を記事にします。2.6系の頃のカーネルと現在のカーネルでは割込みハンドラの扱いが大きく変化したため、この経緯なども踏まえて説明できればと思います。

・Linuxカーネルのコードは5.4を対象としています。
・アーキテクチャ依存の処理はArm(32bit)アーキテクチャで説明します。

割込みとは

初めに用語の定義から入ります。
割込みとは外部デバイスからイベントを通知する為の仕組みです。例えば、キーボードのキーを押下すると、キーボード デバイスからキーが押下されたことをCPUへ通知されることを割込みと言います。
割込みと似たような仕組みで例外があります。
例外は、プログラム実行時にメモリ アクセス違反や未定義命令などを実行した時に、イベントとして通知される仕組みです。
両者を区別する観点は幾つかあります。
一例としては、ソフトウェアの実行という観点で見ると、
・ソフトウェアの実行に同期して発生するイベントが例外
・ソフトウェアの実行と非同期で発生するイベントが割込み
と区別する事ができます。
また、イベントを発生させるハードウェアという観点で見ると
・CPU内部で発生するイベントが例外
・それ以外のデバイスから通知されるイベントは割込み
と捉えることもできます。

CPUが割込みを受けると、(ハードウェアで自動的に)ベクタ テーブルのIRQエントリのアドレスに制御が移ります。Arm(32bit)を例としてベクタ テーブルを説明します。ベクタ テーブルは下記のように定義されており、割込みの場合はオフセット0x18に制御が移ります。
このベクタ テーブルのアドレスは、0x00000000(ローベクタ)、または0xFFFF0000(ハイベクタ)のいずれかに配置されます。

ベクタ オフセット 要因
0x00 リセット
0x04 未定義命令
0x08 スーパーバイザー コール
0x0C プリフェッチ アボート
0x10 データ アボート
0x14 予約
0x18 IRQ
0x1C FIQ

ベクタ オフセット0x18に制御が移った後はソフトウェアで割込み発生時に実行していたコンテキストのレジスタ群を退避し、割込みハンドラを実行した後に、退避したレジスタ群を復元します。その後、元のコンテキストの実行を再開します。

割込みハンドラ

割込みハンドラは対応する割込みを受けた時に実行する処理のことを指します。
基本的に割込みハンドラはリアルタイム応答性能が求められるため、それまで実行していた処理に割り込んで実行する必要があります。
Linuxでは、処理の緊急性に応じて「ハードウェア割込みハンドラ」と「ソフトウェア割込みハンドラ」の2種類の割込みハンドラの仕組みが用意されています。

ハードウェア割込みハンドラ

緊急性の高い必要最低限の処理を実装します。
キーボードのキーを押下した場合の割込みを例で考えてみましょう。
割込み概要.jpg

ハードウェア割込みハンドラはキーボード デバイスの割込みを刈り取った後、押下されたキー情報を取得して、上位レイヤに通知して終了します。
押下されたキーに対するアクション(画面に表示する処理など)はハードウェア割込みハンドラでは行いません。
私なりの基本的な考え方は、至ってシンプルです。
割込みを上げたハードウェアにアクセスする処理はハードウェア割込みハンドラで、それ以外の処理はソフトウェア割込みハンドラで、を基準に考えています。

ソフトウェア割込みハンドラ

Linuxを扱う書籍、Webでは様々な呼び方がされています。ソフトウェア割込み、SoftIRQ、割込みの遅延処理など。
本記事ではこのソフトウェア割込みハンドラについてそれほど触れる予定もないので、この機能については別途記事を書きたいと思います。
割込み処理のうち、緊急度がそれほど高くなく、遅れて実行される処理と思っていただければ良いと思います。

従来の割込みハンドラの仕組み

以降はハードウェア割込みハンドラを「割込みハンドラ」と記述します。

割込みハンドラが使用するスタック

Linuxの割込みハンドラは、自身を実行する為のスタックを持ちません(少なくともx86、Arm(32bit)、Powerアーキテクチャにおいては)。下図の通り、割込みが発生した時点に実行していたプロセスのカーネルスタックを使用します。

割込みスタック.jpg

この図だけではイメージが湧かないと思いますので、実際の割込みハンドラ実行時のスタックの中を確認してみましょう。
今回はArmv7アーキテクチャのCPUを搭載したボード向けのLAN割込みハンドラの先頭にBUG( )を挿入しました。その時の出力情報を以下に示します。

まず初めに、1行目と35行目のコールトレースの情報から割り込ハンドラtest_interrupt( )を実行中にBUG( )を呼び出して、本メッセージが出力されていることが判ります。
次に6行目の"ti: c0af2000"からthread_info構造体の先頭アドレスが判ります。thread_info構造体とカーネルスタック領域は上図の関係にあるため、カーネルスタックが8KBの場合、このアドレスに0x2000を加算した0xc0af3FFFがスタックの底のアドレスになります。余談ですが、カーネルスタックのアドレスに対して0x1FFFをマスクすることで、そのプロセスのthread_info構造体のアドレスを算出できます。
次に注目して貰いたい箇所は4行目です。
実行しているプロセス情報が出力されており、PID 0のCPU 0用swapperプロセス実行時にBUG( )が呼ばれたことを意味しています。swapperプロセスは、システムがアイドル状態の時に実行されるIDLEクラスのプロセスです。
18 - 34行目はスタック領域の内容を表示しています。左端の4桁の数字は、スタックアドレスの下位16bitを表しています。
35 - 49行目のスタックトレースを見ると、47 - 49行目はswapperプロセスのコールトレースが表示されています。一方で、35 - 46行目は割込み発生後のコールトレースが表示されています。

これらのことから、割込みハンドラtest_interrupt( )までの一連の割込み処理はswapperプロセスのスタックを用いて動作していることが判ります。

 1 kernel BUG at drivers/net/ethernet/test.c:3639!
 2 Internal error: Oops - BUG: 0 [#1] PREEMPT SMP ARM
 3 Modules linked in:
 4 CPU: 0 PID: 0 Comm: swapper/0 Not tainted 4.14.139 #8
 5 Hardware name: Test Board
 6 task: c0af97b0 ti: c0af2000 task.ti: c0af2000
 7 PC is at test_interrupt+0x14/0x18
 8 LR is at handle_irq_event_percpu+0x9c/0x30c
 9 pc : [<c04c8684>]    lr : [<c007d844>]    psr: 60010193
10 sp : c0af3df8  ip : c0af3e08  fp : c0af3e04
11 r10: c075dd90  r9 : c0af2000  r8 : 0000002b
12 r7 : 00000000  r6 : ed877f40  r5 : ee19e9b0  r4 : ed877f40
13 r3 : c04c8670  r2 : c0b89cc8  r1 : ed888000  r0 : 0000002b
14 Flags: nZCv  IRQs off  FIQs on  Mode SVC_32  ISA ARM  Segment kernel
15 Control: 10c5387d  Table: 6db8804a  DAC: 00000015
16 Process swapper/0 (pid: 0, stack limit = 0xc0af2210)
17 Stack: (0xc0af3df8 to 0xc0af4000)
18 3de0:                                                       c0af3e54 c0af3e08
19 3e00: c007d844 c04c867c c0af3e7c c0af3e18 c0011380 ee19e940 c0b89cc8 c0b89cdc
20 3e20: 00000000 00000000 c0098060 ee19e940 ee19e9b0 ed877f40 00000000 00000001
21 3e40: ee02c000 c0af3f00 c0af3e74 c0af3e58 c007db04 c007d7b4 ee19e940 ee19e9b0
22 3e60: c0b5d980 00000000 c0af3e94 c0af3e78 c00811f4 c007dac0 0000002b 00000000
23 3e80: 0000002b 00000000 c0af3eac c0af3e98 c007cd8c c0081108 c0aede24 00000000
24 3ea0: c0af3edc c0af3eb0 c007d0bc c007cd5c c0af3f00 fd10010c c0af5e98 c0af3f00
25 3ec0: fd100100 00000000 c0b8a268 c0af3f58 c0af3efc c0af3ee0 c00093f0 c007d03c
26 3ee0: c0011380 60010013 ffffffff c0af3f34 c0af3f54 c0af3f00 c0015400 c00093c8
27 3f00: 00000001 00000000 00000000 c0024980 c0af2000 c0af4ab8 c075dd90 00000000
28 3f20: 00000000 c0b8a268 c0af3f58 c0af3f54 c0af3f58 c0af3f48 c001137c c0011380
29 3f40: 60010013 ffffffff c0af3f94 c0af3f58 c006c6ec c0011344 c0ac5038 ef7fcb40
30 3f60: c0af4ac4 c0b89970 c0aef940 c0073b80 c0af5d08 c0b99700 c0aeeee8 c0aeb35c
31 3f80: c0757200 ffffffff c0af3fac c0af3f98 c074f860 c006c2d0 c0b99750 c0b99700
32 3fa0: c0af3ff4 c0af3fb0 c0a76d44 c074f7d4 ffffffff ffffffff c0a766f0 00000000
33 3fc0: 00000000 c0ac5038 00000000 c0b99994 c0af4a40 c0ac5034 c0afac80 4000406a
34 3fe0: 414fc091 00000000 00000000 c0af3ff8 4000807c c0a76990 00000000 00000000
35 [<c04c8684>] (test_interrupt) from [<c007d844>] (handle_irq_event_percpu+0x9c/0x30c)
36 [<c007d844>] (handle_irq_event_percpu) from [<c007db04>] (handle_irq_event+0x50/0x74)
37 [<c007db04>] (handle_irq_event) from [<c00811f4>] (handle_fasteoi_irq+0xf8/0x1b8)
38 [<c00811f4>] (handle_fasteoi_irq) from [<c007cd8c>] (generic_handle_irq+0x3c/0x4c)
39 [<c007cd8c>] (generic_handle_irq) from [<c007d0bc>] (__handle_domain_irq+0x8c/0xfc)
40 [<c007d0bc>] (__handle_domain_irq) from [<c00093f0>] (gic_handle_irq+0x34/0x70)
41 [<c00093f0>] (gic_handle_irq) from [<c0015400>] (__irq_svc+0x40/0x88)
42 Exception stack(0xc0af3f00 to 0xc0af3f48)
43 3f00: 00000001 00000000 00000000 c0024980 c0af2000 c0af4ab8 c075dd90 00000000
44 3f20: 00000000 c0b8a268 c0af3f58 c0af3f54 c0af3f58 c0af3f48 c001137c c0011380
45 3f40: 60010013 ffffffff
46 [<c0015400>] (__irq_svc) from [<c0011380>] (arch_cpu_idle+0x48/0x4c)
47 [<c0011380>] (arch_cpu_idle) from [<c006c6ec>] (cpu_startup_entry+0x428/0x4ac)
48 [<c006c6ec>] (cpu_startup_entry) from [<c074f860>] (rest_init+0x98/0x9c)
49 [<c074f860>] (rest_init) from [<c0a76d44>] (start_kernel+0x3c0/0x3cc)
50 Code: e92dd800 e24cb004 e52de004 e8bd4000 (e7f001f2)
---[ end trace 9bdaf8a954bc3572 ]---

スケジューラとの関係

スケジューラはプロセスをtask_struct構造体を用いて管理します。一方で、割込みハンドラは自分のtask_struct構造体は持たず、割込み発生時に実行していたプロセスのスタックを利用します。このことから、割込みハンドラはスケジューラの管理対象外にであることが理解できます。

皆さんは割込みハンドラからスケジューラ (例えばschedule())を呼び出してはいけない(休眠してはいけない)と認識されている方も多いと思います。
実際に呼び出したコードを実行すると下記メッセージとスタックトレースが出力されます。

BUG: scheduling while atomic: プロセス名/PID/プリエンプトカウント

これは"atomicなコンテキストから(実行してはいけない)schedule()を実行しようとしているよ。カーネルに不具合があるよ"という警告メッセージです。
では、なぜ割込みハンドラからスケジューラを呼び出してはいけない(休眠してはいけない)のでしょうか。
この理由をサラッと説明できる方は本節は飛ばしてもらって構いません。
基本的なことですが、書籍などではあまり触れられていないので、ここでは割込みハンドラからスケジューラを呼び出してはいけない理由について述べます。

下図を見てください。
割込みハンドラでスケジューラ.jpg

割込みハンドラ内でschedule( )を呼び出した場合を考えます。この時、スケジューラは優先度に基づいて次に実行するプロセスをディスパッチします。上図の場合では、RTクラスSCHED_FIFOのプロセスBが実行されます。プロセスBの実行後は、nice値に基づいてプロセスCが実行されます(プロセスA、プロセスCのvruntimeの値によってプロセスAが先に実行される可能性もありますが、ここでは考慮しません)。
スケジューラは、task_struct構造体を持たない割込みハンドラは管理対象外でありプロセスAとして扱うため、割込みハンドラの実行はプロセスAの優先度に引っ張られる形になります。つまり、プロセスAが再度ディスパッチされるタイミングまで割込みハンドラの実行が待たされることになります。
このような状態では、割込みハンドラの「緊急度の高い処理を優先して行う」という本来の目的を全く達成できません。
そもそも緊急性が求められる割込みハンドラで休眠するなという話ですが、割込みハンドラ内でmutexやセマフォなど内部で休眠する可能性がある機能で使用しようとする人もいるかもしれないので…。

現在の割込みハンドラの仕組み

現在の割込みハンドラの仕組みは大きく変わりました。
Linux FoundationのコラボラティブプロジェクトにReal-Time Linuxというプロジェクトがあります。
このプロジェクトではPreempt-RT又はRTパッチと呼ばれるリアルタイム応答性向上に取り組んでいます。私が把握している範囲では少なくとも15年前から活動しています。
このプロジェクトの目的は、一言で述べると、リアルタイムで処理したいプロセスのレイテンシをできる限り削減することです。
この目的を実現する為に、本プロジェクトではリアルタイム処理をしたいプロセスの実行を妨げるプリエンプション禁止区間を排除する、というアプローチをとっています。つまり、フルプリエンプティブ(カーネルのどの区間においてもプリエンプション可能)にすることです。
また、リアルタイム処理をしたいプロセスを即座に実行する為に、場合によっては特定の割込みハンドラより優先して実行しないといけない場合もあります。割込みハンドラより優先して実行するということは、スケジューラが割込みハンドラを制御するということです。
しかし、前述の通り、既存の仕組みではスケジューラは割込みハンドラは管理対象外としていました。そこで、Preempt-RTでは、割込みハンドラをスレッド化することで、スケジューラから制御できるように仕組みを変更しました。この機能がmainlineカーネルに取り込まれた版数はアーキテクチャによって異なります。x86では2.6.35、Powerでは3.3、Arm(32bit)では3.12から割込みハンドラのスレッド化を採用しています。

実際に見てみましょう。
以下にpsコマンドの実行結果を示します。
★はSDカードの割込みハンドラに対応するカーネルスレッドです。
割込み毎に"irq/IRQ番号-識別子" という命名規則でカーネルスレッドが生成されます。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  6.6  0.0  24824  5376 ?        Ss   16:12   0:05 /sbin/init
root         2  0.0  0.0      0     0 ?        S    16:12   0:00 [kthreadd]
: (snip)
root        98  0.0  0.0      0     0 ?        S    16:12   0:00 [irq/28-mmc0] ★
: (snip)

Preempt-RTでは、この仕組み以外にもリアルタイム応答性を向上させるためにフルプリエンプティブにする機能を提供していますが、詳細は別記事で紹介したいと思います。

割込みハンドラの登録処理

割込みハンドラの登録処理を読解することで仕組みがおよそ理解できます。
実際のコードをポイントを絞って追っていきましょう。
ここでは、各種ドライバから割込みハンドラ登録関数として最も使用されているrequest_irq( )を起点に見ていきます。

include/linux/interrupt.h
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev)
{
        return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

この関数は、request_threaded_irq( )のラッパー関数です。
request_threaded_irq( )の第3引数にはNULLを設定しています。
次にrequest_threaded_irq( )を見ましょう。

kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                         irq_handler_t thread_fn, unsigned long irqflags,
                         const char *devname, void *dev_id)
{
        struct irqaction *action;
: (snip)

        action->handler = handler;
        action->thread_fn = thread_fn;
        action->flags = irqflags;
        action->name = devname;
        action->dev_id = dev_id;
: (snip)
        retval = __setup_irq(irq, desc, action);
: (snip)
}

この関数のポイントは、上記で抜粋した部分です。
request_irq( )に渡した引数をirqaction構造体に格納して__setup_irq( )を呼び出します。
続いて__setup_irq( )のポイントとなる部分を抜粋します。

kernel/irq/manage.c
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
        struct irqaction *old, **old_ptr;
        unsigned long flags, thread_mask = 0;
        int ret, nested, shared = 0;
: (snip)
        if (nested) {
: (snip)
        } else {
                if (irq_settings_can_thread(desc)) {
                       ret = irq_setup_forced_threading(new); ★1
                        if (ret)
                                goto out_mput;
                }
        }

        /*
         * Create a handler thread when a thread function is supplied
         * and the interrupt does not nest into another interrupt
         * thread.
         */
         if (new->thread_fn && !nested) {                       2
                ret = setup_irq_thread(new, irq, false);        3
                if (ret)
                        goto out_mput;
: (snip)
}

★1のirq_setup_forced_threading( )を下記に抜粋します。
要点は以下の通りです。
new->flagsの値、つまりrequest_irq( )の第3引数flagsにIRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOTのいずれかを指定している場合はその時点で復帰します。
例えば、タイマ割込みで使用するタイマー デバイスの割込みハンドラを登録する場合、IRQF_NO_THREADを指定してrequest_irq( )を呼び出します。そうすると、本関数ではnew->thread_fnにnew->handlerを設定せずにNULLの状態で復帰します。
一方で、IRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOTを指定していない場合はnew->thread_fnにrequest_irqの第2引数で指定した関数のアドレスをセットし、new->handlerにirq_default_primary_handler( )を設定します。
纏めると下表のとおりです。

new->thread_fn new->handler
flagsにIRQF_NO_THREAD、IRQF_PERCPU_IRQ、IRQF_ONESHOTのいずれかを設定している場合 request_irq( )の第2引数で指定した関数 irq_default_primary_handler( )
上記以外 NULL request_irq( )の第2引数で指定した関数
kernel/irq/manage.c
static int irq_setup_forced_threading(struct irqaction *new)
{
: (snip)
        if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
                return 0;
: (snip)
        /* Deal with the primary handler */
        set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
        new->thread_fn = new->handler;
        new->handler = irq_default_primary_handler;
        return 0;
}

上記内容を踏まえて、__setup_irqの★2を見てください。
new->thread_fnが存在する場合のみ★3のsetup_irq_thread( )を実行します。
では、setup_irq_thread( )で何をしているか見ましょう。

kernel/irq/manage.c
static int
setup_irq_thread(struct irqaction *new, unsigned int irq, bool secondary)
{
1        struct task_struct *t;
2        struct sched_param param = {
                .sched_priority = MAX_USER_RT_PRIO/2,
         };

3        if (!secondary) {
4                t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
5        } else {
6                t = kthread_create(irq_thread, new, "irq/%d-s-%s", irq,new->name);
7                param.sched_priority -= 1;
8        }

9        if (IS_ERR(t))
10                return PTR_ERR(t);

11        sched_setscheduler_nocheck(t, SCHED_FIFO, &param);
: (snip)
}

4行目でカーネルスレッドを生成します。その時の識別子は、"irq/IRQ番号-識別子"で前述のpsコマンドの結果と一致します。
そして、11行目でこのカーネルスレッドのスケジューリングクラスと優先度を設定します。
スケジューリングクラスはRTクラスのSCHED_FIFO、優先度はMAX_USER_RT_PRIOが100なので50になります。

このように、割込みハンドラの登録ごとにrequest_irq( )の第3引数flagsにIRQF_NO_THREAD、IRQF_PERCPU、IRQF_ONESHOTのいずれも設定されていない場合は、それぞれの割込みに対応する割込みハンドラ用スレッドを生成します。逆にこれらのフラグのいずれかが設定されている場合はスレッドを生成しません。
タイマー割込みなどプロセスの実行より常に優先すべき割込みは従来同様に割込み発生時に実行していたプロセスのスタックを使用して実行します。従って緊急度が高い割込みはIRQF_NO_THREADを設定します。

割込みハンドラの実行

ここでは割込みハンドラの実行時の動作について見ます。
ポイントを絞って記載したいと思います。
Linuxでは、
(1)アーキ固有割込み入口処理
(2)アーキ共通割込み入口処理
(3)割込みハンドラ
(4)アーキ共通割込み出口処理
(5)アーキ固有割込み出口処理
があります。

ここでは(2)の後半と(3)に注目してコードを見てみます。
(2)の一番最後に呼ばれる関数((3)を呼び出す関数)はhandle_irq_event( )です。
この関数ではhandle_irq_event_percpu( )を呼び出します。

kernel/irq/handle.c
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
        irqreturn_t ret;
: (snip)

        ret = handle_irq_event_percpu(desc);
: (snip)
}

handle_irq_event_percpu( )は以下の通りです。

kernel/irq/handle.c
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
        irqreturn_t retval;
        unsigned int flags = 0;

        retval = __handle_irq_event_percpu(desc, &flags); ★

        add_interrupt_randomness(desc->irq_data.irq, flags);

        if (!noirqdebug)
                note_interrupt(desc, retval);
        return retval;
}

本関数では__handle_irq_event_percpu( )を呼び出します。(★)

kernel/irq/handle.c
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
 1        irqreturn_t retval = IRQ_NONE;
 2        unsigned int irq = desc->irq_data.irq;
 3        struct irqaction *action;

 4        record_irq_time(desc);

 5        for_each_action_of_desc(desc, action) {
 6                irqreturn_t res;

 7               trace_irq_handler_entry(irq, action);
 8               res = action->handler(irq, action->dev_id);
 9               trace_irq_handler_exit(irq, action, res);

10                if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n", irq, action->handler))
11                        local_irq_disable();

12                switch (res) {
13                case IRQ_WAKE_THREAD:
                        /*
                         * Catch drivers which return WAKE_THREAD but
                         * did not set up a thread function
                         */
14                        if (unlikely(!action->thread_fn)) {
15                                warn_no_thread(irq, action);
16                                break;
17                        }

18                        __irq_wake_thread(desc, action);

                        /* Fall through - to add to randomness */
19                case IRQ_HANDLED:
20                        *flags |= action->flags;
21                        break;

22                default:
23                        break;
24                }

25                retval |= res;
26        }

27        return retval;
  }

まず8行目に注目してください。
ここで割込みハンドラを実行します。
8行目で実行する関数と復帰値を纏めると下表のようになります。

action->handler関数 復帰値
flagsにIRQF_NO_THREAD、IRQF_PERCPU_IRQ、IRQF_ONESHOTのいずれかを設定している場合 request_irq( )の第二引数で指定した関数 (正常終了した場合)IRQ_HANDLED
上記以外 irq_default_primary_handler( ) IRQ_WAKE_THREAD

参考までにirq_default_primary_handler( )を示します。
常にIRQ_WAKE_THREADを返すのみの関数です。

kernel/irq/manage.c
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
        return IRQ_WAKE_THREAD;
}

その後の12行目のswitch文で、スレッド化された割込みハンドラとされていない割込みハンドラでシーケンスが異なります。
スレッド化されていない従来同様の割込みハンドラは20、21行目を実行します。
スレッドした割込みハンドラは14 - 18行目を実行します。
ポイントは18行目です。

kernel/irq/manage.c
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action)
{
: (snip)
        wake_up_process(action->thread);
}

wake_up_process( )で対象の割込みハンドラ用スレッドの起床要求をします。
wake_up_process( )の動作は、別記事で紹介した通り、対象のプロセスをスケジューラの実行キューに繋ぎます。従って、ここで起床要求をしても直ぐにはスケジューリングが行われず、(5)アーキ固有割込み出口処理直後にスケジューリングが行われ、割込みハンドラ用スレッドに制御が渡ります。

注意事項

システム設計が複雑になります。デフォルトで割込みハンドラ用スレッドがSCHED_FIFO、優先度50で実行されていることを考慮した上でプロセスの優先度を決定したり、割込みハンドラをスレッド化させるべきか否か、といった従来の仕組みでは考える必要がなかった要因まで検討しなければいけません。
例えば、プロセスの優先度を最高優先度付近に設定すると、当然、デフォルトの割込みハンドラ用スレッドより当該プロセスの実行が優先されます。
このことから、リアルタイム応答性に関連する割込み/プロセス、その他の割込みハンドラ用スレッド/プロセスの優先度に関する方針を上流工程で設計しておく必要があります。

他の記事:【読解入門】Linuxカーネル (概要編)

36
35
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
35