Linux內核調度分析(進程調度)


做者:不洗碗工做室 - Marklux

出處:marklux.cn/blog/64node

版權歸做者全部,轉載請註明出處linux

本文是《Linux內核設計與實現》第四章的閱讀筆記,代碼則是摘自最新的4.6版本linux源碼(github),轉載請註明出處。git

多任務

併發和並行

Linux做爲一個多任務操做系統,必須支持程序的併發執行。github

分類

  1. 非搶佔式多任務算法

    除非任務本身結束,不然將會一直執行。安全

  2. 搶佔式多任務(Linux)bash

    這種狀況下,由調度程序來決定何時中止一個進程的運行,這個強制的掛起動做即爲**「搶佔」。採用搶佔式多任務的基礎是使用時間片輪轉**機制來爲每一個進程分配能夠運行的時間單位。數據結構

Linux進程調度

發展歷史

Linux從2.5版本開始引入一種名爲O(1)的調度器,後在2.6版本中將公平的的調度概念引入了調度程序,代替以前的調度器,稱爲CFS算法(徹底公平調度算法)。併發

策略

I/O消耗型和處理器消耗型

I/O消耗型進程是指那些大部分時間都在等待I/O操做的進程,處理器耗費型的進程則是指把大多數時間用於執行代碼的進程,除非被搶佔,他們通常都一直在運行。dom

爲了保證交互式應用和桌面系統的性能,通常Linux更傾向於優先調度I/O消耗型進程。

進程優先級

Linux採用了兩種不一樣的優先級範圍。

  1. 使用nice值:越大的nice值意味着更低的優先級。 (-19 ~ 20之間)

  2. 實時優先級:可配置,越高意味着進程優先級越高。

    任何實時的進程優先級都高於普通的進程,所以上面的兩種優先級範圍處於互不相交的範疇。

  3. 時間片:Linux中並非以固定的時間值(如10ms)來分配時間片的,而是將處理器的使用比做爲「時間片」劃分給進程。這樣,進程所得到的實際CPU時間就和系統的負載密切相關。

Linux中的搶佔時機取決於新的可運行進程消耗了多少處理器使用比,
若是消耗的使用比當前進程小,則馬上投入運行,不然將推遲其運行。
複製代碼

舉例

如今咱們來看一個簡單的例子,假設咱們的系統只有兩個進程在運行,一個是文本編輯器(I/O消耗型),另外一個是視頻解碼器(處理器消耗型)。

理想的狀況下,文本編輯器應該獲得更多的處理器時間,至少當它須要處理器時,處理器應該馬上被分配給它(這樣才能完成用戶的交互),這也就意味着當文本編輯器被喚醒的時候,它應該搶佔視頻解碼程序。

按照普通的狀況,OS應該分配給文本編輯器更大的優先級和更多的時間片,但在Linux中,這兩個進程都是普通進程,他們具備相同的nice值,所以它們將獲得相同的處理器使用比(50%)。

但實際的運行過程當中會發生什麼呢?CFS將可以注意到,文本編輯器使用的處理器時間比分配給它的要少得多(由於大多時間在等待I/O),這種狀況下,要實現全部進程「公平」地分享處理器,就會讓文本編輯器在須要運行時馬上搶佔視頻解碼器(每次都是如此)。

Linux調度算法

調度器類

Linux的調度器是以模塊的方式提供的,這樣使得不一樣類型的進程按照本身的須要來選擇不一樣的調度算法。

上面說講到的CFS算法就是一個針對普通進程的調度器類,基礎的調度器會按照優先級順序遍歷調度類,擁有一個可執行進程的最高優先級的調度器類勝出,由它來選擇下一個要執行的進程。

Unix中的進程調度

存在的問題:

  1. nice值必須映射處處理器的絕對時間上去,這意味着一樣是瓜分100ms的兩個一樣優先級的進程,發生上下文切換的次數並不相同,可能會差異很大。優先級越低的進程分到的時間片單位越小,可是實際上他們每每是須要進行大量後臺計算的,這樣很不合理。

  2. 相對的nice值引起的問題:兩個nice值不一樣但差值相同的進程,分到的時間片的大小是受到其nice值大小影響的:好比nice值18和19的兩個進程分到的時間片是10ms和5ms,nice值爲0和1的兩個進程分到的倒是100ms和95ms,這樣的映射並不合理。

  3. 若是要進行nice值到時間片的映射,咱們必須可以擁有一個能夠測量的「絕對時間片」(這牽扯到定時器和節拍器的相關概念)。實際上,時間片是會隨着定時器的節拍而改變的,一樣的nice值最終映射處處理器時間時可能會存在差別。

  4. 爲了可以更快的喚醒進程,須要對新的要喚醒的進程提高優先級,可是這可能會打破「公平性」。

爲了解決上述的問題,CFS對時間片的分配方式進行了根本性的從新設計,摒棄了時間片,用處理器使用比重來代替它。

公平調度(CFS)

出發點:進程調度的效果應該如同系統具有一個理想的多任務處理器——咱們能夠給任何進程調度無限小的時間週期,因此在任何可測量範圍內,能夠給n個進程桐鄉多的運行時間。

舉個例子來區分Unix調度和CFS:有兩個運行的優先級相同的進程,在Unix中多是每一個各執行5ms,執行期間徹底佔用處理器,但在「理想狀況」下,應該是,可以在10ms內同時運行兩個進程,每一個佔用處理器一半的能力。

CFS的作法是:在全部可運行進程的總數上計算出一個進程應該運行的時間,nice值再也不做爲時間片分配的標準,而是用於處理計算得到的處理器使用權重。

接下來咱們考慮調度週期,理論上,調度週期越小,就越接近「完美調度」,但實際上這必然會帶來嚴重的上下文切換消耗。在CFS中,爲可以實現的最小調度週期設定了一個近似值目標,稱爲「目標延遲」,於此同時,爲了不不可接受的上下文切換消耗,爲每一個進程所能得到的時間片大小設置了一個底線——最小粒度(一般爲1ms)。

在每一個進程的平均運行時間大於最小粒度的狀況下,CFS無疑是公平的,nice值用於計算一個進程在當前這個最小調度週期中所應得到的處理器時間佔比,這樣就算nice值不一樣,只要差值相同,老是能獲得相同的時間片。咱們假設一個最小調度週期爲20ms,兩個進程的nice值差值爲5:

  • 兩進程的nice值分別爲0和5,後者得到的時間片是前者的1/3,所以最終分別得到15ms和5ms
  • 兩進程的nice值分別爲10和15,後者得到的時間片是前者的1/3,最終結果也是15ms和5ms

關於上面這個推論,可能有些難以理解,因此咱們深刻一下,看看在底層nice差值到底是如何影響處處理區佔比的。

首先,在底層,在實際計算一個進程的處理器佔比以前,內核會先把nice值轉換爲一個權重值weight,這個轉換的公式以下:

weight = 1024/(1.25^nice)
複製代碼

舉個例子,默認nice值的進程獲得的權重就是1024/(1.25^0) = 1024/1 = 1024。

這個轉換公式保證了咱們能夠獲得非負的權重值,而且nice對權重的影響是在指數上的。

好,如今假設咱們的可運行進程隊列中有n個進程,他們的權重和w(1)+w(2)+...+w(n)記爲w(queue),那麼任意一個進程i最終獲得的處理器佔比將是w(i)/w(queue)

接着,咱們不難推導出,任意兩個進程i和j所分配的到的處理器佔比的比例應該是w(i)/w(j),通過簡單的數學推導就能夠獲得最後的結果:1.25^(nice(i)-nice(j)),這意味着只要兩個nice值的差值相同,兩個進程所得到處理器佔比永遠是相同的比例,從而解決了上面的第3點問題。

上述的轉換公式參考自:https://oakbytes.wordpress.com/2012/06/06/linux-scheduler-cfs-and-nice

總結一下,任何進程所得到的處理器時間是由它本身和全部其餘可運行進程nice值的相對差值決定的,所以咱們能夠說,CFS至少保證了給每一個進程公平的處理器佔用比,算是一種近乎完美的多任務調度方式了。

Linux調度的實現

下面咱們來看看CFS是如何實現的,通常咱們把它分爲4個主要的部分來分析。

時間記帳

全部的調度器都必須對進程的運行時間記帳,換句話說就是要知道當前調度週期內,進程還剩下多少個時間片可用(這將會是搶佔的一個重要標準)

1. 調度器實體結構

CFS中用於記錄進程運行時間的數據結構爲「調度實體」,這個結構體被定義在<linux/sched.h>中:

struct sched_entity {
	/* 用於進行調度均衡的相關變量,主要跟紅黑樹有關 */
	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; // 進程在切換CPU時的sum_exec_runtime,簡單說就是上個調度週期中運行的總時間

	u64				nr_migrations;

	struct sched_statistics		statistics;
	
	// 如下省略了一些在特定宏條件下才會啓用的變量
}
複製代碼

注:本文中全部用到的linux源碼均來自linux在github上官方的git庫(2018.01)

2. 虛擬實時 (vruntime)

如今咱們來談談上面結構體中的vruntime變量所表示的意義。咱們稱它爲「虛擬運行時間」,該運行時間的計算是通過了全部可運行進程總數的標準化(簡單說就是加權的)。它以ns爲單位,與定時器節拍再也不相關。

能夠認爲這是CFS爲了可以實現理想多任務處理而不得不虛擬的一個新的時鐘,具體地講,一個進程的vruntime會隨着運行時間的增長而增長,但這個增長的速度由它所佔的權重load來決定。

結果就是權重越高,增加越慢:所獲得的調度時間也就越小 —— CFS用它來記錄一個程序到底運行了多長時間以及還應該運行多久。

下面咱們來看一下這個記帳功能的實現源碼(kernel/sched/fair.c)

/*
 * Update the current task's runtime statistics. */ 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); // 計算虛擬時間,具體的轉換算法寫在clac_delta_fair函數中 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); } 複製代碼

該函數計算了當前進程的執行時間,將其存放在delta_exec變量中,而後使用clac_delta_fair函數計算對應的虛擬運行時間,並更新vruntime值。

這個函數是由系統定時器週期性調用的(不管進程的狀態是什麼),所以vruntime能夠準確地測量給定進程的運行時間,並以此爲依據推斷出下一個要運行的進程是什麼。

進程選擇

這裏即是調度的核心部分,用一句話來梗概CFS算法的核心就是選擇具備最小vruntime的進程做爲下一個須要調度的進程。

爲了實現選擇,固然要維護一個可運行的進程隊列(教科書上常說的ready隊列),CFS使用了紅黑樹來組織這個隊列。

紅黑樹是一種很是著名的數據結構,但這裏咱們不討論它的實現和諸多特性(過於複雜),咱們記住:紅黑樹是一種自平衡二叉樹,再簡單一點,它是一種以樹節點方式儲存數據的結構,每一個節點對應了一個鍵值,利用這個鍵值能夠快速索引樹上的數據,而且它能夠按照必定的規則自動調整每一個節點的位置,使得經過鍵值檢索到對應節點的速度和整個樹節點的規模呈指數比關係。

1. 找到下一個任務節點

先假設一個紅黑樹儲存了系統中全部的可運行進程,節點的鍵值就是它們的vruntime,CFS如今要找到下一個須要調度的進程,那麼就是要找到這棵紅黑樹上鍵值最小的那個節點:就是最左葉子節點。

實現此過程的源碼以下(kernel/sched/fair.c):

static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	struct sched_entity *left = __pick_first_entity(cfs_rq);
	struct sched_entity *se;

	/*
	 * If curr is set we have to see if its left of the leftmost entity
	 * still in the tree, provided there was anything in the tree at all.
	 */
	if (!left || (curr && entity_before(curr, left)))
		left = curr;

	se = left; /* ideally we run the leftmost entity */

	/*
	 * 下面的過程主要針對一些特殊狀況,咱們在此不作討論
	 */
	if (cfs_rq->skip == se) {
		struct sched_entity *second;

		if (se == curr) {
			second = __pick_first_entity(cfs_rq);
		} else {
			second = __pick_next_entity(se);
			if (!second || (curr && entity_before(curr, second)))
				second = curr;
		}

		if (second && wakeup_preempt_entity(second, left) < 1)
			se = second;
	}

	if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
		se = cfs_rq->last;

	if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
		se = cfs_rq->next;

	clear_buddies(cfs_rq, se);

	return se;
}
複製代碼

2. 向隊列中加入新的進程

向可運行隊列中插入一個新的節點,意味着有一個新的進程狀態轉換爲可運行,這會發生在兩種狀況下:一是當進程由阻塞態被喚醒,二是fork產生新的進程時。

將其加入隊列的過程本質上來講就是紅黑樹插入新節點的過程:

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);

	/*
	 * Otherwise, renormalise after, such that we're placed at the current * moment in time, instead of some random moment in the past. Being * placed in the past could significantly boost this task to the * fairness detriment of existing tasks. */ if (renorm && !curr) se->vruntime += cfs_rq->min_vruntime; /* * 更新對應調度器實體的各類記錄值 */ update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH); update_cfs_group(se); 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()來把數據真正插入紅黑樹中:

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);
		/*
		 * 具備相同鍵值的節點會被放在一塊兒
		 */
		if (entity_before(se, entry)) {
			link = &parent->rb_left;
		} else {
			link = &parent->rb_right;
			leftmost = false;
		}
	}

	rb_link_node(&se->run_node, parent, link);
	rb_insert_color_cached(&se->run_node,
			       &cfs_rq->tasks_timeline, leftmost);
}
複製代碼

while()循環是遍歷樹以尋找匹配鍵值的過程,也就是搜索一顆平衡樹的過程。找到後咱們對要插入位置的父節點執行rb_link_node()來將節點插入其中,而後更新紅黑樹的自平衡相關屬性。

3. 從隊列中移除進程

從隊列中刪除一個節點有兩種可能:一是進程執行完畢退出,而是進程受到了阻塞。

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	/*
	 * 更新「當前進程」的運行統計數據
	 */
	update_curr(cfs_rq);

	/*
	 * When dequeuing a sched_entity, we must:
	 *   - Update loads to have both entity and cfs_rq synced with now.
	 *   - Substract its load from the cfs_rq->runnable_avg.
	 *   - Substract its previous weight from cfs_rq->load.weight.
	 *   - For group entity, update its weight to reflect the new share
	 *     of its group cfs_rq.
	 */
	update_load_avg(cfs_rq, se, UPDATE_TG);
	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 excess runtime on last dequeue */
	return_cfs_rq_runtime(cfs_rq);

	update_cfs_group(se);

	/*
	 * Now advance min_vruntime if @se was the entity holding it back,
	 * except when: DEQUEUE_SAVE && !DEQUEUE_MOVE, in this case we'll be * put back on, and if we advance min_vruntime, we'll be placed back
	 * further than we started -- ie. we'll be penalized. */ if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) == DEQUEUE_SAVE) update_min_vruntime(cfs_rq); } 複製代碼

和插入同樣,實際對樹節點操做的工做由__dequeue_entity()實現:

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline);
}

複製代碼

能夠看到刪除一個節點要比插入簡單的多,這得益於紅黑樹自己實現的rb_erase()函數。

調度器入口

正如上文所述,每當要發生進程的調度時,是有一個統一的入口,從該入口選擇真正須要調用的調度類。

這個入口是內核中一個名爲schedule()的函數,它會找到一個最高優先級的調度類,這個調度類擁有本身的可運行隊列,而後向其詢問下一個要運行的進程是誰。

這個函數中惟一重要的事情是執行了pick_next_task()這個函數(定義在kenerl/sched/core.c中),它以優先級爲順序,依次檢查每個調度類,而且從最高優先級的調度類中選擇最高優先級的進程。

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	const struct sched_class *class;
	struct task_struct *p;

	/*
	 * 優化:若是當前全部要調度的進程都是普通進程,那麼就直接採用普通進程的調度類(CFS)
	 */
	if (likely((prev->sched_class == &idle_sched_class ||
		    prev->sched_class == &fair_sched_class) &&
		   rq->nr_running == rq->cfs.h_nr_running)) {

		p = fair_sched_class.pick_next_task(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))
			goto again;

		/* Assumes fair_sched_class->next == idle_sched_class */
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev, rf);

		return p;
	}

// 遍歷調度類
again:
	for_each_class(class) {
		p = class->pick_next_task(rq, prev, rf);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

	/* The idle class should always have a runnable task: */
	BUG();
}
複製代碼

每一個調度類都實現了pick_next_task()方法,它會返回下一個可運行進程的指針,沒有則返回NULL。調度器入口從第一個返回非NULL的類中選擇下一個可運行進程。

睡眠和喚醒

睡眠和喚醒的流程在linux中是這樣的:

  • 睡眠:進程將本身標記成休眠狀態,而後從可執行紅黑樹中移除,放入等待隊列,而後調用schedule()選擇和執行一個其餘進程。
  • 喚醒:進程被設置爲可執行狀態,而後從等待隊列移到可執行紅黑樹中去。

休眠在Linux中有兩種狀態,一種會忽略信號,一種則會在收到信號的時候被喚醒並響應。不過這兩種狀態的進程是處於同一個等待隊列上的。

1.等待隊列

和可運行隊列的複雜結構不一樣,等待隊列在linux中的實現只是一個簡單的鏈表。全部有關等待隊列的數據結構被定義在include/linux/wait.h中,具體的實現代碼則被定義在kernel/sched/wait.c中。

內核使用wait_queue_head_t結構來表示一個等待隊列,它其實就是一個鏈表的頭節點,可是加入了一個自旋鎖來保持一致性(等待隊列在中斷時能夠被隨時修改)

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;
複製代碼

而休眠的過程須要進程本身把本身加入到一個等待隊列中,這可使用內核所提供的、推薦的函數來實現。

一個可能的流程以下:

  1. 調用宏DEFINE_WAIT()建立一個等待隊列的項(鏈表的節點)
  2. 調用add_wait_queue()把本身加到隊列中去。該隊列會在進程等待的條件知足時喚醒它,固然喚醒的具體操做須要進程本身定義好(你能夠理解爲一個回調)
  3. 調用prepare_to_wait()方法把本身的狀態變動爲上面說到的兩種休眠狀態中的其中一種。

下面是上述提到的方法的源碼:

void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	__add_wait_queue(wq_head, wq_entry);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	list_add(&wq_entry->entry, &wq_head->head);
}
複製代碼
void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	if (list_empty(&wq_entry->entry))
		__add_wait_queue(wq_head, wq_entry);
	// 標記本身的進程狀態
	set_current_state(state);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}
複製代碼

2.喚醒

喚醒操做主要經過wake_up()實現,它會喚醒指定等待隊列上的全部進程。內部由try_to_wake_up()函數將對應的進程標記爲TASK_RUNNING狀態,接着調用enqueue_task()將進程加入紅黑樹中。

wake_up()系函數由宏定義,通常具體內部由下面這個函數實現:

/*
 * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
 * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve * number) then we wake all the non-exclusive tasks and one exclusive task. * * There are circumstances in which we can try to wake a task which has already * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns * zero in this (rare) case, and we handle it by continuing to scan the queue. */ static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode, int nr_exclusive, int wake_flags, void *key, wait_queue_entry_t *bookmark) { wait_queue_entry_t *curr, *next; int cnt = 0; if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) { curr = list_next_entry(bookmark, entry); list_del(&bookmark->entry); bookmark->flags = 0; } else curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry); if (&curr->entry == &wq_head->head) return nr_exclusive; list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) { unsigned flags = curr->flags; int ret; if (flags & WQ_FLAG_BOOKMARK) continue; ret = curr->func(curr, mode, wake_flags, key); if (ret < 0) break; if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) && (&next->entry != &wq_head->head)) { bookmark->flags = WQ_FLAG_BOOKMARK; list_add_tail(&bookmark->entry, &next->entry); break; } } return nr_exclusive; } 複製代碼

搶佔與上下文切換

上下文切換

上下文切換是指從一個可執行進程切換到另外一個可執行進程。由定義在kernel/sched/core.ccontext_switch()實現:

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
	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;
		mmgrab(oldmm);
		enter_lazy_tlb(oldmm, next);
	} else
		switch_mm_irqs_off(oldmm, mm, next);

	if (!prev->mm) {
		prev->active_mm = NULL;
		rq->prev_mm = oldmm;
	}

	rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);

	/*
	 * 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: */ rq_unpin_lock(rq, rf); spin_release(&rq->lock.dep_map, 1, _THIS_IP_); /* Here we just switch the register state and the stack. */ // 切換處理器狀態到新進程,這包括保存、恢復寄存器和棧的相關信息 switch_to(prev, next, prev); barrier(); return finish_task_switch(prev); } 複製代碼

上下文切換由schedule()函數在切換進程時調用。可是內核必須知道何時調用schedule(),若是隻靠用戶代碼顯式地調用,代碼可能會永遠地執行下去。

爲此,內核爲每一個進程設置了一個need_resched標誌來代表是否須要從新執行一次調度,當某個進程應該被搶佔時,scheduler_tick()會設置這個標誌,當一個優先級高的進程進入可執行狀態的時候,try_to_wake_up()也會設置這個標誌位,內核檢查到此標誌位就會調用schedule()從新進行調度。

用戶搶佔

內核即將返回用戶空間的時候,若是need_reshced標誌位被設置,會致使schedule()被調用,此時就發生了用戶搶佔。意思是說,既然要從新進行調度,那麼能夠繼續執行進入內核態以前的那個進程,也徹底能夠從新選擇另外一個進程來運行,因此若是設置了need_resched,內核就會選擇一個更合適的進程投入運行。

簡單來講有如下兩種狀況會發生用戶搶佔:

  • 從系統調用返回用戶空間
  • 從中斷處理程序返回用戶空間

內核搶佔

Linux和其餘大部分的Unix變體操做系統不一樣的是,它支持完整的內核搶佔。

不支持內核搶佔的系統意味着:內核代碼能夠一直執行直到它完成爲止,內核級的任務執行時沒法從新調度,各個任務是以協做方式工做的,並不存在搶佔的可能性。

在Linux中,只要從新調度是安全的,內核就能夠在任什麼時候間搶佔正在執行的任務,這個安全是指,只要沒有持有鎖,就能夠進行搶佔。

爲了支持內核搶佔,Linux作出了以下的變更:

  • 爲每一個進程的thread_info引入了preempt_count計數器,用於記錄持有鎖的數量,當它爲0的時候就意味着這個進程是能夠被搶佔的。
  • 從中斷返回內核空間的時候,會檢查need_reschedpreempt_count的值,若是need_resched被標記,而且preempt_count爲0,就意味着有一個更須要調度的進程須要被調度,並且當前狀況是安全的,能夠進行搶佔,那麼此時調度程序就會被調用。

除了響應中斷後返回,還有一種狀況會發生內核搶佔,那就是內核中的進程因爲阻塞等緣由顯式地調用schedule()來進行顯式地內核搶佔:固然,這個進程顯式地調用調度進程,就意味着它明白本身是能夠安全地被搶佔的,所以咱們不用任何額外的邏輯去檢查安全性問題。

下面羅列可能的內核搶佔狀況:

  • 中斷處理正在執行,且返回內核空間以前
  • 內核代碼再一次具備可搶佔性時
  • 內核中的任務顯式地調用schedule()
  • 內核中的任務被阻塞
相關文章
相關標籤/搜索