Linux 内核调试篇7

Linux 内核调试篇7(基于Linux6.6)---内核卡死分析

一、概述

内核卡死(Kernel Hang)是指操作系统的内核无法继续执行,系统处于一种不可响应的状态,通常会导致整个计算机系统无法响应用户输入,无法执行任何进程,甚至无法进行正常的系统操作。这种情况常常与内核或硬件的严重问题有关,通常需要通过重启系统来恢复。

内核卡死的原因可以很多,通常涉及硬件故障、驱动程序问题、内核漏洞或资源的竞争等。不同于内核崩溃(Oops)或内核恐慌(Panic),卡死通常指的是内核没有完全崩溃,但因为某些原因导致系统无法继续正常工作。

1、内核卡死的表现

  1. 完全无响应:系统完全没有响应用户输入或任何外部命令。
  2. CPU 占用率异常:卡死时,系统可能出现 CPU 占用过高的情况,特别是处于等待状态的进程或内核线程。
  3. 无法进行进程调度:用户进程、内核线程都无法执行,通常表现为系统无法启动新的进程,也无法关闭现有进程。
  4. 无法登录:系统无法响应登录请求,无法进入命令行或图形界面。
  5. 设备不可访问:硬盘、网络接口等硬件可能无法响应,导致系统无法进行磁盘读写或网络操作。

2、常见的原因

  1. 死锁(Deadlock): 死锁是内核中的多个线程或进程因相互等待资源而无法继续执行,导致系统无法做出响应。死锁通常发生在内核和驱动程序中的资源管理不当,尤其是在多线程或多进程环境下。

  2. 硬件故障

    • 内存故障:损坏的内存条、内存泄漏、内存映射错误等,可能导致内核无法正常访问内存或执行指令,进而卡死。
    • 硬盘问题:硬盘故障(如坏道、硬盘控制器问题等)可能导致 I/O 操作长时间卡住,进而引起系统无响应。
    • CPU 故障:如果 CPU 发生硬件故障或者超频不稳定,也可能导致系统无法继续执行。
  3. 驱动程序或内核模块问题: 驱动程序或内核模块的错误、死锁或资源竞争可能导致内核卡死。例如,设备驱动程序未能处理完 I/O 请求,导致系统等待永久锁定。

  4. 系统资源耗尽

    • 进程表溢出:如果系统中有大量进程被创建,而系统的进程表没有得到及时清理,可能导致进程管理崩溃,从而卡死。
    • 内存不足:当系统内存耗尽,内存管理子系统无法正常工作时,可能会导致系统卡死。尤其是内存的交换(swap)操作发生问题时,可能会导致长时间的等待。
  5. 内核中的 Bug 或设计缺陷

    • 内核中未被及时发现的 Bug 也可能导致卡死,例如死循环、资源释放不当、错误的同步操作等。
    • 内核中的设计缺陷,尤其是多核处理器或 SMP(对称多处理)系统中的竞态条件,可能会导致部分进程或线程无法执行。
  6. I/O 阻塞: 当内核或某个设备的 I/O 操作发生阻塞时(比如等待磁盘、网络接口或外设响应),如果没有适当的超时处理,可能导致系统卡住。

3、卡死的调试方法

调试内核卡死的问题非常具有挑战性,因为卡死时系统无法响应外部命令。以下是一些常见的调试方法:

  1. 使用 Kernel Crash Dump(内核崩溃转储): 配置 kdumpcrash 工具,可以在系统卡死或崩溃时收集内存转储,并将转储信息用于分析。通过分析转储数据,您可以查看卡死时的内存状态、调用堆栈等,从而找出问题的根源。

  2. 查看日志文件

    • 系统日志(如 /var/log/messagesdmesg)可以提供一些线索,尤其是在卡死前是否有硬件错误或驱动程序报错。
    • 如果是由于某个设备或驱动程序引起的卡死,日志中可能会包含相应的错误信息。
  3. 通过调试器(如 KGDB 或 GDB)调试: 如果系统支持内核调试,可以使用 KGDB(Kernel GNU Debugger)等调试工具,通过串口或网络与内核进行调试,实时查看内核执行过程,并捕获发生卡死时的状态。

  4. 配置内核调试选项: 在内核编译时启用 CONFIG_DEBUG_KERNELCONFIG_PROFILING 等调试选项,可以生成更详细的内核运行时日志,帮助查找卡死原因。

  5. 检查硬件

    • 使用内存诊断工具(如 Memtest86)检查内存是否有故障。
    • 检查硬盘健康状况,使用 SMART 工具检查硬盘的健康状态。
    • 如果有可能,测试替换硬件以排除硬件故障的影响。
  6. 减少系统负载: 如果内核卡死与系统负载有关,可以通过调整系统配置、禁用一些不必要的服务或减少并发访问来减轻系统负载,观察卡死是否依然发生。

二、实例分析

内核卡死有很多种可能,

  • 驱动程序因为逻辑问题,出现死循环
  • 共享资源出现死锁
  • 系统跑飞等

一般情况下,系统跑飞内核会打印Oops信息。

有了Oops信息,我们就可以通过上一节的方法来推断出出错位置。

而死锁或者驱动程序死循环并不会打印Oops信息。

这个时候需要自己打印出来,出错位置的寄存器( r0 ~ r15等)信息来反推出出错位置。

因为这个时候卡住的位置基本已经是死掉了,所以只能采用别的方式来打印出内核的寄存器信息或者oops信息。

那这个时候什么东西是还在正常运行的呢?

中断!

因为中断是是突发的,不受到其它驱动和应用程序的影响。

而且,如果某个程序执行死循环时,中断发生后,也会把这个死循环的程序的寄存器进行存储的。

无论是执行传统的中断函数

 .L__vectors_start:
	W(b)	vector_rst
	W(b)	vector_und
	W(ldr)	pc, .L__vectors_start + 0x1000
	W(b)	vector_pabt
	W(b)	vector_dabt
	W(b)	vector_addrexcptn
	W(b)	vector_irq        @----->>>>>>>>
	W(b)	vector_fiq
	vector_stub	irq, IRQ_MODE, 4
	.long	__irq_usr			@  0  (USR_26 / USR_32)    ------->
	.long	__irq_invalid			@  1  (FIQ_26 / FIQ_32)
	.long	__irq_invalid			@  2  (IRQ_26 / IRQ_32)
	.long	__irq_svc			@  3  (SVC_26 / SVC_32)   -------->
	.long	__irq_invalid			@  4
	.long	__irq_invalid			@  5
	.long	__irq_invalid			@  6
	.long	__irq_invalid			@  7
	.long	__irq_invalid			@  8
	.long	__irq_invalid			@  9
	.long	__irq_invalid			@  a
	.long	__irq_invalid			@  b
	.long	__irq_invalid			@  c
	.long	__irq_invalid			@  d
	.long	__irq_invalid			@  e
	.long	__irq_invalid			@  f
	.align	5
__irq_svc:
	svc_entry
	irq_handler            @-------->>>>>>>>>
#ifdef CONFIG_PREEMPT
	ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
	ldr	r0, [tsk, #TI_FLAGS]		@ get flags
	teq	r8, #0				@ if preempt count != 0
	movne	r0, #0				@ force flags to 0
	tst	r0, #_TIF_NEED_RESCHED
	blne	svc_preempt
#endif
 
	svc_exit r5, irq = 1			@ return from exception
 UNWIND(.fnend		)
ENDPROC(__irq_svc)
 
 
 
 
 
 
/*
 * Interrupt handling.
 */
	.macro	irq_handler
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
	ldr	r1, =handle_arch_irq                @新的中断方式,分发中断
	mov	r0, sp
	badr	lr, 9997f
	ldr	pc, [r1]
#else
	arch_irq_handler_default                @老的传统中断方式
#endif
9997:
	.endm

先看老的传统中断方式

 /*
 * Interrupt handling.  Preserves r7, r8, r9
 */
	.macro	arch_irq_handler_default
	get_irqnr_preamble r6, lr
1:	get_irqnr_and_base r0, r2, r6, lr
	movne	r1, sp
	@
	@ routine called with r0 = irq number, r1 = struct pt_regs *
	@
	badrne	lr, 1b
	bne	asm_do_IRQ
 
 
 
asmlinkage void asm_do_IRQ(struct pt_regs *regs)
{
	irq_hw_number_t hwirq = get_intr_src();
	handle_domain_irq(root_domain, hwirq, regs);
}

接下来看新的中断处理

kernel/irq/handle.c 

 /*
 * 定义了函数指针
 */
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
void (*handle_arch_irq)(struct pt_regs *) __ro_after_init;
#endif
 
 
/* 
 * 运行期间可以更改这个指针,来改变中断处理函数
 */
 
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
	if (handle_arch_irq)
		return -EBUSY;
 
	handle_arch_irq = handle_irq;
	return 0;
}
#endif

下面是新的几种中断的处理当时,包括vic,gic等

drivers/irqchip/irq-vic.c 

 /*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;
 
	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

drivers/irqchip/irq-gic.c

  
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;
 
	do {
		irqnr = gic_read_iar();
 
		if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
			int err;
 
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_eoir(irqnr);
			else
				isb();
 
			err = handle_domain_irq(gic_data.domain, irqnr, regs);
			if (err) {
				WARN_ONCE(true, "Unexpected interrupt received!\n");
				if (static_branch_likely(&supports_deactivate_key)) {
					if (irqnr < 8192)
						gic_write_dir(irqnr);
				} else {
					gic_write_eoir(irqnr);
				}
			}
			continue;
		}
		if (irqnr < 16) {
			gic_write_eoir(irqnr);
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_dir(irqnr);
#ifdef CONFIG_SMP
			/*
			 * Unlike GICv2, we don't need an smp_rmb() here.
			 * The control dependency from gic_read_iar to
			 * the ISB in gic_write_eoir is enough to ensure
			 * that any shared data read by handle_IPI will
			 * be read after the ACK.
			 */
			handle_IPI(irqnr, regs);
#else
			WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
			continue;
		}
	} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}

drivers/irqchip/irq-gic.c

  
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;
 
	do {
		irqnr = gic_read_iar();
 
		if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {
			int err;
 
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_eoir(irqnr);
			else
				isb();
 
			err = handle_domain_irq(gic_data.domain, irqnr, regs);
			if (err) {
				WARN_ONCE(true, "Unexpected interrupt received!\n");
				if (static_branch_likely(&supports_deactivate_key)) {
					if (irqnr < 8192)
						gic_write_dir(irqnr);
				} else {
					gic_write_eoir(irqnr);
				}
			}
			continue;
		}
		if (irqnr < 16) {
			gic_write_eoir(irqnr);
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_dir(irqnr);
#ifdef CONFIG_SMP
			/*
			 * Unlike GICv2, we don't need an smp_rmb() here.
			 * The control dependency from gic_read_iar to
			 * the ISB in gic_write_eoir is enough to ensure
			 * that any shared data read by handle_IPI will
			 * be read after the ACK.
			 */
			handle_IPI(irqnr, regs);
#else
			WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
			continue;
		}
	} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}

上面看到一个共同点,就是这几个c函数的参数是一样的。

这里看一下他的定义,可以看到18个long,存放的也是中断调转前的那个程序的18个寄存器。

 struct pt_regs {
	unsigned long uregs[18];
};

这里看一下这十八个寄存分别是放的那个寄存器。

 arch/arm/include/uapi/asm/ptrace.h

#define ARM_cpsr	uregs[16]
#define ARM_pc		uregs[15]
#define ARM_lr		uregs[14]
#define ARM_sp		uregs[13]
#define ARM_ip		uregs[12]
#define ARM_fp		uregs[11]
#define ARM_r10		uregs[10]
#define ARM_r9		uregs[9]
#define ARM_r8		uregs[8]
#define ARM_r7		uregs[7]
#define ARM_r6		uregs[6]
#define ARM_r5		uregs[5]
#define ARM_r4		uregs[4]
#define ARM_r3		uregs[3]
#define ARM_r2		uregs[2]
#define ARM_r1		uregs[1]
#define ARM_r0		uregs[0]
#define ARM_ORIG_r0	uregs[17]

如果向要看pc的值,只需要在内核中C中断处理函数的入口添加打印即可。

比如vic:

 /*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;
 
        printk(KERN_ERR"vic_handle_irq entry pt_regs->pc = %x\n ",regs->ARM_pc);
 
	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

上面这个是任何时候触发中断都会打印。一般不是想要的。

而且像内核心跳定时器是以ms级别都在触发的,不可能让不同的打印。

这个时候就要根据卡死时的现象来决定处理打印,中断执行前的pc。

比如假设,5s时间当前进程都没有进行切换,那就证明卡死,需要打印pc值(或者其他值)。

 /*
 * Keep iterating over all registered VIC's until there are no pending
 * interrupts.
 */
static void __exception_irq_entry vic_handle_irq(struct pt_regs *regs)
{
	int i, handled;
 
    static pid_t old_pid = 0;
    static unsigned int cnt = 0;
 
 
    /* 进程不一样,表示没卡死 */
    if(current->pid != old_pid )
    {
        old_pid  = current->pid;
        cnt  = 0;
    }
    else
    {
        /* 进程一样,且连续5s进程都一样,表示卡死,这是打印出我们需要的进中断前的寄存器信息 */
        if(cnt  >= 5* HZ)
        {
             printk(KERN_ERR"vic_handle_irq entry pt_regs->pc = %x\n ",regs->ARM_pc);
             cnt = 0;  
        }  
    }
  
 
	do {
		for (i = 0, handled = 0; i < vic_id; ++i)
			handled |= handle_one_vic(&vic_devices[i], regs);
	} while (handled);
}

打印出来后,根据pc值,知道对应位置的代码,进而找打卡死的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值