您的位置:首页 > 其它

第五讲 中断、异常和信号

2016-01-05 20:53 381 查看

在第一讲中提到过异常的分类,根据同步或异步产生、无意或故意产生以及最终的的返回行为可以分为四类。但不管是哪种,CPU的响应过程基本一致。即CPU根据中断向量,在内存中找到相应的服务程序入口并调用该服务程序。中断向量和中断处理函数内内核定义。处理方法也不尽相同。

对于异常,一般是抛出一个信号给当前进程,因此在本讲的最后将涉及信号相关的内容。

x86对中断的硬件支持

中断和异常向量

x86中,每个中断和异常由0~255之间的一个数来标识,称为中断向量。非屏蔽中断和异常的向量是固定的,范围在0~31之间,而可屏蔽中断的向量可以通过对中断控制器的编程来改变,范围在32~255间。

在单处理器中,可编程中断控制器PIC来负责监视所有硬件并向CPU产生中断。在多处理器上,引入I/O高级可编程控制器(I/O APIC)的组件,形成一个多APIC的系统来将中断分发给所有CPU。这个分发过程既可以是静态的,即传递给指定的某个、某些或所有CPU;也可以是动态的,即分发给一个当前负荷最低的CPU上或通过仲裁确定分发到哪个CPU上。另外多APIC系统还可以在处理器间产生中断。



x86处理器有约20种不同的异常。内核必须为每种异常提供一个专门的异常处理程序。下面是一些重要的异常及向量、名称、类型和描述。

0 - “Divide error”故障:整数除0操作时产生。

1 - “Debug”陷阱或故障:调试程序时使用。

2 - 未用。

3 - “Breakpoint”陷阱:由int 3指令引起,通常由debugger程序插入。

4 - “Overflow”陷阱:计算溢出。

5 - “Bounds check”故障:地址边界有误。

6 - “Invalid opcode”:无效操作码。

7 - “Device not available”故障:

8 - “Double Fault”异常中止:少数情况下,异常处理过程中又出现异常。

10 - “Invalid TSS”故障:CPU试图让一个CPU切换到有无效TSS的进程。

13 - “General Protection”故障:违反了x86保护模式下的保护规则。

14 - “Page Fault”故障:寻址的页不在内存,或违反了一种页保护机制。

15 - 由Intel保留

16 - “Floating Point Error”故障:浮点单元出错。

17 - “Alignment Check”故障:操作数的地址没有正确对齐。

18 - “Machine Check”异常中止:机器检测到一个CPU错误或总线错误。

19 - “SIMD Floating Point Exception”故障:SSE或SSE2单元浮点操作异常。

20~31由Intel留作将来使用。

进入中断服务程序

我们接下来着重讨论CPU在得到中断向量后如何进入相应中断服务程序。

最早在实模式中,CPU把内存从0开始的1KB字节作为一个中断向量表,每个表项由两字节的段地址和两字节的位移组成一个四字节地址,即中断服务程序的入口地址。这与ARM等CPU处理方式一致。但是这个机制中并没有提供运行模式的转换,即ARM中PSW寄存器的功能。

后来Intel在实现保护模式时,对中断响应机制作了大幅修改。中断向量表变为中断描述符表IDT,其中的表项也从单纯的入口地址改成了一个描述符表项,称为“门”。门中附带一个段选择符,再按照段式寻址的方法找到中断服务程序的入口。



门的引入在增加了一个寻址步骤的同时实现了CPU运行优先级的切换。只要想切换CPU的运行状态,就必须通过一道门。按不同用途和目的,有任务门、中断门、陷阱门和调用门四种。





除任务门外,其余三种门的结构基本相同。区别是类型码不同。中断门和陷阱门在使用上的区别只在于进入中断服务程序时CPU是否自动关中断。

门中的段选择码字段就是段选择符,与位移字段一起决定了一个地址。可以参考第一讲中段式寻址的内容。

在Linux内核中,实际上只使用GDT。对于中断门、陷阱门和调用门来说,段描述表中的相应表项显然应该是一个代码段描述项,而任务门指向的描述项则是专门为TSS而设置的TSS描述项。

每个段描述符中都有一个两位的DPL位段,当CPU通过中断门找到一个代码段描述符,从而转入相应的服务程序时,就把这个代码段描述符装入CPU中,而描述项的DPL就变成CPU的当前运行级别,称为CPL。中断门中还有额外一个DPL字段,这种重复是由于x86出奇复杂的优先级别检验机制造成的。这里不详述,最终导致通过中断门时只允许保持或提升CPU的运行级别,而不允许降低其级别。如果出现错误,会产生一次全面保护异常。

进入中断服务程序时,CPU要将当前EFLAGS寄存器的内容以及返回地址压入堆栈,返回地址是由段寄存器CS的内容和取指令指针EIP的内容共同组成的。如果中断是由异常引起的,则还要将一个表示异常原因的出错代码也压入堆栈。进一步,如果中断前后运行级别不同,就要更换堆栈。

具体到Linux内核。当中断发生在用户态,中断后进入内核态,因此要引起堆栈的更换,即从用户栈更换到系统堆栈。而当中断发生在系统状态时,则不需要更换堆栈。

最后,在保护模式中,中断向量表在内存中的位置不再限于从地址0开始,而是像GDT和LDT一样可以放在内存的任意地方,由新增的寄存器IDTR指示。x86的256个中断向量一共需要占用2KB空间来存放IDT。Linux利用中断门处理中断,用陷阱门处理异常,因此IDT内存放的主要是中断门或陷阱门,还有特殊异常是任务门。

当中断发生后,根据上图中的方式,最终寻址到中断服务程序开始执行。当执行完毕退出时,需要产生一条IRET指令,使CPU进行一些硬件检查,具体的返回策略在后面从中断和异常返回中描述。

中断和异常的嵌套

每个中断或异常都会引起一个内核控制路径,即代表当前进程在内核态执行。

内核控制路径可以任意嵌套,即一个中断处理程序可以被另一个中断处理程序“中断”,如下图所示。



中断处理程序必须永不阻塞,即中断处理程序运行期间不能发生进程切换。整个过程中所需要的数据都存放在进程的内核态堆栈中。

中断处理程序可以抢占其他中断处理程序或异常处理程序,但异常处理程序从不抢占中断处理程序。在内核态唯一会触发的异常是缺页异常,其他所有异常都只发生在用户态。中断处理程序从不执行可以导致缺页的操作,因为这样的操作有可能引起进程切换。

初始化IDT

从上面可见,在允许中断前,内核必须适当初始化IDT。

当计算机还在实模式时,IDT被初始化并由BIOS例程使用。然而当Linux接管,IDT就被移到RAM的另一个区域,并进行第二次初始化。

IDT存放在idt_table表中,共有256个表项。6字节的idt_descr变量指定了IDT的大小和它的地址,内核用lidt汇编指令使用idt_descr变量初始化IDTR寄存器。

在内核初始化过程中,
setup_idt()
汇编语言函数用同一个中断门来填充所有表项,该中断门指向
ignore_int()
处理程序。这个程序什么都不做,也不应该被执行,否则就意味着出现了硬件或内核的问题。

然后IDT被第二次初始化,用有意义的陷阱和中断处理程序替换这个空处理程序。这个过程在
start_kernel()
中调用
trap_init()
init_IRQ()
完成。

==================== arch/x86/kernel/traps.c 827 884 ====================
void __init trap_init(void)
{
int i;

#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

set_intr_gate(0, ÷_error);
set_intr_gate_ist(2, &nmi, NMI_STACK);
/* int4 can be called from all */
set_system_intr_gate(4, &overflow);
set_intr_gate(5, &bounds);
set_intr_gate(6, &invalid_op);
set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(9, &coprocessor_segment_overrun);
set_intr_gate(10, &invalid_TSS);
set_intr_gate(11, &segment_not_present);
set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
set_intr_gate(13, &general_protection);
set_intr_gate(15, &spurious_interrupt_bug);
set_intr_gate(16, &coprocessor_error);
set_intr_gate(17, &alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(18, &machine_check, MCE_STACK);
#endif
set_intr_gate(19, &simd_coprocessor_error);

/* Reserve all the builtin and the syscall vector: */
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors);

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

#ifdef CONFIG_X86_32
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif

/*
* Should be a barrier for any external CPU state:
*/
cpu_init();

x86_init.irqs.trap_init();
}


忽略一开始的EISA设置,程序先设置中断向量表开头的19个陷阱门,我们注意到其中漏掉几个,这在之前其他函数如early_trap_init()中已经设置完毕。然后是对系统调用向量的初始化,常数SYSCALL_VECTOR定义为0x80。

这里一共使用了五个函数。

=========== arch/x86/include/asm/desc.h 330 334 =============
static inline void set_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0, 0, __KERNEL_CS);
}


它插入一个中断门,段选择符指向内核代码段,DPL为0。

set_intr_gate_ist()
在32位系统上与
set_intr_gate()
相同。

=============== arch/x86/include/asm/desc.h 359 363 ===============
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0x3, 0, __KERNEL_CS);
}


它插入一个中断门,段选择符指向内核代码段,DPL为3。

================= arch/x86/include/asm/desc.h 377 381 =================
static inline void set_task_gate(unsigned int n, unsigned int gdt_entry)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TASK, (void *)0, 0, 0, (gdt_entry<<3));
}


它插入一个任务门,段选择符指向一个TSS描述符,DPL为3。

================ arch/x86/include/asm/desc.h 365 369 ================
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, 0, __KERNEL_CS);
}


它插入一个陷阱门,段选择符指向内核代码段,DPL为3。

DPL的设置考虑了x86对穿过门的特权级别的要求,使得大部分的异常都不能从用户态发出,而保留个别中断、异常和陷阱可以由用户态进入。这就形成了Linux里对门的细分。

中断门:用户态的进程不能访问的一个Intel中断门。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。

系统门:用户态的进程可以访问的一个Intel陷阱门。通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128。因此在用户态下,可以发布into、bound及int $0x80三条汇编语言指令。

系统中断门:能够被用户态进程访问的Intel中断门。与向量3相关的异常是由系统中断门激活的,因此,在用户态可以使用汇编指令int 3。

陷阱门:用户态的进程不能访问的一个Intel陷阱门。大部分Linux异常处理程序都通过陷阱门来激活。

任务门:不能被用户态进程访问的Intel任务门。Linux对“Double fault”异常的处理程序是由任务门激活的。

再来看
init_IRQ()
,它对多处理器的核0做特殊处理,然后用函数指针调用到
lguest_init_IRQ()
。这个函数对于0x80以外的每个中断向量,插入中断门,配置中断处理函数为函数数组interrupt中的函数。

==================== arch/x86/lguest/boot.c 821 837 ====================
static void __init lguest_init_IRQ(void)
{
unsigned int i;

for (i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) {
/* Some systems map "vectors" to interrupts weirdly.  Not us! */
__get_cpu_var(vector_irq)[i] = i - FIRST_EXTERNAL_VECTOR;
if (i != SYSCALL_VECTOR)
set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
}

/*
* This call is required to set up for 4k stacks, where we have
* separate stacks for hard and soft interrupts.
*/
irq_ctx_init(smp_processor_id());
}


数组interrupt[]中的每个函数都把与中断号有关的一个值压入栈中,然后都跳入函数
common_interrupt()
中执行处理。这个过程后面再描述。

一旦配置完IDT,对异常和中断就可以正常处理。

异常的处理

CPU产生的大部分异常都由Linux解释为出错条件。当异常发生时,内核就向引起异常的进程发送一个信号,从而由该进程的信号处理程序来从错误中恢复,如果没有信号处理程序,则进程中止。

但是有两种情况例外。一是在上一讲中描述过的Device not available异常。二是Page Fault异常。这在第三讲中缺页异常处理程序中描述过。

异常处理程序有一个标准的结构,由以下三部分组成:

1.在内核堆栈中保存大多数寄存器的内容(汇编实现)

2.通过C函数处理异常

3.通过ret_from_exception()函数从异常处理程序退出

每个异常处理程序都以下列的汇编指令开始:

handler_name:
pushl $0
pushl $do_handler_name
jmp error_code


当异常发生时,如果控制单元没有自动地把一个硬件出错代码插入到栈中,相应的汇编语言会包含一条pushl $0指令,在栈中垫上一个空值。然后把异常处理程序地址压入栈中。然后跳入一段标号为error_code汇编代码中,这段代码对几乎所有异常处理程序都是相同的。它为C语言的处理程序做一些准备工作,然后跳转到C代码里执行。

异常处理程序的C函数名都是do_前缀和处理程序名组成。它们做一些处理工作,最后又都跳转到
do_trap()
中。这个函数判断是否是在用户态,如果是,把硬件出错码和异常向量保存在当前进程的描述符中,然后向当前进程发送一个适当的信号。这个信号由进程处理,处理过程在下面讲信号时描述。

============== arch/x86/kernel/traps.c 145 146 ==============
tsk->thread.error_code = error_code;
tsk->thread.trap_no = trapnr;
=============== arch/x86/kernel/traps.c 163 163 =============
force_sig(signr, tsk);


如果在内核态,要检查是否是系统调用的无效参数引起,这在系统调用一讲中描述。最后出现在内核态的任何其他异常都是由于内核的bug引起的,这时为了避免更进一步的故障,调用
die()
函数打印所有CPU寄存器(kernel oops)并终止当前进程。

============= arch/x86/kernel/traps.c 163 163 ==============
kernel_trap:
if (!fixup_exception(regs)) {
tsk->thread.error_code = error_code;
tsk->thread.trap_no = trapnr;
die(str, regs, error_code);
}


最后执行异常处理的C函数终止时候,执行一条jmp指令跳到
ret_from_exception()
函数,这个函数在后面讲述。

中断的处理

中断的处理与异常有所不同。因为到达的中断可能与当前进程完全无关,因此给当前进程发送信号是毫无意义的。

中断的处理与中断类型有关。我们这里先讨论I/O中断和处理器间中断,在下一讲时间管理时再讨论时钟中断。

I/O中断处理

随着系统的逐渐复杂,硬件产生的中断个数越来越多,给每个中断分配一个中断向量早已不现实。中断处理程序通过两种方式实现了一定的灵活性来解决这个问题。

IRQ共享:每个中断向量关系到多个外设,因此每次中断执行多个ISR,如果设备没有产生中断,则ISR什么都不做,否则执行中断服务操作。

IRQ动态分配:只有在某个外设在使用时才分配IRQ线,否则不分配。

当中断发生时,有些操作需要立即执行,而另一些操作不适合在中断服务函数中执行。为此,Linux把紧随中断要执行的操作分为三类:

紧急的:如对PIC应答中断,对PIC或设备控制器重编程,或修改由设备和处理器同时访问的数据结构。这些都能很快执行,而且必须尽快执行。紧急操作要在一个中断处理程序内立即执行,而且是在禁止可屏蔽中断的情况下。

非紧急的:如修改只有处理器才会访问的数据结构。这些操作也要很快完成,因此由中断处理程序立即执行,而且是在开中断的情况下。

非紧急可延迟的:如把缓冲区的内容拷贝到某个进程的地址空间。这些操作可能被延迟较长时间而不影响内核操作。非紧急可延迟的操作由独立的函数来执行,我们将在软中断及tasklet一节讨论。

所有I/O中断处理程序都执行四个相同的基本动作:

1.在内核态堆栈中保存IRQ的值和寄存器的内容。

2.为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发送中断。

3.执行共享这个IRQ的所有设备的ISR。

4.跳到ret_from_intr()。

中断发生时,需要用几个描述符来表示IRQ线的状态和需要执行的函数。

中断向量

在x86体系中,物理IRQ可以使用的中断向量为32~238范围内(除128以外)。实际使用哪个值一部分中断向量由硬件体系固定,另一些可以在安装设备时由系统分配,还有一些可以是由硬件协议在启动时协商。不管是哪种方式,最终在启用中断前已将中断服务函数写入interrupt[]数组中。

IRQ数据结构

每个中断向量都有自己的irq_desc描述符,所有描述符一起组成了一个irq_desc数组。

================ include/linux/irqdesc.h 33 87 ================
struct irq_desc {

#ifdef CONFIG_GENERIC_HARDIRQS_NO_DEPRECATED
struct irq_data     irq_data;
#else
/*
* This union will go away, once we fixed the direct access to
* irq_desc all over the place. The direct fields are a 1:1
* overlay of irq_data.
*/
union {
struct irq_data     irq_data;
struct {
unsigned int        irq;
unsigned int        node;
struct irq_chip     *chip;
void            *handler_data;
void            *chip_data;
struct msi_desc     *msi_desc;
#ifdef CONFIG_SMP
cpumask_var_t       affinity;
#endif
};
};
#endif

struct timer_rand_state *timer_rand_state;
unsigned int        *kstat_irqs;
irq_flow_handler_t  handle_irq;
struct irqaction    *action;    /* IRQ action list */
unsigned int        status;     /* IRQ status */

unsigned int        depth;      /* nested irq disables */
unsigned int        wake_depth; /* nested wake enables */
unsigned int        irq_count;  /* For detecting broken IRQs */
unsigned long       last_unhandled; /* Aging timer for unhandled count */
unsigned int        irqs_unhandled;
raw_spinlock_t      lock;
#ifdef CONFIG_SMP
const struct cpumask    *affinity_hint;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t       pending_mask;
#endif
#endif
atomic_t        threads_active;
wait_queue_head_t       wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry   *dir;
#endif
const char      *name;
} ____cacheline_internodealigned_in_smp;

#ifndef CONFIG_SPARSE_IRQ
extern struct irq_desc irq_desc[NR_IRQS];
#endif




如前所述,多个设备能共享一个单独的IRQ。因此,内核要维护多个irqaction描述符,每个描述符涉及一个特定的硬件设备和一个特定的中断。

=============== include/linux/interrupt.h 114 125 ===============
struct irqaction {
irq_handler_t handler;
unsigned long flags;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};


handler指向一个I/O设备的中断服务例程。name是设备名,在/proc/interrupts中列出了所有设备名。next指针将多个结构体连接成一个单链表。flags标志由中断服务程序使用,可能取值有:

==================== include/linux/interrupt.h 39 71 ====================
/*
* These flags used only by the kernel as part of the
* irq handling routines.
*
* IRQF_DISABLED - keep irqs disabled when calling the action handler.
*                 DEPRECATED. This flag is a NOOP and scheduled to be removed
* IRQF_SAMPLE_RANDOM - irq is used to feed the random generator
* IRQF_SHARED - allow sharing the irq among several devices
* IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
* IRQF_PERCPU - Interrupt is per cpu
* IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
* IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
*                registered first in an shared interrupt is considered for
*                performance reasons)
* IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
*                Used by threaded interrupts which need to keep the
*                irq line disabled until the threaded handler has been run.
* IRQF_NO_SUSPEND - Do not disable this IRQ during suspend
*
*/
#define IRQF_DISABLED       0x00000020
#define IRQF_SAMPLE_RANDOM  0x00000040
#define IRQF_SHARED     0x00000080
#define IRQF_PROBE_SHARED   0x00000100
#define __IRQF_TIMER        0x00000200
#define IRQF_PERCPU     0x00000400
#define IRQF_NOBALANCING    0x00000800
#define IRQF_IRQPOLL        0x00001000
#define IRQF_ONESHOT        0x00002000
#define IRQF_NO_SUSPEND     0x00004000

#define IRQF_TIMER      (__IRQF_TIMER | IRQF_NO_SUSPEND)


如果一个中断内核没有处理,那么这个中断就是意外中断,也就是说,与某个IRQ线相关的ISR不存在,或者与某个中断线相关的所有例程都识别不出是否是自己的硬件设备发出的中断。通常,内核检查从IRQ线接收的意外中断的数量,当这条IRQ线连接的有故障设备没完没了地发中断时,就禁用这条IRQ线。由于几个设备可能共享IRQ,内核不会在每检测到一个意外中断时就立即禁用IRQ线,而是把中断和意外中断的总次数分别存放在irq_desc描述符的irq_count和irq_unhandled字段中,当第100000次中断产生时,如果意外中断次数超过99900,内核才禁用这条IRQ线。

status描述IRQ状态,取值如下

=============== include/linux/irq.h 52 73 ===============
/* Internal flags */
#define IRQ_INPROGRESS      0x00000100  /* IRQ handler active - do not enter! */
#define IRQ_DISABLED        0x00000200  /* IRQ disabled - do not enter! */
#define IRQ_PENDING     0x00000400  /* IRQ pending - replay on enable */
#define IRQ_REPLAY      0x00000800  /* IRQ has been replayed but not acked yet */
#define IRQ_AUTODETECT      0x00001000  /* IRQ is being autodetected */
#define IRQ_WAITING     0x00002000  /* IRQ not yet seen - for autodetection */
#define IRQ_LEVEL       0x00004000  /* IRQ level triggered */
#define IRQ_MASKED      0x00008000  /* IRQ masked - shouldn't be seen again */
#define IRQ_PER_CPU     0x00010000  /* IRQ is per CPU */
#define IRQ_NOPROBE     0x00020000  /* IRQ is not valid for probing */
#define IRQ_NOREQUEST       0x00040000  /* IRQ cannot be requested */
#define IRQ_NOAUTOEN        0x00080000  /* IRQ will not be enabled on request irq */
#define IRQ_WAKEUP      0x00100000  /* IRQ triggers system wakeup */
#define IRQ_MOVE_PENDING    0x00200000  /* need to re-target IRQ destination */
#define IRQ_NO_BALANCING    0x00400000  /* IRQ is excluded from balancing */
#define IRQ_SPURIOUS_DISABLED   0x00800000  /* IRQ was disabled by the spurious trap */
#define IRQ_MOVE_PCNTXT     0x01000000  /* IRQ migration from process context */
#define IRQ_AFFINITY_SET    0x02000000  /* IRQ affinity was set from userspace*/
#define IRQ_SUSPENDED       0x04000000  /* IRQ has gone through suspend sequence */
#define IRQ_ONESHOT     0x08000000  /* IRQ is not unmasked after hardirq */
#define IRQ_NESTED_THREAD   0x10000000  /* IRQ is nested into another, no own handler thread */


depth字段和IRQ_DISABLED标志表示IRQ是否被禁用。每次调用
disable_irq()
disable_irq_nosync()
函数,depth字段增加,每次调用
enable_irq()
函数,depth字段减少。

IRQ在多处理器系统上的分发

Linux遵循对称多处理模型SMP,即本质上每个CPU都是对等的。因而在处理中断上也应该是对等的。在x86上,内核利用多核x86的多APIC系统的硬件特性,在初始化时做好配置,使得硬件在CPU间动态分发IRQ信号,最终做到所有CPU服务于I/O中断的执行时间片几乎相同。

但是有些硬件如某些Pentium 4的主板不能公平地在处理器间分配中断,这种情况下内核利用kirqd的特殊内核线程对IRQ的分配做纠正。该线程周期性执行
do_irq_balance()
函数,该函数追踪在最近时间时隔内每个CPU接收的中断次数,如果有不严重平衡问题,就通过配置硬件寄存器让IRQ从一个CPU转移到另一个CPU。

处理中断

前面提到,中断由interrupt[]数组中的函数处理,每个函数都把中断号减256保存在框中,然后跳转到
common_interrupt()
函数。这个函数保存除eflags、cs、eip、ss和esp外所有CPU寄存器,然后调用
do_IRQ()
函数做处理。

=============== arch/x86/kernel/irq.c 221 251 ===============
/*
* do_IRQ handles all normal device IRQ's (the special
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);

/* high bit used in ret_from_ code  */
unsigned vector = ~regs->orig_ax;
unsigned irq;

exit_idle();
irq_enter();

irq = __get_cpu_var(vector_irq)[vector];

if (!handle_irq(irq, regs)) {
ack_APIC_irq();

if (printk_ratelimit())
pr_emerg("%s: %d.%d No irq handler for vector (irq %d)\n",
__func__, smp_processor_id(), vector, irq);
}

irq_exit();

set_irq_regs(old_regs);
return 1;
}




函数的入参指向汇编代码中保存的CPU寄存器,这使得在C语言中可以对其进行访问。

1.执行irq_enter(),使中断处理程序嵌套计数器递增。

2.在进程拥有4KB内核栈的情况下,需要切换到硬中断请求栈,否则在当前栈执行中断处理函数。处理函数是通过中断号在irq_desc数组中找到描述符,进而寻址到的。

3.执行irq_exit(),递减中断计数器并检查是否有可延迟中函数正在等待执行。

4.恢复调用本函数前的栈。

一直到Linux2.6的早期版本,内核都在第2步中调用到__do_IRQ()函数,但在最新的内核结构中,在32位体系上没有这个步骤,直接调用中断处理函数。

中断处理函数的注册、编写和中断共享等内容在驱动程序一讲中再讨论。

处理器间中断

处理器间中断是从一个CPU向系统中的其他CPU发送的中断。Linux定义了三种处理器间中断:CALL_FUNCTION_VECTOR、RESCHEDULE_VECTOR和INVALIDATE_TLB_VECTOR,分别有不同的用途。

调用如下函数产生处理器间中断:
send_IPI_all()
send_IPI_allbutself()
send_IPI_self()
send_IPI_mask()
,函数功能通过函数名很容易理解。

处理器间中断处理程序先保存寄存器,从栈顶压入向量号减256的值,然后调用不同的C函数应答中断并执行处理操作。

软中断及tasklet

在上一节讲到,有些处理过程是非紧急可延迟的。软中断和tasklet就是其中一种实现延迟处理的方法。

软中断

软中断只用于少数场合,也只有少数几个软中断。所有可用的软中断类型都在一个枚举类型中列举。

==================== include/linux/interrupt.h 371 391 ====================
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/

enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};


可见软中断类型有限,是稀缺资源,并不能随意使用。这里的软中断有两个用于网络的发送和接收、两个用于块层、一个用于调度器、两个用于定时器、两个用于实现tasklet。

所有软中断保存在一个softirq_vec表中,通过调用
open_softirq()
函数注册软中断来对该表进行初始化。

=============== kernel/softirq.c 55 55 =================
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
============== kernel/softirq.c 368 371 ================
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}


调用
raise_softirq()
来激活一个软中断,其参数是软中断号。该函数只是标记对应软中断应该得到执行,当内核下次检查到这个标记时,会调用
do_softirq()
来处理软中断。

=============== kernel/softirq.c 272 288 ===============
asmlinkage void do_softirq(void)
{
__u32 pending;
unsigned long flags;

if (in_interrupt())
return;

local_irq_save(flags);

pending = local_softirq_pending();

if (pending)
__do_softirq();

local_irq_restore(flags);
}


这个函数首先检查当前是否在中断上下文中,如果在,就立即结束。因为软中断用于执行ISR的非时间关键部分,所以其代码本身一定不能在中断处理程序内调用。

然后通过
local_softirq_pending()
来判断是否有挂起的软中断待处理,如果有,就调用
__do_softirq()
处理。这个函数遍历所有软中断标记位图,逐一调用注册的处理函数来处理,直到软中断标记位图为空。

软中断可以重新激活自己,外部高频率事件也可能频繁激活软中断,这都将导致在软中断处理过程中又出现新的软中断。这时内核有两种选择:一是忽略新的软中断,等到下一次进入软中断处理函数时再处理;二是不断重新检查,直到所有软中断都被处理再返回。第一种选择会导致很大的延时,而第二种选择在高频率软中断下会导致
do_softirq()
永不返回,实际上占用了过多时间片。

为了解决这种问题,内核对每个CPU都分配一个软中断守护进程,用于异步处理软中断。在
do_softirq()
中确定哪些软中断是已处理且又激活的,则唤醒该内核守护进程。由于内核进程有较低优先级,因此用户程序有机会运行,但是如果机器空闲,挂起的软中断又可以很快执行。这就实现了两种矛盾的平衡。

tasklet

软中断的分配是静态的,而tasklet的分配和初始化可以在运行时进行。软中断可以并发地执行在多个CPU上,因此,软中断是可重入函数并且必须明确使用自旋锁保护其数据结构。tasklet则不必担心这些问题,相同类型的tasklet总是串行执行,这样可以简化驱动的开发工作。

tasklet在软中断之上实现,它建立在HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断之上。两个软中断没有真正区别,只是在处理顺序上有先后。

创建tasklet

描述tasklet的数据结构是tasklet_struct:

============ include/linux/interrupt.h 460 467 ============
struct tasklet_struct
{
struct tasklet_struct *next;
unsigned long state;
atomic_t count;
void (*func)(unsigned long);
unsigned long data;
};


各个结构体用next指针实现一个链表,这容许几个任务排队执行。

state表示任务的当前状态,类似于真正的进程,但只有两个选项:TASKLET_STATE_SCHED和TASKLET_STATE_RUN,前一个状态表示tasklet是挂起的,后一个状态只在SMP系统上使用,用于保护tasklet在多处理器上并行执行。

count用于禁止tasklet,当此值不为零时,将忽略处理该tasklet。

func和data是处理函数指针和参数。

tasklet_init()
函数用于初始化这个结构体。

注册tasklet

tasklet_schedule()
将一个tasklet注册到系统中。

执行tasklet

当激活软中断时,
do_softirq()
函数得到执行,它遍历所有软中断位图,其中就包括HI_SOFTIRQ和TASKLET_SOFTIRQ,与之相关的处理函数分别是
tasklet_hi_action()
tasklet_action()
。这两个函数非常相似,以前者为例进行简单分析。

============== kernel/softirq.c 459 492 ==============
static void tasklet_hi_action(struct softirq_action *a)
{
struct tasklet_struct *list;

local_irq_disable();
list = __get_cpu_var(tasklet_hi_vec).head;
__get_cpu_var(tasklet_hi_vec).head = NULL;
__get_cpu_var(tasklet_hi_vec).tail = &__get_cpu_var(tasklet_hi_vec).head;
local_irq_enable();

while (list) {
struct tasklet_struct *t = list;

list = list->next;

if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}

local_irq_disable();
t->next = NULL;
*__get_cpu_var(tasklet_hi_vec).tail = t;
__get_cpu_var(tasklet_hi_vec).tail = &(t->next);
__raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
}
}


1.禁用本地中断。

2.把本地CPU数据tasklet_hi_vec指向的链表地址存入局部变量list。

3.清空本地CPU数据tasklet_hi_vec指向的链表。

4.打开本地中断。

5.遍历list链表,对其中的每个tasklet描述符依次处理后放回tasklet_hi_vec指向的链表。

6.对于每个tasklet,如果其count不为零或TASKLET_STATE_SCHED标志未置位,则不处理,否则调用t->func(t->data)来处理。

工作队列

工作队列是延迟执行的另一种手段。软中断和tasklet运行在中断上下文中,而工作队列运行在进程上下文中,因此可调用阻塞函数。

工作队列子系统可以用于创建内核线程,在内核线程中执行一个队列里的任务。它创建的这些内核线程称作工作者线程。使用工作者队列只需要三步:创建工作队列,向其中添加任务,执行任务。

创建工作队列

调用
create_workqueue()
create_singlethread_workqueue()
函数来创建工作队列,前一个函数在所有CPU上都创建一个工作线程,后者只在系统的第一个CPU上创建一个线程。两个函数都调用
__alloc_workqueue_key()
实现,返回的是一个工作队列描述符workqueue_struct。

============= kernel/workqueue.c 221 245 ============
struct workqueue_struct {
unsigned int        flags;      /* I: WQ_* flags */
union {
struct cpu_workqueue_struct __percpu    *pcpu;
struct cpu_workqueue_struct     *single;
unsigned long               v;
} cpu_wq;               /* I: cwq's */
struct list_head    list;       /* W: list of all workqueues */

struct mutex        flush_mutex;    /* protects wq flushing */
int         work_color; /* F: current work color */
int         flush_color;    /* F: current flush color */
atomic_t        nr_cwqs_to_flush; /* flush in progress */
struct wq_flusher   *first_flusher; /* F: first flusher */
struct list_head    flusher_queue;  /* F: flush waiters */
struct list_head    flusher_overflow; /* F: flush overflow list */

mayday_mask_t       mayday_mask;    /* cpus requesting rescue */
struct worker       *rescuer;   /* I: rescue worker */

int         saved_max_active; /* W: saved cwq max_active */
const char      *name;      /* I: workqueue name */
#ifdef CONFIG_LOCKDEP
struct lockdep_map  lockdep_map;
#endif
};


它包括一个每CPU变量cpu_workqueue_struct结构体指针,其指向的每个结构体代表对应CPU的一组任务。这个每CPU变量是工作队列的核心。

向工作队列添加任务

queue_work()
调用了
__queue_work()
函数,把一个用work_struct结构描述的函数插入工作队列。

============ include/linux/workqueue.h 79 86 ==============
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func;
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};


entry照例是一个链表,用于将几个work_struct链接到一个链表中。func是要执行的函数指针,该函数有一个指针类型的参数,指向本work_struct实例,这样可以获得data数据。

这里的data本可以是一个void*类型的数据,却定义成了atomic_long_t,这是内核里的一个技巧。指针在4字节对齐,因此空余两字节可以挪作标志位使用。将data定义为原子数据类型,可以保证对标志位的修改不会引发并发问题。

内核还提供了一种使函数延迟指定时间后执行的方法,即
queue_delayed_work()
,把一个delayed_work结构体描述的函数插入工作队列。

============ include/linux/workqueue.h 92 95 ============
struct delayed_work {
struct work_struct work;
struct timer_list timer;
};


这个函数首先创建一个内核定时器,在超时后回调函数里调用
queue_work()
把工作添加到工作队列中。延时相关的问题在下一讲时间管理中描述。

除了自己创建一个专门的工作者线程来处理需要推后的工作,还可以使用内核缺省的工作者线程。缺省的这个工作者线程叫作events/n,其中n是处理器编号。内核提供了两个函数将新的工作添加该缺省队列。

============ kernel/workqueue.c 2613 2615 ============
int schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
============ kernel/workqueue.c 2640 2644 ============
int schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work(system_wq, dwork, delay);
}


其中system_wq是在系统初始化期间创建的工作队列。

============ kernel/workqueue.c 3690 3690 ============
system_wq = alloc_workqueue("events", 0, 0);


注意如果是会长时间占用CPU或长时间睡眠的任务,最好不要加入到这个缺省工作队列中,因为同一个工作队列中的任务是串行执行的,因此太长的延迟会对其他用户产生不良影响。

执行任务

每个工作者线程在
worker_therad()
函数内部不断地执行循环操作,大部分时间都处于睡眠状态并等待有工作插入到队列中。

工作线程一旦被唤醒就开始执行任务,它从工作者线程的工作队列链表中删除所有work_struct描述符,并执行相应的函数。由于工作队列函数可以阻塞,因此,工作者线程可能睡眠,甚至有可能会迁移到另一个CPU上恢复执行——虽然某个工作者线程与某个CPU相关,但其实际上可以被所有CPU执行。

从中断和异常返回

从中断和异常返回时,应该选择一个程序恢复执行。这时内核需要考虑几个问题:

如果仅有一个内核控制路径在并发执行,那么要切换到用户态;如果有挂起进程的切换请求,那么要执行进程调度;如果有挂起的信号,那么处理它;如果在单步执行模式或virtual-8086模式,那么需要特殊处理。

这些事项都在thread_info描述符的flags字段中记录,这个字段还记录一些其他标志,与中断和异常返回相关的标志在下面列出。

=========== arch/x86/include/asm/thread_info.h 76 81 ============
#define TIF_SYSCALL_TRACE   0   /* syscall trace active */
#define TIF_NOTIFY_RESUME   1   /* callback before returning to user */
#define TIF_SIGPENDING      2   /* signal pending */
#define TIF_NEED_RESCHED    3   /* rescheduling necessary */
#define TIF_SINGLESTEP      4   /* reenable singlestep on user return*/
#define TIF_IRET        5   /* force IRET */
=========== arch/x86/include/asm/thread_info.h 83 83 ============
#define TIF_SYSCALL_AUDIT   7   /* syscall auditing active */
=========== arch/x86/include/asm/thread_info.h 90 90 ============
#define TIF_MEMDIE      20  /* is terminating due to OOM killer */


前面讲到,中断和异常处理最后一步分别是跳到
ret_from_intr()
ret_from_exception()
函数。事实上两者并非是函数,而是两个汇编标记入口点。两者非常相似,唯一区别是,如果内核在编译时选择了支持内核抢占,那么从异常返回时要立即禁用本地中断。

下面的流程图给出了恢复执行被中断中程序所必需的步骤。



信号

前面提到,在Linux对绝大多数异常的处理都是给进程抛一个信号。本节来专门讨论信号。

信号是很短的消息,可以被发给一个进程或一个进程组,它通常只含一个数字,用SIG前缀的一组宏来标识。下面是x86下Linux2.6所处理的一些信号 :

=============== arch/x86/include/asm/signal.h 43 79 ===============
#define SIGHUP       1
#define SIGINT       2
#define SIGQUIT      3
#define SIGILL       4
#define SIGTRAP      5
#define SIGABRT      6
#define SIGIOT       6
#define SIGBUS       7
#define SIGFPE       8
#define SIGKILL      9
#define SIGUSR1     10
#define SIGSEGV     11
#define SIGUSR2     12
#define SIGPIPE     13
#define SIGALRM     14
#define SIGTERM     15
#define SIGSTKFLT   16
#define SIGCHLD     17
#define SIGCONT     18
#define SIGSTOP     19
#define SIGTSTP     20
#define SIGTTIN     21
#define SIGTTOU     22
#define SIGURG      23
#define SIGXCPU     24
#define SIGXFSZ     25
#define SIGVTALRM   26
#define SIGPROF     27
#define SIGWINCH    28
#define SIGIO       29
#define SIGPOLL     SIGIO
/*
#define SIGLOST     29
*/
#define SIGPWR      30
#define SIGSYS      31
#define SIGUNUSED   31


发送信号可以让进程知道已经发生了一个特定事件,从而强迫进程执行它自己代码中的信号处理程序。

信号的一些特点有:

一个常规信号被连续发送多次,那么只有其中的一个发送到接收进程。

信号可以随时发送给状态经常不可预知的进程。发送给非运行进程的信号必须由内核保存,直到进程恢复执行。

信号传递有两个阶段:

信号产生:内核更新目标进程的数据结构以表示一个新信号已被发送。

信号传递:内核强迫目标进程改变执行状态或/且开始执行一个特定的信号处理程序。

每个所产生的信号至多被传递一次,信号是可消费资源,一旦它们已经传递出去,进程描述符中有关这个信号的所有信息都被取消。

已经产生但还没有传递的信号称为挂起信号。任何时候,一个进程仅存在给定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只被简单地丢弃。但是实时信号可以有好几个。

进程有三种方式对一个信号做出应答:

1.显式地忽略信号

2.执行与信号相关的缺省操作:可能是终止、终止并转储、忽略、停止、继续。

3.调用相应的信号处理函数捕获信号。

对一个信号的阻塞和忽略不同,只要信号被阻塞,它就不被传递,直到信号解除后才传递它。而一个被忽略的信号总被传递,只是没有进一步的操作。

SIGKILL和SIGSTOP信号不可以被显式忽略、捕获或阻塞,因此,通常要执行它们的缺省操作。

如果信号的传递总会引起内核杀死一个进程,那么这个信号对该进程就是致命的。如SIGKILL信号总是致命的。而且缺省操作为终止的每个信号以及不被进程捕获的信号对该进程也是致命的。

信号相关的数据结构

与信号有关的几个重要数据结构在下图中表示。



进程描述符task_struct的详细结构可以参考上一讲。该结构体中有一个blocked和real_blocked字段存放当前进程屏蔽的信号和实时信号。

进程描述符的signal字段指向信号描述符,用来跟踪共享挂起信号。

此外每个进程还引用一个信号处理程序描述符,用来描述每个信号要被如何处理。其中的count字段表示共享该结构的进程个数。在一个POSIX的多线程应用中,线程组中的所有轻量级进程都引用相同的信号描述符和信号处理程序描述符。

产生信号

除了异常处理会发送信号外,内核和进程都可以通过一些系统调用或内核函数发送信号给其他进程或进程组。

向进程发送信号的内核函数最后都调用
__send_signal()
函数在挂起信号队列中插入一个新的元素。

来将可以来自内核,也可以来自另一个进程。

传递信号

与信号有关的系统调用

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: