適用於桌面應用的Linux調度器BFS/MuqSS

粉漿裏面有糞,豬血裏面有shi。


大家都知道Linux內核task調度器經歷了O(n)O(n)O(1)O(1)調度器,目前是CFS,期間也出現了幾個優秀的候選調度器,但最終都沒能併入內核,我們只能從一些零散的patch和文章中知道它們的存在。

但Linux內核的世界乃是非常之寬廣,在主線內核之外還有很多支線可供觀摩。

本文我來介紹Linux主線內核之外的兩個非常有意思的適合桌面使用的task調度器BFS和MuqSS。


Linux內核其實有很多支線分支,其中Linux-CK就是著名的一支:
https://wiki.archlinux.org/index.php/Linux-ck
該支線由Con Kolivas維護,所以稱之爲CK(並非內褲哦…)。

Con Kolivas的信仰是 萬事不能一刀切,沒有放之四海而皆準的真理! 所以,他主要關注Linux桌面,他不相信存在one-size-fits-all的調度器,所以他設計了專門適用於桌面交互的BFS。

BFS不是一個普適的task調度器,相反,它僅僅適用於桌面交互的環境。所以,爲你的水冷遊戲機使用BFS,而不是在攜帶衆核CPU嗡嗡作響的2U服務器上使用它。

MuqSS則是BFS的改進版。MuqSS的全稱是 Multiple Queue Skiplist Scheduler

有必要先介紹一下什麼是BFS,然後我們看MuqSS進行了什麼改進。

簡單來講,BFS是一種向O(n)O(n)算法的迴歸,Con Kolivas認爲:

  • 桌面系統的task數量不會太多。
  • 對於主線內核的調度算法太複雜。

基於這種認知,Con Kolivas設計了BFS。他認爲 讓一個支持4096個CPU的調度器去調度桌面交互應用的task是錯誤且可笑的做法 ,下面的BFS宣傳漫畫說明了這一點:
在這裏插入圖片描述
此外,BFS的命名也很有意思。

本文主要概括BFS以及其增強版MuqSS的核心思路而不是實現細節。

關於BFS的細節請參考下面的一篇文章:
返璞歸真的Linux BFS調度器:https://blog.csdn.net/dog250/article/details/7459533

BFS的核心數據結構非常簡單,就是普通雙向鏈表,每次選擇task時,該鏈表會被遍歷,具有最小 Virtual Deadline 的task將會被選中。

顯然,這個過程的時間複雜度是O(n)O(n)。其中, Virtual Deadline 的計算方法如下:

Virtual Deadline = jiffies + (user_priority * rr_interval)

BFS雖然簡單,但是兩個問題卻非常明顯:

  1. 遍歷查找的O(n)問題。
    鏈表爲什麼不基於Virtual Deadline進行預排序呢?***
  2. 多CPU操作全局鏈表的鎖問題。

衆所周知, O(n)O(n)時間複雜度和鎖” 在計算機領域一直飽受詬病,它們似難兄難弟一般的存在。BFS卻毫不忌諱地同時將它們採納到自己的核心算法中。

對此,Con Kolivas的解釋就是 保持實現的簡單:

啓發式的複雜算法終究還是啓發式的,總是伴隨誤判。在單一的環境中,與其以龐大的代碼量維持不必要的啓發式算法,不如放棄啓發式算法,退回到最簡單的數據結構和代碼實現。

既然那些複雜的調度算法沒有帶來讓人期望的收益,爲什麼不試試最簡單的方法呢?即便同樣糟糕,至少代碼簡單了不是嗎?

我們看看BFS的算法簡單到何種程度:

  • task插入:直接將task插入鏈表末尾。
  • task選擇:冒泡選擇Virtual Deadline最小的task。
    【在遍歷過程中會有trick,發現當前jiffies大於task的VD,就退出,這像極了Linux內核的timer處理】

如果要實現Virtual Deadline的預排序,必然要在下面二者之間作出權衡:

  1. 鏈表操作的O(n)O(n)時間複雜度代價。
  2. 複雜數據結構實現的代價和副作用。

最終,Con Kolivas認爲:

  1. 在task數量並不太大的情況下,O(n)O(n)算法沒有任何問題。
  2. 在CPU數量保持在16個以內時,爭鎖的開銷可以忽略。

因此,沒有必要爲了可以忽略不計的問題而付出複雜性的代價。

所以,結論是BFS很好,它顯示出了一種幹練。有人如此評價BFS, “快 !人能感覺到的快!”

問題確實存在,Con Kolivas只是覺得爲了解決那些在桌面環境下不足以帶來嚴重影響的問題而以引入複雜性爲代價,這不值得。
除非可以在保持簡單的前提下零代價解決問題!

有這樣的方案嗎?

當然有!這就是MuqSS的算法!

MuqSS零代價解決了BFS存在的兩個問題:

  1. 遍歷查找的O(n)問題。
    引入Skiplist數據結構替換雙向鏈表,在O(logn)O(\log n)的插入代價下將查找的時間複雜度降爲O(1)O(1)
    【關於Skiplist,可以參考我的另一篇文章:https://blog.csdn.net/dog250/article/details/46997155
  2. 多CPU操作全局鏈表的鎖問題。
    引入每CPU鏈表,避免全局爭鎖。同時以trylock代替lock,以損失準確性爲代價實現無鎖操作。

Con Kolivas在 保持簡單 這個約束下設計了MuqSS,其要點是:

  • Skiplist的作用類似主線Linux內核CFS中的紅黑樹,但比紅黑樹簡單得多。
  • 選擇task的算法遍歷所有CPU的Skiplist表頭,選擇當前全局最優task。
  • 鎖粒度細化到每個CPU的Skiplist。
  • 遍歷過程針對每CPU鎖採用trylock,失敗則繼續下一個CPU,實現無鎖化。

我們看一下MuqSS調度算法中選擇task的具體操作流程:
在這裏插入圖片描述

和BFS不同,BFS是在全局的鏈表中遍歷找VD最小的task,而MuqSS則遍歷每CPU鏈表的表頭查找VD最小的task。

之所以要遍歷所有CPU的Skiplist上的表頭,是因爲每次查找操作均要找到一個全局最優解,這樣也就消除了主動負載均衡的必要,極大降低了算法的複雜性。

時間複雜度同樣都是O(n)O(n),但MuqSS的nn指的是CPU數量而非task數量。我們看一下MuqSS選擇task算法相比BFS的改進:
在這裏插入圖片描述
上圖所示的改進不是一蹴而就的,MuqSS經歷了多個版本。

下面,我摘錄並精簡Con Kolivas補丁集中的相關函數,給出一個觀感上的體會。

首先我們看一下0.104版本的MuqSS的選擇task算法,請側重代碼的註釋而不是代碼本身:

/*
 * Task selection with skiplists is a simple matter of picking off the first
 * task in the sorted list, an O(1) operation. The only time it takes longer
 * is if tasks do not have suitable affinity and then we iterate over entries
 * till we find the first that does. Worst case here is no tasks with suitable
 * affinity and taking O(k) where k is number of processors.
 *
 * As many runqueues as can be locked without contention are grabbed via
 * lock_rqs and only those runqueues are examined. All balancing between CPUs
 * is thus done here in an extremely simple first come best fit manner.
 *
 * This iterates over runqueues in cache locality order. In interactive mode
 * it iterates over all CPUs and finds the task with the earliest deadline.
 * In non-interactive mode it will only take a task if it's from the current
 * runqueue or a runqueue with more tasks than the current one.
 */
static inline struct
task_struct *earliest_deadline_task(struct rq *rq, int cpu, struct task_struct *idle)
{
	// 冒泡基準
	u64 earliest_deadline = ~0ULL;
	...
	lock_rqs(rq, &locked); // 這裏還是統一上鎖的版本

	// 一個for循環,冒泡找出VD最小的task
	for (i = 0; i < num_possible_cpus(); i++) {
		struct rq *other_rq = rq->rq_order[i];
		struct task_struct *p;
		...
		p = other_rq->node.next[0]->value;
		...
		if (!deadline_before(p->deadline, earliest_deadline))
			continue;
		earliest_deadline = p->deadline;
		edt = p;
	}
	...
	unlock_rqs(rq, &locked);

	return edt;
}

我刪掉了不影響理解算法核心的代碼,以突出重點,完整的代碼請參看下面的鏈接:
http://ck.kolivas.org/patches/4.0/4.8/4.8-ck2/patches/0008-MuQSS-version-0.104.patch

我們再看下最新的版本中選擇task的實現,它實現了查找過程的無鎖化,代價只是稍微損失了全局最優的準確度:

/*
 * Task selection with skiplists is a simple matter of picking off the first
 * task in the sorted list, an O(1) operation. The lookup is amortised O(1)
 * being bound to the number of processors.
 *
 * Runqueues are selectively locked based on their unlocked data and then
 * unlocked if not needed. At most 3 locks will be held at any time and are
 * released as soon as they're no longer needed. All balancing between CPUs
 * is thus done here in an extremely simple first come best fit manner.
 *
 * This iterates over runqueues in cache locality order. In interactive mode
 * it iterates over all CPUs and finds the task with the best key/deadline.
 * In non-interactive mode it will only take a task if it's from the current
 * runqueue or a runqueue with more tasks than the current one with a better
 * key/deadline.
 */
#ifdef CONFIG_SMP
static inline struct task_struct
*earliest_deadline_task(struct rq *rq, int cpu, struct task_struct *idle)
{
	struct rq *locked = NULL, *chosen = NULL;

	for (i = 0; i < total_runqueues; i++) {
		struct rq *other_rq = rq_order(rq, i);
		skiplist_node *next;
		...
			if (other_rq->best_key >= best_key)
				continue;
			// 採用trylock的方式,如果鎖定失敗便繼續嘗試下一個CPU,不再阻塞等鎖。
			if (unlikely(!trylock_rq(rq, other_rq)))
				continue;
		...
		next = other_rq->node;
		while ((next = next->next[0]) != other_rq->node) {
			struct task_struct *p;
			// 這裏增加了新的約束:並不是所有的skiplist表頭的task都是無可厚非的候選者,某些條件下,它們也會落選的。
			edt = p;
			break;
		}
		...
	}
	...
	return edt;
}

完整的代碼請參見下面的鏈接:
http://ck.kolivas.org/patches/muqss/5.0/5.2/0001-MultiQueue-Skiplist-Scheduler-version-0.193.patch

關於BFS以及MuqSS的核心原理就介紹到這裏。關於MuqSS更詳細的信息,出了上述給出的源碼鏈接之外,其本身的Doc以及Lwn文章也是值得一讀:
Doc: http://ck.kolivas.org/patches/muqss/sched-MuQSS.txt
Lwn:https://lwn.net/Articles/720227/

接下來我們看看作爲Linux Kernel patch的BFS,MuqSS是怎樣一種存在。


Linux內核的調度器並不是可插拔的,內核中不存在sched_class的任何註冊接口和替換接口,這意味着如果你想實現自己的調度器,會非常麻煩。

當然,最直接的方式就是直接修改內核源碼了,Con Kolivas便採用了這個方法從而分裂出了自己的分支,你可以在下面的鏈接找到他的patch:
http://ck.kolivas.org/patches/

MuqSS調度器是通過直接修改schedule函數實現的:

+static void __sched notrace __schedule(bool preempt)
+{
+	...
+	next = earliest_deadline_task(rq, cpu, idle);
+	if (likely(next->prio != PRIO_LIMIT))
+		clear_cpuidle_map(cpu);
+	else {
+		set_cpuidle_map(cpu);
+		update_load_avg(rq, 0);
+	}
+
+	set_rq_task(rq, next);
+	next->last_ran = niffies;

由於MuqSS是專門針對桌面交互應用設計的調度器,而Linus本人則不認同主線內核中的任何專門化的定製設計,因此可以預見Con Kolivas的patch很難合入主線。

這背後是Linus Torvalds和Con Kolivas的理念之爭:

  • Linus Torvalds討厭一切定製。
  • Con Kolivas不相信one-size-fits-all。

Con Kolivas將長期維護他自己的CK分支或者如Linus本人那般,Con Kolivas也可能基於Linux-CK生成另一個自己的CK主線,徹底和Linux決裂!事實上,Con Kolivas早該這麼做了,雖然名字裏保留Linux字眼看上去有些不妥…

最後一個問題是,我要如何做才能享用MuqSS的收益呢?

確保你的環境是桌面環境,做下面的事:

  1. 下載4.9版本內核以上的社區主線代碼。
  2. http://ck.kolivas.org/patches/4.0/找到對應版本的patchset,然後打上這些patch。
  3. 重新編譯打上patch的內核,安裝內核並啓動新內核。
  4. 享用。


好了,這就是我要跟你講的BFS和MuqSS的故事。

浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章