深入理解Linux內核之進程睡眠(上)

宋寶華 2021-08-15 06:31:09 阅读数:369

本文一共[544]字,预计阅读时长:1分钟~
深入 入理 理解 linux 睡眠

1開場白

環境:

  • 處理器架構:arm64

  • 內核源碼:linux-5.10.50

  • ubuntu版本:20.04.1

  • 代碼閱讀工具:vim+ctags+cscope

無論是任務處於用戶態還是內核態,經常會因為等待某些事件而睡眠(可能是等待IO讀寫完成,也可能等待其他內核路徑釋放一把鎖等)。本文來探討一下,任務處於睡眠中有哪些狀態?睡眠對於任務來說究竟意味著什麼?內核是如何管理睡眠的任務的?我們會結合內核源代碼來分析任務的睡眠,力求全方比特角度來剖析。

注:由於篇幅問題,文章分為上下兩篇,且這裏不區分進程和任務,統一使用任務來錶示進程。

主要講解以下內容:

  • 睡眠的三種狀態

  • 睡眠的內核原理

  • 用戶態睡眠

  • 內核態睡眠

  • 總結

2. 睡眠的三種狀態

任務睡眠有三種狀態:

淺度睡眠 

中度睡眠 

深度睡眠

2.1 淺度睡眠

進程描述符的state使用TASK_INTERRUPTIBLE錶示這種狀態。

為可中斷的睡眠狀態,這裏可中斷是可以被信號所打斷(喚醒)。

這裏給出被信號打斷/喚醒的代碼路徑:

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

可以看到在信號傳遞的時候,會通過signal_wake_up喚醒從處於可中斷睡眠狀態的任務。

2.2 中度睡眠

進程描述符的state使用TASK_KILLABLE錶示這種狀態。

可以被致命信號所打斷。

這裏給出被致命信號打斷/喚醒的代碼路徑:

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)) {  //致命信號
                            
                                    ...
                                    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 深度睡眠

進程描述符的state使用TASK_UNINTERRUPTIBLE錶示這種狀態。

為不可中斷的睡眠狀態,不能被任何信號所喚醒(特定條件沒有滿足發生信號喚醒可能導致數據不一致等問題,這種場景使用這種睡眠狀態,如等待IO讀寫完成)。

3. 睡眠的內核原理

睡眠都是主動發生調度,即主動調用主調度器。

睡眠的主要步驟如下:

1)設置任務狀態為睡眠狀態 

2)記錄睡眠的任務 

3)發起主動調度

下面我們來詳細解讀下這幾個步驟:

3.1 設置任務狀態為睡眠狀態

這一步很有必要,一來標識進入了睡眠狀態,二來是主調度器會根據睡眠標志將任務從運行隊列删除。

注:睡眠狀態描述見上一小節!

3.2 記錄睡眠的任務

這一步也非常有必要,內核會將即將睡眠的任務記錄下來,要麼加入到鏈錶中管理,要麼使用數據結構記錄。

如延遲睡眠場景,內核將即將睡眠的任務記錄在定時器相關的數據結構中;可睡眠的信號量場景中,內核將即將睡眠的任務加入到信號量的相關鏈錶中。

記錄的目的在於:當喚醒條件滿足時,喚醒函數能够找到想要喚醒的任務。

3.3 發起主動調度

這一步是真正進行睡眠的操作,主要是調用主調度器來發起主動調度讓出處理器。

下面我們來看下主調度器為任務睡眠所作的處理:

kernel/sched/core.c
__schedule
->
    prev_state = prev->state;     //獲得前一個任務狀態
    if (!preempt && prev_state) {  //如果是主動調度   且任務狀態不為0                         
            if (signal_pending_state(prev_state, prev)) {   //有掛起的信號
                    prev->state = TASK_RUNNING;       //設置狀態為可運行      
            } else {                                        
                  deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);  //cpu運行隊列中删除任務
            }
    }
    
   next = pick_next_task(rq, prev, &rf);  //選擇下一個任務
   context_switch  //進行上下文切換

來看下deactivate_task對於睡眠任務做的主要工作:

deactivate_task
->deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK)
    ->p->on_rq = (flags & DEQUEUE_SLEEP) ? 0 : TASK_ON_RQ_MIGRATING;  //設置任務的on_rq 為0  標識是睡眠
    dequeue_task(rq, p, flags);
    ->p->sched_class->dequeue_task(rq, p, flags)
        ->dequeue_task_fair
            ->dequeue_entity
            
                ...
                if (se != cfs_rq->curr)        //不是cpu當前 任務
                      __dequeue_entity(cfs_rq, se); //cfs運行隊列删除
                ->se->on_rq = 0;  //標識調度實體不在運行隊列!!!
                
                ->if (!(flags & DEQUEUE_SLEEP))
                       se->vruntime -= cfs_rq->min_vruntime; //調度實體的虛擬運行時間 减去 cfs運行隊列的最小虛擬運行時間 

deactivate_task會設置任務的on_rq 為0來 標識是睡眠 ,然後 調用到調度類的dequeue_task方法,在cfs中設置se->on_rq = 0標識調度實體不在cfs隊列。

可以看到,發起主動調度的時候,在主調度器中會做判斷:如果是主動調度且任務狀態不為0 (即為不是可運行的TASK_RUNNING)時,如果沒有掛起的信號,就會將任務從cpu的運行隊列中“删除”,然後選擇下一個任務,進行上下文切換。

將即將睡眠的任務從cpu的運行隊列中“删除”意義重大:主調度器再次選擇下一個任務的時候不會在選擇睡眠的任務(因為主調度器總是在運行隊列中選擇任務運行,除非任務被喚醒,重新加入運行隊列)。

注意:1.這裏的删除指的是設置對應標志如p->on_rq=0,se->on_rq = 0,當選擇下一個任務的時候不會在加入運行隊列中。2.即將睡眠的任務是cpu上的當前任務(curr指向)。3.調用主調度器後,即將睡眠的任務不會再次加入cpu運行隊列,除非被喚醒。

再來看下選擇下一個任務的時候會做哪些事情和睡眠有關(暫不考慮組調度情况):

pick_next_task
->class->pick_next_task
    ->pick_next_task_fair  //kernel/sched/fair.c
        ->if (prev)                          
           put_prev_task(rq, prev);   //對前一個任務處理
          se = pick_next_entity(cfs_rq, NULL); //選擇下一個任務
        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) { //前一個任務的調度實體on_rq不為0?
                update_stats_wait_start(cfs_rq, prev);
                /* Put 'current' back into the tree. */
                __enqueue_entity(cfs_rq, prev);   //重新加入cfs運行隊列
                /* in !on_rq case, update occurred at dequeue */
                update_load_avg(cfs_rq, prev, 0);
              }
           cfs_rq->curr = NULL; //設置cfs運行隊列的curr為NULL

put_prev_task所做的主要工作就是將前一個任務從cfs運行隊列中删除,在這裏就是通過調用__enqueue_entity將對應的調度實體重新加入cfs隊列的紅黑樹,但是對於即將睡眠的任務之前在主調度器中通過deactivate_task將prev->on_rq設置為0了,所以對於即將睡眠的任務來說,它對應的調度實體不會在重新加入cfs運行隊列的紅黑樹

下面來看下睡眠圖示:

版权声明:本文为[宋寶華]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/08/20210815063039047f.html