aarch64异常模型以及Linux arm64中断处理

top
严格来说,中断是说软件执行流程的东西,但是,在arm术语中,统称为异常。异常是需要特权软件(异常处理程序)执行某些操作以确保系统顺利运行的条件或系统事件。每种异常类型都有一个异常处理程序。一旦处理完异常,特权软件就会让内核准备好恢复它在处理异常之前所做的任何事情。下面介绍了几种异常:

  • Interrupt
    一般有两种,分为irq和fiq。fiq的优先级高于IRQ,这两种异常通常都与内核上的输入引脚相关。假设中断未被禁用,外部硬件断言了一个中断请求并在当前指令完成执行时触发相应的异常类型(irq or fiq),fiq和irq对core来说都是物理信号,当被断言时,如果内核当前已启用,则会发生相应的异常。在几乎所有系统上,各种中断源都使用中断控制器连接。中断控制器对中断进行仲裁并确定其优先级,进而提供串行化的单个中断信号,然后将其连接到内核的FIQ或IRQ信号。由于IRQ和FIQ中断的发生在任何时间,与内核正在执行的软件没有直接关系,因此它们被归类为异步异常

  • Abort
    在指令获取失败(指令中止)或数据访问失败(数据中止)时生成中止。它可来源于内存访问错误时的外部内存系统(可能表明指定的地址与系统中的实际内存不对应)。也可来源于core内的内存管理单元 (MMU) 生成中止。操作系统可以使用MMU中止来为应用程序动态分配内存。
    当一条指令被提取时,它在流水线中被标记为abort。仅当内核随后尝试执行指令时,才会发生指令中止异常。异常发生在指令执行之前。如果在中止指令到达流水线执行阶段之前清除了流水线,则不会发生中止异常。数据中止异常是由于加载或存储指令而发生的,并且被认为是在数据读取或写入之后发生的。
    如果abort是由于执行或试图执行指令流而生成的,并且返回的地址提供了引起中止指令的详细信息,则该中止被描述为同步abort。
    异步abort不是通过执行指令生成的,而返回地址可能并不总是会提供引起abort原因的详细信息。在ARMv8-A中,指令和数据abort是同步的。异步异常包括IRQ/FIQ和System errors (SError)。参考 Synchronous and asynchronous exceptions。

  • Reset
    复位被视为最高异常级别的特殊向量。
    这是 ARM 处理器在触发异常时的指令跳转位置。这个向量使用 IMPLEMENTATION DEFINED 地址。RVBAR_ELn 包含此复位向量地址,其中 n 是实现的最高异常级别的编号。所有内核都有一个复位输入,并在复位后立即发生复位异常。它是最高优先级的异常,不能被屏蔽。此异常用于在上电后在内核上执行代码以对其进行初始化。

  • 生成异常的指令
    执行某些指令会产生异常。通常执行此类指令可以从运行更高权限级别的软件请求服务:

    • Supervisor Call (SVC) 指令可以使用户模式程序能够请求操作系统(OS)服务。
    • Hypervisor Call (HVC) 指令使客户操作系统(guest OS)能够请求管理程序服务。
    • Secure monitor Call(安全监控 SMC)指令使非安全世界请求安全世界服务

一、异常处理寄存器

我们已知的当前处理器的状态是存在于PSTATE寄存器内的。如果发生了异常,PSTATE寄存器里的内容会被保存到Saved Program Status Register(SPSR_ELn)中。而当前的PC的地址则存在于Exception Link Register(ELR_ELn),在异常结束时使用的返回地址存储在ELR_ELn中
1731479501810.png

二、同步与异步中断

在 AArch 中,异常可以是同步的,也可以是异步的。如果异常是由于执行或尝试执行指令而生成,并且返回的地址提供了指令的详细信息,则将其看作为同步异常。一个异步异常不会通过执行指令产生,并且返回的地址中,也许不会提供引起此异常的细节信息。
异步异常的来源是 IRQ(正常优先级中断)、FIQ(快速中断)或 SError(系统错误)。 系统错误有许多可能的原因,最常见的是异步数据中止(例如,将错误数据从高速缓存行写回外部存储器触发的中止)。

同步异常有多种来源:

  • 指令从 MMU 中止。例如,通过从标记为从不执行的内存位置读取指令。
  • 从 MMU 中止数据。例如,权限失败或对齐检查。
  • SP 和 PC 对齐检查。
  • 同步外部中止。例如,读取翻译表时发生的 abort。
  • 未分配指令。
  • 调试异常

2.1 同步中断

同步中断可能通过多种可能的原因产生

  • 产生于 MMU. 例如权限失效或者访问标志错误的内存区域
  • SP 和 PC 对齐检查
  • 未定义的指令
  • 服务呼叫(SVC,SMC,HVC)

2.2 Exception Syndrome Register(ESR_ELn)

异常状态寄存器(Exception Syndrome Register, ESR_ELn)包含允许异常处理程序确定异常原因的信息。它只对针对同步异常和 SError 做更新,不为 IRQ 或 FIQ 更新,因为这些中断处理程序通常从通用中断控制器(GIC)的寄存器中获取状态信息。
1731480307024.png

  • ESR_ELn 的 Bits[31:26] 表示异常类,它允许处理程序区分各种可能的异常原因 (如未分配的指令,源自 MCR/MRC 的到 CP15 的异常,FP 操作的异常,执行了 SVC,HVC 或 SMC,数据中止和对齐异常)。
  • 位 [25] 表示陷入的指令长度 (0 表示 16 位指令,1 表示 32 位指令),并且也为某些异常类设置。
  • 位 [24:0] 形成指令特定症状 (ISS) 字段,该字段包含特定于该异常类型的信息。例如,当执行系统调用指令 (SVC、HVC 或 SMC) 时,该字段包含与操作码相关的立即值,例如对于 SVC 0x123456 的0x123456。

三、AArch64异常向量表

当异常发生时,处理器必须执行与异常对应的处理程序代码。 存储处理程序的内存位置称为异常向量。 在 ARM 体系结构中,异常向量存储在一个表中,称为异常向量表。
每个异常级别都有自己的向量表,即 EL3、EL2 和 EL1 各有一个。 该表包含要执行的指令,而不是一组地址。 个别异常的向量位于表开头的固定偏移量处。 每个表基的虚拟地址由基于向量的地址寄存器 VBAR_EL3、VBAR_EL2和 VBAR_EL1 设置。
向量表中的每个条目有 16 条指令长。 与每个条目为 4 个字节的 ARMv7 相比,这本身就是一个重大变化。ARMv7 向量表的这种间距意味着每个条目几乎总是某种形式的分支,指向内存中其他地方的实际异常处理程序。在 AArch64 中,向量的间距更宽,因此顶层处理程序可以直接写入向量表中。

下图展示了一个向量表。基址是由VBAR_ELn给出,然后每一项都与一个定义好了的偏移量。每一个表都有16项,每项128字节(32调指令)大小。该表实际上由4组4项组成。使用哪一项取决于几个因素

  • 异常类型(SError、FIQ、IRQ 或同步)
  • 如果在相同的异常级别处理异常,则要使用的堆栈指针(SP0 或 SPx)
  • 如果异常是在较低的异常级别上被接受的,则降低一个异常的执行状态 (AArch64 或 AArch32)

1731481336861.png

举一个例子:
如果内核代码在EL1处执行并且产生IRQ中断信号,发生了IRQ异常。在Linux内核中经过处理,在SP_EL1处设置了SPSel位,因此使用了SP_EL1。因此会从VBAR_EL1+0x280开始执行。那问题来了VBAR_EL1+0x280的地址是什么呢?为啥要从这个地址来执行?
这里介绍一下VBAR_EL1寄存器:
VBAR寄存器,全名为Vector Base Address Register
1731490590531.png
这个寄存器其实就是存放向量表基地址的,下面介绍向量表基地址在linux内核中是如何被设置的?

3.1 linux kernel arm64 中断向量表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
/*
* Exception vectors.
*/
.pushsection ".entry.text", "ax"

.align 11
SYM_CODE_START(vectors)
kernel_ventry 1, t, 64, sync // Synchronous EL1t
kernel_ventry 1, t, 64, irq // IRQ EL1t
kernel_ventry 1, t, 64, fiq // FIQ EL1h
kernel_ventry 1, t, 64, error // Error EL1t

///linux异常向量入口,这里是同步异常
kernel_ventry 1, h, 64, sync // Synchronous EL1h
kernel_ventry 1, h, 64, irq // IRQ EL1h
kernel_ventry 1, h, 64, fiq // FIQ EL1h
kernel_ventry 1, h, 64, error // Error EL1h

///aarch64 异常向量入口
kernel_ventry 0, t, 64, sync // Synchronous 64-bit EL0
kernel_ventry 0, t, 64, irq // IRQ 64-bit EL0
kernel_ventry 0, t, 64, fiq // FIQ 64-bit EL0
kernel_ventry 0, t, 64, error // Error 64-bit EL0

///aarch32 异常向量入口
kernel_ventry 0, t, 32, sync // Synchronous 32-bit EL0
kernel_ventry 0, t, 32, irq // IRQ 32-bit EL0
kernel_ventry 0, t, 32, fiq // FIQ 32-bit EL0
kernel_ventry 0, t, 32, error // Error 32-bit EL0
SYM_CODE_END(vectors)

.macro kernel_ventry, el:req, ht:req, regsize:req, label:req
.align 7
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
.if \el == 0
alternative_if ARM64_UNMAP_KERNEL_AT_EL0
.if \regsize == 64
mrs x30, tpidrro_el0
msr tpidrro_el0, xzr
.else
mov x30, xzr
.endif
alternative_else_nop_endif
.endif
#endif

sub sp, sp, #PT_REGS_SIZE
#ifdef CONFIG_VMAP_STACK
/*
* Test whether the SP has overflowed, without corrupting a GPR.
* Task and IRQ stacks are aligned so that SP & (1 << THREAD_SHIFT)
* should always be zero.
*/
add sp, sp, x0 // sp' = sp + x0
sub x0, sp, x0 // x0' = sp' - x0 = (sp + x0) - x0 = sp
tbnz x0, #THREAD_SHIFT, 0f
sub x0, sp, x0 // x0'' = sp' - x0' = (sp + x0) - sp = x0
sub sp, sp, x0 // sp'' = sp' - x0 = (sp + x0) - x0 = sp
b el\el\ht\()_\regsize\()_\label

0:
/*
* Either we've just detected an overflow, or we've taken an exception
* while on the overflow stack. Either way, we won't return to
* userspace, and can clobber EL0 registers to free up GPRs.
*/

/* Stash the original SP (minus PT_REGS_SIZE) in tpidr_el0. */
msr tpidr_el0, x0

/* Recover the original x0 value and stash it in tpidrro_el0 */
sub x0, sp, x0
msr tpidrro_el0, x0

/* Switch to the overflow stack */
adr_this_cpu sp, overflow_stack + OVERFLOW_STACK_SIZE, x0

/*
* Check whether we were already on the overflow stack. This may happen
* after panic() re-enables interrupts.
*/
mrs x0, tpidr_el0 // sp of interrupted context
sub x0, sp, x0 // delta with top of overflow stack
tst x0, #~(OVERFLOW_STACK_SIZE - 1) // within range?
b.ne __bad_stack // no? -> bad stack pointer

/* We were already on the overflow stack. Restore sp/x0 and carry on. */
sub sp, sp, x0
mrs x0, tpidrro_el0
#endif
b el\el\ht\()_\regsize\()_\label
.endm

3.1.1 kernel_entry

kernel_entry的看起来还是比较复杂的,这里说明几点:

  1. .align=7 这个说明代码是按照2^7=128对齐的,这个和向量表中的每一个offset对齐
  2. 最终调用的函数为 b el\el\ht\()_\regsize\()_\label
1
2
3
4
5
6
7
8
9
10
11
12
	.macro entry_handler el:req, ht:req, regsize:req, label:req
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize //走到了这里
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler //然后执行这个handler
.if \el == 0
b ret_to_user
.else
b ret_to_kernel //恢复上下文
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
.endm

所以也就是调用了kernel_entry 0 64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
	.macro	kernel_entry, el, regsize = 64
.if \regsize == 32
//...
.endif
.if \el=0
//...
.else
//...
.endif // endif for \el=0
mrs x22, elr_el1 //读取elr_el1寄存器
mrs x23, spsr_el1 //读取spsr_el1寄存器
stp lr, x21, [sp, #S_LR]

/*
* For exceptions from EL0, create a final frame record.
* For exceptions from EL1, create a synthetic frame record so the
* interrupted code shows up in the backtrace.
*/
.if \el == 0
stp xzr, xzr, [sp, #S_STACKFRAME]
.else
stp x29, x22, [sp, #S_STACKFRAME]
.endif
add x29, sp, #S_STACKFRAME

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
alternative_if_not ARM64_HAS_PAN
bl __swpan_entry_el\el
alternative_else_nop_endif
#endif

stp x22, x23, [sp, #S_PC]

/* Not in a syscall by default (el0_svc overwrites for real syscall) */
.if \el == 0
mov w21, #NO_SYSCALL
str w21, [sp, #S_SYSCALLNO]
.endif

/* Save pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
mrs_s x20, SYS_ICC_PMR_EL1
str x20, [sp, #S_PMR_SAVE]
mov x20, #GIC_PRIO_IRQON | GIC_PRIO_PSR_I_SET
msr_s SYS_ICC_PMR_EL1, x20
alternative_else_nop_endif

/* Re-enable tag checking (TCO set on exception entry) */
#ifdef CONFIG_ARM64_MTE
alternative_if ARM64_MTE
SET_PSTATE_TCO(0)
alternative_else_nop_endif
#endif

/*
* Registers that may be useful after this macro is invoked:
*
* x20 - ICC_PMR_EL1
* x21 - aborted SP
* x22 - aborted PC
* x23 - aborted PSTATE
*/
.endm

3.1.2 VBAR_EL1寄存器设置

那向量表是什么时候被Linux内核的VBAR寄存器的呢?

1
2
3
4
5
6
7
8
9
10
11
12
//linux-5.15/arch/arm64/kernel/head.S
SYM_FUNC_START_LOCAL(__primary_switched)
///已开启mmu,这里开始访问的都是虚拟地址,
///比如init_task静态定义的虚拟地址,已经映射到对应物理内存地址
adr_l x8, vectors // load VBAR_EL1 with virtual
msr vbar_el1, x8 // vector table address
isb

//...
bl start_kernel //跳转到C语言入口
ASM_BUG()
SYM_FUNC_END(__primary_switched)

okay,到现在为知,我们知道了向量表的创建、VBAR_EL1寄存器的地址的设置。那当EL1_IRQ中断触发后会执行到哪里呢?我们所知道的是中断触发后,会进入我们设置的中断处理函数中去执行,那两者是如何关联起来的呢?

3.1.3 el\el\ht()\regsize()\label()_handler函数

上文分析到kernel_entry,最终调用的是b el\el\ht\()_\regsize\()_\label

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	.macro entry_handler el:req, ht:req, regsize:req, label:req
SYM_CODE_START_LOCAL(el\el\ht\()_\regsize\()_\label)
kernel_entry \el, \regsize
mov x0, sp
bl el\el\ht\()_\regsize\()_\label\()_handler //最终调用的就是这个函数
.if \el == 0
b ret_to_user
.else
b ret_to_kernel
.endif
SYM_CODE_END(el\el\ht\()_\regsize\()_\label)
.endm

/*
* Early exception handlers
*/
entry_handler 1, t, 64, sync //实际函数: el1t_64_sync_handler
entry_handler 1, t, 64, irq //实际函数: el1t_64_irq_handler
entry_handler 1, t, 64, fiq //实际函数: el1t_64_fiq_handler
entry_handler 1, t, 64, error //实际函数: el1t_64_error_handler

entry_handler 1, h, 64, sync //实际函数: el1h_64_sync_handler
entry_handler 1, h, 64, irq //实际函数: el1h_64_irq_handler
entry_handler 1, h, 64, fiq //实际函数: el1h_64_fiq_handler
entry_handler 1, h, 64, error //实际函数: el1h_64_error_handler

entry_handler 0, t, 64, sync //实际函数: el0t_64_sync_handler
entry_handler 0, t, 64, irq //实际函数: el0t_64_irq_handler
entry_handler 0, t, 64, fiq //实际函数: el0t_64_fiq_handler
entry_handler 0, t, 64, error //实际函数: el0t_64_error_handler

entry_handler 0, t, 32, sync //实际函数: el0t_32_sync_handler
entry_handler 0, t, 32, irq //实际函数: el0t_32_irq_handler
entry_handler 0, t, 32, fiq //实际函数: el0t_32_fiq_handler
entry_handler 0, t, 32, error //实际函数: el0t_32_error_handler

所以这个例子,我们中断触发后会走到el1h_64_irq_handler函数

3.1.4 el1h_64_irq_handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
asmlinkage void noinstr el1h_64_irq_handler(struct pt_regs *regs)
{
el1_interrupt(regs, handle_arch_irq);
}

static void noinstr el1_interrupt(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);

enter_el1_irq_or_nmi(regs);
do_interrupt_handler(regs, handler); //走到这儿

/*
* Note: thread_info::preempt_count includes both thread_info::count
* and thread_info::need_resched, and is not equivalent to
* preempt_count().
*/
if (IS_ENABLED(CONFIG_PREEMPTION) &&
READ_ONCE(current_thread_info()->preempt_count) == 0)
arm64_preempt_schedule_irq();

exit_el1_irq_or_nmi(regs);
}

static void do_interrupt_handler(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
if (on_thread_stack())
call_on_irq_stack(regs, handler);
else
handler(regs);
}

所以最终执行了handle_arch_irq函数,而这个函数是在irq.c中被设置的

3.1.5 handle_arch_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//linux-5.15/arch/arm64/kernel/irq.c
static void default_handle_irq(struct pt_regs *regs)
{
panic("IRQ taken without a root IRQ handler\n");
}

void (*handle_arch_irq)(struct pt_regs *) __ro_after_init = default_handle_irq;

int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
if (handle_arch_irq != default_handle_irq)
return -EBUSY;

handle_arch_irq = handle_irq; //注册handle_arch_irq
pr_info("Root IRQ handler: %ps\n", handle_irq);
return 0;
}

3.1.6 gic_handle_irq

kernel启动后,gic中断控制器的驱动启动会执行到gic_init_bases

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//linux-5.15/drivers/irqchip/irq-gic-v3.c
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
//...
set_handle_irq(gic_handle_irq);
//...
}

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;

irqnr = do_read_iar(regs); //读取GICD寄存器,获取中断号

/* Check for special IDs first */
if ((irqnr >= 1020 && irqnr <= 1023)) //这部分可以看GICv3的官方文档
return;

if (gic_supports_nmi() &&
unlikely(gic_read_rpr() == GICD_INT_RPR_PRI(GICD_INT_NMI_PRI))) {
gic_handle_nmi(irqnr, regs);
return;
}

if (gic_prio_masking_enabled()) {
gic_pmr_mask_irqs();
gic_arch_enable_irqs();
}

if (static_branch_likely(&supports_deactivate_key))
gic_write_eoir(irqnr);
else
isb();

// 进入handle_domain_irq中执行
if (handle_domain_irq(gic_data.domain, irqnr, regs)) {
WARN_ONCE(true, "Unexpected interrupt received!\n");
gic_deactivate_unhandled(irqnr);
}
}

3.1.7 handle_domain_irq

这里补充两个知识点:

  1. IRQ number
    CPU需要为每一个外设中断编号,我们称之为IRQ number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断
  2. HW interrupt ID
    对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。

这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc;
int ret = 0;

irq_enter();

/* The irqdomain code provides boundary checks */
desc = irq_resolve_mapping(domain, hwirq); //找到和硬件中断号相关的软件irq
if (likely(desc))
handle_irq_desc(desc); //走到这个函数,其实就是我们注册的中断处理函数
else
ret = -EINVAL;

irq_exit();
set_irq_regs(old_regs);
return ret;
}

int handle_irq_desc(struct irq_desc *desc)
{
struct irq_data *data;

if (!desc)
return -EINVAL;

data = irq_desc_get_irq_data(desc);
if (WARN_ON_ONCE(!in_irq() && handle_enforce_irqctx(data)))
return -EPERM;

generic_handle_irq_desc(desc); //走到这儿
return 0;
}

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc); //调用注册的中断处理函数
}

3.1.8 注册中断处理函数

这段以函数request_irq为例,此函数会调用request_threaded_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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;

//...
desc = irq_to_desc(irq); //获取irq number对应的desc
//...
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;

action->handler = handler; //将中断处理函数放到action里
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;

//...

retval = __setup_irq(irq, desc, action); //这个函数往下比较复杂,本文就不再往下写出来了,可以查看我的源码opengrok/linux-5.15注解

//...
return retval;
}