做者:世至其美
博客地址:hqber.com
轉載須註明以上信息, 更多文章,請訪問我的博客:hqber.comnode
最大限度地利用處理器時間,只要有能夠執行的進程,那麼就總會有進程正在執行。linux
按多任務系統分類算法
按進程分類c#
根據進程的價值和其對處理器的時間需求對進程進行分級。安全
Linux採用了兩種優先級範圍ide
分配給每一個可運行進程的處理器時間段。模塊化
注意:如今操做系統對程序運行都採用了動態時間片計算的方式,而且引入了可配置的計算策略。Linux的「公平」調度算法自己並無採起時間片來達到公平調度。函數
Linux調度算法中,Linux調度器是以模塊的方式提供的,這種模塊化結構叫作調度器類。每一個調度器都有一個優先級,基礎調度器(<kernel/sched/core.c>)會按照優先級順序遍歷調度類,擁有一個可執行進程的最高優先級的調度器類勝出。優化
一個針對普通進程的公平調度類。(SCHED_NORMAL)<kernel/sched/fair.c>Linux的CFS調度器沒有規定時間片的大小,經過nice值做爲權重調整處理器的使用比,nice值越小的進程被賦予高權重,高優先級,搶得更多的處理器時間使用比。最後經過計算(線程的處理器使用比*總處理器時間)獲取每一個進程的處理器時間。搶佔時機是當新進程消耗的處理器使用比小於當前的進程,則新進程馬上投入運行,搶佔當前進程。還設置了每一個進程得到的時間片底線(最小粒度),默認值是1ms,爲了不可運行任務數量趨於無限,他們各自獲取的處理器使用比和時間片都將趨於0,進程切換開銷將是不可接受的。ui
特別關注如下四個方面:
調度器實體結構
CFS使用調度器的實體結構(源代碼 | linux/sched.h | v4.19)追蹤進程運行記帳,而後將實體結構體做爲se的成員變量,嵌入在進程描述符struct task_struct內。
struct sched_entity { /* For load-balancing: */ struct load_weight load;// 權重,跟優先級有關 unsigned long runnable_weight;// 在全部可運行進程中所佔的權重 struct rb_node run_node;// 紅黑樹的節點 struct list_head group_node;// 所在進程組 unsigned int on_rq;// 標記是否處於紅黑樹運行隊列中 u64 exec_start;// 進程開始執行的時間 u64 sum_exec_runtime;// 進程總運行時間 u64 vruntime;// 虛擬運行時間 u64 prev_sum_exec_runtime;// 上個週期中sum_exec_runtime u64 nr_migrations; struct sched_statistics statistics; // 如下省略了一些在特定宏條件下才會啓用的變量 };
虛擬實時
vruntime變量存放進程的虛擬時間,單位爲ns,和定時器節拍再也不相關。由於優先級相同的全部進程的虛擬運行時間是相同的,全部進程都將接收到相等的處理器份額。處理器只能是依次運行每一個進程,沒法實現多任務運行。
所以,CFS使用vruntime變量來記錄一個程序到底運行了多長時間以及還須要運行多久。update_curr函數是由系統定時器週期性調用的,不管進程在哪一種狀態。
update_curr函數(源代碼 | kernel/sched/fair.c | v4.19)
/* * 計算當前進程的執行時間,存放在delta_exec */ static void update_curr(struct cfs_rq *cfs_rq) { struct sched_entity *curr = cfs_rq->curr; u64 now = rq_clock_task(rq_of(cfs_rq)); u64 delta_exec; if (unlikely(!curr)) return; /*獲取從最後一次修改負載後當前任務所佔用的運行總時間 */ delta_exec = now - curr->exec_start; if (unlikely((s64)delta_exec <= 0)) return; //設置開始時間 curr->exec_start = now; //根據當前進程總數對運行時間進行加權計算 schedstat_set(curr->statistics.exec_max, max(delta_exec, curr->statistics.exec_max)); curr->sum_exec_runtime += delta_exec; schedstat_add(cfs_rq->exec_clock, delta_exec); curr->vruntime += calc_delta_fair(delta_exec, curr); update_min_vruntime(cfs_rq); if (entity_is_task(curr)) { struct task_struct *curtask = task_of(curr); trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime); cgroup_account_cputime(curtask, delta_exec); account_group_exec_runtime(curtask, delta_exec); } account_cfs_rq_runtime(cfs_rq, delta_exec); }
在進程選擇方面,CFS調度算法核心是選擇最小vruntime的任務,CFS是經過紅黑樹來組織可運行進程隊列,並利用其迅速找到最小的vruntime值的進程。(紅黑樹最左側的葉子節點)
挑選下一個Task
__pick_next_entity函數(源代碼 | kernel/sched/fair.c | v4.19)
static struct sched_entity *__pick_next_entity(struct sched_entity *se) { //獲取紅黑樹中vruntime值最小的可運行進程 struct rb_node *next = rb_next(&se->run_node); if (!next) return NULL; return rb_entry(next, struct sched_entity, run_node); }
若是沒有可運行線程,CFS調度器會選擇idle的線程執行。
向樹中加入進程
當一個新的進程狀態轉換爲可運行時,須要向可運行隊列中插入一個新的節點。而這個過程本質上是向紅黑樹中插入新節點的過程。
這會發生在兩種狀況下:
enqueue_entity函數(源代碼 | kernel/sched/fair.c | v4.19)
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED); bool curr = cfs_rq->curr == se; /* * 若是要加入的進程就是當前正在運行的進程,從新規範化vruntime */ if (renorm && curr) se->vruntime += cfs_rq->min_vruntime; //更新「當前任務」運行時的統計數據 update_curr(cfs_rq); /* * 若是不是當前正在運行的進程,也要恢復到當前的時間 */ if (renorm && !curr) se->vruntime += cfs_rq->min_vruntime; /* * 更新對應調度器實體的各類記錄值 */ //更新同步實體和cfs_rq update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH); //將se加入group中 update_cfs_group(se); //從新分配權重佔比,將其新的權重添加到cfs_rq-> load.weight enqueue_runnable_load_avg(cfs_rq, se); account_entity_enqueue(cfs_rq, se); if (flags & ENQUEUE_WAKEUP) place_entity(cfs_rq, se, 0); check_schedstat_required(); update_stats_enqueue(cfs_rq, se, flags); check_spread(cfs_rq, se); if (!curr) __enqueue_entity(cfs_rq, se);//將數據項插入紅黑樹 se->on_rq = 1; if (cfs_rq->nr_running == 1) { list_add_leaf_cfs_rq(cfs_rq); check_enqueue_throttle(cfs_rq); } }
__enqueue_entity函數(源代碼 | kernel/sched/fair.c | v4.19)
/* * 將entity加入紅黑樹(rb-tree) */ static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node; struct rb_node *parent = NULL; struct sched_entity *entry; bool leftmost = true; /* * 在紅黑樹中搜索合適的位置 */ while (*link) { parent = *link; entry = rb_entry(parent, struct sched_entity, run_node); /* * 具備相同鍵值的節點會被放在一塊兒,鍵值是vruntime */ if (entity_before(se, entry)) { link = &parent->rb_left; } else { link = &parent->rb_right; leftmost = false; } } //向樹中插入子節點 rb_link_node(&se->run_node, parent, link); //更新紅黑樹的自平衡相關屬性,經過leftmost判斷該節點是否爲vruntime最小進程 rb_insert_color_cached(&se->run_node, &cfs_rq->tasks_timeline, leftmost); }
從樹中刪除進程
當進程堵塞(不可運行態)或者終止時(結束運行),需從紅黑樹中刪除進程。
dequeue_entity函數(源代碼| kernel/sched/fair.c | v4.19)
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags) { /* * 更新加載,保持entity和cfs_rq同步 */ //從cfs_rq->load.weight中減去entity的權重 update_curr(cfs_rq); //從cfs_rq->runnable_avg中減去entity負載 update_load_avg(cfs_rq, se, UPDATE_TG); //對於group的entity,更新其權重以反映其組的cfs_rq新份額 dequeue_runnable_load_avg(cfs_rq, se); update_stats_dequeue(cfs_rq, se, flags); clear_buddies(cfs_rq, se); //從樹中進行刪除節點 if (se != cfs_rq->curr) __dequeue_entity(cfs_rq, se); se->on_rq = 0; account_entity_dequeue(cfs_rq, se); /* * 從新規範化vruntime */ if (!(flags & DEQUEUE_SLEEP)) se->vruntime -= cfs_rq->min_vruntime; /* 返回剩餘運行的時間 */ return_cfs_rq_runtime(cfs_rq); update_cfs_group(se); if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) != DEQUEUE_SAVE) update_min_vruntime(cfs_rq); }
實際對樹節點操做的工做由__dequeue_entity()實現的,
__dequeue_entity函數(源代碼 | kernel/sched/fair.c | v4.19)
static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) { rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline); }
rb-tree中實現的樹節點刪除函數,[stackoverflow | Linux中的EXPORT_SYMBOL和c語言中的extern的區別](https://stackoverflow.com/questions/9836467/whats-meaning-of-export-symbol-in-linux-kernel-code#:~:text=EXPORT_SYMBOL()%20is%20a%20macro,kernel%20modules%20to%20access%20them.)
rb_erase_cached函數( 源代碼 | lib/rbtree.c)
void rb_erase_cached(struct rb_node *node, struct rb_root_cached *root) { struct rb_node *rebalance; rebalance = __rb_erase_augmented(node, &root->rb_root, &root->rb_leftmost, &dummy_callbacks); if (rebalance) ____rb_erase_color(rebalance, &root->rb_root, dummy_rotate); } EXPORT_SYMBOL(rb_erase_cached);//EXPORT_SYMBOL只是一種相似於extern的機制,但它是可加載模塊之間的參考,而不是文件
進程調度的統一入口是__schedule函數,它會選擇一個最高優先級的調度類,每一個調度類都有本身的可運行隊列,而後能夠知道下一個運行的進程。
__schedule函數(源代碼 | kernel/sched/core.c | v4.19 )
static void __sched notrace __schedule(bool preempt) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq_flags rf; struct rq *rq; int cpu; cpu = smp_processor_id(); rq = cpu_rq(cpu); prev = rq->curr; schedule_debug(prev); if (sched_feat(HRTICK)) hrtick_clear(rq); local_irq_disable(); rcu_note_context_switch(preempt); /* * Make sure that signal_pending_state()->signal_pending() below * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE) * done by the caller to avoid the race with signal_wake_up(). * * The membarrier system call requires a full memory barrier * after coming from user-space, before storing to rq->curr. */ rq_lock(rq, &rf); smp_mb__after_spinlock(); /* Promote REQ to ACT */ rq->clock_update_flags <<= 1; update_rq_clock(rq); switch_count = &prev->nivcsw; if (!preempt && prev->state) { if (unlikely(signal_pending_state(prev->state, prev))) { prev->state = TASK_RUNNING; } else { deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK); prev->on_rq = 0; if (prev->in_iowait) { atomic_inc(&rq->nr_iowait); delayacct_blkio_start(); } /* * If a worker went to sleep, notify and ask workqueue * whether it wants to wake up a task to maintain * concurrency. */ if (prev->flags & PF_WQ_WORKER) { struct task_struct *to_wakeup; to_wakeup = wq_worker_sleeping(prev); if (to_wakeup) try_to_wake_up_local(to_wakeup, &rf); } } switch_count = &prev->nvcsw; } //經過優先級獲取最高優先級的調度類,而後從最高優先級的調度類中選擇最高優先級的進程 next = pick_next_task(rq, prev, &rf); clear_tsk_need_resched(prev); clear_preempt_need_resched(); if (likely(prev != next)) { rq->nr_switches++; rq->curr = next; /* * The membarrier system call requires each architecture * to have a full memory barrier after updating * rq->curr, before returning to user-space. * * Here are the schemes providing that barrier on the * various architectures: * - mm ? switch_mm() : mmdrop() for x86, s390, sparc, PowerPC. * switch_mm() rely on membarrier_arch_switch_mm() on PowerPC. * - finish_lock_switch() for weakly-ordered * architectures where spin_unlock is a full barrier, * - switch_to() for arm64 (weakly-ordered, spin_unlock * is a RELEASE barrier), */ ++*switch_count; trace_sched_switch(preempt, prev, next); /* 上下文切換 */ rq = context_switch(rq, prev, next, &rf); } else { rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP); rq_unlock_irq(rq, &rf); } balance_callback(rq); }
pick_next_task主要功能是從發生調度的CPU運行隊列中選擇最高優先級的進程。
系統中的調度順序爲:實時進程→普通進程→空閒進程。(rt_sched_class → fair_sched_class → idle_sched_class )
pick_next_task函數(源代碼 | kernel/sched/core.c | v4.19)
/* * 挑選最高優先級的任務 */ static inline struct task_struct * pick_next_task(struct rq *rq, struct task_struct *prev) { const struct sched_class *class = &fair_sched_class; struct task_struct *p; /* * 優化:若是當前全部要調度的進程都是普通進程,那麼就直接採用普通進程的調度類(CFS) */ //就緒隊列中的進程數是否與普通進程的就緒隊列中的進程數是否相同 if (likely(prev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running)) { p = fair_sched_class.pick_next_task(rq, prev); if (unlikely(p == RETRY_TASK)) goto again; /* 若是沒有cfs調度類的進程處於就緒狀態,也就是fair_sched_class->next == idle_sched_class(空閒進程), * 每一個CPU都有一個空閒調度類進程,永遠不會阻塞。 */ if (unlikely(!p)) p = idle_sched_class.pick_next_task(rq, prev); return p; } // 若是是實時進程,則遍歷調度類 again: for_each_class(class) { p = class->pick_next_task(rq, prev); if (p) { if (unlikely(p == RETRY_TASK)) goto again; return p; } } BUG(); /* the idle class will always have a runnable task */ }
休眠在Linux中有兩種狀態,一種是TASK_UNINTERRUPTIBLE的進程會忽略信號,另外一種是TASK_INTERRUPTIBLE的進程會在收到信號的時候被喚醒並響應。不過這兩種狀態的進程是處於同一個等待隊列上的,等待事件,不能運行。
等待隊列
等待隊列的實現只是一個簡單的鏈表,由等待某些事件發生的進程組成。wait_queue_head_t表示鏈表的頭節點,加入了一個自旋鎖來保持一致性(等待隊列在中斷時能夠被隨時修改)
wait_queue_head_t定義(源代碼 | /include/linux/wait.h | v4.19)
struct __wait_queue_head { spinlock_t lock; struct list_head task_list; }; typedef struct __wait_queue_head wait_queue_head_t;
進入休眠的進程須要把本身加入到一個等待隊列中,主要流程以下:
add_wait_queue函數(源代碼 | /kernel/sched/wait.c | v4.19)
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&q->lock, flags); __add_wait_queue(q, wait); spin_unlock_irqrestore(&q->lock, flags); }
__add_wait_queue函數(源代碼 | /include/linux/wait.h | v4.19)
static inline void __add_wait_queue(wait_queue_head_t *head, wait_queue_t *new) { //將節點加入鏈表 list_add(&new->task_list, &head->task_list); }
prepare_to_wait函數(源代碼 | /kernel/sched/wait.c | v4.19)
void prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) { unsigned long flags; wait->flags &= ~WQ_FLAG_EXCLUSIVE; spin_lock_irqsave(&q->lock, flags); //等待隊列爲空,則將當前節點加入到鏈表 if (list_empty(&wait->task_list)) __add_wait_queue(q, wait); //設置進程當前狀態 set_current_state(state); spin_unlock_irqrestore(&q->lock, flags); }
喚醒
喚醒操做主要經過調用wake_up函數,它會喚醒指定等待隊列上的全部知足事件的進程,並將對應的進程標記爲TASK_RUNNING狀態,接着將進程加入紅黑樹中。具體調用過程以下:
wake_up函數(源代碼 | /include/linux/wait.h | v4.19)
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
__wake_up函數(源代碼 | /kernel/sched/wait.c | v4.19)
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key) { unsigned long flags; //互斥鎖,保證原子操做 spin_lock_irqsave(&q->lock, flags); __wake_up_common(q, mode, nr_exclusive, 0, key); spin_unlock_irqrestore(&q->lock, flags); }
__wake_up_common函數(源代碼 | /kernel/sched/wait.c | v4.19)
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, int wake_flags, void *key) { wait_queue_t *curr, *next; //遍歷等待隊列 list_for_each_entry_safe(curr, next, &q->task_list, task_list) { unsigned flags = curr->flags; if (curr->func(curr, mode, wake_flags, key) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; } }
從一個可執行進程切換到另外一個可執行的進程。
context_switch函數(源代碼 | /kernel/sched/core.c | v4.19)
static __always_inline struct rq * context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next) { struct mm_struct *mm, *oldmm; prepare_task_switch(rq, prev, next); mm = next->mm; oldmm = prev->active_mm; /* * For paravirt, this is coupled with an exit in switch_to to * combine the page table reload and the switch backend into * one hypercall. */ arch_start_context_switch(prev); if (!mm) { next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next); } else switch_mm(oldmm, mm, next);//負責將虛擬內存從上一個進程映射切換到新進程 if (!prev->mm) { prev->active_mm = NULL; rq->prev_mm = oldmm; } /* * Since the runqueue lock will be released by the next * task (which is an invalid locking op but in the case * of the scheduler it's an obvious special-case), so we * do an early lockdep release here: */ lockdep_unpin_lock(&rq->lock); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* 負責將上一個進程處理器狀態切換到新進程的處理器狀態, * 保存、恢復棧信息和寄存器信息,還有其餘與體系相關的狀態信息 */ switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); }
內核即將返回用戶空間的時候,若是need_resched標誌位被設置,會致使schedule()被調用,此時就發生了用戶搶佔。意思是說,既然要從新進行調度,那麼能夠繼續執行進入內核態以前的那個進程,也徹底能夠從新選擇另外一個進程來運行,因此若是設置了need_resched,內核就會選擇一個更合適的進程投入運行。
簡單來講有如下兩種狀況會發生用戶搶佔:
Linux和其餘大部分的Unix變體操做系統不一樣的是,它支持完整的內核搶佔。
不支持內核搶佔的系統意味着:內核代碼能夠一直執行直到它完成爲止,內核級的任務執行時沒法從新調度,各個任務是以協做方式工做的,並不存在搶佔的可能性。
在Linux中,只要從新調度是安全的,內核就能夠在任什麼時候間搶佔正在執行的任務,這個安全是指,只要沒有持有鎖,就能夠進行搶佔。
爲了支持內核搶佔,Linux作出了以下的變更:
除了響應中斷後返回,還有一種狀況會發生內核搶佔,那就是內核中的進程因爲阻塞等緣由顯式地調用schedule()來進行顯式地內核搶佔。固然,這個進程顯式地調用schedule()調度進程,就意味着它明白本身是能夠安全地被搶佔的,所以咱們不用任何額外的邏輯去檢查安全性問題。
下面羅列可能的內核搶佔狀況:
Linux提供了兩種實時調度策略: SCHED_FIFO和SCHED_RR。 而普通的、非實時的調度策略是SCHED_NORMAL。實時進程(SCHED_FIFO和SCHED_RR)比普通進程(SCHED_NORMAL)優先級高,能夠進行搶佔。
這兩種實時調度算法實現的都是靜態優先級。內核不爲實時進程計算動態優先級。這能保證給定優先級別的實時進程總能搶佔優先級比它低的進程。Linux實時調度算法是軟實時工做方式,儘可能使進程
SCHED_FIFO實現了一種簡單的、先入先出的調度算法, 它不使用時間片。
SCHED_FIFO的進程不基於時間片,一旦處於可執行狀態,就會一直執行,直到它本身阻塞或者顯式地釋放處理器爲止。只有較高優先級的SCHED_FIFO或者SCHED_RR任務才能搶佔SCHED_FIFO任務。 只要有SCHED_FIFO級進程在執行,其餘級別較低的進程就只能等待它結束後纔有機會執行,除非它主動讓出處理器纔會退出。
SCHED_RR是一種帶有時間片的SCHED_FIFO。
當SCHED_RR任務耗盡它的時間片,在同一優先級的其餘實時進程被輪流調度。時間片只能用來從新調度同一優先級的進程。對於SCHED_FIFO進程,高優先級老是馬上搶佔低優先級,可是低優先級進程決不能搶佔SCHED_RR任務,即便它的時間片耗盡。
Linux提供了一個系統調用族,用於管理與調度程序的相關參數。這些系統調用能夠用來操做和處理進程優先級、調度策略及處理器綁定,同時還提供了顯式地將處理器交給其餘進程的機制。
系統調用 | 描述 |
---|---|
nice() | 設置進程的nice值 |
sched_setscheduler() | 設置進程的調度策略 |
sched_getscheduler() | 獲取進程的調度策略 |
sched_setparam() | 設置進程的實時優先級 |
sched_getparam() | 獲取進程的實時優先級 |
sched_get_priority_max() | 獲取實時優先級的最大值 |
sched_get_priority_min() | 獲取實時優先級的最小值 |
sched_rr_get_interval() | 獲取進程的時間片 |
sched_setaffinity() | 設置進程的處理器的親和力 |
sched_getaffinity() | 獲取進程的處理器的親和力 |
sched_yield() | 暫時讓出處理器 |
做者:世至其美
博客地址:hqber.com
轉載須註明以上信息, 更多文章,請訪問我的博客:hqber.com