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