六、进阶 | 中断和中断处理(8)
中断和中断处理。第8部分。
非早期的IRQ初始化
这是Linux内核中断和中断处理的章节的第八部分,在我们之前的部分中,我们开始深入外部硬件中断。我们查看了kernel/irq/irqdesc.c源代码文件中early_irq_init
函数的实现,并看到了该函数中irq_desc
结构的初始化。提醒一下,irq_desc
结构(在include/linux/irqdesc.h中定义)是Linux内核中断管理代码的基础,代表一个中断描述符。在这部分,我们将继续深入了解与外部硬件中断相关的初始化工作。
在init/main.c中调用early_irq_init
函数之后,我们可以看到对init_IRQ
函数的调用。这个函数是特定于架构的,并在arch/x86/kernel/irqinit.c中定义。init_IRQ
函数对vector_irq
percpu变量进行初始化,该变量在同一个arch/x86/kernel/irqinit.c源代码文件中定义:
...
DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
[0 ... NR_VECTORS - 1] = -1,
};
...
它代表中断向量号的percpu
数组。vector_irq_t
在arch/x86/include/asm/hw_irq.h中定义,并展开为:
typedef int vector_irq_t[NR_VECTORS];
其中NR_VECTORS
是向量号的数量,正如你从本章的第一部分中记得的,对于x86_64它是256
:
#define NR_VECTORS 256
因此,在init_IRQ
函数的开始,我们用legacy
中断的向量号填充vector_irq
percpu数组:
void __init init_IRQ(void)
{
int i;
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;
...
...
...
}
这个vector_irq
将在arch/x86/kernel/irq.c中的do_IRQ
函数处理外部硬件中断的第一步中使用:
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
...
...
...
irq = __this_cpu_read(vector_irq[vector]);
if (!handle_irq(irq, regs)) {
...
...
...
}
exiting_irq();
...
...
return 1;
}
为什么这里使用legacy
?实际上,所有中断都由现代的IO-APIC控制器处理。但是这些中断(从0x30
到0x3f
)由传统的中断控制器处理,如可编程中断控制器。如果这些中断由I/O APIC
处理,那么这个向量空间将被释放并重用。让我们更仔细地看这段代码。首先,nr_legacy_irqs
在arch/x86/include/asm/i8259.h中定义,并且只返回legacy_pic
结构中的nr_legacy_irqs
字段:
static inline int nr_legacy_irqs(void)
{
return legacy_pic->nr_legacy_irqs;
}
这个结构在同一个头文件中定义,代表非现代可编程中断控制器:
struct legacy_pic {
int nr_legacy_irqs;
struct irq_chip *chip;
void (*mask)(unsigned int irq);
void (*unmask)(unsigned int irq);
void (*mask_all)(void);
void (*restore_mask)(void);
void (*init)(int auto_eoi);
int (*irq_pending)(unsigned int irq);
void (*make_irq)(unsigned int irq);
};
实际的默认传统中断数量由arch/x86/include/asm/irq_vectors.h中的NR_IRQ_LEGACY
宏表示:
#define NR_IRQS_LEGACY 16
在循环中,我们通过IRQ0_VECTOR + i
索引使用per_cpu
宏访问vecto_irq
per-cpu数组,并将传统向量号写入其中。IRQ0_VECTOR
宏在arch/x86/include/asm/irq_vectors.h头文件中定义,并展开为0x30
:
#define FIRST_EXTERNAL_VECTOR 0x20
#define IRQ0_VECTOR ((FIRST_EXTERNAL_VECTOR + 16) & ~15)
为什么是0x30
?你可以从本章的第一部分中记得,前32个向量号从0
到31
被处理器保留,并用于处理架构定义的异常和中断。从0x30
到0x3f
的向量号为ISA保留。所以,这意味着我们从IRQ0_VECTOR
开始填充vector_irq
,它等于32
到IRQ0_VECTOR + 16
(在0x30
之前)。
在init_IRQ
函数的末尾,我们可以看到以下函数的调用:
x86_init.irqs.intr_init();
来自arch/x86/kernel/x86_init.c源代码文件。如果你读过关于Linux内核初始化过程的章节,你可能记得x86_init
结构。这个结构包含几个文件,这些文件指向与平台设置(在我们的情况下是x86_64
)相关的函数,例如resources
与内存资源相关,mpparse
与解析多处理器配置表表相关,等等。正如我们所看到的,x86_init
还包含irqs
字段,包含以下三个字段:
struct x86_init_ops x86_init __initdata
{
...
...
...
.irqs = {
.pre_vector_init = init_ISA_irqs,
.intr_init = native_init_IRQ,
.trap_init = x86_init_noop,
},
...
...
...
}
现在,我们对native_init_IRQ
感兴趣。正如我们所注意到的,native_init_IRQ
函数的名称包含native_
前缀,这意味着这个函数是特定于架构的。它在arch/x86/kernel/irqinit.c中定义,并执行本地APIC的一般初始化和ISA irq的初始化。让我们看看native_init_IRQ
函数的实现,并尝试理解那里发生了什么。native_init_IRQ
函数从执行以下函数开始:
x86_init.irqs.pre_vector_init();
正如我们上面看到的,pre_vector_init
指向init_ISA_irqs
函数,它在同一个源代码文件中定义,正如我们从函数名称中理解的,它进行ISA
相关中断的初始化。init_ISA_irqs
函数从定义chip
变量开始,该变量具有irq_chip
类型:
void __init init_ISA_irqs(void)
{
struct irq_chip *chip = legacy_pic->chip;
...
...
...
irq_chip
结构在include/linux/irq.h头文件中定义,代表硬件中断芯片描述符。它包含:
name
- 设备的名称。在/proc/interrupts
中使用:
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
0: 16 0 0 0 0 0 0 0 IO-APIC 2-edge timer
1: 2 0 0 0 0 0 0 0 IO-APIC 1-edge i8042
8: 1 0 0 0 0 0 0 0 IO-APIC 8-edge rtc0
看最后一列;
(*irq_mask)(struct irq_data *data)
- 屏蔽中断源;(*irq_ack)(struct irq_data *data)
- 新中断的开始;(*irq_startup)(struct irq_data *data)
- 启动中断;(*irq_shutdown)(struct irq_data *data)
- 关闭中断- 等等。
字段。注意irq_data
结构代表一组每个irq芯片数据,传递到芯片函数。它包含mask
- 用于访问芯片寄存器的预计算位掩码,irq
- 中断号,hwirq
- 硬件中断号,本地到中断域芯片低级中断硬件访问,等等。
在此之后,根据CONFIG_X86_64
和CONFIG_X86_LOCAL_APIC
内核配置选项调用arch/x86/kernel/apic/apic.c中的init_bsp_APIC
函数:
#if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
init_bsp_APIC();
#endif
这个函数对bootstrap processor
(或首先启动的处理器)的APIC进行初始化。它首先检查我们是否找到了SMP配置(在Linux内核初始化过程的第六部分中了解更多)并且处理器有APIC
:
if (smp_found_config || !cpu_has_apic)
return;
否则,我们从这个函数返回。下一步我们调用同一个源代码文件中的clear_local_APIC
函数,该函数关闭本地APIC
(在高级可编程中断控制器
章节中了解更多),并通过设置unsigned int value
为APIC_SPIV_APIC_ENABLED
来启用第一个处理器的APIC
:
value = apic_read(APIC_SPIV);
value &= ~APIC_VECTOR_MASK;
value |= APIC_SPIV_APIC_ENABLED;
并使用apic_write
函数的帮助将其写入:
apic_write(APIC_SPIV, value);
在启用了引导处理器的APIC
之后,我们回到init_ISA_irqs
函数,在下一步中我们初始化传统的Programmable Interrupt Controller
并为每个传统irq设置传统芯片和处理程序:
legacy_pic->init(0);
for (i = 0; i < nr_legacy_irqs(); i++)
irq_set_chip_and_handler(i, chip, handle_level_irq);
我们在哪里可以找到init
函数?legacy_pic
在arch/x86/kernel/i8259.c中定义,它是:
struct legacy_pic *legacy_pic = &default_legacy_pic;
其中default_legacy_pic
是:
struct legacy_pic default_legacy_pic = {
...
...
...
.init = init_8259A,
...
...
...
}
init_8259A
函数在同一个源代码文件中定义,并执行Intel 8259Programmable Interrupt Controller
的初始化(关于它的更多信息将在关于Programmable Interrupt Controllers
和APIC
的单独章节中介绍)。
现在我们可以回到native_init_IRQ
函数,在init_ISA_irqs
函数完成其工作后。下一步是调用apic_intr_init
函数,该函数分配特殊的中断门,这些中断门由SMP架构用于处理器间中断。alloc_intr_gate
宏在arch/x86/include/asm/desc.h中用于中断描述符分配:
#define alloc_intr_gate(n, addr) \
do { \
alloc_system_vector(n); \
set_intr_gate(n, addr); \
} while (0)
正如我们所看到的,首先它展开为调用alloc_system_vector
函数,该函数检查给定向量号是否在used_vectors
位图中(在上一个部分中了解更多),如果它没有在used_vectors
位图中设置,我们就设置它。之后我们测试first_system_vector
是否大于给定的中断向量号,如果是,则分配给它:
if (!test_bit(vector, used_vectors)) {
set_bit(vector, used_vectors);
if (first_system_vector > vector)
first_system_vector = vector;
} else {
BUG();
}
我们已经看到了set_bit
宏,现在让我们看看test_bit
和first_system_vector
。首先test_bit
宏定义在arch/x86/include/asm/bitops.h中,如下所示:
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
我们在这里看到了三元运算符,它使用gcc内置函数__builtin_constant_p
测试给定向量号(nr
)是否在编译时已知。如果你对__builtin_constant_p
感到困惑,我们可以做一个简单的测试:
#include <stdio.h>
#define PREDEFINED_VAL 1
int main() {
int i = 5;
printf("__builtin_constant_p(i) is %d\n", __builtin_constant_p(i));
printf("__builtin_constant_p(PREDEFINED_VAL) is %d\n", __builtin_constant_p(PREDEFINED_VAL));
printf("__builtin_constant_p(100) is %d\n", __builtin_constant_p(100));
return 0;
}
并查看结果:
$ gcc test.c -o test
$ ./test
__builtin_constant_p(i) is 0
__builtin_constant_p(PREDEFINED_VAL) is 1
__builtin_constant_p(100) is 1
现在我想你应该明白了。让我们回到test_bit
宏。如果__builtin_constant_p
返回非零值,我们调用constant_test_bit
函数:
static inline int constant_test_bit(int nr, const void *addr)
{
const u32 *p = (const u32 *)addr;
return ((1UL << (nr & 31)) & (p[nr >> 5])) != 0;
}
以及另一种方式的variable_test_bit
:
static inline int variable_test_bit(int nr, const void *addr)
{
u8 v;
const u32 *p = (const u32 *)addr;
asm("btl %2,%1; setc %0" : "=qm" (v) : "m" (*p), "Ir" (nr));
return v;
}
这两个函数之间有什么区别,为什么我们需要两个不同的函数来达到相同的目的?正如你已经猜到的,主要目的是优化。如果我们使用这些函数的简单示例:
#define CONST 25
int main() {
int nr = 24;
variable_test_bit(nr, (int*)0x10000000);
constant_test_bit(CONST, (int*)0x10000000)
return 0;
}
并查看我们示例的汇编输出,我们将看到以下汇编代码:
pushq %rbp
movq %rsp, %rbp
movl $268435456, %esi
movl $25, %edi
call constant_test_bit
对于constant_test_bit
,以及:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl $24, -4(%rbp)
movl -4(%rbp), %eax
movl $268435456, %esi
movl %eax, %edi
call variable_test_bit
对于variable_test_bit
。这两个代码列表以相同的部分开始,首先我们保存当前栈帧的基础到%rbp
寄存器。但之后两个示例的代码不同。在第一个示例中,我们将$268435456
(这里$268435456
是我们的第二个参数 - 0x10000000
)放入esi
寄存器,并将$25
(我们的第一参数)放入edi
寄存器,并调用constant_test_bit
。我们将函数参数放入esi
和edi
寄存器是因为我们正在学习x86_64
架构的Linux内核,我们使用System V AMD64 ABI
调用约定。一切都非常简单。当我们使用预定义的常量时,编译器可以直接替换它的值。现在让我们看看第二部分。正如你看到的,编译器不能替换来自nr
变量的值。在这种情况下,编译器必须计算它在程序堆栈帧上的偏移量。我们从rsp
寄存器减去16
,为局部变量数据分配栈空间,并将$24
(nr
变量的值)放入带有偏移-4
的rbp
。我们的栈帧将如下所示:
<- stack grows
%[rbp]
|
+----------+ +---------+ +---------+ +--------+
| | | | | return | | |
| nr |-| |-| |-| argc |
| | | | | address | | |
+----------+ +---------+ +---------+ +--------+
|
%[rsp]
之后我们将这个值放入eax
寄存器,所以现在eax
寄存器包含nr
的值。最后我们做和第一个示例相同的事情,我们将$268435456
(variable_test_bit
函数的第一个参数)和eax
的值(nr
的值)放入edi
寄存器(variable_test_bit
函数的第二个参数)。
在apic_intr_init
函数完成工作后的下一步是设置从FIRST_EXTERNAL_VECTOR
或0x20
到0x100
的中断门:
i = FIRST_EXTERNAL_VECTOR;
#ifndef CONFIG_X86_LOCAL_APIC
#define first_system_vector NR_VECTORS
#endif
for_each_clear_bit_from(i, used_vectors, first_system_vector) {
set_intr_gate(i, irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR));
}
但由于我们使用了for_each_clear_bit_from
辅助工具,我们只设置了未初始化的中断门。之后我们使用相同的for_each_clear_bit_from
辅助工具将中断表中未填充的中断门用spurious_interrupt
填充:
#ifdef CONFIG_X86_LOCAL_APIC
for_each_clear_bit_from(i, used_vectors, NR_VECTORS)
set_intr_gate(i, spurious_interrupt);
#endif
其中spurious_interrupt
函数代表spurious
中断的处理程序。这里used_vectors
是一个unsigned long
,包含已经初始化的中断门。我们已经在arch/x86/kernel/setup.c源代码文件的trap_init
函数中填充了前32
个中断向量:
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors);
你可以在本章的第六部分中记得我们是如何做到的。
在native_init_IRQ
函数的末尾,我们可以看到以下检查:
if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
setup_irq(2, &irq2);
首先让我们处理条件。acpi_ioapic
变量表示I/O APIC的存在。它在arch/x86/kernel/acpi/boot.c中定义。这个变量在处理Multiple APIC Description Table
时设置,在arch/x86/kernel/setup.c中初始化架构特定的东西时调用(我们将在关于APIC的另一章中了解更多)。注意acpi_ioapic
变量的值取决于CONFIG_ACPI
和CONFIG_X86_LOCAL_APIC
Linux内核配置选项。如果这些选项没有设置,这个变量的值将只是零:
#define acpi_ioapic 0
第二个条件 - !of_ioapic && nr_legacy_irqs()
检查我们不使用Open Firmware I/O APIC
和传统中断控制器。我们已经知道nr_legacy_irqs
。第二个是of_ioapic
变量在arch/x86/kernel/devicetree.c中定义,并在dtb_ioapic_setup
函数中初始化,该函数构建设备树中APICs
的信息。注意of_ioapic
变量取决于CONFIG_OF
Linux内核配置选项。如果此选项未设置,of_ioapic
的值也将为零:
#ifdef CONFIG_OF
extern int of_ioapic;
...
...
...
#else
#define of_ioapic 0
...
...
...
#endif
如果条件返回非零值,我们调用:
setup_irq(2, &irq2);
函数。首先关于irq2
。irq2
是定义在arch/x86/kernel/irqinit.c源代码文件中的irqaction
结构,并代表用于级联查询连接设备的IRQ 2
线:
static struct irqaction irq2 = {
.handler = no_action,
.name = "cascade",
.flags = IRQF_NO_THREAD,
};
很久以前,中断控制器由两个芯片组成,其中一个芯片连接到第二个芯片。第二个芯片通过这个IRQ 2
线连接到第一个芯片。这个芯片服务于从8
到15
的线路,然后是第一个芯片的线路。例如,Intel 8259A有以下线路:
IRQ 0
- 系统时钟;IRQ 1
- 键盘;IRQ 2
- 用于级联连接的设备;IRQ 8
- RTC;IRQ 9
- 保留;IRQ 10
- 保留;IRQ 11
- 保留;IRQ 12
-ps/2
鼠标;IRQ 13
- 协处理器;IRQ 14
- 硬盘控制器;IRQ 15
- 保留;IRQ 3
-COM2
和COM4
;IRQ 4
-COM1
和COM3
;IRQ 5
-LPT2
;IRQ 6
- 驱动控制器;IRQ 7
-LPT1
。
setup_irq
函数定义在kernel/irq/manage.c中,并且接受两个参数:
- 中断的向量号;
- 与中断相关的
irqaction
结构。
这个函数从给定向量号的中断描述符开始初始化:
struct irq_desc *desc = irq_to_desc(irq);
并调用__setup_irq
函数来设置给定的中断:
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, act);
chip_bus_sync_unlock(desc);
return retval;
注意,在__setup_irq
函数工作期间中断描述符是锁定的。__setup_irq
函数执行许多不同的操作:如果提供了线程函数并且中断不嵌套在另一个中断线程中,则创建处理线程,设置芯片的标志,填充irqaction
结构等等。
所有上述操作都创建了/proc/irq/向量号
目录并填充了它,但如果你使用的是现代计算机,那里的所有值都将为零:
$ cat /proc/irq/2/node
0
$cat /proc/irq/2/affinity_hint
00
cat /proc/irq/2/spurious
count 0
unhandled 0
last_unhandled 0 ms
因为可能APIC
在机器上处理中断。
就这样。
结论
这是中断和中断处理章节的第八部分的结尾,我们在这一部分继续深入外部硬件中断。在之前的部分中,我们开始这样做,并看到了IRQs
的早期初始化。在这部分中,我们已经看到了init_IRQ
函数中非早期中断初始化。我们看到了存储中断向量号的vector_irq
per-cpu数组的初始化,它将在中断处理中使用,以及与外部硬件中断相关的其他东西的初始化。
在下一部分中,我们将继续学习与中断处理相关的内容,并看到softirqs
的初始化。
如果你有任何问题或建议,请在评论中告诉我,或在twitter上联系我。
请注意,英语不是我的第一语言,如果有任何不便,我深表歉意。如果你发现任何错误,请向我发送PR到linux-insides。
链接
- IRQ
- percpu
- x86_64
- Intel 8259
- Programmable Interrupt Controller
- ISA
- MultiProcessor Configuration Table
- Local APIC
- I/O APIC
- SMP
- Inter-processor interrupt
- ternary operator
- gcc
- calling convention
- PDF. System V Application Binary Interface AMD64
- Call stack
- Open Firmware
- devicetree
- RTC
- Previous part
"《Linux嵌入式必考必会》专栏,专为嵌入式开发者量身打造,聚焦Linux环境下嵌入式系统面试高频考点。涵盖基础架构、内核原理、驱动开发、系统优化等核心技能,实战案例与理论解析并重。助你快速掌握面试关键,提升竞争力。