版权声明:本文为本文为博主原创文章,转载请注明出处。如有问题,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/

X86 ipipe接管中断/异常

本文主要讲述X86 下xenomai ipipe是如何接管中断的,关于异常将会放到双核异常处理介绍。

一、回顾

上篇文章(X86中断/异常与APIC)我们详细介绍了X86平台中断处理机制:

X86平台有256个中断向量,表示256个异常或中断,前32个vector为处理器保留用作异常处理,从32到255的vector编号被指定为用户定义的中断,不被处理器保留。 这些中断通常分配给外部I / O设备(部分固定为APIC中断,如LAPIC Timer、温度中断等),以使这些设备能够将中断发送到处理器,每个vector用一个门描述符来表示,也称为中断门,其结构入下。

描述符大小为128位,其主要保存了段选择符、权限和中断处理程序入口地址。在计算机的内存里,会保存一个中断描述符表(IDT),共256项。为了直接定位中断描述符表,每个CPU都有个特殊的寄存器IDTR来保存IDT的在内存中的位置。

当CPU收到一个中断/异常后,CPU 执行以下流程:

  1. 读取由IDTR寄存器保存的IDT(中断向量表)中对应的门描述符。CPU将vector乘以16作为偏移地址来找到该vector的中断描述符条目(32位系统是乘以8)。
  2. 从中断门描述符中得到保存的段选择符。
  3. 根据段选择符获取对于的段描述符。
  4. 进行DPL特权级检查。
  5. 切换堆栈。
  6. 压栈保存原来上下文。
  7. 执行IDT中的中断服务程序。
  8. 返回原来上下文。

(保护模式下的中断处理,图来源:https://blog.csdn.net/qq_39376747/article/details/113736525?spm=1001.2014.3001.5501)

本文从软件的角度,来看Linux中这个流程是怎样的,着重于硬件相关部分,只有这部分涉及ipipe,linux通用的中断子系统不涉及,所以linux通用的中断子系统本文不做描述。

二、X86 linux异常中断处理

1. 中断门及IDT

CPU主要将门分为三种:任务门,中断门,陷阱门。虽然CPU把门描述符分为了三种,但是linux为了处理更多种情况,把门描述符分为了五种,分别为中断门,系统门,系统中断门,陷阱门,任务门;但其存储结构与CPU定义的门不变。门结构如下:

linux中中断门由结构体struct gate_struct描述,如下:

struct idt_bits {
u16 ist : 3, /*提供切换到新堆栈以进行中断处理的功能*/
zero : 5,
type : 5,/*IDT条目类型:中断,陷阱,任务门*/
dpl : 2,/*描述符权限级别*/
p : 1;/*段是否处于内存中*/
} __attribute__((packed)); struct gate_struct {
u16 offset_low; /*中断处理程序入口点的偏移低15bit*/
u16 segment; /*GDT或LDT中的代码段选择子*/
struct idt_bits bits;
u16 offset_middle;/*中断处理程序入口点的偏移中15bit*/
#ifdef CONFIG_X86_64
u32 offset_high;/*中断处理程序入口点的偏移高32bit*/
u32 reserved;
#endif
} __attribute__((packed));

五种门结构可通过宏INTG(_vector, _addr)SYSG(_vector, _addr)ISTG(_vector, _addr)SISTG(_vector, _addr)TSKG(_vector, _addr)来初始化。

/*arch\x86\kernel\idt.c*/
#define DPL0 0x0
#define DPL3 0x3 #define DEFAULT_STACK 0 #define G(_vector, _addr, _ist, _type, _dpl, _segment) \
{ \
.vector = _vector, \
.bits.ist = _ist, \
.bits.type = _type, \
.bits.dpl = _dpl, \
.bits.p = 1, \
.addr = _addr, \
.segment = _segment, \
} /* Interrupt gate */
#define INTG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS) /* System interrupt gate */
#define SYSG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS) /* Interrupt gate with interrupt stack */
#define ISTG(_vector, _addr, _ist) \
G(_vector, _addr, _ist, GATE_INTERRUPT, DPL0, __KERNEL_CS) /* System interrupt gate with interrupt stack */
#define SISTG(_vector, _addr, _ist) \
G(_vector, _addr, _ist, GATE_INTERRUPT, DPL3, __KERNEL_CS) /* Task gate */
#define TSKG(_vector, _gdt) \
G(_vector, NULL, DEFAULT_STACK, GATE_TASK, DPL0, _gdt << 3)

linux中vector 0-31、APIC和SMP相关门描述使用这几个宏进行初始化,其余中断门描述符会通过函数set_intr_gate()进行初始化。

static void set_intr_gate(unsigned int n, const void *addr)
{
struct idt_data data; BUG_ON(n > 0xFF);/*大于255,出错*/ memset(&data, 0, sizeof(data));
data.vector = n; /*vector*/
data.addr = addr; /*中断程序入口地址*/
data.segment = __KERNEL_CS;/*内核代码段*/
data.bits.type = GATE_INTERRUPT; //门类型
data.bits.p = 1; idt_setup_from_table(idt_table, &data, 1, false);/*写入idt_table,不记录到bitmap*/
}

中断描述符表IDT 由数组idt_table[256]描述,用来保存每个CPU的256个Vector的中断门描述符:

/* Must be page-aligned because the real IDT is used in a fixmap. */
gate_desc idt_table[IDT_ENTRIES] __page_aligned_bss; /*IDT_ENTRIES = 256*/

保存中断描述符表地址的特殊寄存器IDTR在Linux代码中使用struct desc_ptr表示:

struct desc_ptr {
unsigned short size; /*16bit*/
unsigned long address; /*32bit*/
} __attribute__((packed)) ;

内核需要将itd_table 存储到IDTR寄存中,中断时CPU才能正确处理,Linux中用定义了一个idt_desc变量来存放全局IDT信息:

struct desc_ptr idt_descr __ro_after_init = {
.size = (IDT_ENTRIES * 2 * sizeof(unsigned long)) - 1,
.address = (unsigned long) idt_table,
};

通过指令lidt将 idt_desc保存到IDTR寄存器:

static inline void native_load_idt(const struct desc_ptr *dtr)
{
asm volatile("lidt %0"::"m" (*dtr));
}

2. 初始化门描述符

中断向量表中保存的是中断和异常描述符。我们知道,内核需要经过多个阶段才完成启动。在启动过程中,也会产生一些异常,这些异常辅助完成内核启动工作,所以各个阶段的中断异常函数是不同的,这主要分为4个部分,1-3部门为各个启动阶段异常和陷阱的描述符(vector 0-31),第4部分为中断描述符初始化(vector 32-255):

第一部分:引导程序结束后,进入head_64.s后,start_kernel()执行之前的early(早期)阶段产生的异常处理,主要是处理page_fault

第二部分:start_kernel()执行过程中,cpu_init()准备TSS段前,此时异常处理堆栈还为准备好,填充DEFAULT_STACK 上运行的早期陷阱门,有debug、page_fault、int3。

第三部分:以上关于异常和陷阱的描述符只是临时填充使用,最终的异常描述符将在trap_init()中完整初始化,填充每个CPU完整的异常处理gate,cpu_init()会设置每个CPU的idtr寄存器。

第四部分: 中断描述符初始化,包含SMP、APIC中断。

2.1 早期异常处理

x86_64_start_kernel()函数中,进入通用和独立于体系结构的内核代码之前,做的最后一个工作就是填充early_idt_handle,填充函数为 idt_setup_early_handler()

void __init idt_setup_early_handler(void)
{
int i; for (i = 0; i < NUM_EXCEPTION_VECTORS; i++)
set_intr_gate(i, early_idt_handler_array[i])
#ifdef CONFIG_X86_32
for ( ; i < NR_VECTORS; i++)
set_intr_gate(i, early_ignore_irq);
#endif
load_idt(&idt_descr);
}
/*arch\x86\include\asm\segment.h*/
#define NUM_EXCEPTION_VECTORS 32
#define EARLY_IDT_HANDLER_SIZE 9
extern const char early_idt_handler_array[NUM_EXCEPTION_VECTORS][EARLY_IDT_HANDLER_SIZE];

中断向量 0-31的处理程序的入口设置为early_idt_handler_array[vector],set_intr_gate()函数将early_idt_handler_array按IDT条目格式填充到idt_table,中断向量32-255中断处理入口设置为early_ignore_irq

early_idt_handler_array里面是什么?在哪儿定义?early_idt_handler_arrayarch/x86/kernel/entry_64.S中定义,汇编代码循环填充32个中断入口,可以看到这个阶段产生的中断和异常统一由early_idt_handler_common函数处理:

ENTRY(early_idt_handler_array)
i = 0 /*循环初始量*/
.rept NUM_EXCEPTION_VECTORS /*循环32*/
.if ((EXCEPTION_ERRCODE_MASK >> i) & 1) == 0
UNWIND_HINT_IRET_REGS
pushq $0 # Dummy error code, to make stack frame uniform
.else
UNWIND_HINT_IRET_REGS offset=8
.endif
pushq $i # 72(%rsp) Vector number
jmp early_idt_handler_common /*执行中断处理*/
UNWIND_HINT_IRET_REGS
i = i + 1
.fill early_idt_handler_array + i*EARLY_IDT_HANDLER_SIZE - ., 1, 0xcc
.endr
UNWIND_HINT_IRET_REGS offset=16
END(early_idt_handler_array)

可以看到使用汇编宏生成32个一样的异常的中断处理程序。

处理流程为 ,如果异常具有错误代码,那么我们什么也不做;如果异常没有错误代码,则将零压入堆栈。 这样做是因为堆栈是统一的。 之后,将vector编号压入堆栈,然后跳转到Early_idt_handler_common,这是目前的阶段所有异常中断的处理程序。

early_idt_handler_array数组每项有九个字节,代表可选的错误代码压栈、vcetor压栈和跳转到Early_idt_handler_common三条指令。 可以在使用objdump util查看:

$ objdump -D vmlinux
...
...
...
ffffffff81fe5000 <early_idt_handler_array>:
ffffffff81fe5000: 6a 00 pushq $0x0
ffffffff81fe5002: 6a 00 pushq $0x0
ffffffff81fe5004: e9 17 01 00 00 jmpq ffffffff81fe5120 <early_idt_han
dler_common>
ffffffff81fe5009: 6a 00 pushq $0x0
ffffffff81fe500b: 6a 01 pushq $0x1
ffffffff81fe500d: e9 0e 01 00 00 jmpq ffffffff81fe5120 <early_idt_han
dler_common>
ffffffff81fe5012: 6a 00 pushq $0x0
ffffffff81fe5014: 6a 02 pushq $x2
...
...
...

我们知道,CPU在调用中断处理程序之前将寄存器flags、CS和RIP压入堆栈。 因此,在执 early_idt_handler_common之前,堆栈将包含以下数据:

|--------------------|
| %rflags |
| %cs |
| %rip |
| error code |
| vector number |<-- %rsp
|--------------------|

现在,让我们看一下early_idt_handler_common具体实现。 它位于相同的arch/x86/kernel/head_64.S汇编文件中。 这里有一个标志位early_recursion_flag,来防止在early_idt_handler_common递归,进入前:

early_idt_handler_common:
cld
incl early_recursion_flag(%rip)
/*通用寄存器保存堆栈上:*/
pushq %rsi /* pt_regs->si */
movq 8(%rsp), %rsi /* RSI = vector number */
movq %rdi, 8(%rsp) /* pt_regs->di = RDI */
pushq %rdx /* pt_regs->dx */
pushq %rcx /* pt_regs->cx */
pushq %rax /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
pushq %rbx /* pt_regs->bx */
pushq %rbp /* pt_regs->bp */
pushq %r12 /* pt_regs->r12 */
pushq %r13 /* pt_regs->r13 */
pushq %r14 /* pt_regs->r14 */
pushq %r15 /* pt_regs->r15 */
UNWIND_HINT_REGS cmpq $14,%rsi /* Page fault? */
jnz 10f /*非 page fault*/
GET_CR2_INTO(%rdi) /* Can clobber any volatile register if pv */
call early_make_pgtable /*早期创建页表*/
andl %eax,%eax
jz 20f /* All good */ 10:
movq %rsp,%rdi /* RDI = pt_regs; RSI is already trapnr */
call early_fixup_exception /*处理其他异常*/ 20:
decl early_recursion_flag(%rip)
jmp restore_regs_and_return_to_kernel
END(early_idt_handler_common)

从中断处理程序返回前,我们需要这样做以防止寄存器的错误值。 此后,我们检查向量编号,如果它是Page Fault,则将值从cr2放入rdi寄存器(Page Fault异常会将访问产生异常的地址放到cr2寄存器中),并调用early_make_pgtable处理Page Fault异常。我们只了解异常发生及处理的过程,具体是怎样处理的不关心,所以不再描述。

2.2 start_kernel中的异常向量初始化一

start_kernel()执行过程中,cpu_init()准备TSS段前,setup_arch()中首先将debug(vector 1)、breakpoint(vector 3)、Page Fault(vector 14)异常处理条目添加到idt_table

void __init idt_setup_early_traps(void)
{
idt_setup_from_table(idt_table, early_idts, ARRAY_SIZE(early_idts),
true);
load_idt(&idt_descr);
} static const __initconst struct idt_data early_idts[] = {
INTG(X86_TRAP_DB, debug),
SYSG(X86_TRAP_BP, int3),
#ifdef CONFIG_X86_32
INTG(X86_TRAP_PF, page_fault),
#endif
};

根据异常使用的中断堆栈、特权级别、中断类型不一样使用不同的宏进行定义异常处理条目,当前堆栈还没准备好,使用DEFAULT_STACK

#define DEFAULT_STACK	0
/* Interrupt gate */
#define INTG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL0, __KERNEL_CS) /* System interrupt gate *//*SYSG 代表DPL或特权级别,DPL3*/
#define SYSG(_vector, _addr) \
G(_vector, _addr, DEFAULT_STACK, GATE_INTERRUPT, DPL3, __KERNEL_CS)

中断处理函数debugint3page_faultarch\x86\entry\entry_64.S中定义:

/*\arch\x86\entry\entry_64.S*/
idtentry debug do_debug has_error_code=0 paranoid=1 trapnr=1
idtentry int3 do_int3 has_error_code=0 trapnr=3
idtentry page_fault do_page_fault has_error_code=1 trapnr=14
idtentry stack_segment do_stack_segment has_error_code=1 trapnr=12

每个异常处理程序可以由两部分组成。 第一部分是通用部分,所有异常处理程序都相同。 异常处理程序应将通用寄存器保存在堆栈上,如果异常来自用户空间(处于不同特权等级),则应切换到内核堆栈,并将控制权转移到异常处理程序的第二部分。 异常处理程序的第二部分完成某些工作取决于什么异常。 例如,page fault异常处理程序应找到给定地址的虚拟页面,invalid opcode异常处理程序应发送SIGILL信号等。

异常处理程序处理入口使用idtentry宏定义:

.macro idtentry sym do_sym has_error_code:req paranoid=0 shift_ist=-1
ENTRY(\sym)
......
END(\sym)
.endm

idtentry是一个宏,有五个参数:

  • sym —使用 .globl name 定义全局符号,该符号将是异常处理程序入口点的名称。
  • do_sym—表示异常处理程序的具体处理函数。
  • has_error_code—是否具有中断错误代码,对于如debug和int3等没有提供错误码的异常,idtentry内部伪造一个错误码-1。

最后两个是可选参数:

  • paranoid— 此参数= 1,则切换到特殊堆栈,定义是来自用户空间还是来自异常处理程序,确定的最简单方法是通过判断CS段寄存器中的CPL或当前特权级别。如果等于3,则来自用户空间,如果等于零,则来自内核空间:;
  • shift_ist — 中断期间切换的堆栈

2.3 idtentry宏(DB异常为例)

以早期debug为例,看一下idtentry宏的实现:

idtentry debug do_debug has_error_code=0 paranoid=1 shift_ist=DEBUG_STACK

在早期发生中断之后,当前堆栈将具有以下格式:

如果需要切换到特殊堆栈,检查给定的参数是否正确。

/* Sanity check */
.if \shift_ist != -1 && \paranoid == 0
.error "using shift_ist requires paranoid=1"
.endif

如果中断向量号具有与之相关的错误代码,则将错误代码压入堆栈。对于未提供错误码的异常,伪造一个错误码放入堆栈,不仅是伪造的错误代码。此外,-1还代表无效的系统调用号码,因此不会触发系统调用重新启动逻辑.

    .if \has_error_code == 0
pushq $-1 /* ORIG_RAX: no syscall to restart */
.endif

检查来自用户空间的中断.ORIRG_RAX宏为120字节。 通用寄存器将占用这120个字节,因为在中断处理期间将所有寄存器存储在堆栈中。

    .if \paranoid < 2
testb $3, CS-ORIG_RAX(%rsp) /* If coming from userspace, switch stacks */
jnz .Lfrom_usermode_switch_stack_\@
.endif .if \paranoid
call paranoid_entry /**/
.else
call error_entry
.endif

在这里,我们检查CS中的第一位和第二位。 CS寄存器包含段选择子,其中前两位是RPL。 所有特权级别都是0到3范围内的整数,其中最小的数字对应于最高的特权。 所以如果中断来自内核模式,我们称为paranoid_entry,否则跳转到标签.Lfrom_usermode_switch_stack_\@上。 在paranoid_entry中,我们将所有通用寄存器存储在堆栈中,并在需要时将用户gs切换到内核gs上:

ENTRY(paranoid_entry)
UNWIND_HINT_FUNC
cld
PUSH_AND_CLEAR_REGS save_ret=1
ENCODE_FRAME_POINTER 8
movl $1, %ebx
movl $MSR_GS_BASE, %ecx
rdmsr
testl %edx, %edx
js 1f /* negative -> in kernel */
SWAPGS
xorl %ebx, %ebx 1:
SAVE_AND_SWITCH_TO_KERNEL_CR3 scratch_reg=%rax save_reg=%r14
ret
END(paranoid_entry)

在接下来的步骤中,我们将pt_regs指针指向rdi,如果有错误代码,则将其保存在rsi中,然后从arch / x86 / kernel / traps.c调用中断处理程序-do_debug。

    movq	%rsp, %rdi			/* pt_regs pointer */

    .if \has_error_code
movq ORIG_RAX(%rsp), %rsi /* get error code */
movq $-1, ORIG_RAX(%rsp) /* no syscall to restart */
.else
xorl %esi, %esi /* no error code */
.endif .if \shift_ist != -1
subq $EXCEPTION_STKSZ, CPU_TSS_IST(\shift_ist)
.endif call \do_sym /*二级异常处理程序*/

与其他处理程序一样,do_debug也有两个参数:

  • pt_regs-是显示一组CPU寄存器的结构,这些寄存器保存在进程的内存区域中;
  • 错误代码-中断的错误代码。

中断处理程序完成工作后,调用paranoid_exit以恢复堆栈,如果中断来自那里,则打开用户空间并调用iret。 就这样。 当然,这还不是全部:),但是我们将在有关中断的单独章节中更深入地了解。

/* these procedures expect "no swapgs" flag in ebx */
.if \paranoid
jmp paranoid_exit
.else
jmp error_exit
.endif

这是早期#DB中断的idtentry宏的一般视图。 所有中断都与此实现类似,并且也使用idtentry进行了定义。

2.4 start_kernel中的异常初始化二-trap_init()

系统中有个used_vectors变量,是一个bitmap,它用于记录中断向量表中哪些中断已经被系统注册和使用,哪些未被注册使用。

void __init idt_setup_traps(void)
{
idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}
static const __initconst struct idt_data def_idts[] = {
INTG(X86_TRAP_DE, divide_error),
INTG(X86_TRAP_NMI, nmi),
INTG(X86_TRAP_BR, bounds),
INTG(X86_TRAP_UD, invalid_op),
INTG(X86_TRAP_NM, device_not_available),
INTG(X86_TRAP_OLD_MF, coprocessor_segment_overrun),
INTG(X86_TRAP_TS, invalid_TSS),
INTG(X86_TRAP_NP, segment_not_present),
INTG(X86_TRAP_SS, stack_segment),
INTG(X86_TRAP_GP, general_protection),
INTG(X86_TRAP_SPURIOUS, spurious_interrupt_bug),
INTG(X86_TRAP_MF, coprocessor_error),
INTG(X86_TRAP_AC, alignment_check),
INTG(X86_TRAP_XF, simd_coprocessor_error), #ifdef CONFIG_X86_32
TSKG(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS),
#else
INTG(X86_TRAP_DF, double_fault),
#endif
INTG(X86_TRAP_DB, debug), #ifdef CONFIG_X86_MCE
INTG(X86_TRAP_MC, &machine_check),
#endif SYSG(X86_TRAP_OF, overflow),
#if defined(CONFIG_IA32_EMULATION)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_compat),
#elif defined(CONFIG_X86_32)
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),
#endif
};

入口函数还是由宏idtentry定义:

idtentry divide_error			do_divide_error			has_error_code=0	trapnr=0
idtentry overflow do_overflow has_error_code=0 trapnr=4
idtentry bounds do_bounds has_error_code=0 trapnr=5
idtentry invalid_op do_invalid_op has_error_code=0 trapnr=6
idtentry device_not_available do_device_not_available has_error_code=0 trapnr=7
idtentry double_fault do_double_fault has_error_code=1 paranoid=2 trapnr=8
idtentry coprocessor_segment_overrun do_coprocessor_segment_overrun has_error_code=0 trapnr=9
idtentry invalid_TSS do_invalid_TSS has_error_code=1 trapnr=10
idtentry segment_not_present do_segment_not_present has_error_code=1 trapnr=11
idtentry spurious_interrupt_bug do_spurious_interrupt_bug has_error_code=0 trapnr=15
idtentry coprocessor_error do_coprocessor_error has_error_code=0 trapnr=16
idtentry alignment_check do_alignment_check has_error_code=1 trapnr=17
idtentry simd_coprocessor_error do_simd_coprocessor_error has_error_code=0 trapnr=19

到这,异常和陷阱已经初始化完毕,内核也已经开始使用新的中断向量表了,BIOS的中断向量表就已经遗弃,不再使用了。至于各种异常具体处理函数过程分析忽略。

2.5 初始中断门描述符

上面内核已经完成异常和陷阱门初始化,下面进行进行中断门的初始化,中断门的初始化必须提到IRQ,所以会简单带过架构无关的Linux中断子系统的知识。中断门的初始化也是处于start_kernel()函数中,分为两个部分,分别是early_irq_init()init_IRQ()early_irq_init()是第一步的初始化,其工作主要是跟硬件无关的一些初始化,比如一些变量的初始化,分配必要的内存等。init_IRQ()是第二步,其主要就是关于硬件部分的初始化了。

2.5.1 IRQ

IRQ:在PIC和单核时代,irq、vector、pin这个概念的确是合三为一的,irq就是PIC控制器的pin引脚,irq也暗示着中断优先级,例如IRQ0比IRQ3有着更高的优先级。当进入MP多核时代,多核CPU下中断处理带来很多问题(如如何决定哪个中断在哪个核上处理,如何保证各核上中断负载均衡等),为了解决这些问题,vector、pin等概念都从irq中剥离出来,irq不再含有特定体系架构下中断控制器的硬件属性,只是内核中对中断的一个通用的软件抽象,与特定硬件解耦,增强其通用性。

在linux kernel中,我们使用下面两个ID来标识一个来自外设的中断:

1、IRQ number。CPU需要为每一个外设中断编号,我们称之IRQ Number。这个IRQ number是一个虚拟的interrupt ID,和硬件无关,仅仅是被CPU用来标识一个外设中断。

2、HW interrupt ID。对于interrupt controller而言,它收集了多个外设的interrupt request line并向上传递,因此,interrupt controller需要对外设中断进行编码。Interrupt controller用HW interrupt ID来标识外设的中断。在interrupt controller级联的情况下,仅仅用HW interrupt ID已经不能唯一标识一个外设中断,还需要知道该HW interrupt ID所属的interrupt controller(HW interrupt ID在不同的Interrupt controller上是会重复编码的)。

这样,CPU和interrupt controller在标识中断上就有了一些不同的概念,但是,对于驱动工程师而言,我们和CPU视角是一样的,我们只希望得到一个IRQ number,而不关系具体是那个interrupt controller上的那个HW interrupt ID。这样一个好处是在中断相关的硬件发生变化的时候,驱动软件不需要修改。因此,linux kernel中的中断子系统需要提供一个将HW interrupt ID映射到IRQ number上来的机制。(来自蜗窝科技

上面说到的HW interrupt ID即我们说到中断向量vector 32-255。

2.5.2 early_irq_init
int __init early_irq_init(void)
{
/*irq描述符计数器,循环计数器,内存节点和irq_desc描述符*/
int i, initcnt, node = first_online_node;
struct irq_desc *desc; init_irq_default_affinity(); initcnt = arch_probe_nr_irqs();/*体系结构相关的代码来决定预先分配的中断描述符的个数 */
printk(KERN_INFO "NR_IRQS: %d, nr_irqs: %d, preallocated irqs: %d\n",
NR_IRQS, nr_irqs, initcnt);//33024 1448 16
/*NR_IRQS是irq描述符的最大数量,或者换句话说是最大中断数,其值取决于CONFIG_X86_IO_APIC内核配置选项的状态*/
if (WARN_ON(nr_irqs > IRQ_BITMAP_BITS))
nr_irqs = IRQ_BITMAP_BITS; if (WARN_ON(initcnt > IRQ_BITMAP_BITS))
initcnt = IRQ_BITMAP_BITS; if (initcnt > nr_irqs)
nr_irqs = initcnt; /*遍历所有需要在循环中分配的中断描述符,并为描述符分配空间并插入到irq_desc_tree*/
for (i = 0; i < initcnt; i++) {
desc = alloc_desc(i, node, 0, NULL, NULL);/*分配中断描述符*/
set_bit(i, allocated_irqs);/*设定已经alloc的flag*/
irq_insert_desc(i, desc);/*irq与desc映射,使用radix tree*/
}
return arch_early_irq_init();/*对IO_APIC做早期初始化*/
}

1.init_irq_default_affinity()

我们知道,当硬件(如磁盘控制器或键盘)需要处理器注意时,它会抛出一个中断。中断告诉处理器发生了某些事情,处理器应该中断当前进程并处理传入事件。为了防止多个设备发送相同的中断,建立了IRQ系统,linux为计算机系统中的每个设备分配了自己特定的IRQ,使其中断是唯一的。Linux内核可以指派特定的IRQ到特定的处理器处理,这就是SMP IRQ affinity,它允许我们控制系统如何响应各种硬件事件。

2.首先调用arch_probe_nr_irqs()获取预先分配的irq数量initcnt.

3.确定nr_irqs数量

4.为预先分配的irq分配irq_desc ,并在Bitmap allocated_irqs中标记已分配,将分配的irq与irq_desc插入基数树irq_desc_tree,irq作为索引对应irq_desc地址作为叶子节点。irq_desc_tree是全局变量,定义如下:

/*include\linux\radix-tree.h*/
struct radix_tree_root {
gfp_t gfp_mask; /*标示内存从哪分配*/
struct radix_tree_node __rcu *rnode;
};
#define RADIX_TREE_INIT(mask) { \
.gfp_mask = (mask), \
.rnode = NULL, \
}
/*kernel\irq\irqdesc.c*/
static RADIX_TREE(irq_desc_tree, GFP_KERNEL); static void irq_insert_desc(unsigned int irq, struct irq_desc *desc)
{
radix_tree_insert(&irq_desc_tree, irq, desc);
}

5.调用arch_early_irq_init()对IO_APIC做早期初始化,为预先分配的irq 0 to 15分配apic_chip_data空间,并设置到每个irq的irq_desc

创建Irq_domain x86_vector_domain,并将其设置为irq_default_domain,创建子irq_domain pci_msi_domain_infohtirq_domain

irq_desc结构体是Linux中断管理的基础,代表一个中断描述符在include/linux/irqdesc.h 中定义。

struct irq_desc {
struct irq_common_data irq_common_data;/* */
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;/*每个CPU的中断状态*/
#ifdef CONFIG_IPIPE
void (*ipipe_ack)(struct irq_desc *desc);
void (*ipipe_end)(struct irq_desc *desc);
#endif /* CONFIG_IPIPE */
irq_flow_handler_t handle_irq;/*高级irq事件处理程序*/
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list 标识IRQ发生时要调用的中断服务程序; */
unsigned int status_use_accessors;/*包含中断源的状态,它是enum来自include/linux/irq.h的值和在同一源代码文件中定义的不同宏的组合;*/
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* 如果IRQ已启用,则为正值,如果0已至少禁用一次*/
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* IRQ线路上发生中断的计数器*/
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;/*未处理中断的计数*/
atomic_t threads_handled;
int threads_handled_last;
raw_spinlock_t lock;/*用于序列化对IRQ描述符的访问的自旋锁;*/
struct cpumask *percpu_enabled;
const struct cpumask *percpu_affinity;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;/*等待重新平衡的中断;*/
#endif
#endif
unsigned long threads_oneshot;
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PM_SLEEP
unsigned int nr_actions;
unsigned int no_suspend_depth;
unsigned int cond_suspend_depth;
unsigned int force_resume_depth;
#endif
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu;
struct kobject kobj;
#endif
struct mutex request_mutex;
int parent_irq;
/*中断描述符的所有者。中断描述符可以从模块中分配。该字段需要在提供中断的模块上证明refcount;*/
struct module *owner;
const char *name;
} ____cacheline_internodealigned_in_smp;

关于linux中断子系统后续文章介绍。

2.5.3 init_IRQ

init_IRQ函数是特定于体系结构的,在arch/X86/kenel/irqinit.c中定义,函数init_IRQ首先每个CPU初始化一个irq_desc指针数组vector_irq[];

/*\arch\ia64\include\asm\native\irq.h*/
#define NR_VECTORS 256
/*arch\x86\include\asm\hw_irq.h*/
typedef struct irq_desc* vector_irq_t[NR_VECTORS];
/*arch\x86\kernel\irqinit.c*/
DEFINE_PER_CPU(vector_irq_t, vector_irq) = {
[0 ... NR_VECTORS - 1] = VECTOR_UNUSED, /*[256]*/
};
void __init init_IRQ(void)
{
int i;
for (i = 0; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, 0)[ISA_IRQ_VECTOR(i)] = irq_to_desc(i);/*使用中断描述符填充vector_irq*/
x86_init.irqs.intr_init();/*native_init_IRQ ;在\arch\x86\kernel\x86_init.c中设置 */
}

init_IRQ开头,填充cpu0上的vector_irq[]0x30-0x3f项,用于ISA中断,其实就是0-15,经过ISA_IRQ_VECTOR宏转换变为0x30-0x3f,如果这些IRQ由PIC等传统中断控制器处理,则此配置在引导后可能是静态的,这里只是预先填充,如果系统使用的是APIC,这些向量空间将会被动态分配使用。

/*arch\x86\kernel\i8259.c*/
struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY,/*16*/
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.probe = probe_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
};
struct legacy_pic *legacy_pic = &default_legacy_pic;
/*arch\x86\include\asm\i8259.h*/
static inline int nr_legacy_irqs(void)
{
return legacy_pic->nr_legacy_irqs;
}

只之后调用x86_init.irqs.intr_init(),x86_init一个平台相关结构,指向平台设置相关的功能,还有与内存、处理器、定时器等相关的函数,这里中断只用到irqs段:

struct x86_init_ops x86_init __initdata = {
/*与内存资源有关*/
.resources = {
.probe_roms = probe_roms,
.reserve_resources = reserve_standard_io_resources,
.memory_setup = e820__memory_setup_default,
},
/*与解析多处理器配置表有关*/
.mpparse = {
.mpc_record = x86_init_uint_noop,
.setup_ioapic_ids = x86_init_noop,
.mpc_apic_id = default_mpc_apic_id,
.smp_read_mpc_oem = default_smp_read_mpc_oem,
.mpc_oem_bus_info = default_mpc_oem_bus_info,
.find_smp_config = default_find_smp_config,
.get_smp_config = default_get_smp_config,
},
/*IRQ相关*/
.irqs = {
.pre_vector_init = init_ISA_irqs,
.intr_init = native_init_IRQ,
.trap_init = x86_init_noop,
}, .oem = {
.arch_setup = x86_init_noop,
.banner = default_banner,
}, .paging = {
.pagetable_init = native_pagetable_init,
}, .timers = {
.setup_percpu_clockev = setup_boot_APIC_clock,
.timer_init = hpet_time_init,
.wallclock_init = x86_init_noop,
}, .iommu = {
.iommu_init = iommu_init_noop,
}, .pci = {
.init = x86_default_pci_init,
.init_irq = x86_default_pci_init_irq,
.fixup_irqs = x86_default_pci_fixup_irqs,
}, .hyper = {
.init_platform = x86_init_noop,
.x2apic_available = bool_x86_init_noop,
.init_mem_mapping = x86_init_noop,
},
};

........

发现展开有很多东西o(╥﹏╥)o,我们关注Vector 32-255的中断门是怎样填充的。所以仅看下图即可,native_init_IRQ处理过程如下;

用与APIC与SMP的vector在arch\x86\kernel\idt.c义如下,中断入口均在rch\x86\entry\entry_64.S使用宏picinterruptapicinterrupt2apicinterrupt3定义:

#ifdef CONFIG_SMP
apicinterrupt3 IRQ_MOVE_CLEANUP_VECTOR irq_move_cleanup_interrupt smp_irq_move_cleanup_interrupt
apicinterrupt3 REBOOT_VECTOR reboot_interrupt smp_reboot_interrupt
#endif
apicinterrupt LOCAL_TIMER_VECTOR apic_timer_interrupt smp_apic_timer_interrupt
apicinterrupt X86_PLATFORM_IPI_VECTOR x86_platform_ipi smp_x86_platform_ipi
......
#ifdef CONFIG_IPIPE
apicinterrupt2 IPIPE_HRTIMER_VECTOR ipipe_hrtimer_interrupt
#endif apicinterrupt ERROR_APIC_VECTOR error_interrupt smp_error_interrupt
apicinterrupt SPURIOUS_APIC_VECTOR spurious_interrupt smp_spurious_interrupt #ifdef CONFIG_IRQ_WORK
apicinterrupt IRQ_WORK_VECTOR irq_work_interrupt smp_irq_work_interrupt
#endif
/*arch\x86\kernel\idt.c*/
/*
* The APIC and SMP idt entries
*/
static const __initconst struct idt_data apic_idts[] = {
#ifdef CONFIG_SMP
INTG(RESCHEDULE_VECTOR, reschedule_interrupt), /*重新调度*/
INTG(CALL_FUNCTION_VECTOR, call_function_interrupt),/**/
INTG(CALL_FUNCTION_SINGLE_VECTOR, call_function_single_interrupt),
INTG(IRQ_MOVE_CLEANUP_VECTOR, irq_move_cleanup_interrupt),
INTG(REBOOT_VECTOR, reboot_interrupt),
#ifdef CONFIG_IPIPE
INTG(IPIPE_RESCHEDULE_VECTOR, ipipe_reschedule_interrupt),
INTG(IPIPE_CRITICAL_VECTOR, ipipe_critical_interrupt),
#endif
#endif #ifdef CONFIG_X86_THERMAL_VECTOR
INTG(THERMAL_APIC_VECTOR, thermal_interrupt),
#endif #ifdef CONFIG_X86_MCE_THRESHOLD
INTG(THRESHOLD_APIC_VECTOR, threshold_interrupt),
#endif #ifdef CONFIG_X86_MCE_AMD
INTG(DEFERRED_ERROR_VECTOR, deferred_error_interrupt),
#endif #ifdef CONFIG_X86_LOCAL_APIC
INTG(LOCAL_TIMER_VECTOR, apic_timer_interrupt),
INTG(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi),
# ifdef CONFIG_HAVE_KVM
INTG(POSTED_INTR_VECTOR, kvm_posted_intr_ipi),
INTG(POSTED_INTR_WAKEUP_VECTOR, kvm_posted_intr_wakeup_ipi),
INTG(POSTED_INTR_NESTED_VECTOR, kvm_posted_intr_nested_ipi),
# endif
# ifdef CONFIG_IRQ_WORK
INTG(IRQ_WORK_VECTOR, irq_work_interrupt),
# endif
#ifdef CONFIG_X86_UV
INTG(UV_BAU_MESSAGE, uv_bau_message_intr1),
#endif
INTG(SPURIOUS_APIC_VECTOR, spurious_interrupt),
INTG(ERROR_APIC_VECTOR, error_interrupt),
#ifdef CONFIG_IPIPE
INTG(IPIPE_HRTIMER_VECTOR, ipipe_hrtimer_interrupt),
#endif
#endif
};

vector 32-236除APIC和SMP固定的vector外,其余中断的中断入口地址在rq_entries_start内定义,均将vector压入栈后调用do_IRQ处理。

该宏定义在arch\x86\entry\entry_64.S中定义,32位系统相应的在entry_32.S中。

	.align 8
ENTRY(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR/*定义0x20-0xec个中断*/
/*NR_VECTORS-FIRST_EXTERNAL_VECTOR个函数入口
.rept表示循环 236-32 */
.rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
UNWIND_HINT_IRET_REGS
pushq $(~vector+0x80) /* 压入中断向量号 然后跳转到common_interrupt */
jmp common_interrupt
.align 8 /*8字节对齐*/
vector=vector+1
.endr
END(irq_entries_start)

该宏使用rept宏循环创建FIRST_EXTERNAL_VECTOR个中断入口,入口处的指令均为jmp common_interrupt,这些中断全都跳转到common_interrupt处理。common_interrupt处代码如下。

common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
interrupt do_IRQ
/* 0(%rsp): old RSP */
ret_from_intr:
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF LEAVE_IRQ_STACK testb $3, CS(%rsp)
jz retint_kernel /*返回内核态*/ /* Interrupt came from user space */
GLOBAL(retint_user)/*返回用户态*/
mov %rsp,%rdi
call prepare_exit_to_usermode
retint_user_early:
TRACE_IRQS_IRETQ

common_interrupt首先判断中断向量号范围,然后由do_IRQ函数去处理中断,接下来就是熟悉的linux中断处理子系统了。

三、linux x86_64中断/异常处理总结

总结X86中断的基本框架,X86 系统中有256个vector,用来识别中断或异常的类型,vector 0-31处理器保留,有固定的用途, 从32到255的vector编号被指定为用户定义的中断,不被处理器保留。 这些中断通常分配给外部I / O设备(部分固定为APIC中断),以使这些设备能够将中断发送到处理器,每个vector的处理程序都保存在一个特殊的位置--IDT(中断描述符表),IDT的基地址保存在寄存器IDTR,在64位x86下IDT是一个16字节描述的数组(32位系统为8字节),当中断发生时CPU将vector乘以16(32位系统是乘以8)来找到IDT中的对应条目idt_data,然后根据条目信息跳转到处理入口执行中断和异常处理。

四、ipipe接管中断处理

上面知道了打补丁前Linux的异常处理流程,可以想到,ipipe要优先处理中断那就不能给linux中断子系统去处理,只能从中断入口去拦截,ipipe也的确是这样做的,打补丁后的入口代码如下:

common_interrupt:
ASM_CLAC
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
#ifdef CONFIG_IPIPE
interrupt __ipipe_handle_irq /*IPIPE中断拦截*/
testl %eax, %eax
jnz ret_from_intr
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel_early
jmp retint_user_early
#else
interrupt do_IRQ
#endif
/* 0(%rsp): old RSP */
ret_from_intr:
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_OFF LEAVE_IRQ_STACK testb $3, CS(%rsp)
jz retint_kernel /*返回内核态*/ /* Interrupt came from user space */
GLOBAL(retint_user)/*返回用户态*/
mov %rsp,%rdi
call prepare_exit_to_usermode
retint_user_early:
TRACE_IRQS_IRETQ

可以看到,启用了CONFIG_IPIPE后中断就不是给do_IRQ()处理了,而是由__ipipe_handle_irq()处理,同样对于APIC中断:

/*
* APIC interrupts.
*/
#ifdef CONFIG_IPIPE
.macro apicinterrupt2 num sym
ENTRY(\sym)
UNWIND_HINT_IRET_REGS
ASM_CLAC
pushq $~(\num)
.Lcommon_\sym:
interrupt __ipipe_handle_irq /*IPIPE中断拦截*/
testl %eax, %eax
jnz ret_from_intr
LEAVE_IRQ_STACK
testb $3, CS(%rsp)
jz retint_kernel_early
jmp retint_user_early
END(\sym)
.endm
.macro apicinterrupt3 num sym do_sym
apicinterrupt2 \num \sym
.endm
#else /* !CONFIG_IPIPE */
.macro apicinterrupt3 num sym do_sym
ENTRY(\sym)
UNWIND_HINT_IRET_REGS
ASM_CLAC
pushq $~(\num)
.Lcommon_\sym:
interrupt \do_sym
jmp ret_from_intr
END(\sym)
.endm
#endif /* !CONFIG_IPIPE */

除CPU保留的vector 0-31外,均被ipipe插入函数__ipipe_handle_irq()拦截,这是保证xenomai实时性的基础,对于处理器保留的trap vector 0-31,不是由__ipipe_handle_irq()处理,涉及xenomai核与linux核异常处理后面会单独详细说。

接下来分析__ipipe_handle_irq()是怎么实现中断处理的。

int __ipipe_handle_irq(struct pt_regs *regs)
{
struct ipipe_percpu_data *p = __ipipe_raw_cpu_ptr(&ipipe_percpu);
int irq, vector = regs->orig_ax, flags = 0;
struct pt_regs *tick_regs;
struct irq_desc *desc; if (likely(vector < 0)) {
vector = ~vector;
if (vector >= FIRST_SYSTEM_VECTOR) /*>0xec*/
irq = ipipe_apic_vector_irq(vector);
else {
desc = __this_cpu_read(vector_irq[vector]);/*获取irq_desc*/
if (IS_ERR_OR_NULL(desc)) {
#ifdef CONFIG_X86_LOCAL_APIC
__ack_APIC_irq();
#endif
.....
}
irq = irq_desc_get_irq(desc);/*获取irq*/
}
} else { /* 软中断*/
irq = vector;
flags = IPIPE_IRQF_NOACK;
} ipipe_trace_irqbegin(irq, regs); …… __ipipe_dispatch_irq(irq, flags); /*中断分发*/ ……
return 1;
}

中断到达哪个CPU就由哪个CPU 调用__ipipe_handle_irq()处理,首先先获取到记录管理该cpu上运行的情况的ipipe_percpu_dataipipe domian管理),然后取出产生中断的vector,x86架构中,产生中断的vector是存放在寄存器orig_ax中的,然后将vector转换为中断号irq,最后调用__ipipe_dispatch_irq(irq, flags)进行进一步处理,ipipeline是怎样在两个内核之间管理中断的,在后面文章中介绍。

【原创】X86下ipipe接管中断/异常的更多相关文章

  1. x86保护模式-七中断和异常

    x86保护模式-七中断和异常 386相比较之前的cpu   增强了中断处理能力   并且引入了 异常概念 一 80386的中断和异常 为了支持多任务和虚拟存储器等功能,386把外部中断称为中断     ...

  2. X86中断/异常与APIC

    异常(exception)是由软件或硬件产生的,分为同步异常和异步异常.同步异常即CPU执行指令期间同步产生的异常,比如常见的除零错误.访问不在RAM中的内存 .MMU 发现当前虚拟地址没有对应的物理 ...

  3. C#下没有注册类 (异常来自 HRESULT:0x80040154 (REGDB_E_CLASSNOTREG))

    C#下没有注册类 (异常来自 HRESULT:0x80040154 (REGDB_E_CLASSNOTREG)) 原因:没有原生支持64位,而是以32位兼容方式运行 解决办法:在项目属性里设置“生成” ...

  4. Java 中断异常的正确处理方式

    处理InterruptedException 这个故事可能很熟悉:你正在写一个测试程序,你需要暂停某个线程一段时间,所以你调用 Thread.sleep().然后编译器或 IDE 就会抱怨说 Inte ...

  5. Java中断异常 InterruptedException 的正确处理方式

    你看到这篇文件可能是因为你已经调用了一个抛出 InterruptedException 异常的方法,并且需要以某种方式处理它. 首先,需要了解为一个方法为啥会 throws InterruptedEx ...

  6. 80x86保护模式下IDT和中断调用过程分析

    80x86保护模式下IDT和中断调用过程分析 1.中断描述符表(IDT),将每个异常或中断向量分别与它们的处理过程联系起来.与GDT和LDT类似,IDT也是由8字节长度的描述符组成.IDT空描述符的存 ...

  7. sleep方法要求处理中断异常:InterruptedException

    package seday08.thread;/*** @author xingsir * 当一个线程调用sleep方法处于阻塞状态的过程中,这个线程的中断方法interrupt被调用时,则sleep ...

  8. JMM在X86下的原理与实现

    JMM在X86下的原理与实现 Java的happen-before模型 众所周知 Java有一个happen-before模型,可以帮助程序员隔离各个平台多线程并发的复杂性,只要Java程序员遵守ha ...

  9. oslab oranges 一个操作系统的实现 实验四 认识保护模式(三):中断异常

    实验目的: 理解中断与异常机制的实现机理 对应章节:第三章3.4节,3.5节 实验内容: 1. 理解中断与异常的机制 2. 调试8259A的编程基本例程 3. 调试时钟中断例程 4. 建立IDT,实现 ...

随机推荐

  1. Javascript图片懒加载

    懒加载的意义 懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数. 懒加载的实现 1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟. 2.第二种是条 ...

  2. Jenkins 分布式和并发构建

    1. 分布式构建 1.1 添加 linux 节点 1.2 添加 windows 节点 2. 并发构建 2.1 原理 2.2 示例:分别用 chrome/IE/Firefox 并行测试 1. 分布式构建 ...

  3. Scrapy 爬虫项目框架

    1. Scrapy 简介 2. Scrapy 项目开发介绍 3. Scrapy 项目代码示例 3.1 setting.py:爬虫基本配置 3.2 items.py:定义您想抓取的数据 3.3 spid ...

  4. 11- jmeter主要元件

    元件分类 HTTP请求默认值(请求行,请求头,空行,消息体) HTTP信息头管理器: HTTPcookie管理器(1.更真实的模拟用户行为 ,多个请求的关联.第一个请求没有cookie第二个就带了co ...

  5. ASP去除所有html标签

    ASP去除所有html标签 function nohtml(str) dim re Set re=new RegExp re.IgnoreCase =true re.Global=True re.Pa ...

  6. php的call_user_func_array()使用场景

    1..动态调用普通函数时,比如参数和调用方法名称不确定的时候很好用 function sayEnglish($fName, $content) { echo 'I am ' . $content; } ...

  7. 【JavaScript】Leetcode每日一题-平方数之和

    [JavaScript]Leetcode每日一题-平方数之和 [题目描述] 给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得 a2 + b2 = c . 示例1: 输入:c = 5 ...

  8. JMeter关联陌生又熟悉

    JMeter关联是什么 JMeter关联,这几个字看着可能会有点陌生,实际上却是工作中经常会做的一件事情,尤其是接口自动化,它指的是把一个接口的响应作为另一个接口的参数,从而把接口关联起来. JMet ...

  9. 【Docker】7. 镜像-加载原理、分层原理、commit镜像

    一.什么是镜像 镜像是一种轻量级.可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件. 它包含运行某个软件所需的所有内容,包括代码.运行时环境.库.环境变量和配置文件. 所有的应用,直接 ...

  10. C++ primer plus读书笔记——第17章 输入、输出和文件

    第17章 输入.输出和文件 1. 对键盘进行输入缓冲可以让用户在将输入传输给程序之前返回并更正.C++程序通常在用户按下回车键时刷新输入缓冲区. 2. 一些I/O类 streambuf类为缓冲区提供了 ...