<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 22226字,需瀏覽 45分鐘

           ·

          2021-08-10 14:19

          1開場白

          環(huán)境:
          • 處理器架構:arm64
          • 內核源碼:linux-5.10.50
          • ubuntu版本:20.04.1
          • 代碼閱讀工具:vim+ctags+cscope

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

          注:由于篇幅問題,文章分為上下兩篇,且這里不區(qū)分進程和任務,統(tǒng)一使用任務來表示進程。

          主要講解以下內容:

          • 睡眠的三種狀態(tài)

          • 睡眠的內核原理

          • 用戶態(tài)睡眠

          • 內核態(tài)睡眠

          • 總結

          2. 睡眠的三種狀態(tài)

          任務睡眠有三種狀態(tài):

          淺度睡眠 

          中度睡眠 

          深度睡眠

          2.1 淺度睡眠

          進程描述符的state使用TASK_INTERRUPTIBLE表示這種狀態(tài)。

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

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

          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喚醒從處于可中斷睡眠狀態(tài)的任務。

          2.2 中度睡眠

          進程描述符的state使用TASK_KILLABLE表示這種狀態(tài)。

          可以被致命信號所打斷。

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

          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表示這種狀態(tài)。

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

          3. 睡眠的內核原理

          睡眠都是主動發(fā)生調度,即主動調用主調度器。

          睡眠的主要步驟如下:

          1)設置任務狀態(tài)為睡眠狀態(tài) 

          2)記錄睡眠的任務 

          3)發(fā)起主動調度

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

          3.1 設置任務狀態(tài)為睡眠狀態(tài)

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

          注:睡眠狀態(tài)描述見上一小節(jié)!

          3.2 記錄睡眠的任務

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

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

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

          3.3 發(fā)起主動調度

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

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

          kernel/sched/core.c

          __schedule
          ->
              prev_state = prev->state;     //獲得前一個任務狀態(tài)
              if (!preempt && prev_state) {  //如果是主動調度   且任務狀態(tài)不為0                         
                      if (signal_pending_state(prev_state, prev)) {   //有掛起的信號
                              prev->state = TASK_RUNNING;       //設置狀態(tài)為可運行      
                      } 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隊列。

          可以看到,發(fā)起主動調度的時候,在主調度器中會做判斷:如果是主動調度且任務狀態(tài)不為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運行隊列的紅黑樹。

          下面來看下睡眠圖示:


          4.用戶態(tài)睡眠

          以sleep為例來說明任務在用戶態(tài)是如何睡眠的。

          首先我們通過strace工具來看下其調用的系統(tǒng)調用:

          $ strace sleep 1

          ...
          close(3)                                = 0
          clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, NULL) = 0
          close(1)                                = 0
          ...

          可以發(fā)現sleep主要調用clock_nanosleep系統(tǒng)調用來進行睡眠(也就是說用戶態(tài)任務睡眠需要調用系統(tǒng)調用陷入內核)。

          下面我們來研究下clock_nanosleep的實現(這里集中到睡眠的實現,先忽略掉定時器等諸多的技術細節(jié)):

          kernel/time/posix-timers.c

          SYSCALL_DEFINE4(clock_nanosleep
          ->const struct k_clock *kc = clockid_to_kclock(which_clock);  //根據時鐘類型得到內核時鐘結構
              return kc->nsleep(which_clock, flags, &t); //調用內核時鐘結構的nsleep回調

          我們傳遞過來的時鐘類型為CLOCK_REALTIME,則調用鏈為:

          kc->nsleep(CLOCK_REALTIME, flags, &t)
          ->clock_realtime.nsleep
              ->common_nsleep
                  ->hrtimer_nanosleep  //kernel/time/hrtimer.c
                      ->hrtimer_init_sleeper_on_stack
                              ->__hrtimer_init_sleeper
                                  ->__hrtimer_init(&sl->timer, clock_id, mode); //初始化高精度定時器
                                      sl->timer.function = hrtimer_wakeup;  //設置超時回調函數
                                      sl->task = current;.//設置超時時要喚醒的任務
                               ->do_nanosleep  //睡眠操作

          可以看到,睡眠函數最終調用到hrtimer_nanosleep,它調用了兩個主要函數:__hrtimer_init_sleeper和do_nanosleep,前者主要設置高精度定時器,后者就是真正的睡眠,主要來看下 do_nanosleep:

           kernel/time/hrtimer.c
           do_nanosleep
           ->
                   do {
                           set_current_state(TASK_INTERRUPTIBLE);  //設置可中斷的睡眠狀態(tài)
                           hrtimer_sleeper_start_expires(t, mode); //開啟高精度定時器

                           if (likely(t->task))
                                   freezable_schedule(); //主動調度
                             

                           hrtimer_cancel(&t->timer);
                           mode = HRTIMER_MODE_ABS;

                   } while (t->task && !signal_pending(current));  //是否記錄的有任務且沒有掛起的信號

                   __set_current_state(TASK_RUNNING);  //設置為可運行狀態(tài)


          do_nanosleep函數是睡眠的核心實現:首先設置任務的狀態(tài)為可中斷的睡眠狀態(tài),然后開啟了之前設置的高精度定時器,隨即調用freezable_schedule進行真正的睡眠。

          來看下freezable_schedule:

          //include/linux/freezer.h
          freezable_schedule
          ->schedule()
              ->__schedule(false); 
           

          可以看到最終調用主調度器__schedule進行主動調度。

          當任務睡眠完成,定時器超時,會調用之前在__hrtimer_init_sleeper設置的超時回調函數hrtimer_wakeup將睡眠的任務喚醒(關于進程喚醒在這里就不在贅述,在后面的進程喚醒專題文章在進行詳細解讀),然后就可以再次獲得處理器的使用權了。

          總結:處于用戶態(tài)的任務,如果想要睡眠一段時間必須向內核請求服務(如調用clock_nanosleep系統(tǒng)調用),內核中會設置一個高精度定時器,來記錄要睡眠的任務,然后設置任務狀態(tài)為可中斷的睡眠狀態(tài),緊接著發(fā)生主動調度,這樣任務就發(fā)生睡眠了。

          5.內核態(tài)睡眠

          當任務處于內核態(tài)時,有時候也需要睡眠一段時間,不像任務處于用戶態(tài)需要發(fā)生系統(tǒng)調用來請求內核進行睡眠,在內核態(tài)可以直接調用睡眠函數。當然,內核態(tài)中,睡眠有兩種場景:一種是睡眠特定的時間的延遲操作(喚醒條件為超時),一種是等待特定條件滿足(如IO讀寫完成,可睡眠的鎖被釋放等)。

          下面分別以msleep和mutex鎖為例講解內核態(tài)睡眠:

          5.1 msleep

          msleep做ms級別的睡眠延遲。

          //kernel/time/timer.c
          void msleep(unsigned int msecs)
          {
                  unsigned long timeout = msecs_to_jiffies(msecs) + 1;  //ms時間轉換為jiffies

                  while (timeout)
                          timeout = schedule_timeout_uninterruptible(timeout);  //不可中斷睡眠
          }

          下面看下schedule_timeout_uninterruptible:

          這里涉及到一個重要數據結構process_timer

          struct process_timer {
                  struct timer_list timer;  //定時器結構
                  struct task_struct *task; //定時器到期要喚醒的任務
          };

          schedule_timeout_uninterruptible
          ->  __set_current_state(TASK_UNINTERRUPTIBLE);  //設置任務狀態(tài)為不可中斷睡眠
            return schedule_timeout(timeout); 
              ->expire = timeout + jiffies;   //計算到期時的jiffies值
                  timer.task = current; //記錄定時器到期要喚醒的任務 為當前任務
                  timer_setup_on_stack(&timer.timer, process_timeout, 0);  //初始化定時器   超時回調為process_timeout
                  __mod_timer(&timer.timer, expire, MOD_TIMER_NOTPENDING); //添加定時器
                  schedule();  //主動調度

          再看下超時回調為process_timeout:

          process_timeout
           ->struct process_timer *timeout = from_timer(timeout, t, timer); //通過定時器結構獲得process_timer
              wake_up_process(timeout->task); //喚醒其管理的任務

          可以看到,msleep實現睡眠也是通過定時器,首先設置當前任務狀態(tài)為不可中斷睡眠,然后設置定時器超時時間為傳遞的ms級延遲轉換的jiffies,超時回調為process_timeout,然后將定時器添加到系統(tǒng)中,最后調用schedule發(fā)起主動調度,當定時器超時的時候調用process_timeout來喚醒睡眠的任務。

          5.2 mutex鎖

          mutex鎖是可睡眠鎖的一種,當申請mutex鎖時發(fā)現其他內核路徑已經持有這把鎖,當前任務就會睡眠等待在這把鎖上。

          下面我們來看他的實現,主要看睡眠的部分:

          kernel/locking/mutex.c

          mutex_lock
          ->__mutex_lock_slowpath
              ->__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_)  //睡眠的狀態(tài)為不可中斷睡眠
                  ->__mutex_lock_common
                      ->
                      ...
                      waiter.task = current;  //記錄需要喚醒的任務為當前任務
                      set_current_state(state);  //設置睡眠狀態(tài)
                      for (;;) {
                          
                               if (__mutex_trylock(lock))  //嘗試獲得鎖
                                   goto acquired;

                              schedule_preempt_disabled(); 
                                  ->schedule();  //主動調度

                      }
                 acquired:
                      __set_current_state(TASK_RUNNING);//設置狀態(tài)為可運行狀態(tài)

          可以看到mutex鎖實現睡眠套路和之前是一樣的:申請mutex鎖的時候,如果其他內核路徑已經持有這把鎖,首先通過mutex鎖的相關結構來記錄下當前任務,然后設置任務狀態(tài)為不可中斷睡眠,接著在一個for循環(huán)中調用schedule_preempt_disabled發(fā)生主動調度,于是當前任務就睡眠在這把鎖上。當其他內核路徑釋放了這把鎖,就會喚醒等待在這把鎖上的任務,當前任務就獲得了這把鎖,然后進入鎖的臨界區(qū),喚醒操作就完成了(關于喚醒的技術細節(jié),后面的喚醒專題會詳細講解)。

          6.總結

          進程睡眠按照應用場景可以分為:延遲睡眠和等待某些特定條件而睡眠,實際上都可以歸于等待某些特定條件而睡眠,因為延遲特定時間也可以作為特定條件。進程睡眠按照進程所處的特權級別可以分為:用戶態(tài)進程睡眠和內核態(tài)進程睡眠,用戶態(tài)進程睡眠需要進程通過系統(tǒng)調用陷入內核來發(fā)起睡眠請求。對于進程睡眠,內核主要需要做三大步操作:1.設置任務狀態(tài)為睡眠狀態(tài) 2.記錄睡眠的任務 3.發(fā)起主動調度。這三大步操作都是非常有必要,第一步設置睡眠狀態(tài)為后面調用主調度器做必要的標識準備;第二步記錄下睡眠的任務是為了以后喚醒任務來準備的;第三步是睡眠的主體部分,這里會將睡眠的任務從運行隊列中踢出,選擇下一個任務運行。


          瀏覽 125
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  欧美日韩国产在线播放 | 欧美特黄一区二区三区 | 欧美性精品性爱第一页 | 少妇大战28厘米黑人 | 国产伦精品一区二区三区视频痴汉 |