Linux進程調度筆記 一:Linux進程的四大要素 1:一段供進程執行的程序,該程序能夠被多個進程執行。 2:獨立的內核堆棧。 3:進程控制快(task_struct:有了這個數據結構,進程才能成爲內核調度的一個基本單位接受內核的調度。同時,這個結構還記錄着進程所佔用的各項資源。 4:獨立的存儲空間:即擁有專有的用戶空間,除了前面的內核空間還有用戶空間。 線程:只有前三條,沒有第四條。 內核線程:徹底沒有用戶空間。 用戶線程:共享用戶空間。 二:Linux進程分類: 1:交互式進程:這些進程常常和用戶發生交互,因此花費一些時間等待用戶的操做。當有輸入時,進程必須很快的激活。一般,要求延遲在50-150毫秒。典型的交互式進程有:控制檯命令,文本編輯器,圖形應用程序。 2:批處理進程(Batch Process):不須要用戶交互,通常在後臺運行。因此不須要很是快的反應,他們常常被調度期限制。典型的批處理進程:編譯器,數據庫搜索引擎和科學計算。 3:實時進程:對調度有很是嚴格的要求,這種類型的進程不能被低優先級進程阻塞,而且在很短的時間內作出反應。典型的實時進程:音視頻應用程序,機器人控制等。 批處理進程可能與I/O或者CPU有關,可是實時進程徹底經過Linux的調度算法識別。 其實交互式進程和批處理進程很難區別。 三:Linux進程優先級 1:靜態優先級(priority): 被稱爲「靜態」是由於它不隨時間而改變,只能由用戶進行修改。它指明瞭在被迫和其它進程競爭CPU以前該進程所應該被容許的時間片的最大值(20)。 每一個普通進程都一個靜態優先級,內核爲其分配的優先級數爲:100(高優先級)-139(低優先級)。數值越大,優先級越低。新建立的進程通常繼承父進程 的優先級,可是用戶能夠經過給nice()函數傳遞「nice value「或者setpriority()改變優先級。 2: 動態優先級(counter): counter 即系統爲每一個進程運行而分配的時間片,Linux 兼用它來表示進程的動態優先級。只要進程擁有CPU,它就隨着時間不斷減少;當它爲0 時,標記進程從新調度。它指明瞭在當前時間片中所剩餘的時間量(最初爲20) 事實上,在進程在調度的時候,調度器只察看動態優先級,其值爲100-139。經過下面的公式能夠根據靜態優先計算出相應的動態優先級。 Dynamicy priority = max (100, min (static priority - bonus + 5, 139)) Bonus:0-10,比5小,下降動態優先級,反之,能夠提升動態優先級。Bonus和進程的平均睡眠時間有關。 3: 實時優先級(rt_priority):值爲1000。Linux把實時優先級與counter值相加做爲實時進程的優先權值。較高權值的進程老是優先於較低權值的進程,若是一個進程不是實時進程, 其優先權就遠小於1000,因此實時進程老是優先。 4:Base time quantum:是由靜態優先級決定,當進程耗盡當前Base time quantum,kernel會從新分配一個Base time quantum給它。靜態優先級和Base time quantum的關係爲: (1) 當靜態優先級小於120 Base time quantum(in millisecond)= (140 – static priority) * 20 (2) 當靜態優先級大於等於120 Base time quantum(in millisecond)= (140 – static priority) * 5 四:Linux 進程的調度算法 1. 時間片輪轉調度算法(round-robin):SCHED_RR,用於實時進程。系統使每一個進程依次地按時間片輪流執行的方式。 2. 優先權調度算法:SCHED_NORMAL,用於非實時進程。系統選擇運行隊列中優先級最高的進程運行。Linux 採用搶佔式的優級算法,即系統中當前運行的進程永遠是可運行進程中優先權最高的那個。 3. FIFO(先進先出) 調度算法:SCHED_FIFO,實時進程按調度策略分爲兩種。採用FIFO的實時進程必須是運行時間較短的進程,由於這種進程一旦得到CPU 就只有等到它運行完或因等待資源主動放棄CPU時其它進程才能得到運行機會。 五:Linux 進程的調度時機 1.進程狀態轉換時: 如進程終止,睡眠等; 2.可運行隊列中增長新的進程時; 3.當前進程的時間片耗盡時; 4.進程從系統調用返回到用戶態時; 5.內核處理完中斷後,進程返回到用戶態; 六:進程隊列: 對隊列都有初始化、添加、刪除等功能。 1:運行隊列:Linux系統爲處於就緒態的進程的隊列,只有在這個隊列中的進程纔有機會得到CPU。 2:等待隊列:,Linux系統也爲處於睡眠態的進程組建了一個隊列。 七:調度使用的數據結構 1:runqueue Runqueu是調度器中很是重要的一個數據結構,每一個CPU都有本身的runqueue。 requeue Type Name Description spinlock_t lock 保護進程列表的自旋鎖 unsigned long nr_running runqueue 列表中可運行進程數。 unsigned long cpu_load 基於runqueue平均進程數的CPU 加載因子 unsigned long nr_switches CPU運行的進程切換次數 unsigned long nr_uninterruptible 曾經在runqueue可是如今處於 TASK_UNINTERRUPTIBLE 狀態的進程數 unsigned long expired_timestamp 老進程已經插入expired列表中的時間 unsigned long long timestamp_last_tick 最後一次時鐘中斷的Timestamp值 task_t * curr 當前運行進程描述符的指針 task_t * idle 進程描述符指針,指向當前CPU的swappe進程 struct mm_struct * prev_mm 在進程卻換工程中,保存正被替換的進程的地址空間 prio_array_t * active 指向激活進程列表(arrays 中的一個) prio_array_t * expired 指向expired進程列表(arrays 中的一個) prio_array_t [2] arrays 激活和expired進程的2維數組,每一個prio_array_t表明一組可運行進程,140個雙向列表,靜態bitmap以及這組進程的counter. int best_expired_prio 在expired進程中最低的靜態優先級 atomic_t nr_iowait 曾經在runqueue可是如今正在等待I/O操做完成的進程數 struct sched_domain * sd 指向當前CPU的基本調度域 int active_balance 標誌一些進程將被從一個requeue轉移到其餘requeue隊列 int push_cpu 沒有使用 task_t * migration_thread 內核轉移線程的進程描述符 struct list_head migration_queue 將被從requeue中轉移的進程列表 九:調度使用的重要函數 調度須要一系列函數配合完成調度這一功能,其中最重要的以下: 調度重要函數 scheduler_tick 更新當前進程的time_slice。該函數有兩種調用途徑: 1:timer,調用頻率爲HZ,而且在關中斷的狀況下調用。 2:fork代碼,當改變父進程的timeslice時。 try_to_wake_up 喚醒sleep進程。當進程不在可運行隊列時,將其放在可運行隊列。 recalc_task_prio 更新進程的動態優先級 schedule 選擇一個進程運行 load_balance 保持多系統下runqueue平衡。檢查當前CPU,保證一個域中runqueue平衡。 1:在進程卻換前,scheduler作的事情 Schedule所做的事情是用某一個進程替換當前進程。 (1) 關閉內核搶佔,初始化一些局部變量。 need_resched: preempt_disable( ); prev = current; rq = this_rq( ); 當前進程current被保存在prev,和當前CPU相關的runqueue的地址保存在rq中。 (2) 檢查prev沒有持有big kernel lock. if (prev->lock_depth >= 0) up(&kernel_sem); Schedule沒有改變lock_depth的值,在prev喚醒本身執行的狀況下,若是lock_depth的值不是負的,prev須要從新獲取kernel_flag自旋鎖。因此大內核鎖在進程卻換過程當中是自動釋放的和自動獲取的。 (3) 調用sched_clock( ),讀取TSC,而且將TSC轉換成納秒,獲得的timestamp保存在now中,而後Schedule計算prev使用的時間片。 now = sched_clock( ); run_time = now - prev->timestamp; if (run_time > 1000000000) run_time = 1000000000; (4) 在察看可運行進程的時候,schedule必須關閉當前CPU中斷,而且獲取自旋鎖保護runqueue. spin_lock_irq(&rq->lock); (5) 爲了識別當前進程是否已經終止,schedule檢查PF_DEAD標誌。 if (prev->flags & PF_DEAD) prev->state = EXIT_DEAD; (6) Schedule檢查prev的狀態,若是它是不可運行的,而且在內核態沒有被搶佔,那麼從runqueue刪除它。可是,若是prev有非阻塞等待信號 而且它的狀態是TASK_INTERRUPTBLE,設置其狀態爲TASK_RUNNING,而且把它留在runqueue中。該動做和分配CPU給 prev不同,只是給prev一個從新選擇執行的機會。 if (prev->state != TASK_RUNNING && !(preempt_count() & PREEMPT_ACTIVE)) { if (prev->state == TASK_INTERRUPTIBLE && signal_pending(prev)) prev->state = TASK_RUNNING; else { if (prev->state == TASK_UNINTERRUPTIBLE) rq->nr_uninterruptible++; deactivate_task(prev, rq); } } deactivate_task( )是從runqueue移除進程: rq->nr_running--; dequeue_task(p, p->array); p->array = NULL; (7) 檢查runqueue中進程數, A:若是有多個可運行進程,調用dependent_sleeper( )函數。通常狀況下,該函數當即返回0,可是若是內核支持超線程技術,該函數檢查將被運行的進程是否有比已經運行在同一個物理CPU上一個邏輯CPU上的 兄弟進程的優先級低。若是是,schedule拒絕選擇低優先級進程,而是執行swapper進程。 if (rq->nr_running) { if (dependent_sleeper(smp_processor_id( ), rq)) { next = rq->idle; goto switch_tasks; } } B:若是沒有可運行進程,調用idle_balance( ),從其餘runqueue隊列中移動一些進程到當前runqueue,idle_balance( )和load_balance( )類似。 if (!rq->nr_running) { idle_balance(smp_processor_id( ), rq); if (!rq->nr_running) { next = rq->idle; rq->expired_timestamp = 0; wake_sleeping_dependent(smp_processor_id( ), rq); if (!rq->nr_running) goto switch_tasks; } } 若是idle_balance( )移動一些進程到當前runqueue失敗,schedule( )調用wake_sleeping_dependent( )從新喚醒空閒CPU的可運行進程。 假設schedule( )已經決定runqueue中有可運行進程,那麼它必須檢查可運行進程中至少有一個進程是激活的。若是沒有,交換runqueue中active 和expired域的內容,全部expired進程變成激活的,空數組準備接受之後expire的進程。 if (unlikely(!array->nr_active)) { /* * Switch the active and expired arrays. */ schedstat_inc(rq, sched_switch); rq->active = rq->expired; rq->expired = array; array = rq->active; rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; } (8) 查找在active prio_array_t數組中的可運行進程。Schedule在active數組的位掩碼中查找第一個非0位。當優先級列表不爲0的時候,相應的位掩碼 北設置,因此第一個不爲0的位標示一個有最合適進程運行的列表。而後列表中第一個進程描述符被獲取。 idx = sched_find_first_bit(array->bitmap); queue = array->queue + idx; next = list_entry(queue->next, task_t, run_list); 如今next指向將替換prev的進程描述符。 (9) 檢查next->activated,它標示喚醒進程的狀態。 (10) 若是next是一個普通進程,而且是從TASK_INTERRUPTIBLE 或者TASK_STOPPED狀態喚醒。Scheduler在進程的平均睡眠時間上加從進程加入到runqueue開始的等待時間。 if (!rt_task(next) && next->activated > 0) { unsigned long long delta = now - next->timestamp; if (unlikely((long long)(now - next->timestamp) < 0)) delta = 0; if (next->activated == 1) delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128; array = next->array; new_prio = recalc_task_prio(next, next->timestamp + delta); if (unlikely(next->prio != new_prio)) { dequeue_task(next, array); next->prio = new_prio; enqueue_task(next, array); } else requeue_task(next, array); } next->activated = 0; Scheduler區分被中斷或者被延遲函數喚醒的進程與被系統調用服務程序或者內核線程喚醒的進程。前者,Scheduler加整個runqueue等待時間,後者只加一部分時間。 2:進程卻換時,Scheduler作的事情: 如今,Scheduler已經肯定要運行的進程。 (1) 訪問next的thread_info,它的地址保存在next進程描述符的頂部。 switch_tasks: if (next == rq->idle) schedstat_inc(rq, sched_goidle); prefetch(next) (2) 在替換prev前,執行一些管理工做 clear_tsk_need_resched(prev); rcu_qsctr_inc(task_cpu(prev)); clear_tsk_need_resched清除prev的TIF_NEED_RESCHED,該動做只發生在Scheduler是被間接調用的狀況。 (3) 減小prev的平均睡眠時間到進程使用的cpu時間片。 prev->sleep_avg -= run_time; if ((long)prev->sleep_avg <= 0) prev->sleep_avg = 0; prev->timestamp = prev->last_ran = now; (4) 檢查是否prev和next是同一個進程,若是爲真,放棄進程卻換,不然,執行(5) if (prev == next) { spin_unlock_irq(&rq->lock); goto finish_schedule; } (5) 真正的進程卻換 next->timestamp = now; rq->nr_switches++; rq->curr = next; ++*switch_count; prepare_task_switch(rq, next); prev = context_switch(rq, prev, next); context_switch創建了next的地址空間,進程描述符的active_mm指向進程使用的地址空間描述符,而mm指向進程擁有的地址空間描 述符,一般兩者是相同的。可是內核線程沒有本身的地址空間,mm一直爲NULL。若是next爲內核線程,context_switch保證next使用 prev的地址空間。若是next是一個正常的進程,context_switch使用next的替換prev的地址空間。 struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (unlikely(!mm)) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next); 若是prev是一個內核線程或者正在退出的進程,context_switch在runqueue的prev_mm中保存prev使用的內存空間。 if (unlikely(!prev->mm)) { prev->active_mm = NULL; WARN_ON(rq->prev_mm); rq->prev_mm = oldmm; } 調用switch_to(prev, next, prev)進行prev和next的切換。(參見「進程間的切換「)。 3:進程切換後的工做 (1) finish_task_switch(): struct mm_struct *mm = rq->prev_mm; unsigned long prev_task_flags; rq->prev_mm = NULL; prev_task_flags = prev->flags; finish_arch_switch(prev); finish_lock_switch(rq, prev); if (mm) mmdrop(mm); if (unlikely(prev_task_flags & PF_DEAD)) put_task_struct(prev) 若是prev是內核線程,runqueue的prev_mm保存prev的內存空間描述符。Mmdrop減小內存空間的使用數,若是該數爲0,該函數釋放內存空間描述符,以及與之相關的頁表和虛擬內存空間。 finish_task_switch()還釋放runqueue的自選鎖,開中斷。 (2) 最後 prev = current; if (unlikely(reacquire_kernel_lock(prev) < 0)) goto need_resched_nonpreemptible; preempt_enable_no_resched(); if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) goto need_resched; schedule獲取大內核塊,從新使內核能夠搶佔,而且檢查是否其餘進程設置了當前進程的TIF_NEED_RESCHED,若是真,從新執行schedule,不然該程序結束。