概述
更多内核安全、eBPF分析和实践文章,请关注博客和公众号:
CSDN博客:内核功守道
公众号: 内核功守道
背景
从内核稳定性问题的角度来看内核安全,是基础,也是必备技能。很多时候,一个内核稳定性问题,就是造成系统安全的罪魁祸首。
比如一个简单的内存泄漏问题,前期可能会因为内核泄漏,剩余内存不足导致系统卡顿,此时工程师查找的是Performance性能问题,接下来当系统不能及时回收内存,或者阈值设置不合理,一些进程会被Kill掉,甚至极端情况下,系统会oops或者panic,此时系统展现出来的是Stability稳定性问题。然后如果这个问题触发点很隐蔽,程序发布之前都没有触发而被发现,那后续被黑客利用起来做进一步攻击行为,造成安全漏洞和经济损失,这就是最严重的Security安全问题。
当出现异常死锁、Hang up、死机等问题时,watchdog的作用就很好的体现出来。Watchdog主要用于监测系统运行情况,一旦出现以上异常情况,就会重启系统,并收集crash dump(程序崩溃时保存的运行数据)。
工作流程
Watchdog工作原理:
- 假定某一变量的状态能表征系统运行状态,比如中断次数(如果高优先级中断没有发生,就认为CPU卡死),比如/dev/watchdog时间戳(如果超时时间到了仍没有向watchdog节点写数据,就认为用户空间卡死)。
- 启动一个watchdog程序,定期观测该变量,来判定系统是否正常,并采取相应动作。
- 内核态watchdog主要用于检测内核Lockup。所谓的Lockup,是指某段内核代码一直占着CPU不放,此时内核调度器无法进行调度工作。进一步严重情况下,会导致整个系统卡死。
- Lockup涉及到内核线程、时钟中断。它们有不一样的优先级:内核线程 > 时钟中断 。其中,内核线程可以被调度或被中断打断。
详解Lockup
- 只有内核代码才能引起lockup,因为用户代码是可以被抢占的,只有一种情况例外,就是SCHED_FIFO( 一直运行,直到进程运行完毕才会释放CPU)优先级为99的实时进程。当它被阻塞或被更高优先级进程抢占时,也可能使[watchdog/x]内核线程抢不到CPU而形成soft lockup。
- 内核代码必须处于禁止内核抢占的状态(preemption disabled),Linux是可抢占的,只在某些特定的代码区才禁止抢占(例如spinlock),才可能形成lockup。
Lockup分为两种:soft lockup 和 hard lockup:
- Soft lockup在CPU无法正常调度其他线程时发生,即某段代码一直占用某个CPU,导致watchdog/x内核线程得不到调度,此时中断仍可响应;
- Hard lockup在中断无法正常响应时发生,即关中断时间过长或中断处理程序执行时间过长。
Soft lockup详解:
在驱动中加入以下代码可触发soft lockup,通过spinlock()实现关抢占,使得该CPU上的[watchdog/x]线程无法被调度。
static spinlock_t spinlock;
spin_lock_init(&spinlock);
spin_lock(&spinlock);
while(1);
- 首先给每个CPU开启一个定时(每隔4s执行一次)执行的优先级最高(prio为99)的SCHED_FIFO线程,拥有优先运行的特权)内核线程[watchdog/x],该内核线程会对变量watchdog_touch_ts加加操作,即喂狗。
- 然后给每个CPU分配一个高精度hrtimer,该定时器的中断服务程序会每隔4s(sample_period seconds (4 seconds by default))检测一下变量watchdog_touch_ts是否被更新过,如果20s内该变量仍未更新,就说明CPU卡住,导致watchdog线程无法调度。
hrtimer的中断处理函数是:kernel/watchdog.c/watchdog_timer_fn()。
中断处理函数主要做了以下事情:
- 对变量hrtimer_interrupts加加操作,该变量同时供hard lockup detector用于判断CPU是否响应中断。
- 唤醒[watchdog/x]内核线程(对hrtimer_interrupts进行加加操作,就是在唤醒喂狗线程)。检测变量watchdog_touch_ts是否被更新,如果超过20s未更新,说明[watchdog/x]未得到运行,发生了soft lockup,CPU被霸占。
注意,这里的内核线程[watchdog/x]的目的是操作变量watchdog_touch_ts,该变量是被watch的对象。而真正的看门狗,则是由hrtimer中断触发的,即 watchdog_timer_fn()函数,该函数会唤醒喂狗线程。[watchdog/x]是被scheduler调度执行的,而watchdog_timer_fn()则是被中断触发的。
流程图
源码分析
以kernel4.9为例:
1、注册watchdog线程
static struct smp_hotplug_thread watchdog_threads = {
.store = &softlockup_watchdog,
.thread_should_run = watchdog_should_run,
.thread_fn = watchdog, /* watchdog线程函数 */
.thread_comm = "watchdog/%u",
.setup = watchdog_enable,
.cleanup = watchdog_cleanup,
.park = watchdog_disable,
.unpark = watchdog_enable,
};
void __init lockup_detector_init(void)
{
/* 注册watchdog线程 */
smpboot_register_percpu_thread_cpumask(&watchdog_threads,&watchdog_cpumask);
}
smpboot_register_percpu_thread_cpumask(&watchdog_threads,&watchdog_cpumask)
{
for_each_online_cpu(cpu) {
/* 遍历CPU,为每一个CPU创建watchdog线程 */
__smpboot_create_thread(plug_thread, cpu);
}
}
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
/* 在特定的CPU上创建线程,回调函数是smpboot_thread_fn */
kthread_create_on_cpu(smpboot_thread_fn, td, cpu,ht->thread_comm);
}
/* 再看smpboot_thread_fn回调函数 */
static int smpboot_thread_fn(void *data)
{
while (1) {
switch (td->status) {
case HP_THREAD_NONE:
__set_current_state(TASK_RUNNING);
preempt_enable();
if (ht->setup) /* 使能hard lockup检测 */
ht->setup(td->cpu); /*.setup = watchdog_enable , 创建hrtimer的函数*/
td->status = HP_THREAD_ACTIVE;
continue;
case HP_THREAD_PARKED:
__set_current_state(TASK_RUNNING);
preempt_enable();
if (ht->unpark)
ht->unpark(td->cpu);
td->status = HP_THREAD_ACTIVE;
continue;
}
/* 判断当前CPU是否需要执行watchdog线程 */
if (!ht->thread_should_run(td->cpu)) {
preempt_enable_no_resched();
schedule();
} else {
__set_current_state(TASK_RUNNING);
preempt_enable();
/* 核心调用watchdog_threads的.thread_fn = watchdog */
/* watchdog线程函数 */
ht->thread_fn(td->cpu);
}
}
2、通过以上分析可知,watchdog线程什么时候会被执行,得看thread_should_run函数。
static int watchdog_should_run(unsigned int cpu)
{
/* 当hrtimer_interrupts!= soft_lockup_hrtimer_cnt时 */
/* watchdog函数会执行一次,watchdog线程会更新watchdog_touch_ts */
/* 变量soft_lockup_hrtimer_cnt在watchdog线程函数中会被更新 */
return __this_cpu_read(hrtimer_interrupts) !=__this_cpu_read(soft_lockup_hrtimer_cnt);
}
3、更新watchdog_touch_ts
static void __touch_watchdog(void)
{
__this_cpu_write(watchdog_touch_ts, get_timestamp());
}
static void watchdog(unsigned int cpu)
{
/* 将hrtimer_interrupts的值写到soft_lockup_hrtimer_cnt中 */
/*在watchdog_should_run中会判断两者是否相等,若不等就执行watchdog线程 */
__this_cpu_write(soft_lockup_hrtimer_cnt,
__this_cpu_read(hrtimer_interrupts));
__touch_watchdog();
}
4、绑定hrtimer中断处理函数,watchdog_timer_fn是hrtimer的中断处理函数,以下是中断函数的注册过程:为每个CPU创建watchdog线程时会调用watchdog_enable函数。
static void watchdog_enable(unsigned int cpu)
{
struct hrtimer *hrtimer = &__raw_get_cpu_var(watchdog_hrtimer);
/* 启动定时器 */
hrtimer_init(hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
/* 绑定中断处理函数 */
hrtimer->function = watchdog_timer_fn;
/* 启动hrtimer,timeout 为 4s(4s触发一次hrtimer中断)*/
hrtimer_start(hrtimer, ns_to_ktime(sample_period), HRTIMER_MODE_REL_PINNED);
}
5、watchdog_timer_fn函数实体
/* 每隔4s会执行一次 */
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
unsigned long touch_ts = __this_cpu_read(watchdog_touch_ts);
int duration;
/* 对hrtimer_interrupts加1操作 */
watchdog_interrupt_count();
duration = is_softlockup(touch_ts);
if (unlikely(duration)) {
if (softlockup_panic)
panic("softlockup: hung tasks");
__this_cpu_write(soft_watchdog_warn, true);
} else
__this_cpu_write(soft_watchdog_warn, false);
return HRTIMER_RESTART;
}
该函数做了两件事情:
① 更新hrtimer_interrupts变量(唤醒watchdog线程)。
static void watchdog_interrupt_count(void)
{
__this_cpu_inc(hrtimer_interrupts);
}
之前创建的watchdog线程多久执行一次,和hrtimer_interrupts的值有关系。
static int watchdog_should_run(unsigned int cpu)
{
return __this_cpu_read(hrtimer_interrupts) !=
__this_cpu_read(soft_lockup_hrtimer_cnt);
}
② 判断是否有soft lockup发生。
static int is_softlockup(unsigned long touch_ts)
{
unsigned long now = get_timestamp();
/* 如果某个CPU卡死,该CPU的watchdog线程不会被调度,即watchdog_touch_ts */
/* 不会被更新。如果20s内都没更新watchdog_touch_ts ,就认为出现了soft lockup */
/* get_softlockup_thresh()函数返回20 */
if (time_after(now, touch_ts + get_softlockup_thresh()))
return now - touch_ts;
return 0;
}
问题分析思路:
Soft lockup相关log:
BUG: soft lockup – CPU#2 stuck for 21s!
上述Log说明有进程/线程持续执行的时间超过21s,导致其他进程/线程无法调度,以下情况为形成Soft lockup的主要原因:
- 存在死循环( for循环的退出条件弄错)。
- 不正确使用spinlock,导致了死锁(譬如spinlock嵌套调用,若顺序不对的话就可能导致死锁)。
在处理该类问题时,可以遵循以下原则:
-
查看watchdog_touch_ts变量在最近20秒(watchdog_thresh * 2)内,是否被watchdog 线程更新过。若没有更新,就意味着watchdog线程得不到调度。很有可能某个cpu关抢占或中断执行时间过长,导致调度器无法调度watchdog线程。
-
这种情况下,系统往往不会死掉,但是会很慢。如果将内核参数 softlockup_panic(CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC宏)设置为1,系统会panic。否则,只将warning信息打印出来。
内核配置
- 开启soft lockup(默认为y)
CONFIG_LOCKUP_DETECTOR
- 出现softlockup时,使能系统panic(默认为n)
CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC
- 发生soft lockup时,系统默认会打印相关warning信息。
- 如果需要抛出panic,也可以在应用层做以下设置:
echo 1 > /proc/sys/kernel/softlockup_panic
cat /proc/sys/kernel/watchdog_thresh /*默认10s*/
- watchdog_thresh默认是10s,如果(watchdog_thresh*2)秒内,watchdog_touch_ts未更新,kernel将panic。最大能设到60s,即120s内watchdog_touch_ts未更新,kernel将panic。
结论
Watchdog是内核最常见也是最容易让人忽略的模块功能,从这一个简单的模块展开,就可以深入接触到内核进度调度、锁机制、死锁处理等内核的核心要点。本篇先从Soft lockup开始分析,结合代码分析流程,并传授处理该类问题的解决思路。此类也是我做内核稳定性时期遇到做多类型的问题,现在再从内核安全的角度来看这类问题,会有另外一种领悟,也同时将这些分享给大家。
下篇将从Hard lockup这类更加棘手的问题点出发,深入问题的核心,刨析问题本质,使大家再遇到Hard lockup时不再恐惧和害怕,能够从容面对并处理。
最后
以上就是落后小蘑菇为你收集整理的请记住内核中这个勤劳的监测卫士---Watchdog(Soft lockup篇)的全部内容,希望文章能够帮你解决请记住内核中这个勤劳的监测卫士---Watchdog(Soft lockup篇)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复