在系統(tǒng)的日常運(yùn)維中,最令人頭疼的莫過(guò)于各種應(yīng)用程序或者操作系統(tǒng)hung住不響應(yīng)的問(wèn)題。對(duì)于處在用戶態(tài)的程序相對(duì)來(lái)說(shuō)還比較容易排查,而一旦程序hung在內(nèi)核態(tài)或者操作系統(tǒng)本身hung住,kill信號(hào)甚至硬件中斷都無(wú)法響應(yīng),此時(shí)我們能做的就只有重啟了。然而重啟并不能解決根本問(wèn)題,更麻煩的是這種情況下我們幾乎拿不到任何有用的信息,后續(xù)原因的分析和排查更是步履維艱。好在Linux內(nèi)核早已提供了一系列的機(jī)制來(lái)幫助我們分析此類問(wèn)題。下面我們就來(lái)看下如何配置使用這些機(jī)制以及它們的實(shí)現(xiàn)原理。
■概念
? 第一種比較常見(jiàn)的現(xiàn)象是:進(jìn)程長(zhǎng)時(shí)間處于D(不可中斷睡眠)狀態(tài)。依賴于它的進(jìn)程也會(huì)因?yàn)榈却枞D敲词裁词荄狀態(tài)呢?顧名思義:首先它是一種睡眠狀態(tài),也就意味著處于此狀態(tài)的進(jìn)程不會(huì)消耗CPU。其次睡眠的原因是因?yàn)榈却承┵Y源(比如鎖或者磁盤IO),這也是我們看到非常多D狀態(tài)的進(jìn)程都處在處理IO操作的原因。最后一點(diǎn)就是它不能被中斷,這個(gè)要區(qū)別于“硬件中斷”的中斷,是指不希望在其獲取到資源或者超時(shí)前被終止。因此他不會(huì)被信號(hào)喚醒,也就不會(huì)響應(yīng)kill -9這類信號(hào)。這也是它跟S(可中斷睡眠)狀態(tài)的區(qū)別。
??進(jìn)程進(jìn)入D狀態(tài)發(fā)生在內(nèi)核代碼或者底層驅(qū)動(dòng)代碼中,典型的場(chǎng)景是與硬件進(jìn)行通信。通常情況下我們可以通過(guò)top命令觀察到進(jìn)程快速的的進(jìn)入退出D狀態(tài),但當(dāng)硬件故障或者驅(qū)動(dòng)bug出現(xiàn)時(shí)就會(huì)導(dǎo)致進(jìn)程長(zhǎng)時(shí)間處于D狀態(tài)無(wú)法退出,依賴或等待它的其他進(jìn)程都被阻塞卡死。
處于D狀態(tài)的進(jìn)程
■機(jī)制
??內(nèi)核通常會(huì)創(chuàng)建一個(gè)khungtaskd的守護(hù)進(jìn)程,它會(huì)周期性的檢查所有進(jìn)程的狀態(tài)和上下文切換,進(jìn)而判斷是否有進(jìn)程長(zhǎng)時(shí)間處于D狀態(tài)。我們可以通過(guò)配置如下內(nèi)核參數(shù)來(lái)控制檢測(cè)的超時(shí)時(shí)間、告警打印,以及是否觸發(fā)panic,以幫助問(wèn)題的后續(xù)分析。
# 超時(shí)時(shí)間 kernel.hung_task_timeout_secs = 120 # 告警打印的次數(shù) kernel.hung_task_warnings = 10 # 是否觸發(fā)系統(tǒng)panic kernel.hung_task_panic = 0 # 檢測(cè)的最大進(jìn)程數(shù),系統(tǒng)中進(jìn)程數(shù)超過(guò)此值時(shí)會(huì)忽略超出的部分 kernel.hung_task_check_count = 4194304
進(jìn)程處于D狀態(tài)的超時(shí)打印
■實(shí)現(xiàn)
khungtaskd對(duì)應(yīng)的代碼在hung_task.c中,主要實(shí)現(xiàn)邏輯:
每隔一段時(shí)間(hung_task_timeout_secs定義的超時(shí)時(shí)間),檢查系統(tǒng)中所有進(jìn)程
針對(duì)處于D狀態(tài)的進(jìn)程,記錄并檢查它的上下文切換次數(shù),如果和上次記錄的上下文切換次數(shù)相同,則說(shuō)明此進(jìn)程在超時(shí)時(shí)間內(nèi)一直處于D狀態(tài)。
根據(jù)配置選擇打印告警并觸發(fā)系統(tǒng)panic
static int __init hung_task_init(void)
{
... ...
watchdog_task = kthread_run(watchdog, NULL, "khungtaskd"); //創(chuàng)建khungtaskd進(jìn)程
... ...
}
static int watchdog(void *dummy)
{
... ...
for ( ; ; ) {
while (schedule_timeout_interruptible(timeout_jiffies(timeout))) //休眠一段時(shí)間
timeout = sysctl_hung_task_timeout_secs;
... ...
check_hung_uninterruptible_tasks(timeout); //開(kāi)始檢查進(jìn)程
}
}
static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
unsigned long switch_count = t->nvcsw + t->nivcsw; //計(jì)算上下文切換次數(shù)
... ...
if (switch_count != t->last_switch_count) { //和上次切換次數(shù)進(jìn)行比較
t->last_switch_count = switch_count;
return;
}
... ...
printk(KERN_ERR "INFO: task %s:%d blocked for more than " //打印告警
"%ld seconds.\n", t->comm, t->pid, timeout);
printk(KERN_ERR "\"echo 0 > /proc/sys/kernel/hung_task_timeout_secs\""
" disables this message.\n");
... ...
if (sysctl_hung_task_panic) {
trigger_all_cpu_backtrace();
panic("hung_task: blocked tasks"); //觸發(fā)系統(tǒng)panic
}
}
■概念
??另外一種比較常見(jiàn)的情況就是:一個(gè)進(jìn)程一直占用CPU,其他進(jìn)程始終無(wú)法被調(diào)度執(zhí)行,極端情況下甚至無(wú)法響應(yīng)中斷,此時(shí)系統(tǒng)可能就會(huì)完全hung住不響應(yīng)任何用戶操作。這種情況還是發(fā)生在內(nèi)核代碼或者驅(qū)動(dòng)代碼bug中。應(yīng)用程序不會(huì)出現(xiàn)這個(gè)問(wèn)題,就好像我們寫一個(gè)死循序程序不會(huì)導(dǎo)致系統(tǒng)hung住一樣。
??要理解這種情況,首先得明白Linux是個(gè)搶占式內(nèi)核,進(jìn)程之間可以相互搶占CPU,其次Linux會(huì)為每個(gè)CPU core設(shè)置一個(gè)固定周期的時(shí)鐘中斷,這個(gè)中斷是一個(gè)很重要的搶占的時(shí)機(jī),時(shí)鐘中斷處理程序會(huì)判斷下面需要讓哪個(gè)進(jìn)程搶到CPU。一個(gè)處在用戶態(tài)的進(jìn)程執(zhí)行一段時(shí)間后,時(shí)鐘中斷觸發(fā),進(jìn)程調(diào)度算法(例如:CFS)可能就會(huì)將CPU分配給其他進(jìn)程,從而不會(huì)讓這個(gè)進(jìn)程一直占用CPU。而一個(gè)處在內(nèi)核態(tài)的進(jìn)程則不同,首先它可以屏蔽中斷響應(yīng),這就直接去除了搶占的時(shí)機(jī),其次它也可以顯示的關(guān)閉搶占,同時(shí)如果是個(gè)內(nèi)核進(jìn)程,他的優(yōu)先級(jí)高于普通進(jìn)程并且調(diào)度策略也不同于CFS,以上這些情況下如果不主動(dòng)讓出CPU,其他進(jìn)程就無(wú)法獲取執(zhí)行,最終就會(huì)導(dǎo)致問(wèn)題出現(xiàn)。
上面描述的現(xiàn)象在linux里面稱為:軟鎖(soft_lockup)和硬鎖(hard_lockup)
soft_lockup
CPU被某個(gè)進(jìn)程長(zhǎng)時(shí)間占用,其他進(jìn)程得不到調(diào)用。例如:長(zhǎng)時(shí)間禁用內(nèi)核搶占
soft_lockup告警打印
hard_lockup
CPU被某個(gè)進(jìn)程長(zhǎng)時(shí)間占用,其他進(jìn)程得不到調(diào)用,同時(shí)也不響應(yīng)中斷。例如:長(zhǎng)時(shí)間屏蔽中斷響應(yīng)
hard_lockup告警打印
■機(jī)制
?Linux內(nèi)核通過(guò)watchdog機(jī)制來(lái)檢查系統(tǒng)中是否出現(xiàn)soft_lockup和hard_lockup。watchdog的主要思想是:通過(guò)優(yōu)先級(jí)更高的任務(wù)來(lái)觀察優(yōu)先級(jí)較低的任務(wù)(進(jìn)程/中斷)是否被成功執(zhí)行調(diào)度,因此可以通過(guò)中斷來(lái)觀察進(jìn)程是否被正常調(diào)度,而通過(guò)NMI(不可屏蔽中斷)來(lái)觀察中斷是否被響應(yīng)。
??我們可以通過(guò)如下內(nèi)核參數(shù)來(lái)配置檢查條件和是否觸發(fā)panic,以幫助問(wèn)題的后續(xù)分析。
# 開(kāi)啟watchdog kernel.watchdog = 1 # hardlockup超時(shí)時(shí)間,softlockup超時(shí)時(shí)間=2*watchdog_thresh kernel.watchdog_thresh = 10 # 是否觸發(fā)panic kernel.hardlockup_panic = 1 kernel.softlockup_panic = 1
■實(shí)現(xiàn)
??watchdog對(duì)應(yīng)的代碼在watchdog.c中,主要實(shí)現(xiàn)邏輯:
為每個(gè)CPU core創(chuàng)建一個(gè)(watchdog/%u)內(nèi)核進(jìn)程,它會(huì)周期性的更新watchdog_touch_ts變量
設(shè)置一個(gè)時(shí)鐘中斷,它會(huì)周期性的更新hrtimer_interrupts變量
同時(shí)負(fù)責(zé)檢測(cè)soft_lockup,通過(guò)查看watchdog_touch_ts的值是否被更新,來(lái)判斷(watchdog/%u)進(jìn)程是否被執(zhí)行,從而判斷CPU是否被其他進(jìn)程一直占用
設(shè)置一個(gè)NMI(不可屏蔽中斷),它會(huì)每watchdog_thresh秒觸發(fā)一次
同時(shí)負(fù)責(zé)檢測(cè)hard_lockup,通過(guò)查看hrtimer_interrupts的值是否被更新,來(lái)判斷是否出現(xiàn)hard_lockup
對(duì)應(yīng)內(nèi)核代碼
#watchdog.c
static int watchdog_enable_all_cpus(void)
{
... ...
if (!watchdog_running) {
err = smpboot_register_percpu_thread(&watchdog_threads); //創(chuàng)建(watchdog/%u)內(nèi)核進(jìn)程
... ...
}
static void __touch_watchdog(void)
{
__this_cpu_write(watchdog_touch_ts, get_timestamp()); //更新watchdog_touch_ts
}
static void watchdog_enable(unsigned int cpu)
{
... ...
hrtimer->function = watchdog_timer_fn; //設(shè)置時(shí)鐘中斷
watchdog_nmi_enable(cpu); //設(shè)置NMI(非屏蔽中斷)
hrtimer_start(hrtimer, ns_to_ktime(sample_period), HRTIMER_MODE_REL_PINNED); //啟動(dòng)時(shí)鐘中斷
}
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
... ...
watchdog_interrupt_count(); //更新hrtimer_interrupts
wake_up_process(__this_cpu_read(softlockup_watchdog));
... ...
duration = is_softlockup(touch_ts); //檢查softlockup
... ...
}
static bool is_hardlockup(void) //檢查hardlockup
{
unsigned long hrint = __this_cpu_read(hrtimer_interrupts);
if (__this_cpu_read(hrtimer_interrupts_saved) == hrint)
return true;
__this_cpu_write(hrtimer_interrupts_saved, hrint);
return false;
}
以上介紹了三種最常見(jiàn)的導(dǎo)致進(jìn)程或系統(tǒng)hung住的場(chǎng)景和相關(guān)的背景及原理。在遇到這些情況的時(shí)候我們可以更快速的判斷出現(xiàn)問(wèn)題的基本原因和可能的地方。同時(shí)也介紹了linux內(nèi)核提供的一些機(jī)制,幫助我們檢查并收集必要的日志和信息。有了這些信息,我們就可以通過(guò)分析日志、利用kdump等工具來(lái)進(jìn)一步排查問(wèn)題的最終原因。
上述代碼源自Redhat-7.5,kernel版本:linux-3.10.0-862.el7
服務(wù)電話: 400-678-1800 (周??周五 09:00-18:00)
商務(wù)合作: 0571-87770835
市場(chǎng)反饋: marketing@woqutech.com
地址: 杭州市濱江區(qū)濱安路1190號(hào)智匯中?A座1101室