深入理解Linux內(nèi)核之進(jìn)程睡眠之一
1開場白
環(huán)境:
- 處理器架構(gòu):arm64
- 內(nèi)核源碼:linux-5.10.50
- ubuntu版本:20.04.1
- 代碼閱讀工具:vim+ctags+cscope
無論是任務(wù)處于用戶態(tài)還是內(nèi)核態(tài),經(jīng)常會(huì)因?yàn)榈却承┦录?可能是等待IO讀寫完成,也可能等待其他內(nèi)核路徑釋放一把鎖等)。本文來探討一下,任務(wù)處于睡眠中有哪些狀態(tài)?睡眠對于任務(wù)來說究竟意味著什么?內(nèi)核是如何管理睡眠的任務(wù)的?我們會(huì)結(jié)合內(nèi)核源代碼來分析任務(wù)的睡眠,力求全方位角度來剖析。
注:由于篇幅問題,文章分為上下兩篇,且這里不區(qū)分進(jìn)程和任務(wù),統(tǒng)一使用任務(wù)來表示進(jìn)程。
主要講解以下內(nèi)容:
- 睡眠的三種狀態(tài)
- 睡眠的內(nèi)核原理
- 用戶態(tài)睡眠
- 內(nèi)核態(tài)睡眠
- 總結(jié)
2. 睡眠的三種狀態(tài)
任務(wù)睡眠有三種狀態(tài):
- 淺度睡眠
- 中度睡眠
- 深度睡眠
2.1 淺度睡眠
進(jìn)程描述符的state使用TASK_INTERRUPTIBLE表示這種狀態(tài)。
為可中斷的睡眠狀態(tài),這里可中斷是可以被信號(hào)所打斷(喚醒)。
這里給出被信號(hào)打斷/喚醒的代碼路徑:
- kernel/signal.c
- SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
- ->kill_something_info
- ->__kill_pgrp_info
- ->group_send_sig_info
- ->do_send_sig_info
- ->send_signal
- ->__send_signal
- ->complete_signal
- ->signal_wake_up
- -> signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0)
- ->wake_up_state(t, state | TASK_INTERRUPTIBLE)
- ->try_to_wake_up
可以看到在信號(hào)傳遞的時(shí)候,會(huì)通過signal_wake_up喚醒從處于可中斷睡眠狀態(tài)的任務(wù)。
2.2 中度睡眠
進(jìn)程描述符的state使用TASK_KILLABLE表示這種狀態(tài)。
可以被致命信號(hào)所打斷。
這里給出被致命信號(hào)打斷/喚醒的代碼路徑:
- include/linux/sched.h
- #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
- kernel/signal.c
- SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
- ->kill_something_info
- ->__kill_pgrp_info
- ->group_send_sig_info
- ->do_send_sig_info
- ->send_signal
- ->__send_signal
- ->complete_signal
- ->
- if (sig_fatal(p, sig) &&
- ¦ !(signal->flags & SIGNAL_GROUP_EXIT) &&
- ¦ !sigismember(&t->real_blocked, sig) &&
- ¦ (sig == SIGKILL || !p->ptrace)) { //致命信號(hào)
- ...
- signal_wake_up(t, 1);
- -> signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0) // resume == 1
- -> wake_up_state(t, state | TASK_INTERRUPTIBLE)
- ->try_to_wake_up
- ...
- }
2.3 深度睡眠
進(jìn)程描述符的state使用TASK_UNINTERRUPTIBLE表示這種狀態(tài)。
為不可中斷的睡眠狀態(tài),不能被任何信號(hào)所喚醒(特定條件沒有滿足發(fā)生信號(hào)喚醒可能導(dǎo)致數(shù)據(jù)不一致等問題,這種場景使用這種睡眠狀態(tài),如等待IO讀寫完成)。
3. 睡眠的內(nèi)核原理
睡眠都是主動(dòng)發(fā)生調(diào)度,即主動(dòng)調(diào)用主調(diào)度器。
睡眠的主要步驟如下:
1)設(shè)置任務(wù)狀態(tài)為睡眠狀態(tài)
2)記錄睡眠的任務(wù)
3)發(fā)起主動(dòng)調(diào)度
下面我們來詳細(xì)解讀下這幾個(gè)步驟:
3.1 設(shè)置任務(wù)狀態(tài)為睡眠狀態(tài)
這一步很有必要,一來標(biāo)識(shí)進(jìn)入了睡眠狀態(tài),二來是主調(diào)度器會(huì)根據(jù)睡眠標(biāo)志將任務(wù)從運(yùn)行隊(duì)列刪除。
注:睡眠狀態(tài)描述見上一小節(jié)!
3.2 記錄睡眠的任務(wù)
這一步也非常有必要,內(nèi)核會(huì)將即將睡眠的任務(wù)記錄下來,要么加入到鏈表中管理,要么使用數(shù)據(jù)結(jié)構(gòu)記錄。
如延遲睡眠場景,內(nèi)核將即將睡眠的任務(wù)記錄在定時(shí)器相關(guān)的數(shù)據(jù)結(jié)構(gòu)中;可睡眠的信號(hào)量場景中,內(nèi)核將即將睡眠的任務(wù)加入到信號(hào)量的相關(guān)鏈表中。
記錄的目的在于:當(dāng)喚醒條件滿足時(shí),喚醒函數(shù)能夠找到想要喚醒的任務(wù)。
3.3 發(fā)起主動(dòng)調(diào)度
這一步是真正進(jìn)行睡眠的操作,主要是調(diào)用主調(diào)度器來發(fā)起主動(dòng)調(diào)度讓出處理器。
下面我們來看下主調(diào)度器為任務(wù)睡眠所作的處理:
- kernel/sched/core.c
- __schedule
- ->
- prev_state = prev->state; //獲得前一個(gè)任務(wù)狀態(tài)
- if (!preempt && prev_state) { //如果是主動(dòng)調(diào)度 且任務(wù)狀態(tài)不為0
- if (signal_pending_state(prev_state, prev)) { //有掛起的信號(hào)
- prev->state = TASK_RUNNING; //設(shè)置狀態(tài)為可運(yùn)行
- } else {
- deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); //cpu運(yùn)行隊(duì)列中刪除任務(wù)
- }
- }
- next = pick_next_task(rq, prev, &rf); //選擇下一個(gè)任務(wù)
- context_switch //進(jìn)行上下文切換
來看下deactivate_task對于睡眠任務(wù)做的主要工作:
- deactivate_task
- ->deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK)
- ->p->on_rq = (flags & DEQUEUE_SLEEP) ? 0 : TASK_ON_RQ_MIGRATING; //設(shè)置任務(wù)的on_rq 為0 標(biāo)識(shí)是睡眠
- dequeue_task(rq, p, flags);
- ->p->sched_class->dequeue_task(rq, p, flags)
- ->dequeue_task_fair
- ->dequeue_entity
- ...
- if (se != cfs_rq->curr) //不是cpu當(dāng)前 任務(wù)
- __dequeue_entity(cfs_rq, se); //cfs運(yùn)行隊(duì)列刪除
- ->se->on_rq = 0; //標(biāo)識(shí)調(diào)度實(shí)體不在運(yùn)行隊(duì)列!!!
- ->if (!(flags & DEQUEUE_SLEEP))
- se->vruntime -= cfs_rq->min_vruntime; //調(diào)度實(shí)體的虛擬運(yùn)行時(shí)間 減去 cfs運(yùn)行隊(duì)列的最小虛擬運(yùn)行時(shí)間
deactivate_task會(huì)設(shè)置任務(wù)的on_rq 為0來 標(biāo)識(shí)是睡眠 ,然后 調(diào)用到調(diào)度類的dequeue_task方法,在cfs中設(shè)置se->on_rq = 0標(biāo)識(shí)調(diào)度實(shí)體不在cfs隊(duì)列。
可以看到,發(fā)起主動(dòng)調(diào)度的時(shí)候,在主調(diào)度器中會(huì)做判斷:如果是主動(dòng)調(diào)度且任務(wù)狀態(tài)不為0 (即為不是可運(yùn)行的TASK_RUNNING)時(shí),如果沒有掛起的信號(hào),就會(huì)將任務(wù)從cpu的運(yùn)行隊(duì)列中“刪除”,然后選擇下一個(gè)任務(wù),進(jìn)行上下文切換。
將即將睡眠的任務(wù)從cpu的運(yùn)行隊(duì)列中“刪除”意義重大:主調(diào)度器再次選擇下一個(gè)任務(wù)的時(shí)候不會(huì)在選擇睡眠的任務(wù)(因?yàn)橹髡{(diào)度器總是在運(yùn)行隊(duì)列中選擇任務(wù)運(yùn)行,除非任務(wù)被喚醒,重新加入運(yùn)行隊(duì)列)。
注意:1.這里的刪除指的是設(shè)置對應(yīng)標(biāo)志如p->on_rq=0,se->on_rq = 0,當(dāng)選擇下一個(gè)任務(wù)的時(shí)候不會(huì)在加入運(yùn)行隊(duì)列中。2.即將睡眠的任務(wù)是cpu上的當(dāng)前任務(wù)(curr指向)。3.調(diào)用主調(diào)度器后,即將睡眠的任務(wù)不會(huì)再次加入cpu運(yùn)行隊(duì)列,除非被喚醒。
再來看下選擇下一個(gè)任務(wù)的時(shí)候會(huì)做哪些事情和睡眠有關(guān)(暫不考慮組調(diào)度情況):
- pick_next_task
- ->class->pick_next_task
- ->pick_next_task_fair //kernel/sched/fair.c
- ->if (prev)
- put_prev_task(rq, prev); //對前一個(gè)任務(wù)處理
- se = pick_next_entity(cfs_rq, NULL); //選擇下一個(gè)任務(wù)
- set_next_entity(cfs_rq, se);
主要看下put_prev_task:
- put_prev_task
- ->prev->sched_class->put_prev_task(rq, prev)
- ->put_prev_task_fair
- ->put_prev_entity
- -> if (prev->on_rq) { //前一個(gè)任務(wù)的調(diào)度實(shí)體on_rq不為0?
- update_stats_wait_start(cfs_rq, prev);
- /* Put 'current' back into the tree. */
- __enqueue_entity(cfs_rq, prev); //重新加入cfs運(yùn)行隊(duì)列
- /* in !on_rq case, update occurred at dequeue */
- update_load_avg(cfs_rq, prev, 0);
- }
- cfs_rq->curr = NULL; //設(shè)置cfs運(yùn)行隊(duì)列的curr為NULL
put_prev_task所做的主要工作就是將前一個(gè)任務(wù)從cfs運(yùn)行隊(duì)列中刪除,在這里就是通過調(diào)用__enqueue_entity將對應(yīng)的調(diào)度實(shí)體重新加入cfs隊(duì)列的紅黑樹,但是對于即將睡眠的任務(wù)之前在主調(diào)度器中通過deactivate_task將prev->on_rq設(shè)置為0了,所以對于即將睡眠的任務(wù)來說,它對應(yīng)的調(diào)度實(shí)體不會(huì)在重新加入cfs運(yùn)行隊(duì)列的紅黑樹。
下面來看下睡眠圖示: