diff --git a/articles/20220626-riscv-irq-analysis-part2-Interrupt-handling-plic.md b/articles/20220626-riscv-irq-analysis-part2-Interrupt-handling-plic.md new file mode 100644 index 0000000000000000000000000000000000000000..97e6539ce25a7c04082d51bdf58380d277d19a24 --- /dev/null +++ b/articles/20220626-riscv-irq-analysis-part2-Interrupt-handling-plic.md @@ -0,0 +1,548 @@ +> Author: 通天塔 985400330@qq.com
+> Date: 2022/05/19
+> Revisor:
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) + +# RISC-V 中断子系统分析——PLIC 中断处理 + +## 前言 + + [RISC-V 中断子系统分析——硬件及其初始化](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220519-riscv-irq-analysis.md) 上一篇文章对中断的硬件实现以及硬件初始化进行了分析,本篇文章将对中断的申请、产生、处理进行分析。 + +## RISC-V Linux 中断申请 + +中断的注册分析,我们首先找到一个 Linux 驱动,从设备驱动入手进行分析。 + +``` +rtc@101000 { +interrupts = <0x0b>;//表明连接到0x0b中断线上 +interrupt-parent = <0x09>;//表明归属于0x09中断控制器,经过查找是plic@c000000。 +reg = <0x00 0x101000 0x00 0x1000>; +compatible = "google,goldfish-rtc"; +}; +``` + +从设备树种可以看到,该 rtc 驱动连接到了中断控制器上,该驱动的文件名为:`rtc-goldfish.c` + +驱动与设备匹配成功后,调用以下代码: + +```c +static int goldfish_rtc_probe(struct platform_device *pdev) +{ +··· + err = devm_request_irq(&pdev->dev, rtcdrv->irq, + goldfish_rtc_interrupt, + 0, pdev->name, rtcdrv);//申请irq +··· +} +``` + +其中申请 irq 的函数最终调用为`request_threaded_irq` + +```c +devm_request_irq(&pdev->dev, rtcdrv->irq, goldfish_rtc_interrupt,0, pdev->name, rtcdrv); + +——>devm_request_threaded_irq(&pdev->dev, rtcdrv->irq, goldfish_rtc_interrupt, NULL, 0,pdev->name, dev_id); + +————>request_threaded_irq( rtcdrv->irq, goldfish_rtc_interrupt, NULL, 0, pdev->name, dev_id); +``` + +参数分析: + +rtcdrv->irq: 要分配的中断 + +goldfish_rtc_interrupt:中断产生时要调用的函数 + +NULL:中断处理线程要调用的函数 + +0:中断类型标志位 + + IRQF_SHARED 共享中断 + + IRQF_TRIGGER_* 触发边界或等级 + + IRQF_ONESHOT 只触发1次 + +pdev->name: 声明设备的 ascii 名称 + +dev_id:全局唯一的。因为这个之由处理程序接收,所以设备数据结构的地址常用作dev_id。 + +`request_threaded_irq` 做的事情: + +* 获取 irq_desc +* 创建 irqaction +* 通过 `__setup_irq` 将 irqaction 添加到 IRQ 链表当中 + +`__setup_irq` 函数有将近 400 行,此处暂不分析,再中断函数处理的时候,我们再分析该函数的作用。 + +至此,完成中断的申请及注册。 + +## RISC-V Linux 中断产生 + +本文以 RTC 为例进行中断处理的分析,现在我们创建一个中断,让系统在运行过程中出发中断,以便后续分析中断的处理流程。 + +通过 menuconfig 配置,将 goldfish-rtc 的驱动编译进内核,将 rtc 设备驱动起来。 + +应用创建中断的代码如下: + +```c +#include +#include +#include +#include +#include +#include +struct rtc_time time; //保存时间值 +struct rtc_wkalrm alrm; //保存闹钟值 +int main(int argc,char **argv) +{ + int fd=open("/dev/rtc0",O_RDWR); // 2==O_RDWR + if(fd<0){ + printf("驱动设备文件打开失败!\r\n"); + return 0; + } + alrm.enabled=1; + alrm.pending=0; + ioctl(fd,RTC_RD_TIME,&time); + time.tm_sec+=5;//5 秒后闹钟产生 + alrm.time=time; + ioctl(fd,RTC_WKALM_SET,&alrm); + ioctl(fd,RTC_ALM_READ,&time); + printf("ALM %d-%d-%d %d:%d:%d\r\n",time.tm_year+1900,time.tm_mon+1,time.tm_mday,time.tm_hour,time.tm_min,time.tm_sec); + while(1){ + ioctl(fd,RTC_RD_TIME,&time); + printf("now time=%d-%d-%d %d:%d:%d\r\n",time.tm_year+1900,time.tm_mon+1,time.tm_mday,time.tm_hour,time.tm_min,time.tm_sec); + sleep(1); + } +} + +``` + +编译命令如下: + +> riscv64-linux-gnu-gcc rtc_alarm.c -o rtc_alarm + +运行命令: + +> cp /lib/ld-linux-riscv64-lp64.so.1 /lib/ld-linux-riscv64-lp64d.so.1 +> ./rtc_alarm + +关于 ld 库为什么要复制一个 lp64d 的库的原因是通过命令 `riscv64-linux-gnu-readelf rtc_alarm -a` 读取到 `[Requesting program interpreter: /lib/ld-linux-riscv64-lp64d.so.1]` 说明要使用的库名称不同,故单独进行复制,否则程序将不能正确运行。 + +程序运行起来之后,代码追踪打印信息如下: + +``` +[nfk test] RTC_WKALM_SET +[nfk test] drivers/rtc/dev.c-rtc_dev_ioctl-375 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-464 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-469 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-473 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-477 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-483 +[nfk test] drivers/rtc/interface.c-rtc_timer_enqueue-834 +[nfk test] drivers/rtc/interface.c-rtc_timer_enqueue-837 +[nfk test] drivers/rtc/interface.c-rtc_timer_enqueue-841 +[nfk test] drivers/rtc/interface.c-__rtc_set_alarm-416 +[nfk test] drivers/rtc/interface.c-__rtc_set_alarm-427 +[nfk test] drivers/rtc/interface.c-__rtc_set_alarm-432 +[nfk test] goldfish_rtc_set_alarm +[nfk test] goldfish_rtc_set_alarm +[nfk test] drivers/rtc/interface.c-__rtc_set_alarm-454 +[nfk test] drivers/rtc/interface.c-rtc_timer_enqueue-845 +[nfk test] drivers/rtc/interface.c-rtc_set_alarm-501 +[nfk test] get RTC_ALM_READ +ALM 2022-6-24 15:54:35 +now time=2022-6-24 15:54:30 +... +now time=2022-6-24 15:54:34 +[nfk test] goldfish_rtc_interrupt +[nfk test] goldfish_rtc_alarm_irq_enable +now time=2022-6-24 15:54:35 +... +now time=2022-6-24 15:54:40 + +``` + +通过分析可以知道,闹钟成功的进行了触发中断,rtc 驱动中的中断函数得到了运行,后面我们进行深入分析,看中断是如何触发,并且最终调用到了驱动中的中断函数。 + +## RISC-V Linux 中断处理 + +### 中断处理流程 + +首先在 rtc 驱动中添加函数 `dump_stack` 进行调用栈的回溯,以便找到函数的调用关系。 + +``` +[nfk test] goldfish_rtc_interrupt +CPU: 0 PID: 0 Comm: swapper/0 Not tainted 5.17.0-dirty #85 +Hardware name: riscv-virtio,qemu (DT) +Call Trace: +[] dump_backtrace+0x1c/0x24 +[] show_stack+0x2c/0x38 +[] dump_stack_lvl+0x40/0x58 +[] dump_stack+0x14/0x1c +[] goldfish_rtc_interrupt+0x22/0x74 +[] __handle_irq_event_percpu+0x52/0xe0 +[] handle_irq_event_percpu+0x12/0x4e +[] handle_irq_event+0x5e/0x94 +[] handle_fasteoi_irq+0xac/0x18e +[] generic_handle_domain_irq+0x28/0x3a +[] plic_handle_irq+0x8a/0xec +[] generic_handle_domain_irq+0x28/0x3a +[] riscv_intc_irq+0x34/0x5c +[] generic_handle_arch_irq+0x4a/0x74 +[] ret_from_exception+0x0/0xc +[] rcu_idle_enter+0x10/0x18 + +``` + +`ret_from_exception` 函数表明 CPU 收到了一个异常,并且已经完成了异常的处理。 + +架构设计时,就考虑了 CPU 要进行各种各样的异常处理,于是提前定义好了一些异常向量表,在 CPU 执行特殊的指令时,就会触发异常。 + +本文不对 CPU 如何接收异常和处理异常进行深入分析,这**涉及到 RISC-V 架构相关的汇编指令**的分析,这里先给出参考文档 [linux异常处理体系结构](https://www.cnblogs.com/gulan-zmc/p/11604437.html) ,在下一篇文章中进行重点分析。 + +本文暂时进行 CPU 中断控制器 -> PLIC 中断控制器 -> 驱动中的中断处理函数这一层进行分析。 + +经过查找代码,在 RISC-V 的 entry.S 中的异常处理汇编代码中调用了 `generic_handle_arch_irq` 函数。 + +汇编代码如下: + +``` +#ifdef CONFIG_CONTEXT_TRACKING + /* If previous state is in user mode, call context_tracking_user_exit. */ + li a0, SR_PP + and a0, s1, a0 + bnez a0, skip_context_tracking + call context_tracking_user_exit +skip_context_tracking: +#endif + + /* + * MSB of cause differentiates between + * interrupts and exceptions + */ + bge s4, zero, 1f + + la ra, ret_from_exception + + /* Handle interrupts */ + move a0, sp /* pt_regs */ + la a1, generic_handle_arch_irq + jr a1 +``` + +汇编代码通过寄存器进行传参,将参数传给了函数 `generic_handle_arch_irq`。 + +```c +/** + * generic_handle_arch_irq - root irq handler for architectures which do no + * entry accounting themselves + * @regs: Register file coming from the low-level handling code + */ +asmlinkage void noinstr generic_handle_arch_irq(struct pt_regs *regs) +{ + struct pt_regs *old_regs; + + irq_enter(); + old_regs = set_irq_regs(regs); + handle_arch_irq(regs); + set_irq_regs(old_regs); + irq_exit(); +} +``` + +可以看到这个通用接口中并未调用 `riscv_intc_irq` 中断处理函数,但是dump中却看到了中断处理函数。 + +以下就是原因,可以看到在以下函数中已经配置了对应架构的中断处理函数,该函数被 `riscv_intc_init` 函数调用。 + +```c +int __init set_handle_irq(void (*handle_irq)(struct pt_regs *)) +{ + if (handle_arch_irq) + return -EBUSY; + handle_arch_irq = handle_irq; + return 0; +} +``` + +下面进行分析 RISC-V 架构下的中断处理函数如何调用到 PLIC 中断控制器中。 + +```c +static asmlinkage void riscv_intc_irq(struct pt_regs *regs) +{ + unsigned long cause = regs->cause & ~CAUSE_IRQ_FLAG; + + if (unlikely(cause >= BITS_PER_LONG)) + panic("unexpected interrupt cause"); + + switch (cause) { +#ifdef CONFIG_SMP + case RV_IRQ_SOFT: + /* + * We only use software interrupts to pass IPIs, so if a + * non-SMP system gets one, then we don't know what to do. + */ + handle_IPI(regs);//软中断接口 + break; +#endif + default://硬中断接口 + generic_handle_domain_irq(intc_domain, cause); + break; + } +} +``` + +`generic_handle_domain_irq(intc_domain, cause)` 该函数的 intc_domain 参数是通过 `irq-riscv-intc.c` 驱动的初始化进行建立的,cause 参数是汇编代码中传进来的,cause 参数就是中断号,方便进一步的追踪具体调用哪个中断处理函数。 + +`generic_handle_domain_irq` 代码如下: + +```c +int generic_handle_domain_irq(struct irq_domain *domain, unsigned int hwirq) +{ + WARN_ON_ONCE(!in_irq()); + return handle_irq_desc(irq_resolve_mapping(domain, hwirq)); +} +``` + +可以看到通过两个函数的调用完成进一步的中断处理,第一个是 `irq_resolve_mapping` ,通过中断域和硬件中断号得出中断描述符。这到底是如何实现的?下一小节我们讲清楚 domain 与 hwirq 的来龙去脉。 + +### domain 的来龙去脉 + +#### 中断描述符(irq_desc)获取 + +中断描述符通过硬件中断号和中断域进行获取,中断描述符中存储着一个中断最全的信息,要处理该中断必须获取到这个中断的信息。 + +通过设备树可知,rtc 的 hwirq = 0xb,以下分析都是基于已知 rtc 的 hwirq 的情况下添加打印所进行的追踪分析。 + +```c +int generic_handle_domain_irq(struct irq_domain *domain, unsigned int hwirq) +{ + WARN_ON_ONCE(!in_irq()); + struct irq_desc *my= irq_resolve_mapping(domain, hwirq); + if(hwirq==11) + { + printk("[nfk test]hwirq=%u\n",hwirq); + printk("[nfk test] irq =%d,hwirq=%d",my->irq_data.irq,my->irq_data.hwirq); + printk("[nfk test] name =%s",my->dev_name); + } + return handle_irq_desc(irq_resolve_mapping(domain, hwirq)); +} +``` + +追踪 irq_desc 的获取函数, + +```c +struct irq_desc *__irq_resolve_mapping(struct irq_domain *domain, + irq_hw_number_t hwirq, + unsigned int *irq) +{ + struct irq_desc *desc = NULL; + struct irq_data *data; +...//查找desc不是用的以上代码,故省略 + rcu_read_lock(); + /* Check if the hwirq is in the linear revmap. */ + if (hwirq < domain->revmap_size){ + if(hwirq==11) + printk("hwirq < domain->revmap_size\n");//经验证,走的这条路获取到了irq_data + data = rcu_dereference(domain->revmap[hwirq]);//在RCU保护机制下,通过domain->revmap[hwirq]找到了irq_data, + } + else + data = radix_tree_lookup(&domain->revmap_tree, hwirq); + if (likely(data)) { + if(hwirq==11) + printk("hwirq is in the linear revmap\n"); + desc = irq_data_to_desc(data);//container_of,已知成员变量地址,求出结构体起始地址 + if (irq) + *irq = data->irq; + } + rcu_read_unlock(); + return desc; +} +``` + +通过以上函数,我们确认到 irq_data 就存在 domain->revmap 中,但我们什么时候才将 irq_data 放入到 domain 中的呢? + +#### domain 中的 irq_data 来源 + +通过查找数组 `domain->revmap` 找到以下函数,可以确认 irq_data 就是在这里被添加进去的。 + +```c +static void irq_domain_set_mapping(struct irq_domain *domain, + irq_hw_number_t hwirq, + struct irq_data *irq_data) +{ + if (irq_domain_is_nomap(domain)) + return; + if(hwirq==11) + { + printk("[nfk test] irq_domain_set_mapping,hwirq=%d\n",hwirq); + dump_stack(); + } + + mutex_lock(&domain->revmap_mutex); + if (hwirq < domain->revmap_size) + rcu_assign_pointer(domain->revmap[hwirq], irq_data); + else + radix_tree_insert(&domain->revmap_tree, hwirq, irq_data); + mutex_unlock(&domain->revmap_mutex); +} +``` + +通过栈回溯得到以下信息: + +``` +[nfk test] irq_domain_set_mapping,hwirq=11 +[] dump_backtrace+0x1c/0x24 +[] show_stack+0x2c/0x38 +[] dump_stack_lvl+0x40/0x58 +[] dump_stack+0x14/0x1c //栈回溯 +[] irq_domain_set_mapping+0x84/0x86 //设置映射 +[] __irq_domain_alloc_irqs+0x1cc/0x25a //申请中断内存 +[] irq_create_fwspec_mapping+0xe6/0x248 +[] irq_create_of_mapping+0x50/0x6e +[] of_irq_get+0x4c/0x5e +[] of_irq_to_resource+0x2c/0xcc //解析节点的中断,并为其申请资源。 +[] of_irq_to_resource_table+0x32/0x4c +[] of_device_alloc+0x128/0x292 +[] of_platform_device_create_pdata+0x36/0x9a //平台设备总线创建设备 +[] of_platform_bus_create+0x120/0x172 +[] of_platform_bus_create+0x14a/0x172 //平台总线创建 +[] of_platform_populate+0x36/0x86 +[] of_platform_default_populate_init+0xbe/0xcc +[] do_one_initcall+0x3e/0x168 +[] kernel_init_freeable+0x19e/0x202 +[] kernel_init+0x1e/0x10a +[] ret_from_exception+0x0/0xc +``` + +以上的整个流程回溯起来篇幅有些大,这里不再深入分析,可以确定的是 irq_data 和 hwirq 的对应实在平台总线初始化时,进行设备节点的扫描,然后为每个设备都进行了 irq 的映射。 + +根据代码的追踪,最终确认到 `irq_domain_insert_irq` 调用的 `irq_domain_set_mapping(struct irq_domain *domain,irq_hw_number_t hwirq,struct irq_data *irq_data)` ,这时 irq_data 在获取时,采用的就是从 irq_desc 结构体中获取。相关代码如下: + +```c +static void irq_domain_insert_irq(int virq) +{ + struct irq_data *data; + + for (data = irq_get_irq_data(virq); data; data = data->parent_data) {//获取 irq_data +... + } + + irq_clear_status_flags(virq, IRQ_NOREQUEST); +} +``` + +那么现在问题就变成了 irq_desc 的来源在哪里? + +#### irq_desc 的来源 + +通过以下代码进行栈回溯,可以找到何时进行的 irq_desc 的创建。 + +```c +static void irq_insert_desc(unsigned int irq, struct irq_desc *desc) +{ + if(irq==1) + { + printk("irq_insert_desc irq=1\n"); + dump_stack(); + } + + radix_tree_insert(&irq_desc_tree, irq, desc); +} +``` + +发现流程与irq_data的来源基本一致,经过确认是在为中断申请资源时就已经创建了 irq_desc。栈回溯打印如下: + +``` +irq_insert_desc irq=1 +... +[] dump_backtrace+0x1c/0x24 +[] show_stack+0x2c/0x38 +[] dump_stack_lvl+0x40/0x58 +[] dump_stack+0x14/0x1c +[] __irq_alloc_descs+0x1f2/0x1f4 +[] irq_domain_alloc_descs+0x60/0x80 +[] __irq_domain_alloc_irqs+0x156/0x25a +[] irq_create_fwspec_mapping+0xe6/0x248 +[] irq_create_of_mapping+0x50/0x6e +... + +``` + +通过以上小节的分析,可以确认到整个中断的信息添加到 domain 中的节点是在平台驱动注册时就已经完成了。 + +#### 回讲第一节——中断申请 + +到现在为止,我们搞清楚了 domain 的整个的来龙去脉,那么我们现在再思考中断申请到底干了啥? + +```c +static int goldfish_rtc_probe(struct platform_device *pdev) +{ +··· + err = devm_request_irq(&pdev->dev, rtcdrv->irq, + goldfish_rtc_interrupt, + 0, pdev->name, rtcdrv);//申请irq +··· +} +``` + +无非就是为我们所有已经创建好的 irq_desc 添加上处理函数,以实现中断处理时对中断函数的调用。 + +### 流程续讲 + +上一小节讲清楚 domain 之后,我们继续将整个中断的处理流程。 + +``` +[] dump_backtrace+0x1c/0x24 +[] show_stack+0x2c/0x38 +[] dump_stack_lvl+0x40/0x58 +[] dump_stack+0x14/0x1c +[] goldfish_rtc_interrupt+0x22/0x74 +[] __handle_irq_event_percpu+0x52/0xe0 +[] handle_irq_event_percpu+0x12/0x4e +[] handle_irq_event+0x5e/0x94 +[] handle_fasteoi_irq+0xac/0x18e +[] generic_handle_domain_irq+0x28/0x3a //通过 hwirq=11,进行查找 rtc 的中断处理程序 +[] plic_handle_irq+0x8a/0xec //PLIC 中断处理程序 +[] generic_handle_domain_irq+0x28/0x3a //通过 hwirq=9,进行查找 plic 的中断处理程序 +[] riscv_intc_irq+0x34/0x5c //RISC-V 架构下的中断处理器 +[] generic_handle_arch_irq+0x4a/0x74 //根据架构查找 RISC-V 架构下的中断处理程序 +[] ret_from_exception+0x0/0xc +[] rcu_idle_enter+0x10/0x18 +``` + +可以看到栈回溯过程中执行了两次 generic_handle_domain_irq ,这两次的 domain 参数肯定是不同的,一次的 domain 是 RISC-V 的 domain,另一次是 PLIC 的 domain,通过这种级联的结构,使得 CPU 可以扩展更多的中断,并且通过中断控制器完成更加复杂的功能。 + +通过 PLIC 初始化的相关代码也可以看到,PLIC 的 domain 是由 parent 的,代码如下: + +```c + /* Find parent domain and register chained handler */ + if (!plic_parent_irq && irq_find_host(parent.np)) {//注册处理函数 + plic_parent_irq = irq_of_parse_and_map(node, i); + if (plic_parent_irq) + irq_set_chained_handler(plic_parent_irq, + plic_handle_irq);//为 parent 设置中断处理函数 + printk("[nfk test]set chained handler\n"); + } +``` + +也就是说,本质上 PLIC 也会有一个 irq_desc,也有自己的中断处理函数。通过设备树和打印信息,确认到 PLIC 的 hwirq 和 irq 都为 9 + +## 小结 + +本文的讲述了从中断申请 -> 中断产生 -> 中断处理的流程。 + +在中断申请时,并没有对中断申请进行深入的分析,而是在中断处理小节中,再返回来看中断申请,这样就感觉豁然开朗,中断申请的作用就显得非常简单了。 + +在中断处理小节中,先看了中断的处理流程,但是对于中断处理流程中的 domain 又不明白为什么有 hwirq 就可以找到中断处理函数,又针对性的补上了 domain 的分析。 + +至此,已经能够搞清楚一个 IRQ 从产生到处理的 C 语言部分的全部流程。下一篇文章中,将重点针对 IRQ 产生后 CPU 是如何在汇编层面调用到 C 语言这一部分代码的。 + +## 参考资料 + +* [articles/20220519-riscv-irq-analysis.md · 泰晓科技/RISCV-Linux - Gitee.com](https://gitee.com/tinylab/riscv-linux/blob/master/articles/20220519-riscv-irq-analysis.md) +* [如何分析 Linux 内核 RISC-V 架构相关代码](https://tinylab.org/riscv-linux-quickstart/) +* [Nuclei_N级别指令架构手册](https://www.rvmcu.com/quickstart-show-id-1.html#38) +* [sifive中断处理流程](https://www.elecfans.com/d/1575408.html) +* [linux kernel的中断子系统之(七):GIC代码分析 (wowotech.net)](http://www.wowotech.net/irq_subsystem/gic_driver.html) +* [linux 中断子系统 - GIC 驱动源码分析 - 知乎 (zhihu.com)](https://zhuanlan.zhihu.com/p/363134084) \ No newline at end of file