softlockup与hardlockup

  1. 背景
  2. 相关参数
  3. softlockup
  4. hardlockup
  5. 后记

背景

 经常会遇到内核出现softlockup或者hardlockup等相关的栈导致系统不可用或宕机行为,有必要整理一下这块的内容

相关参数

 先来看一下内核中控制这块的相关参数

  • congfig参数

 softlockup相关:

CONFIG_SOFTLOCKUP_DETECTOR=y				//是否启用softlockup
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC=y			//是否允许内核命令行参数配置softlockup时触发panic的功能
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC_VALUE=1		//默认触发panic的功能,1表示softlockup时触发系统panic;;这个默认值可以通过下面的softlockup_panic sysctl覆盖掉

 hardlockup相关:

CONFIG_HAVE_HARDLOCKUP_DETECTOR_PERF=y			//是否支持PMU硬件检测hardlockup
CONFIG_HARDLOCKUP_DETECTOR_PERF=y			//启用perf检测hardlockup
CONFIG_HARDLOCKUP_CHECK_TIMESTAMP=y			//启用时间戳检测hardlockup(这就是软件层面了)
CONFIG_HARDLOCKUP_DETECTOR=y				//这几条同softlockup
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC=y
CONFIG_BOOTPARAM_HARDLOCKUP_PANIC_VALUE=1

 其他:

CONFIG_TEST_LOCKUP  		//可以用来测试softlockup,打开这个config应该是能m编译一个内核模块出来,modprobe这个模块可以触发lockup测试
  • 内核参数
/proc/sys/kernel/watchdog_thresh			//lockup的时间阈值,超过这个设定的时间的2被还没有调度则表示lockup了
/proc/sys/kernel/softlockup_panic			//检测到softlockup后是否触发panic
/proc/sys/kernel/hardlockup_panic			//检测到hardlockup后是否触发panic
/proc/sys/kernel/nmi_watchdog				//是否开启nmi_watchdog,开启后可以使用nmi检测lockup

softlockup

  • 原理:

 简单版:指内核在内核态下循环超过XX秒,不给其他进程运行的机会。其实就是内核会定时检测内核是否还能调度,如果超过一段时间不能调度,那就认为softlockup了。

 稍微复杂版:如果配置了CONFIG_SOFTLOCKUP_DETECTOR,那么内核在启动时就会起一个hrtimer定时器。这个定时器每隔watchdog_thresh(就是上面的内核参数)就唤醒一次,作用有两个:一,唤醒percpu的任务去更新时间戳;二,检测是否发生了lockup。所以,如果有一种场景,比如,一个线程进入内核态自旋等锁,不释放cpu,hrtimer定时器尝试向每个cpu发stop_cpu任务去更新时间戳。但是因为内核一直在内核态自旋没办法调度到更新时间戳的任务去,于是就不能更新时间戳,当超过watchdog_thresh*2时间还没有更新时间戳时,hrtimer定时器被唤醒就会判断这时候发生了softlockup,如果这时候还配置了softlockup_panic,系统就会触发panic了。

 下面看看过程:

//可以看到kernel初始化阶段,在run_init_process之前
//这里还要disable_sdei_nmi_watchdog才能执行下去?这是arm的什么东西嘛?
kernel_init_freeable
lockup_detector_init

void __init lockup_detector_init(void)
{
    //如果有开启nohz_full的cpu就跳过
    if (tick_nohz_full_enabled())
        pr_info("Disabling watchdog on nohz_full cores by default\n");
    //然后获取非isolated的cpu,注意这里的housekeeping_cpumask返回的是非isolated的cpu mask
    //但是这个type不清楚什么意思,需要看看
    cpumask_copy(&watchdog_cpumask,
                 housekeeping_cpumask(HK_TYPE_TIMER));
    //判断nmi是否可用
    //一般可用且一般是用nmi的
    if (!watchdog_nmi_probe())
        nmi_watchdog_available = true;
    lockup_detector_setup();
    //这是去初始化sysctl参数,也就是上面提到的相关的内核参数
    watchdog_sysctl_init();
}

//基本上就是加mutex然后执行__lockup_detector_reconfigure去
lockup_detector_setup
__lockup_detector_reconfigure

//先只看softlockup
softlockup_start_all

static void softlockup_start_all(void)
{
    int cpu;

    cpumask_copy(&watchdog_allowed_mask, &watchdog_cpumask);
    //给每个cpu发一个softlockup_start_fn的ipi
    for_each_cpu(cpu, &watchdog_allowed_mask)
        smp_call_on_cpu(cpu, softlockup_start_fn, NULL, false);
}

static int softlockup_start_fn(void *data)
{
    watchdog_enable(smp_processor_id());
    return 0;
}

//watchdog_enable里主要就是初始化和设置hrtimer,执行的是watchdog_timer_fn函数
//设置完hrtimer并启动之后,每个cpu上就会定时产生hrtimer中断然后去执行watchdog_timer_fn函数了
//这里还会执行到watchdog_hardlockup_enable?按理来说softlockup和hardlockup不应该耦合吧?
static void watchdog_enable(unsigned int cpu)
{
	struct hrtimer *hrtimer = this_cpu_ptr(&watchdog_hrtimer);
	struct completion *done = this_cpu_ptr(&softlockup_completion);

	WARN_ON_ONCE(cpu != smp_processor_id());

	init_completion(done);
	complete(done);

	hrtimer_init(hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
	hrtimer->function = watchdog_timer_fn;
	hrtimer_start(hrtimer, ns_to_ktime(sample_period),
		      HRTIMER_MODE_REL_PINNED_HARD);

	/* Initialize timestamp */
	update_touch_ts();
	/* Enable the perf event */
	if (watchdog_enabled & NMI_WATCHDOG_ENABLED)
		watchdog_nmi_enable(cpu);
}

//这里负责设置时间戳并检查,如果是softlockup了,就触发panic了
//看一下主要流程
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
    //如果watchdog被关闭了,就不再执行定时器函数了
	if (!watchdog_enabled)
		return HRTIMER_NORESTART;

	/* kick the hardlockup detector */
    //hardlockup相关的,看hardlockup的时候再说
	watchdog_interrupt_count();

	/* kick the softlockup detector */
    //这块就是去要执行一个stop_cpu任务,这个任务就是softlockup_fn
    //softlockup_fn的主要作用就是更新时间戳
	if (completion_done(this_cpu_ptr(&softlockup_completion))) {
		reinit_completion(this_cpu_ptr(&softlockup_completion));
		stop_one_cpu_nowait(smp_processor_id(),
				softlockup_fn, NULL,
				this_cpu_ptr(&softlockup_stop_work));
	}

	/* .. and repeat */
    //hrtimer相关,推进到期时间
	hrtimer_forward_now(hrtimer, ns_to_ktime(sample_period));

    //一些获取时间戳操作

    //判断是否softlockup了
    //其实就是判断时间差是否超过了watchdog_thresh * 2
	duration = is_softlockup(touch_ts, period_ts, now);
	if (unlikely(duration)) {
        //如果发生了softlockup了
        //就进行后续处理,比如dump stack之类
		pr_emerg("BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
			smp_processor_id(), duration,
			current->comm, task_pid_nr(current));
		print_modules();
		print_irqtrace_events(current);
		if (regs)
			show_regs(regs);
		else
			dump_stack();

		add_taint(TAINT_SOFTLOCKUP, LOCKDEP_STILL_OK);
		if (softlockup_panic)
            //如果设置了softlockup_panic,则触发panic
			panic("softlockup: hung tasks");
	}

	return HRTIMER_RESTART;
}

之前看一些资料的时候,说的是通过hrtimer通过唤醒内核线程来更新时间戳,就是那些[watchdog/0]之类的内核线程,怎么分析完没有看到内核线程的事呢?

原来是现在不用内核线程了,也就是新的内核里已经看不到watchdog/XXX了,在18年的一个pacth里已经被cpu_stop_work代替了

pantch: watchdog/softlockup: Replace “watchdog/%u” threads with cpu_stop_work

这样子有什么好处呢?减少了per cpu线程?避免了softlockup的检测和SCHED_DEADLINE的耦合??

hardlockup

  • 原理

 简单版:指内核在内核态下循环超过XXX秒,而不会让其他中断有执行机会。类似softlockup的判定,但是这个更为激进,这个不只是内核不能调度了,甚至连中断都不能响应了。

 稍微复杂版:内核每隔watchdog_thresh时间就发一个NMI perf事件,去检测hrtimer的中断数是否有增加,如果没有增加的话那就说明内核没有响应hrtimer的中断,那就判断为hardlockup了(因为NMI是不可屏蔽的,和SMI那些一样)(另外,这是不是说明hardlockup的检测是依赖softlockup的检测的??因为要注册hrtimer才可以?

lockup_detector_init
lockup_detector_setup
__lockup_detector_reconfigure
softlockup_start_all
watchdog_enable
//上面这些路径和softlockup是一样的

static void watchdog_enable(unsigned int cpu)
{
  //如果开启了watchdog且开启了NMI_WATCHDOG_ENABLED就调用
  //所以看上去是不是如果sysctl中把NMI_WATCHDOG_ENABLED给禁掉,那hardlockup的检测也就停了?
	if (watchdog_enabled & NMI_WATCHDOG_ENABLED)
		watchdog_nmi_enable(cpu);
}

//这个其实是弱实现,可以被架构的实现覆盖
watchdog_nmi_enable
hardlockup_detector_perf_enable
hardlockup_detector_event_create

static int hardlockup_detector_event_create(void)
{

	WARN_ON(!is_percpu_thread());
	cpu = raw_smp_processor_id();
	wd_attr = &wd_hw_attr;
  //获取sample_period
	wd_attr->sample_period = hw_nmi_get_sample_period(watchdog_thresh);
  //注册一个硬件的事件performance monitoring,这个硬件有个功能就是在cpu clock经过多少周期之后发一个nmi中断出来
  //中断的handler就是watchdog_overflow_callback函数
	/* Try to register using hardware perf events */
	evt = perf_event_create_kernel_counter(wd_attr, cpu, NULL,
					       watchdog_overflow_callback, NULL);
}


static void watchdog_overflow_callback(struct perf_event *event,
				       struct perf_sample_data *data,
				       struct pt_regs *regs)
{
//这里就用is_hardlockup检查如果是hardlockup了就产生nmi_panic
//检查的是什么呢?就是percpu的hrtimer的中断数是否在增长
//什么时候增长呢?唉?就是之前watchdog_timer_fn中调用的watchdog_interrupt_count
//所以这里差不多可以确定,hardlockup的检测依赖softlockup。为什么呢?因为hardlockup是检测系统能不能响应中断
//那判断什么中断呢?自然就是softlockup的hrtimer产生的中断最妙
	if (is_hardlockup()) {
		int this_cpu = smp_processor_id();
    
    //同样,判断为hardlockup了就做后续工作,打印栈等
		pr_emerg("Watchdog detected hard LOCKUP on cpu %d\n",
			 this_cpu);
		print_modules();
		print_irqtrace_events(current);
		if (regs)
			show_regs(regs);
		else
			dump_stack();

    //触发nmi中断
		if (hardlockup_panic)
			nmi_panic(regs, "Hard LOCKUP");

		__this_cpu_write(hard_watchdog_warn, true);
		return;
	}

	__this_cpu_write(hard_watchdog_warn, false);
	return;
}




//噢,果然,hrtimer中断计数的方式是依赖softlockup的,可以看这个kconfig
//哈哈哈,懂了,原来就是这样,perf和buddy的hardlockup就是要依赖softlockup的,而且目前也就这两种了
config HARDLOCKUP_DETECTOR_COUNTS_HRTIMER
	bool
	select SOFTLOCKUP_DETECTOR

config HARDLOCKUP_DETECTOR_PERF
	bool
	depends on HARDLOCKUP_DETECTOR
	depends on HAVE_HARDLOCKUP_DETECTOR_PERF && !HARDLOCKUP_DETECTOR_PREFER_BUDDY
	depends on !HAVE_HARDLOCKUP_DETECTOR_ARCH
	select HARDLOCKUP_DETECTOR_COUNTS_HRTIMER
  
//5.10内核是这样
config HARDLOCKUP_DETECTOR_PERF
	bool
	select SOFTLOCKUP_DETECTOR

后记

  • 一般这些问题不会单独出现,可能softlockup hardlockup rcu_stall hungtask oom等,某个问题会导致一连串的连锁反应,这时候一般是要找第一个出现异常的栈。

 比如:大量oom导致softlockup,https://lore.kernel.org/lkml/Z2ZuDTYu3PwV1JmT@tiehlicka/T/

  • 怎么排查这种问题呢?

 一般如果配置了panic的话肯定就有vmcore了,先简单分析一下vmcore看一下有没有等锁、io、等资源等问题导致的

 如果一眼看上去没什么问题,还可以看看ipi队列上是不是挂着任务很多任务,比如ipi风暴(好名字,这本来是我下班骑电动车回家路上想到的名字,回来一搜发现本来就有哈哈哈)导致的softlockup,这个后面写一下这个案例

  • 根据之前的代码,设置nohz_full的cpu和isolated cpu上是不会检测lockup的

 为什么呢?一个是设置nohz_full的cpu就会尽量减少不必要的中断,所以就不再这些cpu上检测lockup了?另一个是设置了isolated cpu上不希望有一些别的任务,所以也就不去检测lockup了

  • HK_TYPE_TIMER什么意思?

 chatgpt回答如下:

 在 Linux 内核中,enum hk_type 枚举定义了一些用于指定不同类型的“housekeeping”(维护)任务的标识符。这些类型用于描述哪些任务应该在系统的某些 CPU 上运行,尤其是在某些 CPU 被隔离或配置为减少干扰(例如,通过 isolatednohz_full 参数)时。以下是每个枚举值的解释:

 HK_TYPE_TIMER:与定时器相关的维护任务。定时器中断是用于在特定时间点执行某些任务的机制,通常用于计划任务或触发周期性操作。

 HK_TYPE_RCU:与 RCU(Read-Copy-Update)机制相关的任务。RCU 是一种同步机制,允许读取操作并发执行,同时更新操作被推迟,以确保数据一致性。

 HK_TYPE_MIS:杂项(miscellaneous)任务,涵盖其他未具体分类的维护任务。这些任务可能包括系统中各种需要的背景活动。

 HK_TYPE_SCHED:与调度器相关的任务。调度器负责管理 CPU 上的任务执行顺序,包括任务切换和负载均衡。

 HK_TYPE_TICK:与时钟滴答(tick)相关的任务。时钟滴答是周期性中断,用于维护系统时间和调度周期性任务。

 HK_TYPE_DOMAIN:与调度域(scheduling domain)相关的任务,这可能涉及跨 CPU 的负载均衡和调度策略。

 HK_TYPE_WQ:与工作队列(workqueue)相关的任务。工作队列是内核用于延迟执行任务的机制,通常用于将工作从中断上下文移至进程上下文。

 HK_TYPE_MANAGED_IRQ:与管理的中断(managed IRQ)相关的任务。管理的中断是指那些被细致管理以降低中断处理对系统性能影响的中断。

 HK_TYPE_KTHREAD:与内核线程(kernel thread)相关的任务。内核线程是在内核模式下运行的轻量级线程,用于执行各种内核任务。

 HK_TYPE_MAX:这个标识符通常用于定义枚举的大小或边界。在代码实现中,它用于确定类型的总数,并通常作为数组大小或循环的终止条件。

todo:

 stop_cpu这块需要看一下


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 857879363@qq.com