六、进阶 | 中断和中断处理(7)

中断和中断处理。第7部分。

外部中断介绍

这是Linux内核中断和中断处理章节的第七部分,上一个部分我们完成了由处理器生成的异常处理。在这一部分,我们将继续深入中断处理,并从外部硬件中断处理开始。如你所记得的,在上一部分我们以arch/x86/kernel/trap.c中的trap_init函数结束,下一步是调用init/main.c中的early_irq_init函数。

中断是由硬件或软件通过IRQ中断请求线发送的信号。外部硬件中断允许设备如键盘、鼠标等,指示它需要处理器的注意。一旦处理器接收到中断请求,它将暂时停止运行程序并调用一个特殊程序,这个程序取决于中断类型。我们已经知道这个程序被称为中断处理程序(或者从这部分开始我们将其称为ISR中断服务例程)。ISR中断处理程序可以在位于内存中固定地址的中断向量表中找到。处理完中断后,处理器恢复被中断的进程。在启动/初始化时,Linux内核识别机器中的所有设备,并将适当的中断处理程序加载到中断表中。正如我们在前几部分看到的,大多数异常简单地通过向被中断的进程发送Unix信号来处理。这就是内核可以快速处理异常的方式。不幸的是,我们不能使用这种方法来处理外部硬件中断,因为它们通常在它们相关的进程被挂起之后(有时是很久之后)到达。所以向当前进程发送Unix信号是没有意义的。外部中断处理取决于中断的类型:

  • I/O中断;
  • 定时器中断;
  • 处理器间中断。

我将尝试在这本书中描述所有类型的中断。

一般来说,一个I/O中断的处理程序必须足够灵活,能够同时为多个设备提供服务。例如,在PCI总线架构中,多个设备可能共享相同的IRQ线。在最简单的情况下,当一个I/O中断发生时,Linux内核必须做以下事情:

  • 保存IRQ的值和寄存器的内容到内核栈上;
  • 向服务IRQ线的硬件控制器发送确认;
  • 执行与设备相关联的中断服务例程(接下来我们将称之为ISR);
  • 恢复寄存器并从中断返回;

好的,我们了解了一点理论,现在让我们从early_irq_init函数开始。early_irq_init函数的实现在kernel/irq/irqdesc.c中。这个函数对irq_desc结构进行早期初始化。irq_desc结构是Linux内核中断管理代码的基础。这个结构的数组,名字也是irq_desc,跟踪Linux内核中的每一个中断请求源。这个结构定义在include/linux/irqdesc.h中,如你所见,它依赖于CONFIG_SPARSE_IRQ内核配置选项。这个内核配置选项启用了稀疏IRQ的支持。irq_desc结构包含许多不同的字段:

  • irq_common_data - 每个中断和芯片数据传递给芯片函数;
  • status_use_accessors - 包含中断源的状态,这是来自include/linux/irq.h中的enum值和在同一源代码文件中定义的不同宏的组合;
  • kstat_irqs - 每个CPU的IRQ统计;
  • handle_irq - 高级irq事件处理程序;
  • action - 确定在IRQ发生时要调用的中断服务例程;
  • irq_count - IRQ线上中断发生次数的计数器;
  • depth - 如果IRQ线被启用则为0,如果它至少被禁用过一次则为正值;
  • last_unhandled - 未处理计数的老化计时器;
  • irqs_unhandled - 未处理中断的计数;
  • lock - 用于序列化对IRQ描述符访问的自旋锁;
  • pending_mask - 待平衡的中断;
  • owner - 中断描述符的所有者。中断描述符可以从模块分配。这个字段需要提供对提供中断的模块的引用计数;
  • 等等。

当然,这不是irq_desc结构的所有字段,因为它太长了,无法描述这个结构的每个字段,但我们很快就会看到所有的。现在让我们开始深入early_irq_init函数的实现。

早期外部中断初始化

现在,让我们看看early_irq_init函数的实现。注意early_irq_init函数的实现依赖于CONFIG_SPARSE_IRQ内核配置选项。现在我们考虑当CONFIG_SPARSE_IRQ内核配置选项未设置时early_irq_init函数的实现。这个函数从声明以下变量开始:irq描述符计数器、循环计数器、内存节点和irq_desc描述符:

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

node是一个在线的NUMA节点,它取决于MAX_NUMNODES值,该值取决于CONFIG_NODES_SHIFT内核配置参数:

#define MAX_NUMNODES    (1 << NODES_SHIFT)
...
...
...
#ifdef CONFIG_NODES_SHIFT
    #define NODES_SHIFT     CONFIG_NODES_SHIFT
#else
    #define NODES_SHIFT     0
#endif

正如我已经写过的,first_online_node宏的实现取决于MAX_NUMNODES值:

#if MAX_NUMNODES > 1
  #define first_online_node       first_node(node_states[N_ONLINE])
#else
  #define first_online_node       0

node_states是在include/linux/nodemask.h中定义的枚举,代表节点状态的集合。在我们的情况下,我们正在寻找一个在线节点,如果MAX_NUMNODES是一或零,它将是0。如果MAX_NUMNODES大于一,node_states[N_ONLINE]将返回1first_node宏将展开为调用__first_node函数,它将返回最小的或第一个在线节点:

#define first_node(src) __first_node(&(src))

static inline int __first_node(const nodemask_t *srcp)
{
        return min_t(int, MAX_NUMNODES, find_first_bit(srcp->bits, MAX_NUMNODES));
}

更多关于这方面的内容将在另一章关于NUMA中介绍。在声明了这些局部变量之后的下一步是调用:

init_irq_default_affinity();

函数。init_irq_default_affinity函数定义在同一个源代码文件中,并且依赖于CONFIG_SMP内核配置选项为给定的cpumask结构(在我们的情况下是irq_default_affinity)分配空间:

#if defined(CONFIG_SMP)
cpumask_var_t irq_default_affinity;

static void __init init_irq_default_affinity(void)
{
        alloc_cpumask_var(&irq_default_affinity, GFP_NOWAIT);
        cpumask_setall(irq_default_affinity);
}
#else
static void __init init_irq_default_affinity(void)
{
}
#endif

我们知道,当硬件,如磁盘控制器或键盘,需要处理器的注意时,它会抛出一个中断。中断告诉处理器发生了一些事情,处理器应该中断当前进程并处理即将到来的事件。为了防止多个设备发送相同的中断,建立了IRQ系统,其中计算机系统中的每个设备都被分配了自己特殊的IRQ,以便其中断是唯一的。Linux内核可以为特定的处理器分配某些IRQs。这被称为SMP IRQ affinity,它允许您控制系统将如何响应各种硬件事件(这就是为什么它只有在设置了CONFIG_SMP内核配置选项时才有特定的实现)。在我们分配了irq_default_affinity cpumask之后,我们可以看到printk输出:

printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS);

它打印出NR_IRQS

~$ dmesg | grep NR_IRQS
[    0.000000] NR_IRQS:4352

NR_IRQSirq描述符或换句话说最大中断数的数量。它的值取决于CONFIG_X86_IO_APIC内核配置选项的状态。如果CONFIG_X86_IO_APIC未设置,并且Linux内核使用旧的PIC芯片,NR_IRQS是:

#define NR_IRQS_LEGACY                    16

#ifdef CONFIG_X86_IO_APIC
...
...
...
#else
# define NR_IRQS                        NR_IRQS_LEGACY
#endif

在另一种情况下,当设置了CONFIG_X86_IO_APIC内核配置选项时,NR_IRQS取决于处理器的数量和中断向量的数量:

#define CPU_VECTOR_LIMIT               (64 * NR_CPUS)
#define NR_VECTORS                     256
#define IO_APIC_VECTOR_LIMIT           ( 32 * MAX_IO_APICS )
#define MAX_IO_APICS                   128

# define NR_IRQS                                       \
        (CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT ?     \
                (NR_VECTORS + CPU_VECTOR_LIMIT)  :     \
                (NR_VECTORS + IO_APIC_VECTOR_LIMIT))
...
...
...

我们记得从前面的部分,我们可以在Linux内核配置过程中用CONFIG_NR_CPUS配置选项设置处理器的数量:

图片上传失败,请重新上传

在第一种情况(CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT)中,NR_IRQS将是4352,在第二种情况(CPU_VECTOR_LIMIT < IO_APIC_VECTOR_LIMIT)中,NR_IRQS将是768。在我的情况下,NR_CPUS8,正如你在我的配置中所看到的,CPU_VECTOR_LIMIT512IO_APIC_VECTOR_LIMIT4096。所以我的配置的NR_IRQS4352

~$ dmesg | grep NR_IRQS
[    0.000000] NR_IRQS:4352

接下来,我们将IRQ描述符数组分配给在early_irq_init函数开始时定义的irq_desc变量,并使用ARRAY_SIZE宏计算irq_desc数组的计数:

desc = irq_desc;
count = ARRAY_SIZE(irq_desc);

irq_desc数组定义在同一个源代码文件中,如下所示:

struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
        [0 ... NR_IRQS-1] = {
                .handle_irq     = handle_bad_irq,
                .depth          = 1,
                .lock           = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
        }
};

irq_descirq描述符的数组。它有三个已经初始化的字段:

  • handle_irq - 如上所述,这个字段是高级irq事件处理程序。在我们的情况下,它用定义在kernel/irq/handle.c源代码文件中的handle_bad_irq函数初始化,该函数处理杂散和未处理的IRQ;
  • depth - 如果IRQ线被启用则为0,如果它至少被禁用过一次则为正值;
  • lock - 用于序列化对IRQ描述符访问的自旋锁。

由于我们已经计算了中断的数量并初始化了我们的irq_desc数组,我们开始在循环中填充描述符:

for (i = 0; i < count; i++) {
    desc[i].kstat_irqs = alloc_percpu(unsigned int);
    alloc_masks(&desc[i], GFP_KERNEL, node);
    raw_spin_lock_init(&desc[i].lock);
    lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
	desc_set_defaults(i, &desc[i], node, NULL);
}

我们将遍历所有中断描述符,并执行以下操作:

首先,我们使用alloc_percpu宏为irq内核统计分配每个CPU变量。这个宏为系统中的每个处理器分配一个给定类型的一个对象实例。你可以通过/proc/stat从用户空间访问内核统计信息:

~$ cat /proc/stat
cpu  207907 68 53904 5427850 14394 0 394 0 0 0
cpu0 25881 11 6684 679131 1351 0 18 0 0 0
cpu1 24791 16 5894 679994 2285 0 24 0 0 0
cpu2 26321 4 7154 678924 664 0 71 0 0 0
cpu3 26648 8 6931 678891 414 0 244 0 0 0
...
...
...

其中第六列是服务中断的数量。在这之后,我们为给定的IRQ描述符亲和性分配cpumask,并为给定的中断描述符初始化自旋锁。在这之后,在临界区之前,锁将通过调用raw_spin_lock获取,并通过调用raw_spin_unlock解锁。下一步,我们调用lockdep_set_class宏,为给定中断描述符的锁设置锁验证器irq_desc_lock_class类。关于lockdepspinlock和其他同步原语的更多信息将在单独的章节中描述。

在循环的最后,我们调用kernel/irq/irqdesc.c中的desc_set_defaults函数。这个函数接受四个参数:

  • 中断号;
  • 中断描述符;
  • 在线NUMA节点;
  • 中断描述符的所有者。中断描述符可以从模块分配。这个字段需要提供对提供中断的模块的引用计数;

并填充剩余的irq_desc字段。desc_set_defaults函数填充中断号、irq芯片、特定于平台的每个芯片私有数据用于芯片方法、每个irq_chip方法的每个IRQ数据和每个irqirq芯片数据的MSI描述符:

desc->irq_data.irq = irq;
desc->irq_data.chip = &no_irq_chip;
desc->irq_data.chip_data = NULL;
desc->irq_data.handler_data = NULL;
desc->irq_data.msi_desc = NULL;
...
...
...

irq_data.chip结构提供了通用API,如irq_set_chipirq_set_irq_type等,用于irq控制器驱动程序。你可以在kernel/irq/chip.c源代码文件中找到它。

在这之后,我们为给定的描述符设置访问器的状态,并设置中断的禁用状态:

...
...
...
irq_settings_clr_and_set(desc, ~0, _IRQ_DEFAULT_INIT_FLAGS);
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
...
...
...

接下来,我们将高级中断处理程序设置为handle_bad_irq,它处理杂散和未处理的IRQ(由于硬件尚未初始化,我们设置这个处理程序),将irq_desc.desc设置为1,这意味着一个IRQ被禁用,重置未处理中断的计数,以及一般中断的计数:

...
...
...
desc->handle_irq = handle_bad_irq;
desc->depth = 1;
desc->irq_count = 0;
desc->irqs_unhandled = 0;
desc->name = NULL;
desc->owner = owner;
...
...
...

在这之后,我们遍历所有可能的处理器,使用for_each_possible_cpu辅助函数,并将给定中断描述符的kstat_irqs设置为零:

	for_each_possible_cpu(cpu)
		*per_cpu_ptr(desc->kstat_irqs, cpu) = 0;

并调用kernel/irq/irqdesc.c中的desc_smp_init函数,该函数初始化给定中断描述符的NUMA节点,设置默认的SMP亲和性,并根据CONFIG_GENERIC_PENDING_IRQ内核配置选项的值清除给定中断描述符的pending_mask

static void desc_smp_init(struct irq_desc *desc, int node)
{
        desc->irq_data.node = node;
        cpumask_copy(desc->irq_data.affinity, irq_default_affinity);
#ifdef CONFIG_GENERIC_PENDING_IRQ
        cpumask_clear(desc->pending_mask);
#endif
}

early_irq_init函数的末尾,我们返回arch_early_irq_init函数的返回值:

return arch_early_irq_init();

这个函数定义在kernel/apic/vector.c中,只包含一个调用kernel/apic/io_apic.c中的arch_early_ioapic_init函数的调用。正如我们从arch_early_ioapic_init函数的名称可以理解的,这个函数对I/O APIC进行早期初始化。首先,它通过调用nr_legacy_irqs函数检查传统中断的数量。如果我们没有使用Intel 8259可编程中断控制器的传统中断,我们将io_apic_irqs设置为0xffffffffffffffff

if (!nr_legacy_irqs())
	io_apic_irqs = ~0UL;

在这之后,我们遍历所有的I/O APICs并为寄存器分配空间,通过调用alloc_ioapic_saved_registers

for_each_ioapic(i)
	alloc_ioapic_saved_registers(i);

arch_early_ioapic_init函数的末尾,我们遍历所有的传统IRQ(从IRQ0IRQ15)并在循环中为irq_cfg分配空间,它表示给定NUMA节点上的IRQ配置:

for (i = 0; i < nr_legacy_irqs(); i++) {
    cfg = alloc_irq_and_cfg_at(i, node);
    cfg->vector = IRQ0_VECTOR + i;
    cpumask_setall(cfg->domain);
}

就这些。

稀疏IRQ

我们已经在本部分的开头看到,early_irq_init函数的实现依赖于CONFIG_SPARSE_IRQ内核配置选项。之前我们看到了当CONFIG_SPARSE_IRQ配置选项未设置时early_irq_init函数的实现,现在让我们看看当这个选项设置时它的实现。这个函数的实现非常相似,但略有不同。我们可以看到在early_irq_init函数的开头有相同的变量定义和init_irq_default_affinity的调用:

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

	init_irq_default_affinity();
	...
	...
	...
}
#else
...
...
...

但在这之后我们可以看到以下调用:

initcnt = arch_probe_nr_irqs();

arch_probe_nr_irqs函数定义在arch/x86/kernel/apic/vector.c中,计算预先分配的IRQ数量,并用这个数字更新nr_irqs。但是,停下来。为什么会有预先分配的IRQ呢?有一种称为消息信号中断的替代形式的中断在PCI中可用。与分配固定数量的中断请求不同,设备被允许在RAM的特定地址记录一条消息,实际上是在本地APIC上显示。MSI允许设备分配12481632个中断,而MSI-X允许设备分配多达2048个中断。现在我们知道IRQ可以预先分配。关于MSI的更多信息将在下一部分中介绍,但现在让我们看看arch_probe_nr_irqs函数。我们可以看到检查哪个将系统中每个处理器的中断向量数量分配给nr_irqs如果它更大,并计算代表MSI中断数量的nr

int nr_irqs = NR_IRQS;

if (nr_irqs > (NR_VECTORS * nr_cpu_ids))
	nr_irqs = NR_VECTORS * nr_cpu_ids;

nr = (gsi_top + nr_legacy_irqs()) + 8 * nr_cpu_ids;

看看gsi_top变量。每个APIC都通过自己的ID和其IRQ开始的偏移量来识别。它被称为GSI基址或全局系统中断基址。所以gsi_top代表它。我们从多处理器配置表中获取全局系统中断基址(你可以记得我们在Linux内核初始化过程的第六章中解析了这个表)。

在这之后,我们根据gsi_top的值更新nr

#if defined(CONFIG_PCI_MSI) || defined(CONFIG_HT_IRQ)
        if (gsi_top <= NR_IRQS_LEGACY)
                nr +=  8 * nr_cpu_ids;
        else
                nr += gsi_top * 16;
#endif

更新nr_irqs如果它小于nr并返回传统IRQ的数量:

if (nr < nr_irqs)
    nr_irqs = nr;

return nr_legacy_irqs();
}

arch_probe_nr_irqs之后的下一步是打印关于IRQ数量的信息:

printk(KERN_INFO "NR_IRQS:%d nr_irqs:%d %d\n", NR_IRQS, nr_irqs, initcnt);

我们可以在dmesg输出中找到它:

$ dmesg | grep NR_IRQS
[    0.000000] NR_IRQS:4352 nr_irqs:488 16

在这之后,我们做一些检查以确保nr_irqsinitcnt的值不大于最大允许的IRQ数量:

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

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

其中IRQ_BITMAP_BITS等于NR_IRQS如果CONFIG_SPARSE_IRQ未设置,另一种情况下是NR_IRQS + 8196。下一步是遍历循环中需要分配的所有中断描述符,并为描述符分配空间并插入到irq_desc_tree基数树中:

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

early_irq_init函数的末尾,我们像之前在CONFIG_SPARSE_IRQ选项未设置的情况下已经做过的那样,返回arch_early_irq_init函数的调用值:

return arch_early
_init();

这就是全部。

结论

这是中断和中断处理章节的第七部分的结尾,我们在这一部分开始深入外部硬件中断。我们看到了irq_desc结构的早期初始化,它代表一个外部中断的描述,并包含有关它的信息,如irq动作列表、中断处理程序的信息、中断的所有者、未处理中断的计数等。在下一部分,我们将继续研究外部中断。

如果您有任何问题或建议,请发表评论或在twitter上联系我。

请注意,英语不是我的第一语言,如果有任何不便,我深表歉意。如果您发现任何错误,请向我发送PR到linux-insides

链接

#嵌入式##面经#
Linux嵌入式必考必会 文章被收录于专栏

&quot;《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。

全部评论

相关推荐

从某个大疆员工采访视频里弄出来的 面试以这为根基去说应该会好一些工程师文化Smart&nbsp;is&nbsp;the&nbsp;new&nbsp;sexy用技术改变世界&nbsp;生活方式和生产力-工程师思维用能力去不断适应变化&nbsp;并且知识——去用的思想形成T型人才,专业够深入且再不同领域有一定的涉略快速迭代和高品质 在实践中快速学习&nbsp;充分发挥能力的地方有目标感要努力的去追求自己的目标&nbsp;真知灼见&nbsp;快速迭代&nbsp;有事干精神的梦想家永远不要停止学习&nbsp;尝试不断的去否定自己(获得新知)为世界科技之美增添科技之美团队的进步速度等于管理者的进步速度&nbsp;保持空杯形态&nbsp;快速的去学习知识&nbsp;识别到自己的做事本身到组件团队一起把事情做好的成就感&nbsp;一起解决&nbsp;成就他人内心追求的理想是不是一样的分工是不是合理的&nbsp;不断磨合改革是自我迭代的进步 对理想有着执着的信念的人&nbsp;理想和信念是点燃你所有passion的前提&nbsp;强大的内心&nbsp;强烈的信念&nbsp;把世界变得更好工程师文化:不断孜孜不倦的去摸索事务发展的一个规律服务于自己的产品&nbsp;服务与我们用户的实践体验&nbsp;差异化的精品做到顶尖怎么看?量变到质变再量变再质变&nbsp;所有技术服务于产品&nbsp;机身更加轻巧和结实&nbsp;设计的线条流体符合悬停的最佳状态&nbsp;声音又小动力动力又主&nbsp;应用科学上的探索空间智能时代&nbsp;不同应用场景上的体现技术如何发展和产品要如何交互一个人的努力要适应潮流的发展&nbsp;才能够获得最大的回报&nbsp;大疆作为行业龙头&nbsp;在技术领域钻研非常深的公司&nbsp;机器人类或者自动驾驶类这种新鲜产业&nbsp;发展快的行业龙头,来到大疆可以跟随这样的大势,让我自己有更好的助力不俱权威敢于提出自己的想法的&nbsp;亲自动手实践保证自己的心态&nbsp;空杯心态把自己所学所用运用到自己的产品开发中去保证有激情有理想和自己的愿景 #软件开发笔面经#
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务