kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

内核源码分析之linux内核栈

http://www.cnblogs.com/liangning/p/3879177.html

基于3.16-rc4

在3.16-rc4内核源码中,内核给每个进程分配的内核栈大小为8KB。这个内核栈被称为异常栈,在进程的内核空间运行时或者执行异常处理程序时,使用的都是异常栈,看下异常栈的代码(include/linux/sched.h):

1
2
3
4
union thread_union {
	struct thread_info thread_info;
	unsigned long stack[THREAD_SIZE/sizeof(long)];
};

THREAD_SIZE值为8KB,因此内核为进程的异常栈(内核栈)分配了两个页框大小(页框大小4KB)。另外,进程的thread_info结构体保存在栈顶部。

此外,内核为每个cpu分配一个硬中断栈和一个软中断栈(这两个栈也是内核栈),用来执行中断服务例程和下半部(软中断),看看代码(arch/x86/kernel/irq_32.c)。这两个栈属于cpu,不属于进程,这和异常栈是有区别的。

1
2
DEFINE_PER_CPU(struct irq_stack *, hardirq_stack);
DEFINE_PER_CPU(struct irq_stack *, softirq_stack);

定义了两个数组hardirq_stack和softirq_stack,每个数组元素对应一个cpu,指向了该cpu的硬中断栈或者软中断栈。再来看下struct irq_stack结构体(arch/x86/include/asm/processor.h):

1
2
3
struct irq_stack {
	u32                     stack[THREAD_SIZE/sizeof(u32)];
} __aligned(THREAD_SIZE);

可见,硬中断栈和软中断栈的大小均为8KB。

内核在执行中断处理程序时,在do_IRQ函数中会调用handle_irq函数,在handle_irq函数中要进行堆栈切换,代码如下(arch/x86/kernel/irq_32.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool handle_irq(unsigned irq, struct pt_regs *regs)
{
	struct irq_desc *desc;
	int overflow;

	overflow = check_stack_overflow();

	desc = irq_to_desc(irq);
	if (unlikely(!desc))
	return false;

	if (user_mode_vm(regs) || !execute_on_irq_stack(overflow, desc, irq)) {
		if (unlikely(overflow))
			print_stack_overflow();
		desc->handle_irq(irq, desc);
	}

	return true;
}

第12行中执行execute_on_irq_stack函数来判断是否需要堆栈切换,如果不需要,则执行if体的中断服务例程,即在当前堆栈中执行中断服务例程,如果需要切换堆栈,则在execute_on_irq_stack函数中切换堆栈并在该函数中(新堆栈中)执行中断服务例程。下面看下execute_on_irq_stack代码(arch/x86/kernel/irq_32.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static inline int
execute_on_irq_stack(int overflow, struct irq_desc *desc, int irq)
{
	struct irq_stack *curstk, *irqstk;
	u32 *isp, *prev_esp, arg1, arg2;

	curstk = (struct irq_stack *) current_stack();
	irqstk = __this_cpu_read(hardirq_stack);

	/*
	 * this is where we switch to the IRQ stack. However, if we are
	 * already using the IRQ stack (because we interrupted a hardirq
	 * handler) we can't do that and just have to keep using the
	 * current stack (which is the irq stack already after all)
	 */
	if (unlikely(curstk == irqstk))
	    return 0;

	isp = (u32 *) ((char *)irqstk + sizeof(*irqstk));

	/* Save the next esp at the bottom of the stack */
	prev_esp = (u32 *)irqstk;
	*prev_esp = current_stack_pointer;

	if (unlikely(overflow))
	    call_on_stack(print_stack_overflow, isp);

	asm volatile("xchgl    %%ebx,%%esp    \n"
	         "call    *%%edi        \n"
	         "movl    %%ebx,%%esp    \n"
	         : "=a" (arg1), "=d" (arg2), "=b" (isp)
	         :  "0" (irq),   "1" (desc),  "2" (isp),
	        "D" (desc->handle_irq)
	         : "memory", "cc", "ecx");
	return 1;
}

第7行获取当前堆栈的指针,第8行获取本地cpu的硬中断栈指针,第16行对二者进行比较,如果相等,则不需要切换堆栈(说明当前堆栈就是硬中断栈,也说明是在中断处理程序中时又发生了中断)。如果不相等,就要进行堆栈切换,第22-23行将当前堆栈指针保存在将要切换到的堆栈中(用于返回)。第28行,交换ebx和esp寄存器的值(实现了堆栈切换,将中断栈指针给了esp),第29行跳转到相应的中断服务例程,第30行从中断服务例程返回后,又将原来的堆栈指针赋给esp,切换到原先堆栈。第33行将中断服务例程函数名存放在%edi中。

中断栈溢出后的结果

http://www.lenky.info/archives/2013/03/2247

说一下上文中最开始提到的“某个问题”:如果一台主机网卡比较多,然后每个网卡分队列又比较多,总之结果就是系统里的网卡设备的中断号比较多(关于超过256个中断数的情况,请见参考1,2,3),一旦所有这些中断都绑定到同一个CPU,那么如果网卡收到数据包进而触发中断,而众多硬中断一嵌套就非常容易出现中断栈溢出。一旦中断栈溢出,那么将会导致怎样的结果,这曾在之前的文章里隐含的提到过,这里再重新整理一遍。

在继续下面的描述之前,先看两个知识点:

1,Linux 2.4.x的中断栈:

a),由硬中断/软中断共同使用同一个中断栈
b),中断栈与内核栈共享一个栈
c),中断执行的时候使用的栈就是当前进程的内核栈

2,Linux 2.6.x的中断栈:

a),硬中断与软中断分离使用不同的中断栈
b),中断栈与内核栈分离
c),X86_64 double fault、NMI还可以有额外的栈(64bit特性:IST(Interrupt Stack Table))

可以看到,对于Linux 2.4.x内核而言,因为中断处理函数使用内核栈作为中断栈,所以导致更加容易发生内核栈溢出(因内核函数本身用栈过多导致溢出,或内核函数本身还未导致内核栈溢出,但此时来了一个中断,因中断函数使用栈而导致溢出,即中断函数成了压死骆驼的最后一根稻草),而内核栈溢出的直接结果就是踩坏task结构体,从而无法正常执行对应的task进程而出现oops宕机。

由于“中断执行的时候使用的栈就是当前进程的内核栈”,所以如果是执行到中断函数后才溢出,那么导致oops里提示的进程信息可能每次都不一样,因此如果出现这种情况,需要考虑是中断函数导致内核栈溢出,否则需怀疑普通的内核函数导致栈溢出即可。

对于Linux 2.6.x内核而言,因为其中断/内核栈分离、软/硬中断栈分离,即每个CPU私有两个栈(见下面注释)分别处理软中断和硬中断,因此出现内核栈溢出,特别是中断栈溢出的概率大大降低。

注释:这个说法来之书本《Understanding.the.Linux.Kernel.3rd.Edition》4.6.1.4. Multiple Kernel Mode stacks,而这本书针对的内核版本是2.6.11,且主要是指32位架构,所以与现在的新版内核源码有些许出入(比如现在情况的栈大小可能是占用2页),但这些细微改变与本文的具体问题相关不大(无非是溢出的难易程度问题),这里不再深入研究,具体情况请参考源代码自行斟酌。

1
2
3
The hard IRQ stack is used when handling interrupts. There is one hard IRQ stack for each CPU in the system, and each stack is contained in a single page frame.

The soft IRQ stack is used when handling deferrable functions (softirqs or tasklets; see the later section “Softirqs and Tasklets”). There is one soft IRQ stack for each CPU in the system, and each stack is contained in a single page frame. 

回到本文的主题,在之前的文章里提到过,即如果中断/异常处理函数本身在处理的过程中出现异常,那么就有可能发生double fault,比如中断栈溢出。中断栈溢出导致的最终结果有两种情况,这由所使用的具体Linux内核版本来决定,更具体点说是由double fault异常的栈是否单独来决定(见参考1)。

1,double fault的栈被单独出来
这意味着double fault的处理函数还能正常执行,因此打印oops,宕机。

2,double fault的栈没有被单独出来
这意味着double fault的处理函数也无法正常执行,进而触发triple fault,机器直接重启。

对于86-64架构下的Linux 2.6.x内核,因为IST(Interrupt Stack Table)的帮助,所以中断栈溢出导致的最终结果就是打印oops,宕机。

下面来看内核源码文档kernel-stacks,
1,每一个活动线程都有一个内核栈,大小为2页。
2,每一个cpu有一些专门的栈,只有当cpu执行在内核态时,这些栈才有用;一旦cpu回退到用户态,这些特定栈就不再包含任何有用数据。
3,主要的特定栈有:
a,中断栈:外部硬件中断的处理函数使用,单独的栈可以提供给中断处理函数更多的栈空间。
这里还提到,在2.6.x-i386下,如果设置内核栈只有4K,即CONFIG_4KSTACKS,那么中断栈也是单独开的。备注:这个已有修改,2010-06-29 x86: Always use irq stacks,即不管设置的内核栈是否只有4K,中断栈都是独立的了。

另外,这里有个说法与前面的引用有点出入:

1
The interrupt stack is also used when processing a softirq. 

即软中断和硬中断一样,也是使用这个中断栈。

b,x86_64所特有的(也就是i386没有,即同时2.6.30.8内核,32位的Linux就不具备下面所说的这个特性),为double fault或NMI单独准备的栈,这个特性被称为Interrupt Stack Table(IST)。每个cpu最多支持7个IST。关于IST的具体原理与实现暂且不说,直接来看当前已经分配的IST独立栈:

  • STACKFAULT_STACK. EXCEPTION_STKSZ (PAGE_SIZE)
    12号中断Stack Fault Exception (#SS)使用

  • DOUBLEFAULT_STACK. EXCEPTION_STKSZ (PAGE_SIZE)
    8号中断Double Fault Exception (#DF)使用

  • NMI_STACK. EXCEPTION_STKSZ (PAGE_SIZE)
    2号中断non-maskable interrupts (NMI)使用

  • DEBUG_STACK. DEBUG_STKSZ
    1号中断硬件调试和3号中断软件调试使用

  • MCE_STACK. EXCEPTION_STKSZ (PAGE_SIZE)
    18号中断Machine Check Exception (#MC)使用

正因为double fault异常处理函数所使用的栈被单独了出来,所以在出现中断栈溢出时,double fault异常的处理函数还能正常执行,顺利打印出oops信息。

最后的最后,有补丁移除IST功能(貌似是因为如果没有IST功能,那么kvm可以得到更好的优化,具体请见参考5),但通过对比补丁修改与实际源码(2.6.30.8以及3.6.11)来看,这个补丁并没有合入mainline主线。

参考资料:

1,where is hardware timer interrupt?
http://stackoverflow.com/questions/14481032/where-is-hardware-timer-interrupt

2,The MSI Driver Guide HOWTO
https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/Documentation/PCI/MSI-HOWTO.txt?id=v2.6.30.8
对应的翻译版:http://blog.csdn.net/reviver/article/details/6802347

3,[PATCH] x86: 64bit support more than 256 irq v2
http://linux-kernel.2935.n7.nabble.com/PATCH-x86-64bit-support-more-than-256-irq-v2-td323261.html

4,How is an Interrupt handled in Linux?
http://unix.stackexchange.com/questions/5788/how-is-an-interrupt-handled-in-linux

5,Remove interrupt stack table usage from x86_64 kernel (v2)
http://lwn.net/Articles/313029/
http://thread.gmane.org/gmane.comp.emulators.kvm.devel/26741

6,Interrupt Descriptor Table
http://wiki.osdev.org/IDT

转载请保留地址:http://www.lenky.info/archives/2013/03/2247http://lenky.info/?p=2247

对Linux x86-64架构上硬中断的重新认识

http://www.lenky.info/archives/2013/03/2245

对于x86硬中断的概念,一直都落在理论的认识之上,直到这两天才(因某个问题)发现Linux的实现却并非如此,这里纠正一下(注意:Linux内核源码更新太快,一个说法的时效性太短,所以需注意我提到的香草内核版本,并且以x86-64架构为基准)。

以前的认识:Linux对硬中断(本文如无特殊说明,都是指普通意义上的可屏蔽硬件中断)的处理有优先级概念,高优先级硬中断可以打断低优先级硬中断。

重新认识:

1,对于x86硬件而言,在文档325462.pdf卷3章节6.9 PRIORITY AMONG SIMULTANEOUS EXCEPTIONS AND INTERRUPTS 提到一个表格,是指如果在同一时刻有多个异常或中断到达,那么CPU会按照一个指定的优先级顺序对它们进行响应和服务,而并不是我之前所想的判断是否可相互打断执行的高低级别。

2,对于Linux系统而言,硬中断之间并没有优先级的概念(虽然Intel CPU提供支持,请参考文档325462.pdf卷3章节10.8.3 Interrupt, Task, and Processor Priority),或者说优先级只有两个,全部关闭或全部开启,如下:

Regardless of what the hardware might support, typical UNIX-type systems only make use of two levels: the minimum (all interrupts enabled) and the maximum (all interrupts disabled).

这意味着,如果一个硬中断处理函数正在执行,只要当前是处于开启中断的情况,那么此时发生的任何另外一个中断都可以打断当前处理函数,从而出现中断嵌套的情况。 值得注意的是,Linux提供对单个中断开启/禁止的接口(以软件实现为主,比如给对应中断描述符desc的status打上IRQ_DISABLED旗标):

1
2
void disable_irq(unsigned int irq)
void enable_irq(unsigned int irq)

下面来看看Linux的实际处理,其硬中断的一般处理流程(具体可见参考1、2、3以及源代码,以2.6.30.8为例):

1
硬件中断 -> common_interrupt -> do_IRQ -> handle_irq -> generic_handle_irq_desc -> desc->handle_irq或__do_IRQ。

其中desc->handle_irq是一个回调函数,会根据不同中断类型(I/O APIC、MSI)有不同的指向,比如:handle_fasteoi_irq()、handle_edge_irq(),这可以参考设置函数ioapic_register_intr()和setup_msi_irq()。通过/proc/interrupts可以看到各个中断的具体类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[root@localhost ~]# cat /proc/interrupts
	       CPU0       CPU1      
  0:        888          0   IO-APIC-edge      timer
  1:         96        112   IO-APIC-edge      i8042
  3:          1          0   IO-APIC-edge   
  4:          1          0   IO-APIC-edge   
  7:          0          0   IO-APIC-edge      parport0
  8:          1          0   IO-APIC-edge      rtc0
  9:          0          0   IO-APIC-fasteoi   acpi
 12:        204          0   IO-APIC-edge      i8042
 14:          0          0   IO-APIC-edge      ata_piix
 15:     460641        900   IO-APIC-edge      ata_piix
 16:          0          0   IO-APIC-fasteoi   Ensoniq AudioPCI
 17:     118347          0   IO-APIC-fasteoi   ehci_hcd:usb1, ioc0
 18:         70          0   IO-APIC-fasteoi   uhci_hcd:usb2
 19:     115143          0   IO-APIC-fasteoi   eth0
 24:          0          0   PCI-MSI-edge      pciehp
 25:          0          0   PCI-MSI-edge      pciehp
 26:          0          0   PCI-MSI-edge      pciehp
 27:          0          0   PCI-MSI-edge      pciehp
 28:          0          0   PCI-MSI-edge      pciehp
...

不管是desc->handle_irq还是__do_IRQ,它们都会调入到另外一个函数handle_IRQ_event()。重点:从CPU接收到中断信号并开始处理,到这个函数为止,都是处于中断禁止状态。为什么?很简单,因为Intel开发者手册上是这么说的,在文档325462.pdf卷3章节6.8.1 Masking Maskable Hardware Interrupts提到:

1
2
3
When an interrupt is handled through an interrupt gate, the IF flag is automati-
cally cleared, which disables maskable hardware interrupts. (If an interrupt is
handled through a trap gate, the IF flag is not cleared.)

在CPU开始处理一个硬中断到进入函数handle_IRQ_event()为止的这段时间里,因为处于中断禁止状态,所以不会出现被其它中断打断的情况。但是,在进入到函数handle_IRQ_event()后,立马有了这么两句:

1
2
3
4
5
6
7
8
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
	irqreturn_t ret, retval = IRQ_NONE;
	unsigned int status = 0;
 
	if (!(action->flags & IRQF_DISABLED))
	    local_irq_enable_in_hardirq();
...

函数local_irq_enable_in_hardirq()的定义如下:

1
2
3
4
5
#ifdef CONFIG_LOCKDEP
# define local_irq_enable_in_hardirq()  do { } while (0)
#else
# define local_irq_enable_in_hardirq()  local_irq_enable()
#endif

宏CONFIG_LOCKDEP用于表示当前是否开启内核Lockdep功能,这是一个调试功能,用于检测潜在的死锁类风险,如果开启,那么函数local_irq_enable_in_hardirq()为空,即继续保持中断禁止状态,为什么Lockdep功能需要保持中断禁止待后文再述,这里考虑一般情况,即不开启Lockdep功能,那么执行函数local_irq_enable_in_hardirq()就会开启中断。 看函数handle_IRQ_event()里的代码,如果没有带上IRQF_DISABLED旗标,那么就会执行函数local_irq_enable_in_hardirq(),从而启用中断。旗标IRQF_DISABLED可在利用函数request_irq()注册中断处理回调时设置,比如:

1
2
if (request_irq(uart->port.irq, bfin_serial_rx_int, IRQF_DISABLED,
	 "BFIN_UART_RX", uart)) {

如果没有设置,那么到函数handle_IRQ_event()这里的代码后,因为中断已经开启,当前中断的后续处理就可能被其它中断打断,从而出现中断嵌套的情况。

3,如果新来的中断类型与当前正在执行的中断类型相同,那么会暂时挂起。主要实现代码在函数__do_IRQ()(handle_fasteoi_irq()、handle_edge_irq()类似)内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
 * If the IRQ is disabled for whatever reason, we cannot
 * use the action we have.
 */
action = NULL;
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
	action = desc->action;
	status &= ~IRQ_PENDING; /* we commit to handling */
	status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;
 
/*
 * If there is no IRQ handler or it was disabled, exit early.
 * Since we set PENDING, if another processor is handling
 * a different instance of this same irq, the other processor
 * will take care of it.
 */
if (unlikely(!action))
	goto out;

逻辑很简单,如果当前中断被禁止(IRQ_DISABLED)或正在执行(IRQ_INPROGRESS),那么goto cot,所以同种类型中断不会相互嵌套。

4,从这个补丁开始,Linux内核已经全面禁止硬中断嵌套了,即从2.6.35开始,默认就是:

1
run the irq handlers with interrupts disabled.

因为这个补丁,所以旗标IRQF_DISABLED没用了,mainline内核在逐步删除它。

我仔细检查了一下,对于2.6.34以及以前的内核,如果要合入这个补丁,那么有略微影响的主要是两个慢速驱动,分别为rtc-twl4030和twl4030-usb,需要按照类似开启Lockdep功能一样:

1
2
3
4
5
6
7
#ifdef CONFIG_LOCKDEP
/* WORKAROUND for lockdep forcing IRQF_DISABLED on us, which
 * we don't want and can't tolerate.  Although it might be
 * friendlier not to borrow this thread context...
 */
local_irq_enable();
#endif

进行主动启用中断。还有另个一个慢速驱动IDE,其驱动中调用的是函数local_irq_enable_in_hardirq(),即它在开启Lockdep功能的情况下并没有明确要求启用中断,所以它应该不受补丁合入影响。嘛,我只是理论分析研究一下,仅供参考,如有风险,请实际操作者自行承担,:)。其它请看参考4,5,6。

参考:

1,Linux下386中断处理
2,Linux中断基础构架
3,linux源码entry_32.S中interrupt数组的分析
4,http://lwn.net/Articles/321663/
5,http://lwn.net/Articles/380931/
6,http://thread.gmane.org/gmane.linux.kernel/801267

转载请保留地址:http://www.lenky.info/archives/2013/03/2245http://lenky.info/?p=2245