1. 原理
在手机Soc Chip中,里面的AP跑着linux操作系统软件,而任何软件都可能存在各种问题,如果遇到了这些异常,软件可能陷入死循环,导致手机变成“砖头”,如果没有其他硬件辅助,那么只能断电(拔电池)然后重新开机才行。为了避免出现这种情况,芯片内部增加了一个看门狗模块,这个模块专门检测CPU运行状态,只要出现卡死就复位系统。
WDT全称是watchdog timer,就是看门狗模块,看门狗其实就是一个可以在一定时间内被复位的计数器。当看门狗启动后,计数器开始自动计数,经过一定时间,如果计数没有被复位,计数器达到指定数值就会发出复位信号,很多设备包括CPU接到这个信号而复位重启(俗称“被狗咬”)。为了保证看门狗不发出复位信号,就需要在看门狗允许的时间间隔内对看门狗计数器清零(俗称“喂狗”),计数器重新计数。如果系统正常并保证按时“喂狗”,那么就相安无事。一旦程序故障卡死,没有“喂狗”,系统“被咬”复位。
2. 种类
嵌入式系统中主要可以分为两种类型的看门狗:
Soc芯片内部集成WDT,这是Soc常用的设计。当然PC上可能用独立的看门狗芯片。
软件模拟看门狗,只要有个timer就可以模拟。
1. 结构
在现有Soc芯片中,只有1个看门狗硬件模块,这个模块被包含在一个叫RGU的模块里。在早期平台中,比如MT6577,MT6575除了RGU里的WDT外,每个CPU都有一个local WDT,可以保证每个CPU不卡死。而MT6577之后就只有RGU里的WDT了,如图:
在只有一个WDT的情况下如何保证每个CPU不卡死呢?这里需要软件设计使它都可以喂狗才行。软件设计部分将在下一章节介绍。
RGU全称是reset generation unit,在datasheet有如下框图:
可以看到WDT在RGU里只是其中一个模块,还有其他模块可以产生复位信号,比如thermal,当温度过高会触发IRQ或直接复位,这也是一种硬件保护措施。复位信号经过Mixer后会分出2个信号,grst_b会复位芯片内部模块,还有1个信号通过芯片管脚WATCHDOG复位外部芯片。有时为了debug,也可以测量WATCHDOG pin脚(正常是高电平,有复位信号时低电平),看这次重启是否是WDT等触发的。
2. dual mode
WDT功能,大家可以看datasheet里的寄存器介绍,比如包含了
WDT_MODE:功能开关
WDT_LENGTH:超时计数
WDT_RESTART:写该寄存器触发计数清0
WDT_STA:状态
WDT_INTAVEL:reset信号长度
WDT_SWRST:写该寄存器直接触发复位
这里介绍一个dual mode功能,通常CPU发生卡死,我们需要知道卡死位置,分析原因,然后改进,而不希望直接复位。设计的方法是:WDT超时后先不发复位信号,而是先送出IRQ(如上面的框图),WDT IRQ通常配置成CPU的FIQ,如果CPU只是软件卡死(内核死锁,中断过多等),会响应该FIQ,然后我们在FIQ里收集异常信息和CPU寄存器信息,然后再走正常的panic流程。重启后我们就有信息分析此次WDT timeout的原因了。为了保证IRQ发出后,CPU不卡死,WDT再次计时,如果在panic流程又卡死了,就会由WDT发出复位信号复位。
简单讲:WDT超时先发出IRQ,WDT重新计时,如果再次超时WDT直接发出复位信号。可以看到WDT超时后分2个阶段,第1阶段发出IRQ用于收集异常信息,第2阶段直接复位。通常我们在第1阶段就完成异常信息收集并通过WDT_SWRST寄存器完成复位,而不用等到第2阶段的。这种情况我们称为HWT(Hardware Watchdog Timeout)
事实上任何事情总有意外,比如CPU因为PMIC提供的电压低,或硬件故障等原因导致WDT超时发出的IRQ在CPU端得不到响应,WDT只能通过第2阶段复位芯片。这种情况我们称为HW reboot。HW reboot通常和硬件有较大关系。比如:
硬件故障,电压或频率异常
总线卡死
上一章节有讲Soc芯片内部只有1个WDT模块,但CPU有多颗,为了保证每个CPU卡死都可以顺利复位,就必须借助软件。WDT本质是个计数器并且支持dual mode,目前我们将超时时间(第1阶段超时)设定为30s,在发出IRQ之后超时时间(第2阶段超时)不可设定,各个平台可能不一样,一般在30s~60s之间。
配置好后,还需要设计喂狗模块,这是重中之重。流程如下:
bit位代表CPU的喂狗状态:CPU核数每个平台都不太一样,我们用bit位代表1个CPU,比如4个CPU就用4个bit,我们用kick_bit变量表示。
喂狗进程负责喂狗:在喂狗模块初始化时针对每个CPU创建的对应的内核实时进程(优先级最高)并绑定到对应的CPU上(也就是说这个进程只运行在指定CPU上,不会发生迁移),这些进程喂狗(其实就是将kick_bit对应的bit位置1而已,并没有真正喂狗)后进入睡眠,喂狗间隔是20s,如此循环反复。
真正喂外部WDT狗:任何一个喂狗进程在喂狗之后,会检查kick_bit变量,如果该值等于cpus_kick_bit变量(这个变量的每个bit代表对应CPU online情况,如果CPU online,对应bit置1),表示所有CPU都已喂狗,这时才真正喂外部WDT狗,同时将kick_bit清0,进入下一轮喂狗流程,如此循环反复。
用一张框图总结上面的流程(以4核CPU为例):
这里的wdtk-x就是对应CPU的喂狗进程。以上的一套设计可以保证各个CPU卡死都可以通过看门狗复位。
喂狗间隔是20s,而超时时间是30s,也就是说最长能容忍卡住的时间是10s(卡一小会还是可以的),超过这个时间,系统就会复位了。这里还有问题,由于喂狗进程之间没有同步,是否有可能存在刚开始一起喂狗,之后渐渐出现先后呢?误差肯定有的,但在任何30s时间里,喂狗进程都会喂1次狗的(因为喂狗间隔20s,每个CPU肯至少会喂1次狗)。而只要CPU卡死超过10s就复位了。
多核CPU不可能一直都是online,系统会根据负载做hotplug,在某颗CPU power down/power up时,会更新cpus_kick_bit变量并将kick_bit变量清0,同时喂外部WDT狗。
当只剩1个CPU时,并且该CPU要进入睡眠,此时kernel进入suspend状态,WDT模块当然也要关闭的,唤醒时再重启开启。那睡眠后的流程卡死怎么办?那已超出WDT管辖的范围了,需要用其他硬件保证,不在该文章讨论范围。
WDT驱动分布
android系统中,启动分好几个阶段:preloader、lk、kernel、android。因此每个阶段都要配置WDT,才能保证不卡死。这里我们介绍kernel阶段的WDT驱动,其他阶段是类似的。
kernel WDT驱动
这里我们没有用到kernel原生的WDT驱动,而是前面章节讲的那套机制。具体代码位置:
alps/kernel-3.10/drivers/misc/mediatek/wdt/$platform/mtk_wdt.c。下面我们以L1.MP6的MT6752的wdt驱动做讲解。
初始化
流程是:module_init(mtk_wdt_init) => mtk_wdt_init() => mtk_wdt_probe()。
在mtk_wdt_probe()函数里,做了如下几件事:
注册WDT IRQ处理函数wdt_fiq(),kernel本身是没有使用FIQ的,因此WDT FIQ发生时一定会被处理,除非FIQ被关闭或硬件故障了。由于64bit kernel默认支持ATF(ARM Trusted Firmware),FIQ为security interrupt,kernel是无法处理security interrupt的,因此FIQ会路由到ATF里处理。在后面的流程中会详细介绍。
设定WDT超时时间30s。
调用mtk_wdt_mode_config()初始化WDT,这里打开的dual mode。
这样就完成WDT的初始化了,WDT初始化是比较早的,基本上3s内就完成了(kernel时间)。以下是72KK版本的uart log中搜索wdt结果:
睡眠/唤醒
在系统进入睡眠时,WDT要被关闭,不过不是在WDT suspend里关闭WDT,因为suspend不是睡眠最后的步骤。这里会在sleep模块通过调用WDK的wd_suspend_notify()来间接调用mtk_wd_suspend()来关闭WDT的。同理唤醒时也是早于resume流程就被sleep模块通过WDK的resume_notify()调用mtk_wd_resume()重新启动WDT。
重启
系统重启(比如adb reboot)是通过WDT来完成复位的,在驱动里提供了wdt_arch_reset()函数,这个函数通过写WDT_SWRST寄存器完成软件复位。基本上重启的log都长这样:
红线以上是正常的log,写完WDT_SWRST寄存器基本就复位了,但有些平台并不是立即生效,需要过几百毫秒才完成复位,因此有可能看到红线以下的log,这是正常的。
喂狗函数
WDT启动后就开始计时了,如果没人喂狗就会触发FIQ,然后在硬复位。因此需要提供了mtk_wdt_restart()函数,调用一次就将计数清0。
如果有驱动需要长时间关中断运行,比如开机时的TP固件升级,就需要在里面添加喂狗动作,防止HWT。
开关WDT
有时为了调试,我们需要关闭看门狗,驱动里也提供了开启/关闭函数:mtk_wdt_enable()。如果你要关闭WDT,可以直接在代码里调用这个函数,那么WDT就永久关闭了。
在WDK驱动里还提供了用户接口,可以很方便在adb shell里关闭/打开WDT。
结语
WDT驱动比较简单,基本都是控制WDT硬件寄存器达到所需效果。重点还在于WDK驱动(喂狗模块)。
WDK驱动
wdk是喂狗模块,由于只有一个WDT,而需要保护多个CPU不卡死,根据前面设计的原理来设计wdk驱动,驱动具体位置:
kernel-3.10/drivers/misc/mediatek/wdk,里面主要有wd_common_drv.c/wd_api.c
wd_api.c负责对外接口,wd_common_drv.c负责喂狗工作。
对外接口
get_wd_api()可以获取到通用的wdk api结构体,里面提供了WDT和WDK所需的操作。使用方法如下(手动喂狗):
下面介绍常用的几个api函数:
初始化
流程1:arch_initcall(init_wk_check_bit) => init_wk_check_bit() => wk_cpu_update_bit_flag()。完成cpus_kick_bit、kick_bit这2个重要变量初始化。cpus_kick_bit表示需喂狗CPU位图,kick_bit表示已喂狗CPU位图,每个bit对应CPU。
流程2:late_initcall(init_wk) => init_wk() => mtk_rgu_sysfs()、start_kicker_thread_with_default_setting()、wk_proc_init()、register_cpu_notifier(&cpu_nfb)。
通过mtk_rgu_sysfs()创建了sysfs文件系统节点。
通过start_kicker()创建per CPU的喂狗进程wdtk-x,这些进程都是系统中优先级最高的进程,执行的核心函数是kwdt_thread(),里面实现了前面提到的喂狗框架。
通过wk_proc_init()创建了proc文件系统节点/proc/wdk。我们可以通过它来设定超时时间、喂狗时间、打开/关闭WDT。格式如下(如关闭WDT):echo 0 20 30 0 0 > /proc/wdk
参数1:目前没用到,可以忽略,直接写0。
参数2:喂狗间隔,默认20,单位秒。
参数3:超时时间,默认30,单位秒。
参数4:目前没用到,可以忽略。
参数5:打开/关闭WDT。
根据当前online CPU状况更新cpus_kick_bit并注册CPU hotplug回调函数,因为CPU会根据负载上电/下电,因此需要及时更新cpus_kick_bit、kick_bit。
需要注意的是:
WDT初始化和WDK初始化是处于kernel初始化的不同阶段,WDT通过module_init()完成,而WDK通过late_initcall(),中间隔着各种驱动,因此如果有些驱动执行时间太长,会导致开机30s HWT的问题,这个在后面章节提到。
在KK1.MP11以前的版本启动喂狗进程时,会直接调用到kwdt_thread(),里面会设置一遍kick_bit,最后等于cpus_kick_bit然后喂WDT,但实际上第1次kick并没有累加起来,查看log:
看到所有wdk进程起来后,都会设置kick_bit(就是local_bit),但是没有累加起来,无法等于0xf,也就无法喂WDT,真正喂狗是在20s之后,从时间轴来看:
这是为什么呢?原因是start_kicker()里没创建完进程,就会调用wk_cpu_update_bit_flag()将kick_bit清0,因此无法累加。KK1.MP11及之后的版本就没有再调用wk_cpu_update_bit_flag(),因此wdk初始化完就会完成第1次喂狗。
喂狗
kwdt_thread()完成喂狗动作,里面就是一个循环:
如果需要更新超时时间,则在这里调用wdt函数更新。
更新对应CPU的kick_bit,如果等于cpus_kick_bit则清除kick_bit并喂WDT。
显示UTC时间,主要用于kernel和android时间同步,这条log非常重要,如果要看android log当时对应的kernel log,就可以通过这条log找到大致的位置。由于WDK是每20s跑一次,因此这条log也是每20s输出一次,如下:[ 25.876493]<1>[thread:187][RT:25867046616] 2015-01-01 00:10:38.35106 UTC; android time 2015-01-01 00:10:38.35106
睡眠20s
各个CPU的kwdt_thread()没有同步,因此各自喂狗的时间不定,但只要喂狗间隔不超过30s就没有事。
CPU hotplug
由于有注册CPU hotplug回调函数,因此在某颗CPU上电时,会将cpus_kick_bit对应的bit置位并同时喂狗。下电时,会清除cpus_kick_bit对应bit位并同时喂狗。
如果喂狗进程没有及时喂狗,WDT就会超时触发FIQ,而分析解决HWT的问题很依赖WDT FIQ能输出什么信息。因此必须要熟悉WDT FIQ流程。32bit/64bit kernel处理FIQ方式不一样,因此要分开讲解。
32bit kernel FIQ触发流程
在WDT驱动里有注册了WDT FIQ中断处理函数wdt_fiq(),当WDT发出中断后,kernel流程如下:
__vectors_start + 0x1C:这是异常向量表FIQ入口,所有FIQ中断都会导向这里。
fiq_glue():FIQ入口直接调用了这个函数,此时处于FIQ mode,用独立的栈,只要还在这个独立的栈里就无法调用printk输出log,原因是printk使用了current(),会从栈底取出thread_info结构体,而该独立的栈咩有thread_info结构体。
fiq_isr():这个函数更新了fiq_step。后面会继续讲fiq_step内容。
wdt_fiq():到了WDT注册的中断函数了,这里还是无法使用printk,因此只能调用aee_wdt_printf()打印到buffer里。这里印出了最重要的信息kick和check(对应的kick_bit和cpus_kick_bit),比如:kick=0x00000007,check=0x0000000f。得到kick和check就知道是哪些CPU没有喂狗了,接下来重点关注没喂狗的CPU调用栈等信息。
aee_wdt_fiq_info():这个函数首先将栈切换为当前进程栈,就可以用printk了。另外当前CPU的寄存器打印到per CPU buffer里。最后只允许一个CPU继续往下走,其他CPU直接死循环等待重启了。
aee_wdt_irq_info():这个函数将输出重要log
喂狗,防止超时。
停止其他CPU。
将per CPU buffer输出到last_kmsg。注意:只输出到last_kmsg,kernel log是看不到的。last_kmsg里的log如下:
cpu 0 preempt=1, softirq=0, hardirq=0 cpu 0 backtrace : c0344a1c, c0344a1c, cpu 1 preempt=0, softirq=0, hardirq=0 |
如果后面流程卡住了,那么能参考的信息只有last_kmsg了,因此这个信息尤为重要。
调用mt_aee_dump_sched_traces()输出中断信息。这个信息只有eng版本才有。这个有什么用呢?HWT有一个原因是IRQ太过频繁导致,因此IRQ信息能直接看出原因
当前CPU中断信息,可以看到上次IRQ/SoftIRQ/tasklet/hrtimer/SoftTimer信息,如果HWT时调用栈在IRQ里,还可以看到该IRQ的信息,可以判断执行是否太久。
IRQ Status会印出所有中断在Dur时间段里触发次数,如果太多就有问题了(比如>1000次每秒)。
将aee wdt buffer打印到last_kmsg,之前是per CPU buffer,这次是aee wdt buffer:
kick=0x00000007,check=0x0000000f Qwdt at [292641.063701] |
,这里才能从last_kmsg看到kick/check信息。
BUG(),之后就是正常的die/panic流程了,所以kernel log总能看到PC is at aee_wdt_irq_info。看到这个信息就可以判断是HWT了。
64bit kernel FIQ触发流程
64bit kernel有ATF(ARM Trusted Firmware),FIQ是security interrupt,必须路由到secure world处理,之后才会返回kernel完成剩下的工作。
建议了解下ATF相关内容再来看下面的知识点。
ATF初始化时就会注册WDT FIQ处理函数,该函数保存per-CPU寄存器信息并返回kernel处理。
仅有ATF的流程比较简单,由于在ATF时,屏蔽所有中断,因此WDT FIQ只能在EL1 kernel时发生,如下:
如果存在TEE OS则更复杂些,WDT FIQ可能在EL1 kernel或S-EL1 TEE OS触发,最后都会路由到S-EL1 OS处理,然后再返回ATF,最后到EL1 kernel,如下:
除了底层流程不一样外,kernel流程是差不多的,aee_wdt_atf_info()等效于aee_wdt_irq_info()。因此可以参考上面的内容。
FIQ step
kernel发生异常或HWT,当时的系统是不稳定的,无法保证panic或上面的WDT FIQ流程能顺利走完。因此我们使用fiq step标记走到了哪里,如上面的WDT fiq流程里,基本上每走几步就通过aee_rr_rec_fiq_step()记录特定的数字。在db解开后的SYS_REBOOT_REASON、SYS_LAST_KMSG和__exp_main.txt都会有fiq step以供查看,如下:
WDT status: 2 fiq step: 41 exception type: 1
详情可参考:[FAQ14332]SYS_LAST_KMSG里的hw_status和fiq step的含义。
一般kernel异常,fiq step都不为0,为0表示CPU没有走到这些步骤当中,可能是没异常或CPU卡死跑不了代码。
结语
请结合代码理清一遍流程,知道哪条log从哪个函数打印出来。如果需要加log调试,也知道该加在哪个位置。
学完WDT/WDK相关内容并不代表就可以独立解决HWT/HW REBOOT的问题了,还需要很多kernel基础知识支撑才行,因为任何模块都不是独立存在,在后面的案例分析就会有更深刻的体会了。