読者です 読者をやめる 読者になる 読者になる

Ichigo Exception <3

スマブラDXと、Linuxカーネルと、日々のつらい気持ち

割り込みアフィニティ

割り込みは物理的には、割り込みコントローラ(PIC: Programmable Interrupt Controller)によって CPU の割り込みピンがアサートされることで発生します(内部割り込みは違います)。SMP(マルチプロセッサ)環境では、割り込みのアフィニティ(どの CPU に割り込みを上げるのか)を決定するため、PIC の Destination Register をセットする必要があります。現代の普通の CPU では、コア数に対応した PIC がチップ内に収められているので、基板を見ても存在しません。古いドキュメントでは、単独チップの Intel 8259 が割り込みの説明に使われているようです。

割り込みを上げる CPU を決定するのは、Linux では request_irqIRQ を登録するタイミングです。ここでは IRQ は、CPU の物理的なピンを指しています。Linux の /proc/interrupts で見える番号(virq)は、IRQ に対応した仮想的な値です。 以下、カーネルバージョン 2.6.39 、CPU は e500mc コア、PIC は MPIC の例です。

/kernel/irq/manage.c
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
                     irq_handler_t thread_fn,
                     unsigned long flags, const char *name, void *dev);

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_irq は 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;
        struct irq_desc *desc;
        int retval;
        /*
         * Sanity-check: shared interrupts must pass in a real dev-ID,
         * otherwise we'll have trouble later trying to figure out
         * which interrupt is which (messes up the interrupt freeing
         * logic etc).
         */
        if ((irqflags & IRQF_SHARED) && !dev_id)
                return -EINVAL;
        /* desc 番号を取っておく */
        desc = irq_to_desc(irq);
        if (!desc)
                return -EINVAL;
        if (!irq_settings_can_request(desc))
                return -EINVAL;
        if (!handler) {
                if (!thread_fn)
                        return -EINVAL;
                handler = irq_default_primary_handler;
        }
        action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
        if (!action)
                return -ENOMEM;
        action->handler = handler;
        /*
         * request_irq だと thread_fn は NULL
         * thread_fn はカーネルスレッドで実行される
         */
        action->thread_fn = thread_fn;
        action->flags = irqflags;
        action->name = devname;
        action->dev_id = dev_id;
        chip_bus_lock(desc);
        /* action を desc テーブルに登録する */
        retval = __setup_irq(irq, desc, action);
        chip_bus_sync_unlock(desc);
        if (retval)
                kfree(action);

        /* 略 */
        return retval;
}

割り込みハンドラ、フラグ、デバイス名等は、desc テーブル(割り込みディスクリプタ)に登録されます。

/kernel/irq/manage.c
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
        struct irqaction *old, **old_ptr;
        const char *old_name = NULL;
        unsigned long flags, thread_mask = 0;
        int ret, nested, shared = 0;
        cpumask_var_t mask;

        if (!desc)
                return -EINVAL;

        if (desc->irq_data.chip == &no_irq_chip)
                return -ENOSYS;
        /*
         * Some drivers like serial.c use request_irq() heavily,
         * so we have to be careful not to interfere with a
         * running system.
         */
        if (new->flags & IRQF_SAMPLE_RANDOM) {
                /*
                 * This function might sleep, we want to call it first,
                 * outside of the atomic block.
                 * Yes, this might clear the entropy pool if the wrong
                 * driver is attempted to be loaded, without actually
                 * installing a new handler, but is this really a problem,
                 * only the sysadmin is able to do this.
                 */
                rand_initialize_irq(irq);
        }

        /* 略 thread_fn 関連 */

        if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {
                ret = -ENOMEM;
                goto out_thread;
        }

        /*
         * The following block of code has to be executed atomically
         */
        /* IRQ 共有時のチェック */
        raw_spin_lock_irqsave(&desc->lock, flags);
        old_ptr = &desc->action;
        old = *old_ptr;
        if (old) {
                /*
                 * Can't share interrupts unless both agree to and are
                 * the same type (level, edge, polarity). So both flag
                 * fields must have IRQF_SHARED set and the bits which
                 * set the trigger type must match. Also all must
                 * agree on ONESHOT.
                 */
                /*
                 * 異なるタイプの割り込みを SHARED で登録することはできない
                 * 例えば、エッジトリガとレベルトリガのデバイスが同じIRQ番号を共有することはできない
                 */
                if (!((old->flags & new->flags) & IRQF_SHARED) ||
                    ((old->flags ^ new->flags) & IRQF_TRIGGER_MASK) ||
                    ((old->flags ^ new->flags) & IRQF_ONESHOT)) {
                        old_name = old->name;
                        goto mismatch;
                }

                /* All handlers must agree on per-cpuness */
                if ((old->flags & IRQF_PERCPU) !=
                    (new->flags & IRQF_PERCPU))
                        goto mismatch;

                /* add new interrupt at end of irq queue */
                do {
                        thread_mask |= old->thread_mask;
                        old_ptr = &old->next;
                        old = *old_ptr;
                } while (old);
                shared = 1;
        }

        /*
         * Setup the thread mask for this irqaction. Unlikely to have
         * 32 resp 64 irqs sharing one line, but who knows.
         */
        if (new->flags & IRQF_ONESHOT && thread_mask == ~0UL) {
                ret = -EBUSY;
                goto out_mask;
        }
        new->thread_mask = 1 << ffz(thread_mask);

        if (!shared) {
                init_waitqueue_head(&desc->wait_for_threads);

                /* Setup the type (level, edge polarity) if configured: */
                if (new->flags & IRQF_TRIGGER_MASK) {
                        ret = __irq_set_trigger(desc, irq,
                                        new->flags & IRQF_TRIGGER_MASK);

                        if (ret)
                                goto out_mask;
                }

                desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \
                                  IRQS_ONESHOT | IRQS_WAITING);
                irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

                if (new->flags & IRQF_PERCPU) {
                        irqd_set(&desc->irq_data, IRQD_PER_CPU);
                        irq_settings_set_per_cpu(desc);
                }

                if (new->flags & IRQF_ONESHOT)
                        desc->istate |= IRQS_ONESHOT;

                if (irq_settings_can_autoenable(desc))
                        irq_startup(desc);
                else
                        /* Undo nested disables: */
                        desc->depth = 1;

                /* Exclude IRQ from balancing if requested */
                if (new->flags & IRQF_NOBALANCING) {
                        irq_settings_set_no_balancing(desc);
                        irqd_set(&desc->irq_data, IRQD_NO_BALANCING);
                }

                /* Set default affinity mask once everything is setup */
                /* ここで割り込みを上げる CPU の初期値を決定する */
                setup_affinity(irq, desc, mask);

        } else if (new->flags & IRQF_TRIGGER_MASK) {
                unsigned int nmsk = new->flags & IRQF_TRIGGER_MASK;
                unsigned int omsk = irq_settings_get_trigger_mask(desc);

                if (nmsk != omsk)
                        /* hope the handler works with current  trigger mode */
                        pr_warning("IRQ %d uses trigger mode %u; requested %u\n",
                                   irq, nmsk, omsk);
        }

        new->irq = irq;
        *old_ptr = new;

        /*
         * 略 後処理色々
         * スピンロックを解除する
         */

        return 0;

mismatch:
        /* 略 */
out_mask:
        /* 略 */
out_thread:
        /* 略 */
}

setup_affinity が、割り込みの上がる CPU を決定する本体のコードです。

/kernel/irq/manage.c
#ifndef CONFIG_AUTO_IRQ_AFFINITY
/*
 * Generic version of the affinity autoselector.
 */
static int
setup_affinity(unsigned int irq, struct irq_desc *desc, struct cpumask *mask)
{
        struct irq_chip *chip = irq_desc_get_chip(desc);
        struct cpumask *set = irq_default_affinity;
        int ret;

        /* Excludes PER_CPU and NO_BALANCE interrupts */
        if (!irq_can_set_affinity(irq))
                return 0;

        /*
         * Preserve an userspace affinity setup, but make sure that
         * one of the targets is online.
         */
        /* /proc/irq/xxx/smp_affinity での設定を準備すると思われる */
        if (irqd_has_set(&desc->irq_data, IRQD_AFFINITY_SET)) {
                if (cpumask_intersects(desc->irq_data.affinity,
                                       cpu_online_mask))
                        set = desc->irq_data.affinity;
                else
                        irqd_clear(&desc->irq_data, IRQD_AFFINITY_SET);
        }

        cpumask_and(mask, cpu_online_mask, set);
        /*
         * アフィニティを設定する
         * chip->irq_set_affinity はハードウェア(PIC)に依存するコード
         */
        ret = chip->irq_set_affinity(&desc->irq_data, mask, false);
        switch (ret) {
        case IRQ_SET_MASK_OK:
                cpumask_copy(desc->irq_data.affinity, mask);
        case IRQ_SET_MASK_OK_NOCOPY:
                irq_set_thread_affinity(desc);
        }
        return 0;
}
#else
static inline int
setup_affinity(unsigned int irq, struct irq_desc *d, struct cpumask *mask)
{
        return irq_select_affinity(irq);
}
#endif

ここでは PIC は MPIC を使用するので、chip->irq_set_affinity は mpic_set_affinity になります。 MPIC は AMD の OpenPIC 規格がベースになっているマルチプロセッサ向け割り込みコントローラです。

/arch/powerpc/sysdev/mpic.c
int mpic_set_affinity(struct irq_data *d, const struct cpumask *cpumask,
                      bool force)
{
        struct mpic *mpic = mpic_from_irq_data(d);
        unsigned int src = mpic_irq_to_hw(d->irq);

        /* 通常 MPIC_SINGLE_DEST_CPU をコンフィグするはず */
        if (mpic->flags & MPIC_SINGLE_DEST_CPU) {
                int cpuid = irq_choose_cpu(cpumask);

                mpic_irq_write(src, MPIC_INFO(IRQ_DESTINATION), 1 << cpuid);
        } else {
                cpumask_var_t tmp;

                alloc_cpumask_var(&tmp, GFP_KERNEL);

                cpumask_and(tmp, cpumask, cpu_online_mask);

                mpic_irq_write(src, MPIC_INFO(IRQ_DESTINATION),
                               mpic_physmask(cpumask_bits(tmp)[0]));

                free_cpumask_var(tmp);
        }

        return 0;
}
/arch/powerpc/sysdev/mpic.c
static int irq_choose_cpu(const struct cpumask *mask)
{
        int cpuid;

        if (cpumask_equal(mask, cpu_all_mask)) {
                static int irq_rover = 0;
                static DEFINE_RAW_SPINLOCK(irq_rover_lock);
                unsigned long flags;

                /* Round-robin distribution... */
                /* CPU の数に応じて、ラウンドロビン形式で割り込み先 CPU を指定している */
        do_round_robin:
                raw_spin_lock_irqsave(&irq_rover_lock, flags);

                irq_rover = cpumask_next(irq_rover, cpu_online_mask);
                if (irq_rover >= nr_cpu_ids)
                        irq_rover = cpumask_first(cpu_online_mask);

                cpuid = irq_rover;

                raw_spin_unlock_irqrestore(&irq_rover_lock, flags);
        } else {
                cpuid = cpumask_first_and(mask, cpu_online_mask);
                if (cpuid >= nr_cpu_ids)
                        goto do_round_robin;
        }

        return get_hard_smp_processor_id(cpuid);
}

IRQ_DESTINATION は、MPIC の割り込み Destination Register に対応しています。 CPU にもよりますが、グローバルタイマ割り込み・IPI(プロセッサ間割り込み)のように複数 CPU を宛先とする割り込みではない外部割り込みは、このレジスタに複数 CPU に相当するビットを立てると動作が未定義になります。このあたりは CPU のリファレンスを読むと詳しく理解することができるでしょう。

また、CONFIG_AUTO_IRQ_AFFINITY がコンフィグされていたら、以下のオートセレクタが動作します。こちらはセレクト方法が PIC に依存しません。

/linux/arch/alpha/kernel/irq.c
#ifdef CONFIG_SMP
static char irq_user_affinity[NR_IRQS];

int irq_select_affinity(unsigned int irq)
{
        struct irq_data *data = irq_get_irq_data(irq);
        struct irq_chip *chip;
        static int last_cpu;
        int cpu = last_cpu + 1;

        if (!data)
                return 1;
        chip = irq_data_get_irq_chip(data);

        if (!chip->irq_set_affinity || irq_user_affinity[irq])
                return 1;

        /*
         * cpu_possible マクロで搭載された CPU かどうか、
         * この CPU にアフィニティ設定ができるかを検査し、CPU をラウンドロビンで選択する
         *
         */
        while (!cpu_possible(cpu) ||
               !cpumask_test_cpu(cpu, irq_default_affinity))
                cpu = (cpu < (NR_CPUS-1) ? cpu + 1 : 0);
        last_cpu = cpu;

        /* アフィニティを設定する */
        cpumask_copy(data->affinity, cpumask_of(cpu));
        chip->irq_set_affinity(data, cpumask_of(cpu), false);
        return 0;
}
#endif /* CONFIG_SMP */

以上で、request_irq から割り込みのアフィニティが決定するまでの過程は終了です。基本的にはカーネル起動時に登録される順序、モジュールがinsmodされる順序は変わらないので、手動で設定しない限り、再起動しても割り込みが上がるCPUは固定になるはずです。