中断初探

中断初探

最近几个月调了很多中断的bug,啃了很久的源码。整理了一些东西,大佬们笑纳。
离开了架构谈中断都是不深刻的,大佬们肯定玩腻了X86了,今天就以龙芯内核(龙芯官网即可获得:git://cgit.loongnix.org/kernel/linux-3.10.git)为例简单介绍一下哈。中断在内核中的生命周期主要分为三个部分:初始化,注册和中断处理,剩余的所有事情都是硬件完成的。这部分打算分享四节内容:中断初始化、注册中断处理以及中断排错。这次先分享前两部分内容。
在一切的前面,先看看中断相关的数据结构。

数据结构

话不多说,上图
中断初探_第1张图片

在linux kernel中,对于每一个外设的IRQ都用struct irq_desc来描述,我们称之中断描述符(struct irq_desc)。linux kernel中会有一个数据结构保存了关于所有IRQ的中断描述符信息,我们称之中断描述符DB(上图中的数据结构)。当发生中断后,首先获取触发中断的HW interupt ID,然后通过irq domain翻译成IRQ nuber,然后通过IRQ number就可以获取对应的中断描述符。调用中断描述符中的highlevel irq-events handler来进行中断处理就OK了。而highlevel irq-events handler主要进行下面两个操作:

(1)调用中断描述符的底层irq chip driver进行mask,ack等callback函数,进行interrupt flow control。

(2)调用该中断描述符上的action list中的specific handler(我们用这个术语来区分具体中断handler和high level的handler)。

接下来看中断初始化,很多架构中断初始化代码都在arch目录下,龙芯采用mips架构,因此关注点在arch/mips/目录中,一般架构都是定义好某个中断号被哪个设备占用,初始化工作是:申请中断处理需要的desc结构,每个扫描设备给每个desc填充好handle_irq 和chip结构,给中断号绑定处理函数。中断注册都是在init阶段通过两个接口完成的,为什么是两个接口呢,因为龙芯一部分中断是CPU控制,另一部分由桥片控制。由一个宏定义分开,宏定义定义在arch下,名字一般是XXX_IRQ_BASE,XXX是架构名,例如:LS7A_PCH_IRQ_BASE,。中断号小于该宏是有CPU直接控制的中断,比如说时钟中断,串口中断等等。大于该宏则是桥片上的,例如:显卡、网卡、硬盘等等。这也跟龙芯自己的硬件设计有关。

中断初始化

这个很容易理解,中断初始化肯定在内核启动中,因为内核非常需要时钟等中断。那就先从start_kernel开始看吧,这个函数执行的功能十分冗杂,现在我们只关注irq相关的部分。差不多翻两页以后,看到下面这个代码块:

        if (initcall_debug)
                initcall_debug_enable();

        context_tracking_init();
        /* init some links before init_ISA_irqs() */
        early_irq_init();
        init_IRQ();
        tick_init();
        rcu_init_nohz();
        init_timers();
        hrtimers_init();
        softirq_init();
        timekeeping_init();
        time_init();
        printk_safe_init();
        perf_event_init();
        profile_init();
        call_function_init();
        WARN(!irqs_disabled(), "Interrupts were enabled early\n");

        early_boot_irqs_disabled = false;
        local_irq_enable();

这是除了start_kenrel一进来就disable_irq之后第一个出现irq字样的代码块了,各种init irq,nice,盘他!先去看看early_init_irq。定义在kernel下。那就是通用的咯,那就应该是初始化前的准备工作,没关系,先进去看看。

int __init early_irq_init(void)
{
        int i, initcnt, node = first_online_node;
        struct irq_desc *desc;

        init_irq_default_affinity();

        /* Let arch update nr_irqs and return the nr of preallocated irqs */
        initcnt = arch_probe_nr_irqs();
        printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
               NR_IRQS, nr_irqs, initcnt);

        if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
                nr_irqs = IRQ_BITMAP_BITS;

        if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
                initcnt = IRQ_BITMAP_BITS;

        if (initcnt > nr_irqs)
                nr_irqs = initcnt;

        for (i = 0; i < initcnt; i++) {
                desc = alloc_desc(i, node, 0, NULL, NULL);
                set_bit(i, allocated_irqs);
                irq_insert_desc(i, desc);
        }
        return arch_early_irq_init();
}

好,设置irq affinity标志,计算中断数量,分配中断描述符,中断描述符插入DB,然后去arch_early_irq_init,再跳进去看看,恩,kernel下的一个空函数。ok,这时候中断描述符DB已经创建完成了,只是每个desc还是空的。
接下来看init_IRQ——这个看名字就很重要的函数。ctrl+T,arch下的!!开心,一下就过去了。选择arch/mips/下的定义:

void __init init_IRQ(void)
{
        int i;
        unsigned int order = get_order(IRQ_STACK_SIZE);

        for (i = 0; i < NR_IRQS; i++)
                irq_set_noprobe(i);

        if (cpu_has_veic)
                clear_c0_status(ST0_IM);

        arch_init_irq();

        for_each_possible_cpu(i) {
                void *s = (void *)__get_free_pages(GFP_KERNEL, order);

                irq_stack[i] = s;
                pr_debug("CPU%d IRQ stack at 0x%p - 0x%p\n", i,
                        irq_stack[i], irq_stack[i] + IRQ_STACK_SIZE);
        }
}

挨个设置noprobe,判断cpu有没有扩展中断控制模式(external interrupt controller mode, eic),调arch_init_irq,多CPU挨个申请页面作为中断栈。毫无疑问,注册流在arch_init_irq里,就这样一个一个的跳转,最后到这里:

void __init mach_init_irq(void)
{               
        int i;
        u64 intenset_addr;
        u64 introuter_lpc_addr;
 
        clear_c0_status(ST0_IM | ST0_BEV);

        mips_cpu_irq_init();
        if (loongson_pch)
                loongson_pch->init_irq();

        /* setup CASCADE irq */
        setup_irq(LOONGSON_BRIDGE_IRQ, &cascade_irqaction);

        irq_set_chip_and_handler(LOONGSON_UART_IRQ,
                        &loongson_irq_chip, handle_level_irq);

        set_c0_status(STATUSF_IP2 | STATUSF_IP6);
}

这里可以看到,init是分两个过程的,先是mips架构通用的mips_cpu_irq_init,再是调用桥片自己的init_irq。这就是我一开始说的,注册是分两步完成的。mips_cpu_irq_init函数负责初始cpu直接控制的中断,挨个设置chip和handle_irq.

static int mips_cpu_intc_map(struct irq_domain *d, unsigned int irq,
                             irq_hw_number_t hw)
{
        static struct irq_chip *chip;

        if (hw < 2 && cpu_has_mipsmt) {
                /* Software interrupts are used for MT/CMT IPI */
                chip = &mips_mt_cpu_irq_controller;
        } else {
                chip = &mips_cpu_irq_controller;
        }

        if (cpu_has_vint)
                set_vi_handler(hw, plat_irq_dispatch);
        
        irq_set_chip_and_handler(irq, chip, handle_percpu_irq);

        return 0;
}

初始化流程还是比较简单,可以看到,在这里给每一个cpu产生的中断设置了chip和handle_irq。CPU检测到中断触发直接调用handle_percpu_irq最后到真正的中断处理函数。接下来看走msi中断的注册,这部分稍微复杂一点:

void __init ls7a_init_irq(void)
{
        writeq(0x0ULL, LS7A_INT_EDGE_REG);
        writeq(0x0ULL, LS7A_INT_STATUS_REG);
        /* Mask all interrupts except LPC (bit 19) */
        writeq(0xfffffffffff7ffffULL, LS7A_INT_MASK_REG);
        writeq(0xffffffffffffffffULL, LS7A_INT_CLEAR_REG);

        /* Enable the LPC interrupt */
        writel(0x80000000, LS7A_LPC_INT_CTL);
        /* Clear all 18-bit interrupt bits */
        writel(0x3ffff, LS7A_LPC_INT_CLR);

        if (pci_msi_enabled())
                loongson_pch->irq_dispatch = ls7a_msi_irq_dispatch;
        ....

        init_7a_irq(LS7A_IOAPIC_LPC_OFFSET          , LS7A_IOAPIC_LPC_IRQ          );
        init_7a_irq(LS7A_IOAPIC_UART0_OFFSET        , LS7A_IOAPIC_UART0_IRQ        );
        init_7a_irq(LS7A_IOAPIC_I2C0_OFFSET         , LS7A_IOAPIC_I2C0_IRQ         );
  
          .........
        }
}

设置中断掩码、中断清除等寄存器,设置中断分发函数,调用init_7a_irq为每个外设中断设置handle_irq和chip结构。代码如下:

static void init_7a_irq(int dev, int irq) {
        *irq_mask  &= ~(1ULL << dev);
        *(volatile unsigned char *)(LS7A_IOAPIC_ROUTE_ENTRY + dev) = USE_7A_INT0;
        smp_mb();
        irq_set_chip_and_handler(irq, &pch_irq_chip, handle_level_irq);
        if(ls3a_msi_enabled) {
                *irq_msi_en |= 1ULL << dev;
                *(volatile unsigned char *)(irq_msi_vec+dev) = irq;
                smp_mb();
        }
}

这里就是所有中断初始化流程了。看到这里很多人会觉得茫然了,按照上面的流程分析,第一个被初始化到的中断是MIPS_CPU_IRQ_BASE 也就是56,那前面的中断号干嘛去了?前面的中断号是故意留下来,具体原因先卖个关子 ?
接下来看中断生命周期的的第二个部分——注册。

中断注册

中断注册和初始化不一样,注册流程在设备驱动初始化操作里,注册简单点说就是给设备中断号对应的desc数据填充action字段。为了讲的更容易理解,看个中断注册的例子吧。根据上文描述,中断管理方式不一样,那中断注册肯定也不一样。选两个具有代表性的例子吧,第一个是cpu直接管理的中断——时钟中断注册,第二个是显卡中断注册。

时钟中断注册

这里想不到一个更好的引言了,直接开门见山,龙芯时钟中断初始化函数是r4k_clockevent_init。代码如下:

int r4k_clockevent_init(void)
{
        unsigned int cpu = smp_processor_id();
        struct clock_event_device *cd;
        unsigned int irq, min_delta;

        /*
         * With vectored interrupts things are getting platform specific.
         * get_c0_compare_int is a hook to allow a platform to return the
         * interrupt number of its liking.
         */
        irq = get_c0_compare_int();

        cd = &per_cpu(mips_clockevent_device, cpu);

        cd->name                = "MIPS";
        cd->features            = CLOCK_EVT_FEAT_ONESHOT |
                                  CLOCK_EVT_FEAT_C3STOP |
                                  CLOCK_EVT_FEAT_PERCPU;

        min_delta               = calculate_min_delta();

        cd->rating              = 300;
        cd->irq                 = irq;
        cd->cpumask             = cpumask_of(cpu);
        cd->set_next_event      = mips_next_event;
        cd->event_handler       = mips_event_handler;

        clockevents_config_and_register(cd, mips_hpt_frequency, min_delta, 0x7fffffff);

        if (cp0_timer_irq_installed)
                return 0;

        cp0_timer_irq_installed = 1;
        setup_irq(irq, &c0_compare_irqaction);

        return 0;
}

这是设备初始化函数,流程很清晰,获取中断号、初始化设备名称、标识符、频率、以及设备需要的一些钩子函数。这里我们且关注中断相关不跟。首先是中断号从哪里来的。其次是action结构里是什么。从代码中看到irq是调用函数得来的。如下:

unsigned int __weak get_c0_compare_int(void)
{
        return MIPS_CPU_IRQ_BASE + cp0_compare_irq;
}

cp0_compare_irq = (read_c0_intctl() >> INTCTLB_IPTI) & 7;

这就相当于时钟中断中断号从寄存器里读出来的。那接着看看action结构:

struct irqaction c0_compare_irqaction = {
        .handler = c0_compare_interrupt,
        /*
         * IRQF_SHARED: The timer interrupt may be shared with other interrupts
         * such as perf counter and FDC interrupts.
         */
        .flags = IRQF_PERCPU | IRQF_TIMER | IRQF_SHARED,
        .name = "timer",
};

只有一个handler,然后是中断标志位,名字。

显卡中断注册

和时钟中断注册相比,显卡中断稍微复杂一些。先看显卡初始化函数,目前我手头的显卡基本都是用radeon驱动的,那就看这个吧。

int radeon_device_init(struct radeon_device *rdev,
                       struct drm_device *ddev,
                       struct pci_dev *pdev,
                       uint32_t flags)
{
        ......

        r = radeon_asic_init(rdev);
        if (r)
                goto failed;
        ......
}
#define radeon_asic_init(rdev) (rdev)->asic->init((rdev))

显卡的init流程非常冗杂,这里只是拿出和本文相关的一部分代码, 这个init上挂的是evergreen_init函数,

int evergreen_init(struct radeon_device *rdev)
{
        int r;

        /* Read BIOS */
        if (!radeon_get_bios(rdev)) {
                if (ASIC_IS_AVIVO(rdev))
                        return -EINVAL;
        }
        ......
        /* Initialize clocks */
        radeon_get_clock_info(rdev->ddev);
        ......
        r = evergreen_startup(rdev);
        if (r) {
                dev_err(rdev->dev, "disabling GPU acceleration\n");
                ......
                rdev->accel_working = false;
        }


显卡读取bios参数,初始化始终,然后是setup。

static int evergreen_startup(struct radeon_device *rdev)
{
        struct radeon_ring *ring;
        int r;

        ......
        /* Enable IRQ */
        if (!rdev->irq.installed) {
                r = radeon_irq_kms_init(rdev);
                if (r)
                        return r;
        ......
}

int radeon_irq_kms_init(struct radeon_device *rdev)
{
        int r = 0;

        /* enable msi */
        rdev->msi_enabled = 0;

        if (radeon_msi_ok(rdev)) {
                int ret = pci_enable_msi(rdev->pdev);
                if (!ret) {
                        rdev->msi_enabled = 1;
                        dev_info(rdev->dev, "radeon: using MSI.\n");
                }
        }
        ......
        rdev->irq.installed = true;
        r = drm_irq_install(rdev->ddev, rdev->ddev->pdev->irq);


}

int radeon_irq_kms_init(struct drm_device *dev, int irq)
{
        /* Before installing handler */
        if (dev->driver->irq_preinstall)
                dev->driver->irq_preinstall(dev);

        /* Install handler */
        if (drm_core_check_feature(dev, DRIVER_IRQ_SHARED))
                sh_flags = IRQF_SHARED;

        ret = request_irq(irq, dev->driver->irq_handler,
                          sh_flags, dev->driver->name, dev);
}

哇,好深的一个调用啊,从evergreen_startupradeon_irq_kms_init最后是radeon_irq_kms_init。这里注意有一个判断msi的过程。还记得前面留下的一个疑问吗?56号中断之前额中断都被空出来了,这是个msi或者一些老旧的设备预留的。中断注册时候,判断如果硬件支持MSI,则会优先选择MSI管理中断,而不是桥片自己。在pci_enable_msi函数中,pci设备释放已经申请好的中断号和中断描述符,由msi重新准备一份。

这里可以看出,显卡的中断号是从pci来的。当然了,显卡是pci设备嘛。pci设备的中断号是在启动时候扫描pci总线的时候读出来的,就是我们熟悉的dmesg总一开始输出的类似pci 0000:00:06.0: BAR 2: assigned [mem 0x48000000-0x4fffffff 64bit]这样的一大堆数据。

static int pci_device_probe(struct device *dev)
{
        int error;
        struct pci_dev *pci_dev = to_pci_dev(dev);
        struct pci_driver *drv = to_pci_driver(dev->driver);

        pci_assign_irq(pci_dev);

        error = pcibios_alloc_irq(pci_dev);
        if (error < 0)
                return error;

        pci_dev_get(pci_dev);
        if (pci_device_can_probe(pci_dev)) {
                error = __pci_device_probe(drv, pci_dev);
                if (error) {
                        pcibios_free_irq(pci_dev);
                        pci_dev_put(pci_dev);
                }
        }

        return error;
}

这是所有pci设备的探测入口,调用pci_assign_irq函数,读出每个slot的中断号。

void pci_assign_irq(struct pci_dev *dev)
{
        u8 pin;
        u8 slot = -1;
        int irq = 0;
        struct pci_host_bridge *hbrg = pci_find_host_bridge(dev->bus);

        pci_read_config_byte(dev, PCI_INTERRUPT_PIN, &pin);
        /* Cope with illegal. */
        if (pin > 4)
                pin = 1;

        if (pin) { 
                /* Follow the chain of bridges, swizzling as we go.  */
                if (hbrg->swizzle_irq)
                        slot = (*(hbrg->swizzle_irq))(dev, &pin);

                irq = (*(hbrg->map_irq))(dev, slot, pin);
                if (irq == -1)
                        irq = 0;
        }
        dev->irq = irq;
}

u8 pci_swizzle_interrupt_pin(const struct pci_dev *dev, u8 pin)
{
        int slot;

        if (pci_ari_enabled(dev->bus))
                slot = 0;
        else
                slot = PCI_SLOT(dev->devfn);

        return (((pin - 1) + slot) % 4) + 1;
}

从PCI总线上先读pin值,接着读出slot,计算出对应的irq。填充到的pci结构中。这就是中断注册的全部内容了。

你可能感兴趣的:(Linux系统,kernel,linux内核模块)