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

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

非早期中断门的初始化

这是关于Linux内核中断和异常处理的第四部分,在之前的部分中,我们看到了来自arch/x86/kernel/traps.c的第一个早期#DB#BP异常处理程序。我们在early_trap_init函数后停止了,该函数在定义于arch/x86/kernel/setup.csetup_arch函数中被调用。在这部分,我们将继续深入探讨Linux内核中的中断和异常处理,针对x86_64,从我们上次停止的地方继续。与中断和异常处理相关的第一个事项是使用early_trap_pf_init函数设置#PF页面错误处理程序。让我们从这里开始。

早期页面错误处理程序

early_trap_pf_init函数定义在arch/x86/kernel/traps.c中。它使用set_intr_gate宏来填充中断描述符表,给出给定的入口:

void __init early_trap_pf_init(void)
{
#ifdef CONFIG_X86_64
         set_intr_gate(X86_TRAP_PF, page_fault);
#endif
}

这个宏在arch/x86/include/asm/desc.h中定义。我们在之前的部分中已经看到了这样的宏——set_system_intr_gateset_intr_gate_ist。这个宏检查给定的向量号是否不大于255(最大向量号),并像set_system_intr_gateset_intr_gate_ist那样调用_set_gate函数:

#define set_intr_gate(n, addr)                                  \
do {                                                            \
        BUG_ON((unsigned)n > 0xFF);                             \
        _set_gate(n, GATE_INTERRUPT, (void *)addr, 0, 0,        \
                  __KERNEL_CS);                                 \
        _trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\
                        0, 0, __KERNEL_CS);                     \
} while (0)

set_intr_gate宏接受两个参数:

  • 中断的向量号;
  • 中断处理程序的地址;

在我们的例子中它们是:

  • X86_TRAP_PF - 14
  • page_fault - 中断处理程序的入口点。

X86_TRAP_PF是在arch/x86/include/asm/traprs.h中定义的枚举元素:

enum {
	...
	...
	...
	X86_TRAP_PF,            /* 14, 页面错误 */
	...
	...
	...
}

early_trap_pf_init被调用时,set_intr_gate将展开为对_set_gate的调用,这将用页面错误处理程序填充IDT。现在让我们看看page_fault处理程序的实现。page_fault处理程序定义在arch/x86/entry/entry_64.S汇编源代码文件中,就像所有异常处理程序一样。让我们看看它:

trace_idtentry page_fault do_page_fault has_error_code=1

我们在之前的部分中看到了#DB#BP处理程序的定义。它们是用idtentry宏定义的,但在这里我们可以看到trace_idtentry。这个宏在同一个源代码文件中定义,取决于CONFIG_TRACING内核配置选项:

#ifdef CONFIG_TRACING
.macro trace_idtentry sym do_sym has_error_code:req
idtentry trace(\sym) trace(\do_sym) has_error_code=\has_error_code
idtentry \sym \do_sym has_error_code=\has_error_code
.endm
#else
.macro trace_idtentry sym do_sym has_error_code:req
idtentry \sym \do_sym has_error_code=\has_error_code
.endm
#endif

我们现在不会深入异常跟踪。如果CONFIG_TRACING没有设置,我们可以看到trace_idtentry宏只是展开为正常的idtentry。我们已经在之前的部分中看到了idtentry宏的实现,所以让我们从page_fault异常处理程序开始。

正如我们在idtentry定义中看到的,page_fault的处理程序是do_page_fault函数,定义在arch/x86/mm/fault.c中,和所有异常处理程序一样,它接受两个参数:

  • regs - pt_regs结构体,保存被中断进程的状态;
  • error_code - 页面错误异常的错误代码。

让我们看看这个函数的内部。首先我们读取cr2控制寄存器的内容:

dotraplinkage void notrace
do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
	unsigned long address = read_cr2();
	...
	...
	...
}

这个寄存器包含导致页面错误的线性地址。接下来我们调用来自include/linux/context_tracking.hexception_enter函数。exception_enterexception_exit是Linux内核上下文跟踪子系统中的函数,用于RCU在处理器运行用户空间时去除对定时器滴答的依赖。在几乎所有的异常处理程序中,我们都将看到类似的代码:

enum ctx_state prev_state;
prev_state = exception_enter();
...
... // 异常处理程序在这里
...
exception_exit(prev_state);

exception_enter函数检查context tracking是否已启用,如果启用,则使用this_cpu_read获取先前的上下文(更多关于this_cpu_*操作的信息,你可以在文档中阅读)。之后,它调用context_tracking_user_exit函数,通知上下文跟踪处理器正在退出用户空间模式并进入内核:

static inline enum ctx_state exception_enter(void)
{
        enum ctx_state prev_ctx;

        if (!context_tracking_is_enabled())
                return 0;

        prev_ctx = this_cpu_read(context_tracking.state);
        context_tracking_user_exit();

        return prev_ctx;
}

状态可以是以下之一:

enum ctx_state {
    IN_KERNEL = 0,
	IN_USER,
} state;

最后我们返回先前的上下文。在exception_enterexception_exit之间,我们调用实际的页面错误处理程序:

__do_page_fault(regs, error_code, address);

__do_page_fault定义在与do_page_fault相同的源代码文件中——arch/x86/mm/fault.c。在__do_page_fault的开头,我们检查状态的kmemcheck检查器。kmemcheck检测警告一些使用未初始化内存的情况。我们需要检查它,因为页面错误可能是由kmemcheck引起的:

if (kmemcheck_active(regs))
		kmemcheck_hide(regs);
	prefetchw(&mm->mmap_sem);

在此之后,我们可以看到prefetchw调用,它执行具有相同名称的指令,该指令获取X86_FEATURE_3DNOW以获取独占缓存行。预取的主要目的是隐藏内存访问的延迟。接下来我们检查我们得到的页面错误不是在内核空间中,条件如下:

if (unlikely(fault_in_kernel_space(address))) {
...
...
...
}

其中fault_in_kernel_space是:

static int fault_in_kernel_space(unsigned long address)
{
        return address >= TASK_SIZE_MAX;
}

TASK_SIZE_MAX宏展开为:

#define TASK_SIZE_MAX   ((1UL << 47) - PAGE_SIZE)

0x00007ffffffff000。注意unlikely宏。Linux内核中有这两个宏:

#define likely(x)      __builtin_expect(!!(x), 1)
#define unlikely(x)    __builtin_expect(!!(x), 0)

你可以经常在Linux内核的代码中找到这些宏。这些宏的主要目的是优化。有时这种情况是我们需要检查代码的条件,我们知道它很少是truefalse。有了这些宏,我们可以告诉编译器这一点。例如

static int proc_root_readdir(struct file *file, struct dir_context *ctx)
{
        if (ctx->pos < FIRST_PROCESS_ENTRY) {
                int error = proc_readdir(file, ctx);
                if (unlikely(error <= 0))
                        return error;
...
...
...
}

在这里我们可以看到proc_root_readdir函数,当LinuxVFS需要读取root目录内容时,将被调用。如果用unlikely标记的条件,编译器可以将false代码放在分支后面。现在让我们回到我们的地址检查。给定地址与0x00007ffffffff000之间的比较将告诉我们页面错误是在内核模式还是用户模式下发生的。在此之后我们知道了。在此之后__do_page_fault程序将尝试理解引发页面错误异常的问题,然后将地址传递给适当的程序。它可能是kmemcheck错误,错误错误,kprobes错误等等。在这一部分,我们不会深入页面错误异常处理程序的实现细节,因为我们需要了解Linux内核提供的许多不同的概念,但我们将在后面关于Linux内核内存管理的章节中看到它。

回到start_kernel

setup_arch函数中的early_trap_pf_init之后有许多不同的函数调用,来自不同的内核子系统,但没有任何与中断和异常处理相关的。所以,我们必须回到我们来的地方——start_kernel函数,来自init/main.csetup_arch之后的第一件事是来自arch/x86/kernel/traps.ctrap_init函数。这个函数初始化剩余的异常处理程序(记住我们已经为#DB - 调试异常,#BP - 断点异常和#PF - 页面错误异常设置了3个处理程序)。trap_init函数从检查扩展工业标准体系结构开始:

#ifdef CONFIG_EISA
        void __iomem *p = early_ioremap(0x0FFFD9, 4);

        if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
                EISA_bus = 1;
        early_iounmap(p, 4);
#endif

注意它依赖于代表EISA支持的CONFIG_EISA内核配置参数。这里我们使用early_ioremap函数将I/O内存映射到页表。我们使用readl函数从映射区域读取前4个字节,如果它们等于EISA字符串,我们将EISA_bus设置为一。最后我们只是取消映射之前映射的区域。关于early_ioremap的更多信息,你可以在描述固定映射地址和ioremap的部分中阅读。

在此之后,我们开始用不同的中断门填充中断描述符表。首先我们设置#DE除以错误#NMI不可屏蔽中断

set_intr_gate(X86_TRAP_DE, divide_error);
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);

我们使用set_intr_gate宏为#DE异常设置中断门,使用set_intr_gate_ist#NMI设置。你可以记住我们已经在为页面错误处理程序、调试处理程序等设置中断门时使用了这些宏,你可以在之前的部分中找到它的解释。在此之后我们为以下异常设置异常门:

set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);

在这里我们可以看到:

  • #OF溢出异常。此异常表明在执行特殊INTO指令时发生了溢出陷阱;
  • #BR超出范围异常。此异常表明在执行BOUND指令时发生了BOUND-range-exceed故障;
  • #UD无效操作码异常。当处理器尝试执行无效或保留的操作码时发生,处理器尝试执行具有无效操作数的指令等;
  • #NM设备不可用异常。当处理器尝试执行x87 FPU浮点指令时,而控制寄存器cr0中的EM标志被设置。

接下来我们为#DF双重故障异常设置中断门:

set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);

当处理器在调用先前异常的处理程序时检测到第二个异常时,就会发生此异常。通常,当处理器在尝试调用异常处理程序时检测到另一个异常时,两个异常可以串行处理。如果处理器不能串行处理它们,它将发出双重故障或#DF异常。

接下来的一组中断门是:

set_intr_gate(X86_TRAP_OLD_MF, &coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, &invalid_TSS);
set_intr_gate(X86_TRAP_NP, &segment_not_present);
set_intr_gate_ist(X86_TRAP_SS, &stack_segment, STACKFAULT_STACK);
set_intr_gate(X86_TRAP_GP, &general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, &spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, &coprocessor_error);
set_intr_gate(X86_TRAP_AC, &alignment_check);

在这里我们可以看到以下异常处理程序的设置:

  • #CSO协处理器段溢出 - 此异常表明旧处理器的数学协处理器检测到页面或段违规。现代处理器不会产生此异常
  • #TS无效TSS异常 - 表明与任务状态段有关的错误。
  • #NP段不存在异常表明在尝试加载csdsesfsgs寄存器时,段或门描述符的存在标志被清除。
  • #SS栈故障异常表明检测到与栈相关的条件之一,例如在尝试加载ss寄存器时检测到不存在的栈段。
  • #GP一般保护异常表明处理器检测到一类称为一般保护违规的保护违规。有许多不同的条件可以引起一般保护异常。 例如,用系统段的选择器加载ssdsesfsgs寄存器,写入代码段或只读数据段,引用中断描述符表中的条目(跟随中断或异常)不是中断、陷阱或任务门等等。
  • Spurious Interrupt - 不需要的硬件中断。
  • #MFx87 FPU浮点错误异常,当x87 FPU检测到浮点错误时引发。
  • #AC对齐检查异常表明处理器在对齐检查被启用时检测到未对齐的内存操作数。

在此之后,我们设置了这些异常门,我们可以看到Machine-Check异常的设置:

#ifdef CONFIG_X86_MCE
	set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
#endif

注意它依赖于CONFIG_X86_MCE内核配置选项,表明处理器检测到内部机器错误或总线错误,或者外部代理检测到总线错误。下一个异常门是针对SIMD浮点错误的:

set_intr_gate(X86_TRAP_XF, &simd_coprocessor_error);

这表明处理器检测到SSESSE2SSE3SIMD浮点异常。在执行SIMD浮点指令时,可能会发生六类数值异常条件:

  • 无效操作
  • 除以零
  • 非正规数操作数
  • 数值溢出
  • 数值下溢
  • 不精确结果(精度)

接下来我们填充在arch/x86/include/asm/desc.h头文件中定义的used_vectors数组,它表示前32个中断的bitmap

DECLARE_BITMAP(used_vectors, NR_VECTORS);
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
	set_bit(i, used_vectors)

其中FIRST_EXTERNAL_VECTOR是:

#define FIRST_EXTERNAL_VECTOR           0x20

在此之后,我们为ia32_syscall设置中断门,并将0x80添加到used_vectors位图中:

#ifdef CONFIG_IA32_EMULATION
        set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
        set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif

x86_64Linux内核上,有CONFIG_IA32_EMULATION内核配置选项。此选项提供了在兼容模式下执行32位进程的能力。在接下来的部分中,我们将看到它的工作原理,与此同时,我们只需要知道IDT中还有另一个中断门,向量号为0x80。接下来我们将IDT映射到fixmap区域:

__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
idt_descr.address = fix_to_virt(FIX_RO_IDT);

并将它的地址写入idt_descr.address(关于Linux内核中的固定映射地址的更多信息,你可以在Linux内核内存管理章节的第二部分中阅读)。在此之后,我们可以看到对定义在arch/x86/kernel/cpu/common.ccpu_init函数的调用。这个函数初始化所有的per-cpu状态。在cpu_init的开始,我们做以下事情:首先,我们等待当前CPU初始化,然后我们调用cr4_init_shadow函数,为当前CPU存储cr4控制寄存器的阴影副本,并在需要时用以下函数调用加载CPU微码:

wait_for_master_cpu(cpu);
cr4_init_shadow();
load_ucode_ap();

接下来我们获取当前CPU的任务状态段orig_ist结构,它表示原始的中断栈表值:

t = &per_cpu(cpu_tss, cpu);
oist = &per_cpu(orig_ist, cpu);

由于我们得到了当前处理器的任务状态段中断栈表的值,我们在cr4控制寄存器中清除了以下位:

cr4_clear_bits(X86_CR4_VME|X86_CR4_PVI|X86_CR4_TSD|X86_CR4_DE);

通过这个我们禁用了vm86扩展、虚拟中断、时间戳(RDTSC只能以最高权限执行)和调试扩展。在此之后我们重新加载全局描述符表中断描述符表

	switch_to_new_gdt(cpu);
	loadsegment(fs, 0);
	load_current_idt();

在此之后我们设置线程本地存储描述符数组,配置NX并加载CPU微码。现在是时候设置和加载per-cpu任务状态段了。我们在一个循环中通过所有的异常栈,这是N_EXCEPTION_STACKS4,并用中断栈表填充它:

	if (!oist->ist[0]) {
		char *estacks = per_cpu(exception_stacks, cpu);

		for (v = 0; v < N_EXCEPTION_STACKS; v++) {
			estacks += exception_stack_sizes[v];
			oist->ist[v] = t->x86_tss.ist[v] =
					(unsigned long)estacks;
			if (v == DEBUG_STACK-1)
				per_cpu(debug_stack_addr, cpu) = (unsigned long)estacks;
		}
	}

由于我们已经用中断栈表填充了任务状态段,我们可以为当前处理器设置TSS描述符并用以下方式加载它:

set_tss_desc(cpu, t);
load_TR_desc();

set_tss_desc宏来自arch/x86/include/asm/desc.h,将给定的描述符写入给定处理器的全局描述符表

#define set_tss_desc(cpu, addr) __set_tss_desc(cpu, GDT_ENTRY_TSS, addr)
static inline void __set_tss_desc(unsigned cpu, unsigned int entry, void *addr)
{
        struct desc_struct *d = get_cpu_gdt_table(cpu);
        tss_desc tss;
        set_tssldt_descriptor(&tss, (unsigned long)addr, DESC_TSS,
                              IO_BITMAP_OFFSET + IO_BITMAP_BYTES +
                              sizeof(unsigned long) - 1);
        write_gdt_entry(d, entry, &tss, DESC_TSS);
}

load_TR_desc宏展开为ltr加载任务寄存器指令:

#define load_TR_desc()                          native_load_tr_desc()
static inline void native_load_tr_desc(void)
{
        asm volatile("ltr %w0"::"q" (GDT_ENTRY_TSS*8));
}

trap_init函数的末尾,我们可以看到以下代码:

set_intr_gate_ist(X86_TRAP_DB, &debug, DEBUG_STACK);
set_system_intr_gate_ist(X86_TRAP_BP, &int3, DEBUG_STACK);
...
...
...
#ifdef CONFIG_X86_64
        memcpy(&nmi_idt_table, &idt_table, IDT_ENTRIES * 16);
        set_nmi_gate(X86_TRAP_DB, &debug);
        set_nmi_gate(X86_TRAP_BP, &int3);
#endif

在这里我们将idt_table复制到nmi_dit_table并为#DB调试异常#BR断点异常设置异常处理程序。你可以记住我们在之前的部分中已经设置了这些中断门,那么我们为什么需要再次设置它呢?我们再次设置它是因为当我们在early_trap_init函数中初始化它之前,任务状态段还没有准备好,但现在在调用cpu_init函数之后它已经准备好了。

就这样。很快我们将考虑这些中断/异常的所有处理程序。

结论

这是关于Linux内核中断和中断处理的第四部分的结束。在这部分中,我们看到了任务状态段的初始化以及不同中断处理程序的初始化,如除以错误页面错误异常等。你可以注意到我们只是看到了初始化的内容,我们将深入了解这些异常的处理程序的细节。在下一部分中,我们将开始这样做。

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

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

链接

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

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

全部评论

相关推荐

点赞 评论 收藏
分享
不愿透露姓名的神秘牛友
12-18 11:21
优秀的大熊猫在okr...:叫你朋友入职保安,你再去送外卖,一个从商,一个从政,你们两联手无敌了,睁开你的眼睛看看,现在是谁说了算(校长在背后瑟瑟发抖)
选实习,你更看重哪方面?
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务