レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ ソフト割り込み ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回はソフト割り込みについて見ていく。(*nは参考文献のn番目に対応する)

ソフト割り込みとタスクレット

割り込みハンドラであるISRは順次処理されているのに対して、遅延可能な処理は割り込みを許可した状態で行うことが可能である。遅延可能な処理を割り込みハンドラから切り離すことでカーネル応答時間を短縮することができる。

遅延処理の実装としてはソフト割り込みタスクレットがあり、その他にもワークキューで処理されるものもある。タスクレットはソフト割り込みをベースに実装されており、コードにあるsoftirqは多くの場合両方を意味する。

ソフト割り込み及びタスクレットの比較

ソフト割り込みはコンパイル時に定義し静的に割り当てられる、対してタスクレットは動的な割り当てと初期化が可能となっている(カーネルモジュールなど)。ソフト割り込みは同じ物であっても複数のCPU上で実行が可能で、再入可能ではあるがスピンロックを用いて明示的にデータを保護する必要がある。反対にタスクレットでは同一の物は順次処理され、複数のCPU上で同時に実行はできない(異なるタスクレットであれば並列実行可能)。タスクレットは順次実行されるので関数を再入可能にする必要がなくデバイスドライバの開発などの助けとなる。

遅延処理操作

遅延処理の操作には以下の4種類がある。

処理 説明
初期化(Initializaion) 遅延処理の定義。カーネルの初期化若しくはモジュールの組み込み時に行う。
起動要求(Activation) 処理を保留状態(pending)にする。次回のスケジューリングで遅延処理が実行される。
マスク(Masking) 遅延処理の禁止。起動要求があってもカーネルが当該処理を実行しない。
実行(Execution) 保留中の遅延処理を実行する。保留中の同じ種類の処理を同時に実行する。

あるCPUで起動要求された遅延処理は同じCPU上で実行する。起動要求で使用したデータを遅延処理も利用すると考えられ、システム性能面で効果的と言える(ハードウェアキャッシュ)。

ソフト割り込み

ソフト割り込みは以下のような種類がある。

// include/linux/interrupt.h
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    SCSI_SOFTIRQ,
    TASKLET_SOFTIRQ
};

詳細は以下(優先度は小さい方が高い)。

割り込みの種類 インデックス(優先度) 説明
HI_SOFTIRQ 0 優先度の高いタスクレットを処理する
TIMER_SOFTIRQ 1 タイマ割り込みに関連するソフト割り込み
NET_TX_SOFTIRQ 2 パケットをNICに送信する
NET_RX_SOFTIRQ 3 パケットをNICから受信する
SCSI_SOFTIRQ 4 SCSIコマンドの割り込み後半処理
TASKLET_SOFTIRQ 5 タスクレットの処理

データ構造

ソフト割り込みのデータ構造体はsoftirq_vec配列で以下のように定義されている。

// kernel/softirq.c
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;

上記のようにsoftirq_vecsoftirq_action構造体の配列で、32個の要素を持っておりソフト割り込みの優先度は当該配列のインデックスに対応している。ソフト割り込みは上記の表に示した通り先頭の6個のエントリを使用する。

softirq_action構造体は以下のように定義されており、actionがソフト割り込みで実行される関数、dataがその関数が使用するデータとなる。

// include/linux/interrupt.h
struct softirq_action
{
    void   (*action)(struct softirq_action *);
    void   *data;
};

カーネル内プリエンプションとカーネル実行パスの入れ子状態の把握のためにthread_info構造体のpreemt_countメンバを使用している。

// include/asm-i386/thread_info.h
struct thread_info {
    struct task_struct *task;      /* main task structure */
    struct exec_domain *exec_domain;   /* execution domain */
    unsigned long     flags;      /* low level flags */
    unsigned long     status;     /* thread-synchronous flags */
    __u32           cpu;        /* current CPU */
    __s32           preempt_count; /* 0 => preemptable, <0 => BUG */
    mm_segment_t        addr_limit; /* thread address space:
                          0-0xBFFFFFFF for user-thead
                          0-0xFFFFFFFF for kernel-thread
                       */
    struct restart_block    restart_block;
    unsigned long           previous_esp;   /* ESP of the previous stack in case
                          of nested (IRQ) stacks
                       */
    __u8            supervisor_stack[0];
};

preemt_countは以下のようにビットで情報を保持している。

ビット 名前 説明
0~7 プリエンプションカウンタ ローカルCPUでカーネルプリエンプションが明示的に禁止された回数
8~15 ソフト割り込みカウンタ 遅延処理の禁止度合いを示している(0で割り込みが許可される)
16~27 ハード割り込みカウンタ 入れ子になった割り込みハンドラの数を示している(irq_enter()で増え、irq_exit()で減る)
28 PREEMPT_ACTIVEフラグ

コメントにも上記の表と同様の記述がある。

/*
 * We put the hardirq and softirq counter into the preemption
 * counter. The bitmask has the following meaning:
 *
 * - bits 0-7 are the preemption count (max preemption depth: 256)
 * - bits 8-15 are the softirq count (max # of softirqs: 256)
 *
 * The hardirq count can be overridden per architecture, the default is:
 *
 * - bits 16-27 are the hardirq count (max # of hardirqs: 4096)
 * - ( bit 28 is the PREEMPT_ACTIVE flag. )
 *
 * PREEMPT_MASK: 0x000000ff
 * SOFTIRQ_MASK: 0x0000ff00
 * HARDIRQ_MASK: 0x0fff0000
 */

マクロとして以下のように定義されている。

#define PREEMPT_BITS    8
#define SOFTIRQ_BITS   8
#define HARDIRQ_BITS   12

#define PREEMPT_SHIFT  0
#define SOFTIRQ_SHIFT  (PREEMPT_SHIFT + PREEMPT_BITS)
#define HARDIRQ_SHIFT  (SOFTIRQ_SHIFT + SOFTIRQ_BITS)

#define __IRQ_MASK(x)  ((1UL << (x))-1)

#define PREEMPT_MASK   (__IRQ_MASK(PREEMPT_BITS) << PREEMPT_SHIFT)
#define HARDIRQ_MASK   (__IRQ_MASK(HARDIRQ_BITS) << HARDIRQ_SHIFT)
#define SOFTIRQ_MASK   (__IRQ_MASK(SOFTIRQ_BITS) << SOFTIRQ_SHIFT)

#define hardirq_count()    (preempt_count() & HARDIRQ_MASK)
#define softirq_count()    (preempt_count() & SOFTIRQ_MASK)
#define irq_count()    (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK))

カーネルプリエンプションは明示的に禁止している(プリエンプションカウンタが0でない)場合、及びカーネルが割り込みコンテキストを実行している場合には禁止する必要がある。カーネルはプリエンプションを行うことができるかどうかを確認するためにpreempt_countの値を調べる。

カーネルがハード割り込みコンテキストにいるのか、若しくはソフト割り込みコンテキストにいるのか、割り込みコンテキストにいるのかというのは以下のマクロを使用して判断する。

#define in_irq()        (hardirq_count())
#define in_softirq()       (softirq_count())
#define in_interrupt()     (irq_count())

// include/linux/preempt.h
#define preempt_count()    (current_thread_info()->preempt_count)

// include/asm-i386/thread_info.h
/* how to get the thread information struct from C */
static inline struct thread_info *current_thread_info(void)
{
    struct thread_info *ti;
    __asm__("andl %%esp,%0; ":"=r" (ti) : "0" (~(THREAD_SIZE - 1)));
    return ti;
}

カーネルがカレントプロセスのカーネルスタックを使用している場合にはthread_infopreempt_countを確認する。ハード割り込み/ソフト割り込み専用のスタックを使用する場合には以下のようにpreempt_countが設定されている。

// arch/i386/kernel/irq.c
/*
 * allocate per-cpu stacks for hardirq and for softirq processing
 */
void irq_ctx_init(int cpu)
{
    union irq_ctx *irqctx;

    if (hardirq_ctx[cpu])
        return;

    irqctx = (union irq_ctx*) &hardirq_stack[cpu*THREAD_SIZE];
    irqctx->tinfo.task              = NULL;
    irqctx->tinfo.exec_domain       = NULL;
    irqctx->tinfo.cpu               = cpu;
    irqctx->tinfo.preempt_count     = HARDIRQ_OFFSET;
    irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

    hardirq_ctx[cpu] = irqctx;

    irqctx = (union irq_ctx*) &softirq_stack[cpu*THREAD_SIZE];
    irqctx->tinfo.task              = NULL;
    irqctx->tinfo.exec_domain       = NULL;
    irqctx->tinfo.cpu               = cpu;
    irqctx->tinfo.preempt_count     = SOFTIRQ_OFFSET;
    irqctx->tinfo.addr_limit        = MAKE_MM_SEG(0);

    softirq_ctx[cpu] = irqctx;

    printk("CPU %u irqstacks, hard=%p soft=%p\n",
        cpu,hardirq_ctx[cpu],softirq_ctx[cpu]);
}

遅延処理の禁止に用いてるマスク値はirq_cpustat_t構造体の__softirq_pendingメンバが保持している。当該構造体はCPU毎に存在する。

typedef struct {
    unsigned int __softirq_pending;
    unsigned long idle_timestamp;
    unsigned int __nmi_count; /* arch dependent */
    unsigned int apic_timer_irqs; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;

__softirq_pendingメンバの取得及び設定には以下のlocal_softirq_pendingマクロを用いる。当該メンバは32bitのマスクとなっている。

// include/linux/irq_cpustat.h
extern irq_cpustat_t irq_stat[];       /* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member)    (irq_stat[cpu].member)

  /* arch independent irq_stat fields */
#define local_softirq_pending() \
   __IRQ_STAT(smp_processor_id(), __softirq_pending)

ソフト割り込みの初期化

ソフト割り込みの初期化にはopen_softirq()を使用する。

// kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
    softirq_vec[nr].data = data;
    softirq_vec[nr].action = action;
}

引数にはソフト割り込みのインデックス、ソフト割り込み関数ポインタ、当該関数が使用するデータへのポインタを渡す。

ソフト割り込みの起動要求

割り込み処理の起動要求はraise_softirq()を用いて行う。

// kernel/softirq.c
void fastcall raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

// include/asm-i386/system.h
#define local_irq_save(x)  __asm__ __volatile__("pushfl ; popl %0 ; cli":"=g" (x): /* no input */ :"memory")
#define local_irq_restore(x)   do { typecheck(unsigned long,x); __asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"g" (x):"memory", "cc"); } while (0)

当該関数ではまずlocal_irq_save()マクロでEFLAGSレジスタを値をスタックに退避し(pushfl)、当該レジスタのIFフラグをクリアし割り込みを禁止する。raise_softirq_irqoff()を呼び出し後、local_irq_restore()マクロでEFLAGSレジスタ値をスタックからリストアする。

raise_softirq_irqoff()の定義は以下。

// kernel/softirq.c
/*
 * This function must run with irqs disabled!
 */
inline fastcall void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

    /*
    * If we're in an interrupt or softirq, we're done
    * (this also catches softirq-disabled code). We will
    * actually run the softirq once we return from
    * the irq or softirq.
    *
    * Otherwise we wake up ksoftirqd to make sure we
    * schedule the softirq soon.
    */
    if (!in_interrupt())
        wakeup_softirqd();
}

// include/linux/interrupt.h
#define __raise_softirq_irqoff(nr) do { local_softirq_pending() |= 1UL << (nr); } while (0)

まずlocal_softirq_pending()マクロでソフト割り込みのビットマスク値を更新する。次にin_interrupt()マクロで割り込みが禁止されているかを確認する。禁止されていなければwakeup_softirqd()でローカルCPUのksoftirqdを起床させる。

// kernel/softirq.c
/*
 * we cannot loop indefinitely here to avoid userspace starvation,
 * but we also don't want to introduce a worst case 1/HZ latency
 * to the pending events, so lets the scheduler to balance
 * the softirq load for us.
 */
static inline void wakeup_softirqd(void)
{
    /* Interrupts are disabled: no need to stop preemption */
    struct task_struct *tsk = __get_cpu_var(ksoftirqd);

    if (tsk && tsk->state != TASK_RUNNING)
        wake_up_process(tsk);
}

ソフト割り込み発生の確認

カーネルは定期的にソフト割り込みが発生していないかを以下のような箇所で確認する。

  • local_bh_enable()を呼び出し、ローカルCPUのソフト割り込み許可時。
  • do_IRQ()irq_exit()を呼び出し時。
  • smp_apic_timer_interrupt()がローカルタイマ割り込み終了時。
  • CALL_FUNCTION_VECTORプロセッサ割り込みで起動した関数の終了時。
  • ksoftirqdが起動した時。

do_softirq()

ソフト割り込みを検出(前述のlocal_softirq_pendingマクロを使用 )した場合、カーネルdo_softirq()を呼び出してソフト割り込みを処理する。

以下のように定義されている。

// kernel/softirq.c
asmlinkage void do_softirq(void)
{
    __u32 pending;
    unsigned long flags;

    if (in_interrupt())
        return;

    local_irq_save(flags);

    pending = local_softirq_pending();

    if (pending)
        __do_softirq();

    local_irq_restore(flags);
}

in_interrupt()thread_info構造体のpreempt_countメンバを参照しており1を返す(割り込みコンテキストでの呼び出しまたはソフト割り込みが禁止されている)場合、当該関数の実行を終了する。

local_irq_save(flags)EFLAGレジスタのIFフラグを退避し、ローカルCPUに対する割り込みを禁止する。

local_softirq_pending()でソフト割り込みの発生を確認し必要であれば処理する。

その後local_irq_restore(flags)EFLAGレジスタのIFフラグをリストアする。

__do_softirq()

CPUのビットマスクを確認し対応する遅延処理を行う。複数のソフト割り込みが発生している場合は一度で複数の処理を実行するがその回数はMAX_SOFTIRQ_RESTARTで設定されている(ユーザプロセスの実行が遅れるため)。実行できなかった割り込みはksoftirqdが処理する。

以下のように定義されている。

// kernel/softirq.c
#define MAX_SOFTIRQ_RESTART 10

asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    int max_restart = MAX_SOFTIRQ_RESTART;
    int cpu;

    pending = local_softirq_pending();

    local_bh_disable();
    cpu = smp_processor_id();
restart:
    /* Reset the pending bitmask before enabling irqs */
    local_softirq_pending() = 0;

    local_irq_enable();

    h = softirq_vec;

    do {
        if (pending & 1) {
            h->action(h);
            rcu_bh_qsctr_inc(cpu);
        }
        h++;
        pending >>= 1;
    } while (pending);

    local_irq_disable();

    pending = local_softirq_pending();
    if (pending && --max_restart)
        goto restart;

    if (pending)
        wakeup_softirqd();

    __local_bh_enable();
}

まず最大の繰り返し回数として定義されているMAX_SOFTIRQ_RESTARTmax_restartに代入し、local_softirq_pending()で割り込みのビットマスクを取得する。

次にlocal_bh_disable()でソフト割り込みの実行を禁止する。当該関数は以下のように定義されており、preemp_count()を通してthread_indo構造体のpreempt_countを増加している。

// include/linux/interrupt.h
#define local_bh_disable() \
       do { add_preempt_count(SOFTIRQ_OFFSET); barrier(); } while (0)
#define __local_bh_enable() \
       do { barrier(); sub_preempt_count(SOFTIRQ_OFFSET); } while (0)

// include/linux/preempt.h
# define add_preempt_count(val)    do { preempt_count() += (val); } while (0)
# define sub_preempt_count(val)    do { preempt_count() -= (val); } while (0)

上記によりdo_softirq()in_interrupt()で1が返るようになり、ソフト割り込み処理の実行を禁止できるのがわかる。遅延処理はCPU上で順に処理していく必要があるため複数スレッドで動作することを回避しなければならない。

次にlocal_softirq_pending()でビットマスクをリセットし、h = softirq_vecでソフト割り込み用のベクタテーブルを取得する。

以下の反復処理ではビットマスクに対応した関数を順次実行していく。

do {
    if (pending & 1) {
        h->action(h);
        rcu_bh_qsctr_inc(cpu);
    }
    h++;
    pending >>= 1;
} while (pending);

上記の反復処理後である以下の箇所では再度割り込みの発生を確認している。最大反復回数であるmax_restartが0でなく且つ割り込みが存在すれば再度処理する。割り込みが発生しているが最大反復回数の処理が行われていた場合にはソフト割り込みを処理するためのカーネルスレッドがwakeup_softirqd()で呼ばれる。`

pending = local_softirq_pending();
if (pending && --max_restart)
    goto restart;

if (pending)
    wakeup_softirqd();

最後に__local_bh_enable()で割り込みを許可する。

ksoftirqd

ソフト割り込み処理で完了しなかったソフト割り込みを処理するカーネルスレッドであり、CPU毎に当該スレッドを保持している。当該スレッドはksoftirqd()関数を実行する。

// kernel/softirq.c
static int ksoftirqd(void * __bind_cpu)
{
    set_user_nice(current, 19); // 優先度を下げる
    current->flags |= PF_NOFREEZE;

    set_current_state(TASK_INTERRUPTIBLE); // インタラプト可能に

    while (!kthread_should_stop()) { // スレッドにstopがかかっていない間
        if (!local_softirq_pending()) // ソフト割り込みがなければ
            schedule(); // 再度スケジューリング

        __set_current_state(TASK_RUNNING);

        while (local_softirq_pending()) { // ソフト割り込みが存在する
            /* Preempt disable stops cpu going offline.
              If already offline, we'll be on wrong CPU:
              don't process */
            preempt_disable();
            if (cpu_is_offline((long)__bind_cpu))
                goto wait_to_die;
            do_softirq(); // ソフト割り込みを処理
            preempt_enable();
            cond_resched();
        }

        set_current_state(TASK_INTERRUPTIBLE);
    }
    __set_current_state(TASK_RUNNING);
    return 0;

wait_to_die:
    :
    (省略)
    :
}

上記のpreempt_disable()およびpreempt_enable()は以下のように定義されており、内部でpreempt_count()を使用しているのがわかる。

// include/linux/preempt.h
# define add_preempt_count(val)    do { preempt_count() += (val); } while (0)
# define sub_preempt_count(val)    do { preempt_count() -= (val); } while (0)

#define inc_preempt_count() add_preempt_count(1)
#define dec_preempt_count() sub_preempt_count(1)
#define preempt_disable() \
do { \
   inc_preempt_count(); \
   barrier(); \
} while (0)
#define preempt_enable() \
do { \
   preempt_enable_no_resched(); \
   preempt_check_resched(); \
} while (0)

仮にksoftirqカーネルスレッドがない場合、大量の割り込みが発生した際には2つの解決策が考えられる。1つ目はdo_softirq()中に発生した割り込みを無視する方法。これは大量の通信をする際にパケットをこぼしてしまうという問題が発生する。2つ目は発生している割り込みが存在する限り常にソフト割り込み処理を実行し続けるという方法。これは大量の通信をさばくという観点は効率が良いがその間ユーザプロセスは動作しなくなってしまう。この2つの問題を両方解決するのがksoftirqdという事になる。優先度(nice値)も低く設定されているためたとえ割り込みが大量に発生した場合においてもユーザプロセスの実行を妨げるようなことにはならない。

タスクレット

タスクレットはソフト割り込みの種類のうち、HI_SOFTIRQ及びTASKLET_SOFTIRQから実装されている。通常のソフト割り込みはsoftirq_vec配列をイテレートし関数を実行していくのに対してタスクレットでは前述の2種類に対してのみ処理を行う。

タスクレットはI/Oドライバが利用する遅延処理に適している。

データ構造

優先度の高いタスクレットはtasklet_hi_vecに、低いタスクレットはtasklet_vecがリストとして保持している。

// kernel/softirq.c
/* Tasklets */
struct tasklet_head
{
    struct tasklet_struct *list;
};

/* Some compilers disobey section attribute on statics when not
   initialized -- RR */
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec) = { NULL };
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec) = { NULL };

タスクレットの構造体は以下のように定義されている。

// include/linux/interrupt.h
struct tasklet_struct
{
    struct tasklet_struct *next; // リスト内の次の要素
    unsigned long state; // 状態
    atomic_t count; // ロック用カウンタ
    void (*func)(unsigned long); // タスクレット関数へのポインタ
    unsigned long data; // タスクレット関数の引数
};

stateの状態は以下のように定義されている。

// include/linux/interrupt.h
enum
{
    TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */
    TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */
};

初期化

タスクレットを使用する際には上記の構造体を作成し、tasklet_init()にその構造体及び関数、その関数で使用するデータを渡す。

// kernel/softirq.c
void tasklet_init(struct tasklet_struct *t,
          void (*func)(unsigned long), unsigned long data)
{
    t->next = NULL;
    t->state = 0;
    atomic_set(&t->count, 0);
    t->func = func;
    t->data = data;
}

タスクレットの使用を禁止するにはtasklet_disable_nosync()またはtasklet_disable()を用いる。

static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
    atomic_inc(&t->count);
    smp_mb__after_atomic_inc();
}

static inline void tasklet_disable(struct tasklet_struct *t)
{
    tasklet_disable_nosync(t);
    tasklet_unlock_wait(t);
    smp_mb();
}

上記を見てもcountメンバを使用し実装しているのがわかる。

再び使用を許可する場合にはtasklet_enable()を使用する。

static inline void tasklet_enable(struct tasklet_struct *t)
{
    smp_mb__before_atomic_dec();
    atomic_dec(&t->count);
}

起動及び実行

タスクレットの起動にはtasklet_schedule()またはtasklet_hi_schedule()を使用する。

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_schedule(t);
}

static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
        __tasklet_hi_schedule(t);
}

内部で呼び出されている関数__tasklet_schedule()の定義は以下。

void fastcall __tasklet_schedule(struct tasklet_struct *t)
{
    unsigned long flags;

    local_irq_save(flags);
    t->next = __get_cpu_var(tasklet_vec).list;
    __get_cpu_var(tasklet_vec).list = t;
    raise_softirq_irqoff(TASKLET_SOFTIRQ);
    local_irq_restore(flags);
}

raise_softirq_irqoff()ksoftirqdをスケジュールすることがわかる。

前述のksoftirqdが実行するdo_softirq()ではソフト割り込みに対応する関数を実行するため、HI_SOFTIRQではtasklet_hi_action()TASKLET_SOFTIRQではtasklet_action()が実行される。

どちらも類似した処理を行なっている。ここではtasklet_action()の定義を示す。

// kernel/softirq.c
static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;

    local_irq_disable(); // ロック取得
    list = __get_cpu_var(tasklet_vec).list; // リスト取得
    __get_cpu_var(tasklet_vec).list = NULL; // リストの内容を解放
    local_irq_enable(); // ロック解放

    while (list) { // リストの要素が存在する間
        struct tasklet_struct *t = list;
        list = list->next;

        if (tasklet_trylock(t)) { // ロック
            if (!atomic_read(&t->count)) {
                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
                    BUG();
                t->func(t->data); // 関数
                tasklet_unlock(t); // アンロック
                continue; // 関数の実行に成功!!
            }
            tasklet_unlock(t); // アンロック
        }
        
        /* タスクレットが実行できなかった場合には再度ソフト割り込みを発生させ遅延実行を試みる */
        local_irq_disable();
        t->next = __get_cpu_var(tasklet_vec).list;
        __get_cpu_var(tasklet_vec).list = t;
        __raise_softirq_irqoff(TASKLET_SOFTIRQ);
        local_irq_enable();
    }
}

// include/linux/interrupt.h
static inline int tasklet_trylock(struct tasklet_struct *t)
{
    // TASKLET_STATE_RUNビットが立っている場合他のCPUで当該タスクレットが実行されている。
    return !test_and_set_bit(TASKLET_STATE_RUN, &(t)->state);
}

ワークキュー

ワークキューに存在するカーネル関数を、起動要求を行い起動したワーカスレッドというカーネルスレッドに実行させる機能である。遅延処理とは異なりワークキューはカーネルスレッドで実行されるためプロセスコンテキストで動作する、そのため実行が中断する可能性がある関数を実行することができる。

データ構造

ワークキュー用の主なデータ構造はworkqueue_struct

// kernel/workqueue.c
/*
 * The externally visible workqueue abstraction is an array of
 * per-CPU workqueues:
 */
struct workqueue_struct {
    struct cpu_workqueue_struct cpu_wq[NR_CPUS];
    const char *name;
    struct list_head list;     /* Empty if single thread */
};

workqueue_structはCPUの個数cpu_workqueue_structを保持している。

// kernel/workqueue.c
/*
 * The per-CPU workqueue (if single thread, we always use cpu 0's).
 *
 * The sequence counters are for flush_scheduled_work().  It wants to wait
 * until until all currently-scheduled works are completed, but it doesn't
 * want to be livelocked by new, incoming ones.  So it waits until
 * remove_sequence is >= the insert_sequence which pertained when
 * flush_scheduled_work() was called.
 */
struct cpu_workqueue_struct {

    spinlock_t lock; // スピンロック

    long remove_sequence;  /* 最後に追加したシーケンス番号 */
    long insert_sequence;  /* 次の追加で使用するシーケンス番号 */

    struct list_head worklist; // 保留中関数リストの先頭
    wait_queue_head_t more_work;
    wait_queue_head_t work_done;

    struct workqueue_struct *wq; // このデータを保持している`workqueue_struct`をさす
    task_t *thread; // この構造体用のワーカスレッドのプロセスディスクリプタ

    int run_depth;     /* Detect run_workqueue() recursion depth */
} ____cacheline_aligned;

worklistは保留中の関数をリスト形式で登録しておくメンバで保留中の関数はwork_struct構造体で管理される。

// include/linux/workqueue.h
struct work_struct {
    unsigned long pending; // ワークキュー内に存在する場合1、ない場合0を返す
    struct list_head entry; // 保留中の関数の双方向リストの
    void (*func)(void *); // 関数へのポインタ
    void *data; // 関数で使用するデータ
    void *wq_data; // 親である`cpu_workqueue_struct`を指す
    struct timer_list timer; // 遅延実行のためのソフトウェアタイマ
};

ワークキューの作成

create_workqueue()を用いて行う。定義は以下。

// include/linux/workqueue.h
#define create_workqueue(name) __create_workqueue((name), 0)

// kernel/workqueue.c
struct workqueue_struct *__create_workqueue(const char *name,
                        int singlethread)
{
    int cpu, destroy = 0;
    struct workqueue_struct *wq;
    struct task_struct *p;

    BUG_ON(strlen(name) > 10);

    /* カーネル空間にワークキューディスクリプタ分のメモリを確保 */
    wq = kmalloc(sizeof(*wq), GFP_KERNEL);
    if (!wq)
        return NULL;
    memset(wq, 0, sizeof(*wq));

    wq->name = name;
    
    lock_cpu_hotplug();
    /* シングルスレッドのフラグが立っている場合にはスレッドは1つのみとなる */
    if (singlethread) {
        INIT_LIST_HEAD(&wq->list);
        p = create_workqueue_thread(wq, 0);
        if (!p)
            destroy = 1;
        else
            wake_up_process(p);
    /* 指定のない場合にはCPUの個数だけ作成される */
    } else {
        spin_lock(&workqueue_lock);
        list_add(&wq->list, &workqueues);
        spin_unlock(&workqueue_lock);
        for_each_online_cpu(cpu) {
            p = create_workqueue_thread(wq, cpu);
            if (p) {
                kthread_bind(p, cpu);
                wake_up_process(p);
            } else
                destroy = 1;
        }
    }
    unlock_cpu_hotplug();

    /*
    * Was there any error during startup? If yes then clean up:
    */
    if (destroy) {
        destroy_workqueue(wq);
        wq = NULL;
    }
    return wq;
}

実行関数の登録

work_structが保持している関数をキューに登録するにはqueue_work()を使用する。

// kernel/workqueue.c
/*
 * Queue work on a workqueue. Return non-zero if it was successfully
 * added.
 *
 * We queue the work to the CPU it was submitted, but there is no
 * guarantee that it will be processed by that CPU.
 */
int fastcall queue_work(struct workqueue_struct *wq, struct work_struct *work)
{
    int ret = 0, cpu = get_cpu();

    if (!test_and_set_bit(0, &work->pending)) { // 既に登録されていないかを確認
        if (unlikely(is_single_threaded(wq))) // シングルスレッドの場合には0
            cpu = 0;
        BUG_ON(!list_empty(&work->entry));
        __queue_work(wq->cpu_wq + cpu, work); // キューに追加
        ret = 1;
    }
    put_cpu();
    return ret;
}

/* Preempt must be disabled. */
static void __queue_work(struct cpu_workqueue_struct *cwq,
             struct work_struct *work)
{
    unsigned long flags;

    spin_lock_irqsave(&cwq->lock, flags); // プリエンプトを回避するためスピンロックを取得。
    work->wq_data = cwq;
    list_add_tail(&work->entry, &cwq->worklist); // リストに登録
    cwq->insert_sequence++; // 追加用のシーケンス番号をインクリメント
    wake_up(&cwq->more_work);
    spin_unlock_irqrestore(&cwq->lock, flags);
}

ワーカスレッド

ワーカスレッドは以下の関数を実行する。処理はシンプルでリストに作業が登録されていれば実行するというものになっている。

static int worker_thread(void *__cwq)
{
    struct cpu_workqueue_struct *cwq = __cwq;
    :
    (省略)
    :
    set_current_state(TASK_INTERRUPTIBLE);
    while (!kthread_should_stop()) {
        add_wait_queue(&cwq->more_work, &wait);
        if (list_empty(&cwq->worklist)) // リストを確認
            schedule(); // 作業が存在しなければ再度スケジューリング
        else
            __set_current_state(TASK_RUNNING);
        remove_wait_queue(&cwq->more_work, &wait);

        if (!list_empty(&cwq->worklist))
            run_workqueue(cwq); // 作業が存在すれば実行
        set_current_state(TASK_INTERRUPTIBLE);
    }
    __set_current_state(TASK_RUNNING);
    return 0;
}

実際に作業を実行する関数であるrun_workqueue()は以下。リストから実際に作業を取り出し実行する。

static inline void run_workqueue(struct cpu_workqueue_struct *cwq)
{
    unsigned long flags;

    spin_lock_irqsave(&cwq->lock, flags);
    :
    (省略)
    :
    while (!list_empty(&cwq->worklist)) {
        struct work_struct *work = list_entry(cwq->worklist.next,
                        struct work_struct, entry); // リストから作業を取り出す
        void (*f) (void *) = work->func; // 関数
        void *data = work->data; // そのデータ

        list_del_init(cwq->worklist.next); // 作業リストから実行対象を削除
        spin_unlock_irqrestore(&cwq->lock, flags); // ロック

        clear_bit(0, &work->pending); // リストに登録されていることを示すフラグをクリア
        f(data); // 作業を実行

        spin_lock_irqsave(&cwq->lock, flags); // アンロック
        cwq->remove_sequence++; // 削除用のシーケンス番号を更新
        wake_up(&cwq->work_done);
    }
    spin_unlock_irqrestore(&cwq->lock, flags);
}

既存のワークキュー

カーネルはeventsというワークキューをあらかじめ用意しており、そのworkqueue_structディスクリプタkeventd_wqが保持している。

// kernel/workqueue.c
static struct workqueue_struct *keventd_wq;

この既存のワークキューに処理を登録するにはschedule_work()を使用する。

// kernel/workqueue.c
int fastcall schedule_work(struct work_struct *work)
{
    return queue_work(keventd_wq, work);
}

定義を見ると前述のqueue_work()が用いられており第一引数に来るワークキューにkeventd_wqを指定しているのがわかる。

あらかじて用意されているワークキューを用いてることで滅多に呼び出すことのない関数の実行などに対してシステム資源の消費を抑えることが可能となる。

割り込み及びソフト割り込みの復帰処理

復帰処理は主に割り込みの発生によって中断した処理を再開することだが、ここにはいくつか考慮すべき点がある。

  • 実行されているカーネルモードでの処理が何回ネストされた状態であるか(ネストしている回数が1の場合には復帰後ユーザモードに戻る)
  • スケジューリング要求の有無
  • 保留中のシグナルの有無
  • 復帰後の遷移対象ユーザプロセスがデバッガによってトレースされていないか

上記の項目の確認にはthread_info構造体のflagsメンバの値を参照すれば良い。

// include/asm-i386/thread_info.h
/*
 * thread information flags
 * - these are process state flags that various assembly files may need to access
 * - pending work-to-be-done flags are in LSW
 * - other flags in MSW
 */
#define TIF_SYSCALL_TRACE  0  /* syscall trace active */
#define TIF_NOTIFY_RESUME  1  /* resumption notification requested */
#define TIF_SIGPENDING     2  /* signal pending */
#define TIF_NEED_RESCHED   3  /* rescheduling necessary */
#define TIF_SINGLESTEP     4  /* restore singlestep on return to user mode */
#define TIF_IRET       5  /* return with iret */
#define TIF_SYSCALL_AUDIT  7  /* syscall auditing active */
#define TIF_POLLING_NRFLAG 16 /* true if poll_idle() is polling TIF_NEED_RESCHED */
#define TIF_MEMDIE     17
フラグ名 意味
TIF_SYSCALL_TRACE システムコールがトレースされている
TIF_SIGPENDING 保留中のシグナルが存在する
TIF_NEED_RESCHED スケジューリングの必要がある
TIF_SINGLESTEP シングルステップ実行されている
TIF_IRET iretを用いたシステムコールからの復帰
TIF_SYSCALL_AUDIT システムコールが監視されている
TIF_POLLING_NRFLAG idleプロセスがTIF_NEED_RESCHEDをポーリングしている
TIF_MEMDIE プロセスがメモリ回収のため削除されている

自身の過去のエントリを読んでもらうとわかるが

https://k-onishi.hatenablog.jp/entry/2019/01/14/195344

https://k-onishi.hatenablog.jp/entry/2019/02/12/005331

例外からの復帰はret_from_exceptionに処理が遷移し

error_code:
    pushl %ds
    pushl %eax
    xorl %eax, %eax
    pushl %ebp
    pushl %edi
    pushl %esi
    pushl %edx
    decl %eax           # eax = -1
    pushl %ecx
    pushl %ebx
    cld
    movl %es, %ecx
    movl ES(%esp), %edi     # get the function address
    movl ORIG_EAX(%esp), %edx   # get the error code
    movl %eax, ORIG_EAX(%esp)
    movl %ecx, ES(%esp)
    movl $(__USER_DS), %ecx
    movl %ecx, %ds
    movl %ecx, %es
    movl %esp,%eax          # pt_regs pointer
    call *%edi
    jmp ret_from_exception

割り込みからの復帰はret_from_intrに処理が遷移する。

common_interrupt:
    SAVE_ALL
    movl %esp,%eax
    call do_IRQ
    jmp ret_from_intr

ret_from_exception及びret_from_intr

これらの処理は以下のように定義されている。

// arch/i386/kernel/entry.S
#define preempt_stop       cli

CS      = 0x2C
VM_MASK     = 0x00020000

ret_from_exception:
    preempt_stop    # 割り込みから復帰時のみ割り込みを禁止する。
ret_from_intr:
    GET_THREAD_INFO(%ebp)   # ベースポインタからthread_info構造体を取得
    movl EFLAGS(%esp), %eax # EFLAGレジスタ及び
    movb CS(%esp), %al # csを同時にチェックする
    testl $(VM_MASK | 3), %eax # EFLAGレジスタのVM_MASKが立っているか
    jz resume_kernel    # %eaxの値が0の場合カーネルモードでの処理を続ける。
    # この先はユーザモードプロセスへの復帰となる
    :
    (省略)
    :

// include/asm-i386/thread_info.h
/* how to get the thread information struct from ASM */
#define GET_THREAD_INFO(reg) \
   movl $-THREAD_SIZE, reg; \
   andl %esp, reg

// arch/x86_64/kernel/ptrace.c
/*
 * eflags and offset of eflags on child stack..
 */
#define EFLAGS offsetof(struct pt_regs, eflags)

処理としてはthread_info構造体を取得した後efagsレジスタとcsレジスタの値をチェックし0(割り込み元がカーネルモードでの処理)である場合にはカーネルモードの処理へと復帰し。0でない場合にはユーザモードの処理へと復帰する。

カーネルモードの処理への復帰

以下の処理ではプリエンプションが可能でなければそのまま復帰し(後述)、必要であれば再度スケジューリングを行う。

ENTRY(resume_kernel)
    cli
    cmpl $0,TI_preempt_count(%ebp) # プリエンプションカウンタが0(カーネル内プリンプションが可能)かどうか
    jnz restore_all # でなければそのまま復帰する(後述)
need_resched: # 可能な場合
    movl TI_flags(%ebp), %ecx   # フラグを取得
    testb $_TIF_NEED_RESCHED, %cl   #  再度スケジューリングが必要かどうか
    jz restore_all
    testl $IF_MASK,EFLAGS(%esp)     # 例外からの復帰かどうか
    jz restore_all
    call preempt_schedule_irq # スケジューリング
    jmp need_resched

実際にスケジューリングを行うのはpreempt_schedule_irq()となる。

// kernel/sched.c
/*
 * this is is the entry point to schedule() from kernel preemption
 * off of irq context.
 * Note, that this is called and return with irqs disabled. This will
 * protect us against recursive calling from irq.
 */
asmlinkage void __sched preempt_schedule_irq(void)
{
    :
    schedule();

ユーザプロセスへの復帰

eflagレジスタを確認しデバッガが存在すれば必要な処理(work_pending)を実行し、そうでなければそのまま復帰する(restore_all)。

// arch/i386/kernel/entry.S
ENTRY(resume_userspace)
    cli             # 割り込みを禁止
    movl TI_flags(%ebp), %ecx # eflagsレジスタを取得
    andl $_TIF_WORK_MASK, %ecx  # デバッガが張り付いていないかを確認
    jne work_pending # フラグが立っていれば必要な処理に遷移
    jmp restore_all # 立っていな場合はそのまま復帰する

// arch/i386/kernel/asm-offsets.c
OFFSET(TI_flags, thread_info, flags);

#define OFFSET(sym, str, mem) \
   DEFINE(sym, offsetof(struct str, mem));
 
// include/asm-i386/thread_info.h
#define _TIF_WORK_MASK \
  (0x0000FFFF & ~(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT|_TIF_SINGLESTEP))

work_pendingは以下のように定義されている。

// arch/i386/kernel/entry.S
work_pending:
    testb $_TIF_NEED_RESCHED, %cl
    jz work_notifysig
work_resched:
    call schedule
    cli             # 割り込みを禁止
    movl TI_flags(%ebp), %ecx
    andl $_TIF_WORK_MASK, %ecx  # is there any work to be done other
                    # than syscall tracing?
    jz restore_all
    testb $_TIF_NEED_RESCHED, %cl
    jnz work_resched

work_pendingはスケジューリングの必要がなければwork_notifysigへと遷移する。もし必要があれば処理はwork_reschedへと続きschedule()を呼び出した後、必要があれば再度スケジューリングを行う。

work_notifysigは以下のように定義されている。

work_notifysig:            # deal with pending signals and
                    # notify-resume requests
    testl $VM_MASK, EFLAGS(%esp)
    movl %esp, %eax
    jne work_notifysig_v86      # returning to kernel-space or
                    # vm86-space
    xorl %edx, %edx
    call do_notify_resume
    jmp restore_all

do_notify_resume()を呼び出しシグナルの処理とシングルステップ実行を行う。

// arch/i386/kernel/signal.c
/*
 * notification of userspace execution resumption
 * - triggered by current->work.notify_resume
 */
__attribute__((regparm(3)))
void do_notify_resume(struct pt_regs *regs, sigset_t *oldset,
              __u32 thread_info_flags)
{
    :