提到soft lockup,大家都不会陌生:

BUG: soft lockup - CPU#3 stuck for 23s! [kworker/3:0:32]

这个几乎和panic,oops并列,也是非常难以排查甚至比panic更麻烦。至少panic之后你可以去分析一个静态的尸体,然而soft lockup,那是一个动态的过程,甚至转瞬即逝,自带自愈功能。

那么soft lockup是由于什么原因导致的呢?

几乎没有这方面的文章,能找到的也只有个别的案例分析,所以我想趁着周末降至来写一篇关于soft lockup的通用解释。

首先澄清两个关于soft lockup的误区:

  • soft lockup并不仅仅是由死循环引起的。
  • soft lockup并不是说在一段代码里执行了23秒,22秒。

这里简单解释一下上面的两点。

事实上,死循环并不一定会导致soft lockup,比如Linux内核生命周期内的0号进程就是一个死循环,此外很多的内核线程都是死循环。

此外,更难指望一段代码可以执行20多秒,要对现代计算机的速度有所概念。

soft lockup发生的真实场景是:

  1. soft lockup是针对单独CPU而不是整个系统的。
  2. soft lockup指的是发生的CPU上在20秒(默认)中没有发生调度切换。

第一点无须解释,下面重点看第二点。

很显然, 只要让一个CPU在20秒左右的时间内都不发生进程切换,就会触发soft lockup ,这个 “20秒内不切换” 就是soft lockup发生的根因!

Linux内核watchdog机制简介:Linux内核会为每一个CPU启动一个优先级最高的FIFO实时内核线程watchdog,我们通过ps可以看得到:[root@localhost kernel]# ps -e|grep watchdog18 ?        00:00:00 watchdog/019 ?        00:00:00 watchdog/124 ?        00:00:00 watchdog/229 ?        00:00:00 watchdog/3这些实时内核线程默认每4秒执行一次针对自己CPU的喂狗操作,同时喂狗过后会重置一个hrtimer在2倍的watchdog_thresh秒后到期,watchdog_thresh是内核参数,可调,默认为10:kernel.watchdog_thresh = 10hrtimer到期后将会打印以下令人遗憾的信息:BUG: soft lockup - CPU#3 stuck for 23s! [kworker/3:0:32]告诉人们,发生了soft lockup。具体代码参见: kernel/watchdog.c

好了,现在我们来看20秒不切换的场景。

死循环的情况
这是最简单的场景,但细节往往不像看起来那么简单。比如你写了一个死循环在内核中执行,它一定会导致soft lockup吗?
我们来看一个内核死循环:

#include <linux/module.h>
#include <linux/kthread.h>static int loop_func(void *arg)
{int i = 0;while(!kthread_should_stop()) {i++;}return 0;
}struct task_struct *kt;
static int __init init_loop(void)
{kt = kthread_run(loop_func, NULL, "loop_thread");if (IS_ERR(kt)) {return -1;}return 0;
}static void __exit exit_test(void)
{kthread_stop(kt);
}module_init(init_loop);
module_exit(exit_loop);
MODULE_LICENSE("GPL");

加载这个模块,会soft lockup吗?

我们知道,虽然loop thread是一个死循环,但是它看起来正如一个普通用户态进程一样,在执行i++循环的时候,其实是可以被其它task抢占掉的,这是最基本的进程调度的常识。

但是如果你真的去加载这个模块,你会发现在有些机器上,它确实会soft lockup,但有的机器上不会,这又是为什么?

这里的关键在于 内核抢占 。你看下自己系统内核的配置文件,如果下面的配置打开,意味着上述模块的死循环不会造成soft lockup:

CONFIG_PREEMPT=y

如果这个配置没有开,那么便 刑不上内核 了,因为它在内核态执行,所以没有谁可以抢占它,进而发生soft lockup。

关于内核抢占曾经的Linux内核是不可抢占的,只要内核态的执行流不出来,那别的进程就永远不会被调度执行。在2.6内核之后,为了提升系统的响应度,特别是实时进程的响应度,引入了内核抢占的机制。实际上所谓的内核抢占,就是在内核的一些关键点上埋下了显式的调度点,比如自旋锁释放的时候:#define preempt_check_resched() \
do { \if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \preempt_schedule(); \
} while (0)
#define preempt_enable() \
do { \preempt_enable_no_resched(); \barrier(); \preempt_check_resched(); \
} while (0)
...
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{spin_release(&lock->dep_map, 1, _RET_IP_);do_raw_spin_unlock(lock);preempt_enable();
}

在spin_unlock的时候,系统在必要的时候会抢占正在内核态的执行流。

然而,如果没有开启内核抢占,一切就跟2.6内核之前一样,内核中的执行流不可抢占。

我们对上述的死循环代码是否会触发soft lockup已经很明确了,下面我们看另一种情况。

如果死循环不在内核线程上下文,而是在软中断上下文,会怎样?

很显然,软中断不能被进程抢占,所以一定会soft lockup。

当然,如果真的发生了死循环导致的soft lockup,那肯定是在一个循环代码中执行超过20秒了,不说20秒,如果无人干涉,200000秒都是有的…

现在我们来看另一种复杂的情况,即timer的情况。在讨论timer时,我假设系统的内核抢占是开启的,这样更容易分类讨论,否则,如果关闭了内核抢占,那么事情会变得更加严重。

timer的情况
我们先看下面的timer回调函数:

static void timer_func(unsigned long data)
{mdelay(1);mod_timer(&timer, jiffies + 200);
}

仅仅执行1ms的函数,它会导致超过20秒不调度切换的soft lockup吗?

初看,应该不会,但是如果我们详细看了Linux内核timer的执行原理,就会明白:

  • pending在一个CPU上的所有过期timer是顺序遍历执行的。
  • 一轮timer的顺序遍历执行是持有自旋锁的。

这意味着在执行一轮过期timer的过程中,watchdog实时线程将无法被调度从而喂狗,这意味着:

  • 同一CPU上的过期timer积累到一定量,其回调函数的延时之和大于20秒,将会soft lockup。

我们需要进一步了解一下Linux timer的工作机制。

可以把timer的执行过程抽象成下面的逻辑:

run_timers()
{while (now > base.early_jiffies) {for_each_timer(timer, base.list) {detach_timer(timer)forward_early_jiffies(base)call_timer_fn(timer)}}
}

很简单的流程,内核把当前过期的timer执行到结束。run_timers可以在软中断上下文中执行,也可以在softirqd内核线程上下文中执行,为了营造soft lockup,我们假设它是在时钟中断退出时的软中断上下文中执行的(记住之前还有个假设,即系统是开启内核抢占的!),此时,run_timers不能被watchdog抢占。

如果一个timer中耗时1ms,那么一个循环需要20000个timer遍历执行,才能凑齐20秒的不能被抢占的时间,进而引发soft lockup。我的天,20000个timer,不可思议!

其实根本就不需要20000个timer,200个足矣!

问题就出现在call_timer_fn,它实际上是调用该timer回调函数的封装!我们知道,timer回调函数中执行了mod_timer的操作,它的逻辑如下:

mod_timer(timer, expires)
{list_add_timer(timer, expires, base.list)
}

它事实上是把timer又插回了list,如果我们把这个list看作是一条时间线的话,它事实上只是往后移了expires这么远的距离:

假设所有timer的expire都是固定的常量,如果:

  1. 我们的timer的足够多,多到按照其expires重新requeue时恰好能填补中间的那段空隙。
  2. 我们的timer回调函数耗时恰好和timer的expires流逝速率相一致。
  3. 那么,两个甚至多个batch就合并成了一个batch,这意味着一轮timer的执行将不会结束!

我们来试一下:

#include <linux/module.h>
#include <linux/slab.h>
#include <asm-generic/delay.h>static int stop = 1;// timer的数量
static int size = 1;
module_param(size, int, 0644);
MODULE_PARM_DESC(size, "size");// timer的expires
static int interval = 200;
module_param(interval, int, 0644);
MODULE_PARM_DESC(interval, "");// 回调函数耗时
static int dt = 100;
module_param(dt, int, 0644);
MODULE_PARM_DESC(dt, "");struct wrapper {struct timer_list timer;spinlock_t lock;
};struct wrapper *wr;static void timer_func(unsigned long data)
{int i = data;struct wrapper *w = &wr[i];spin_lock_bh(&(w->lock));if (stop == 0) {udelay(dt); // 以忙等模拟耗时}spin_unlock_bh(&(w->lock));w->timer.data = i;if (stop == 0) {mod_timer(&(w->timer), jiffies + interval);}
}static int __init maint_init(void)
{int i;wr = (struct wrapper *)kzalloc(size*sizeof(struct wrapper), GFP_KERNEL);for (i = 0; i < size; i++) {struct wrapper *w = &wr[i];spin_lock_init(&(w->lock));init_timer(&(w->timer));w->timer.expires = jiffies + 20;w->timer.function = timer_func;w->timer.data = i;add_timer(&(w->timer));}stop = 0;return 0;
}static void __exit maint_exit(void)
{int i;stop = 1;udelay(100);for (i = 0; i < size; i++) {struct wrapper *w = &wr[i];del_timer_sync(&(w->timer));}kfree(wr);}module_init(maint_init);
module_exit(maint_exit);
MODULE_LICENSE("GPL");

我的测试虚拟机HZ为1000,这意味1ms将会产生一次时钟中断,我们以每个timer函数持锁执行1ms,一共400个timer来加载模块,看下结果:

单核跑满,这意味着timer已经拼接成龙,20秒后,我们将看到soft lockup:

事实上,每个timer回调函数delay 800us,一共200个timer即可触发soft lockup!使用这个代码,你基本可以确定你要测试的机器的timer执行时间的安全阈值。

这就是timer导致的soft lockup的动力学。

关于HZ1000
1ms间隔的时钟中断对于服务器而言是悲哀的,1ms的时间无法容纳太多的timer,也不允许每个timer中有哪怕稍微的合理耗时,1ms一次中断很容易触发run_timers在软中断上下文中被执行,但很遗憾,这就是事实。抛开timer不谈,HZ1000更多的意义在于快速响应事件而不是增加系统吞吐,这对服务器的单机性能是有伤害的!

说了这么多,现在让我们考虑一下现实。

除了不要在内核中写死循环之外,我们也不应该让timer回调函数执行过久,特别是系统中timer特别多,且expires特别短的情况下。

回到现实中,我们来看一个实例。

假设你使用的内核版本还不支持TCP的lockless listener,那么我们特别要注意一个函数,即 inet_csk_reqsk_queue_prune

  1. 这是一个在TCP的per listener的timer中执行的函数。
  2. 这个函数的实现采用两层循环,循环耗时取决于:
  • 外层循环:该listener的backlog大小,受程序配置控制。
  • 内层循环:该listener的半连接队列的大小,受系统快照控制。

如果系统中的listener特别多,在收到SYN扫描攻击时,特别容易陷入soft lockup的深渊!幸运的是,这个问题已经在TCP lockless listener的版本中修了,它的效果如下:

将per listener的半连接队列timer换成了per request timer,减少了回调函数处理耗时。
per request timer增加了timer的数量,会不会抵消缩短回调耗时带来的收益,需要攻击来验证。
我们看一个相关issue和patch:

https://patchwork.ozlabs.org/patch/452426/

好了,再次回到核心主题。

触发soft lockup的当然不止死循环和timer,我只是用这两个来说明soft lockup的动力学,即 超过2倍的kernel.watchdog_thresh时间不能进行进程调度,就会触发soft lockup告警。 至于说 stuck for 23s! 那只是表象,并不是如其字面表达的那样,23秒的时间在执行一段代码。

此外,频繁的spinlock,rwlock也会导致soft lockup,我这有一个关于IPv6路由查询机制的实例,详情参见:

https://blog.csdn.net/dog250/article/details/91046131

总之,所有的情况将不胜枚举,也不可能通过一篇文章来展示,所以说,遇到此类问题,还是要有一个明确的排查思路或者说范式,才能快速定位问题的根因并且解决之。

当然了,经理并不关注这些烂八七糟的东西。

Linux内核为什么会发生soft lockup?相关推荐

  1. 安装linux系统报softlock,soft lockup 解决思路

    一. 前言 前几天,帮同事一起查一个机器老是挂死无法进入问题,说有一台虚拟机时不时登陆不上挂死,同时甲方竟然没有这些主机监控,判断不了当时的cpu,内存,网络等的基础数据信息,那就只能看看内核信息了. ...

  2. linux内核中watchdog、lockup、stall、hung等检测

    目录 lockup 一.watchdog看门狗 二.soft /hard  lockup 1.soft lockup 2.hard lockup 三.kernel's hung task 四.work ...

  3. Linux 在 soft lockup 时,可以远程调试吗?

    [CSDN 编者按]事件陷入死地无可挽救之际,可能会有人选择不了了之,有人选择就此放弃--但换个思路想一想,既然都无可挽回了,那干嘛不试试弄点有价值的信息回来? 作者 | dog250  责编 | 张 ...

  4. 当运行 Linux 内核的机器死机时...

    [CSDN 编者按]事件陷入死地无可挽救之际,可能会有人选择不了了之,有人选择就此放弃--但换个思路想一想,既然都无可挽回了,那干嘛不试试弄点有价值的信息回来? 作者 | dog250  责编 | 张 ...

  5. Linux 内核软死锁(soft lockup)记录

    问题背景 在特定服务器资源中运行高负载程序 造成NMI watchdog: BUG: soft lockup - CPU#4 stuck for 24s 名词解释 Soft lockup名称解释:所谓 ...

  6. 请记住内核中这个勤劳的监测卫士---Watchdog(Soft lockup篇)

    ​更多内核安全.eBPF分析和实践文章,请关注博客和公众号: CSDN博客:内核功守道 公众号: 内核功守道 背景 从内核稳定性问题的角度来看内核安全,是基础,也是必备技能.很多时候,一个内核稳定性问 ...

  7. NMI watchdog: BUG: soft lockup - CPU#2 stuck for 23s!

    <NMI watchdog: BUG: soft lockup> <kernel:NMI watchdog: BUG: soft lockup - CPU#6 stuck for 2 ...

  8. Linux soft lockup分析

    关键词:watchdog.soft lockup.percpu thread.lockdep等. 近日遇到一个soft lockup问题,打印类似"[ 56.032356] NMI watc ...

  9. linux服务器关不了机,解决Linux关不了机开机,报错NMI watchdog: BUG: soft lockup - CPU#2 stuck for 22s的bug...

    问题描述 在安装完Ubuntu或者其他Linux, 关机时会卡死, 循环报错NMI watchdog: BUG: soft lockup - CPU#2 stuck for 22s!, 无法关机. 在 ...

  10. Linux系列之soft lockup机制 浅析

    Linux系列之soft lockup机制 浅析 1.背景 2.什么是lockup? 2.1 lockup检测机制 2.2 softlockup的工作原理 3.soft lockup机制分析 3.1 ...

最新文章

  1. 【SM2证书】利用BC的X509v3CertificateBuilder组装X509国密证书
  2. JavaWeb 使用nginx负载均衡
  3. Composer的简单安装与使用
  4. (1)搞一搞 seata 之 基础环境搭建
  5. HTTPS中SSL协议总结
  6. 在shop++二次开发中金额数据类型BigDecimal转换,注意细节。
  7. java this关键字的使用_做java两年了,构造方法和方法重载还是搞不明白?一文帮你搞定...
  8. Docker入坑指南之EXEC
  9. Linux多线程同步之相互排斥量和条件变量
  10. linux系统操作mysql数据库_利用workbench对linux/Ubuntu系统中的mysql数据库进行操作
  11. AJAX请求中payload和formdata两种方式
  12. 用vue做一个app
  13. 个人计算机组装主板,电脑主板安装详细图解 可以自己组装电脑了
  14. python语言转换为go_从 Python 到 Golang-Go语言中文社区
  15. boost电路输出电流公式_​boost电路工作原理、参数计算、占空比
  16. 百度经纬度与高德经纬度互转
  17. sharepoint文件夹本地同步_DIY游戏云存档 - 单机游戏存档多机异地同步方案
  18. python爬虫获取下一页_Python爬虫怎么获取下一页的URL和网页内容?
  19. JavaSE + bluecove 蓝牙连接
  20. 阿里,京东,蚂蚁面试题

热门文章

  1. JavaScript入门详解
  2. 1028: [JSOI2007]麻将 - BZOJ
  3. Android 4.0.4模拟器安装完全教程(图文)
  4. mysql 密码保存格式_mysql5.6使用老格式密码
  5. flume学习(六):如何使用event header中的key值
  6. python、MongoDB安装
  7. hibernate 框架学习笔记
  8. uva 10330(最大流)
  9. 解决 No utmpx entry. You must exec login from the lowest level shell.
  10. 考前必练15道题_《系统集成项目管理工程师备考宝典》