show_interrupts函数的进一步解析及irq_desc结构体
一、背景
中断的信息在分析一些系统稳定性问题时比较关键,比如某些中断在开机到现在总共收到了多少次,在某个时间段内,某些中断增加收到了多少次,这些信息,如果都是通过 cat /proc/interrupts来获取有时候就显得有点低效和不灵活,所以有需要自己去获取这些中断信息的需求。
在之前的博客 show_interrupts函数及nr_irqs 里,我们分析了show_interrupts函数,主要侧重于介绍show_interrupts函数作为proc_create_seq创建proc节点所需要的seq_operations的ops里的.show函数其有关的细节,另外,还介绍了不同平台的nr_irqs这个EXPORT_SYMBOL_GPL的这个中断有关的重要的全局变量的值变动的情况,这个nr_irqs也决定了show_interrupts函数进入的总次数。
在这篇博客里,我们继续介绍show_interrupts函数的实现,借此了解中断子系统里的另外一个重要的结构体irq_desc结构体,以及与irq_desc结构体有关的一些细节。
二、show_interrupts函数的进一步展开介绍及irq_desc结构体
2.1 show_interrupts函数的进一步介绍
2.1.1 show_interrupts函数先会根据传入的项序号区分来打印普通中断还是arch相关的特殊中断
show_interrupts函数一开始,会区分当前要打印的项的序号是ACTUAL_NR_IRQS还是小于ACTUAL_NR_IRQS,ACTUAL_NR_IRQS就是nr_irqs(具体见之前的博客 show_interrupts函数及nr_irqs)。
如果是show_interrupts函数这一次进入要打印的项的序号是ACTUAL_NR_IRQS,则表示要打印arch相关的中断,这些中断与具体的平台相关,不同的平台实现完全不同。
比如下图的arm64平台,看似都是核间通讯相关的中断:
而x86平台,如下图,则显得种类庞杂:
2.1.2 irq_to_desc根据irq找到irq_desc
跳过下图里的之前的 show_interrupts函数及nr_irqs 博客里的 2.1.3 一节里讲到的用来计算与显示的栏目及显示的宽度有关的逻辑:
之后,就是下图里的irq_to_desc这个去根据irq的number找到对应的irq_desc结构体:
这里的irq_to_desc在较新版本的内容是用的maple tree来实现,maple tree出现主要是用来替换红黑树,maple tree作为一个支持用rcu锁来低并发竞争的一套机制,替换了内核里不少原本使用红黑树+双向链表的地方。
这里irq_to_desc用maple tree来替换原来的bitmap和radix tree的原因如下:
翻译一下,是两个原因,一个是现有机制上限比较低,不能满足可能越来越多的中断数量的机器的需求,另一个原因是用maple tree可以提供一个机制用来找到空闲的范围,来移除这个硬编码的上限限制。
irq_to_desc的老版本和新版本的实现对比如下:
老版本:
新版本:
回到show_interrupts的逻辑里,要注意,通过irq_to_desc拿到irq_desc的指针后,也要判断是否是NULL,以及irq_settings_is_hidden:
2.1.3 判断kstat_irqs这个per_cpu是否分配,若分配则统计看该终端是否有计数
如下图,进行kstat_irqs的判断:
kstat_irqs是一个per_cpu变量,要进行alloc_percpu的分配,如下图,是在alloc_desc里进行的分配:
如果kstat_irqs有分配,则用any_count这个变量来统计看是否有cpu上有这个中断的计数:
如上图看到,这个any_count临时变量在统计时用了 |= 的运算,而不是+=的运算,是可以加速运算,因为any_count只用于判断是否有计数,而不是用来计算总计数。
2.1.4 考虑级联中断的场景
回到下图里的这个判断:
desc->action是指中断触发时对应的要执行的回调函数(用户注册的中断处理函数),为什么有(!desc->action)和irq_desc_is_chained(desc)这个组合的||的判断,因为一个irq_desc实例可能有两种情况,一种是它本身就对应一个中断“叶子”节点,用“叶子”是方便描述中断级联的情况,由于有级联的情况,某些irq_desc并不是最终的中断“叶子”节点,而是一个级联的根节点,这时候,它就不应该有用户注册的中断处理函数,也就是desc->action是NULL,或者desc->action是一个故意的错误的handler,也就是这个irq_desc_is_chained的逻辑里进行的判断:
如上图里的注释,级联的中断的根节点的这个desc->action这个回调是不应该也不能被调用的。
有关级联中断场景相关的更多的介绍见后续的博客。
2.1.5 有关中断的描述信息的打印
接下来就是下面这一长段的用来打印中断实际是啥的打印了:
与之有关的实验有下面 3.1 和 3.2,有关irq_desc的name及irq_desc的其他描述中断是啥的打印。
2.2 irq_desc结构体
irq_desc是内核终端子系统里的核心结构体,为什么说它是核心,因为它与中断子系统的其他几个重点的结构体都有关联。
2.2.1 irq number即virq,与hwirq,及irq_data
linux irq number也被称为virq,是linux里管理irq处理的一个核心的number序号。不同于hwirq,irq number它是不重复的。
hwirq顾名思义,就是硬件的irq,它是可能重复的。由于它会重复,所以,linux需要用irq number即virq去重新进行按需排列,进行逻辑上的区分,除以以外,还得用一个小模块来关联irq number和hwirq,这个模块就是以后的博客里会讲到的 irq_domain(irq_domain的函数在irq_alloc_desc API的基础上增加了hwirq和irq number之间的映射)。
回到这里讲的irq_desc结构体里来,有关hwirq的数值在irq_desc结构体里也是有记录的,记录在irq_data里:
2.2.2 irq_data.chip打印出中断控制器的芯片名字
在下面 3.2 的实验里有得出,大部分走的分支是用的desc->irq_data.chip->name进行打印中断控制器芯片的名字(下图里的分支逻辑):
2.2.3 irq_desc的name
从下面 3.2 的实验情况来看,irq_desc的name这个field,无有效信息。无论在x86平台还是arm64平台,都无法通过这个name来理解到底是哪个具体的中断,x86平台基本name都是"edge",arm64平台都是NULL。
在x86平台,下图里的CONFIG_GENERIC_IRQ_SHOW_LEVEL一般并没有开:
所以通过desc->name来打印这个edge, arm64平台,若打开了CONFIG_GENERIC_IRQ_SHOW_LEVEL,则desc->name都是NULL。
2.2.4 irq_desc的action
action在上面 2.1.4 里也讲到,是指中断触发时对应的要执行的回调函数(用户注册的中断处理函数),通过打印action->name是可以得知中断具体是啥的信息的,下面 3.2 的实验里有总结。
三、相关的实验
3.1 有关irq_desc的name的实验
下面的代码,是依次捞取各个中断对应的irq_desc的name这个field的内容:
for_each_irq_desc(irq, desc) {if (!desc->kstat_irqs || !desc->name) {printk("zhaoxin:irq[%u]desc->kstat_irqs[0x%llx],desc->name[0x%llx]\n",irq, (u64)desc->kstat_irqs, (u64)desc->name);continue;}printk("zhaoxin:irq[%u]name[%s]\n",irq, desc->name);
}
在arm64的平台里,这个name经常是NULL:
所以不能指望靠这个name来表达中断是哪一个。
3.2 有关irq_desc的描述中断类型中断chip等描述信息
3.2.1 实验有关的代码改动及实验情况
如下代码对show_interrupts进行了改动,增加了额外的一些seq_printf的日志,方便对照cat /proc/interrupts的输出,对应理解含义:
int show_interrupts(struct seq_file *p, void *v)
{static int prec;unsigned long flags, any_count = 0;int i = *(loff_t *) v, j;struct irqaction *action;struct irq_desc *desc;if (i > ACTUAL_NR_IRQS)return 0;if (i == ACTUAL_NR_IRQS)return arch_show_interrupts(p, prec);/* print header and calculate the width of the first column */if (i == 0) {for (prec = 3, j = 1000; prec < 10 && j <= nr_irqs; ++prec)j *= 10;seq_printf(p, "%*s", prec + 8, "");for_each_online_cpu(j)seq_printf(p, "CPU%-8d", j);seq_putc(p, '\n');seq_printf(p, "ACTUAL_NR_IRQS=%d,nr_irqs=%d\n", ACTUAL_NR_IRQS, nr_irqs);}rcu_read_lock();desc = irq_to_desc(i);if (!desc || irq_settings_is_hidden(desc))goto outsparse;if (desc->kstat_irqs) {for_each_online_cpu(j)any_count |= data_race(*per_cpu_ptr(desc->kstat_irqs, j));}if ((!desc->action || irq_desc_is_chained(desc)) && !any_count)goto outsparse;seq_printf(p, "%*d: ", prec, i);for_each_online_cpu(j)seq_printf(p, "%10u ", desc->kstat_irqs ?*per_cpu_ptr(desc->kstat_irqs, j) : 0);raw_spin_lock_irqsave(&desc->lock, flags);if (desc->irq_data.chip) {if (desc->irq_data.chip->irq_print_chip) {seq_printf(p, " irq_print_chip ");desc->irq_data.chip->irq_print_chip(&desc->irq_data, p);}else if (desc->irq_data.chip->name) {seq_printf(p, " irq_data.chip->name %8s", desc->irq_data.chip->name);}elseseq_printf(p, " irq_data.chip %8s", "-");} else {seq_printf(p, " irq_data.chip_none %8s", "None");}if (desc->irq_data.domain)seq_printf(p, " irq_data.domain %*lu", prec, desc->irq_data.hwirq);elseseq_printf(p, " irq_data.domain_false %*s", prec, "");
#ifdef CONFIG_GENERIC_IRQ_SHOW_LEVELseq_printf(p, " %-8s", irqd_is_level_type(&desc->irq_data) ? "Level" : "Edge");
#endifif (desc->name)seq_printf(p, "has_name-%-8s", desc->name);elseseq_printf(p, "no_name");action = desc->action;if (action) {seq_printf(p, " action %s", action->name);while ((action = action->next) != NULL)seq_printf(p, "action, %s", action->name);}seq_putc(p, '\n');raw_spin_unlock_irqrestore(&desc->lock, flags);
outsparse:rcu_read_unlock();return 0;
}
上面代码里的改动部分:
cat /proc/interrupts如下:
3.2.2 实验情况的总结
可以从上图里看到:
1)irq的chip的名字一般都是走的下图里的这个desc->irq_data.chip->name的逻辑进行的irq chip的打印:
2)基本上所有的irq_desc实例都是有irq_data.domain的,打出来的irq_data.hwirq数值来看,属于不同的irq_data.chip下hwirq相同的情况很多(hwirq很多都是0)
3)desc->name的信息从x86平台上来看要么是NULL,要么显示的是edge/fasteoi这些无法知道到底是什么中断的描述
arm64下呢?我也做了个实验,都是NULL:
4)通过desc->action的信息还是能得知中断具体是啥的信息的