首頁>技術>

我其實並不想討論微核心的概念,也並不擅長去闡述概念,這是百科全書的事,但無奈最近由於鴻蒙的釋出導致這個話題過火,也就經不住誘惑,加上我又一直比較喜歡作業系統這個話題,就來個老生常談吧。

說起微核心,其效能往往因為IPC飽受詬病。然而除了這個顯而易見的 “缺陷” ,其它方面貌似被關注的很少。因此我寫點稍微不同的。

微核心的效能 “缺陷” 我假設是高開銷的IPC引起的(實際上也真是),那麼,我接下來便繼續假設這個IPC效能是可以優化的,並且它已經被優化(即便不做任何事,隨著硬體技術的發展,所謂的歷史缺點往往也將逐漸弱化...)。我不公道地迴避了核心問題,這並不是很道德,但為了下面的行文順利,我不得不這麼做。

很多人之所以並不看好微核心,很大程度上是因為它和Linux核心是如此不同,人們認為不同於Linux核心的作業系統核心都有這樣那樣的缺陷,這是因為Linux核心給我們洗了腦。

Linux核心的設計固化了人們對作業系統核心的理解上的觀念 ,以至於 Linux核心做什麼都是對的,反Linux的大概率是錯的。 Linux核心就一定正確嗎?

在我看來,Linux核心只是在恰當的時間出現的一個恰好能跑的核心,並且恰好它是開源的,讓人們可以第一次內窺一個作業系統核心的全貌罷了,這並不意味著它就一定是正確的。相反,它很可能是錯誤的。【 20世紀90年代,Windows NT系統初始,但很難看到它的內在,《windows internal》風靡一時;UNIX陷入糾紛,GNU呼之卻不出,此時Linux核心滿足了人們一切的好奇心,於是先入為主,讓人們覺的作業系統就應該是這個樣子,並且在大多數人看來,這是它唯一的相貌。 】

本文主要說 核心的可擴充套件性 。

先潑一盆冷水,Linux核心在這方面做得並非已經爐火純青。

誠然,近十幾年來Linux核心從2.6發展到5.3,一直在SMP多核擴充套件方面精益求精,但是說實話架構上並沒有什麼根本性的調整,要說比較大的調整,當屬:

$O(1)$排程演算法。SMP處理器域負載均衡演算法。percpu資料結構。資料結構拆鎖。

都是一些細節,沒有什麼讓人哇塞的東西,還有更細節的cache重新整理的管理,這種第二天不用就忘記的東西,引多少人競折腰。

這不禁讓人想起在交換式乙太網出現之前,人們不斷優化CSMA/CD演算法的過程,同樣沒有讓人哇塞,直到交換機的出現,讓人眼前一亮,CSMA/CD隨之幾乎被完全廢棄,因為它不是 正確 的東西。

交換機之所以 正確 的核心在於 仲裁。

當一個共享資源每次只能容納一個實體佔用訪問時,我們稱該資源為 “必須序列訪問的共享資源” ,當有多個實體均意欲訪問這種資源時,one by one是必然的,one by one的方案有兩種:

哪個好?說說看。

爭搶必會產生衝突,衝突便耽誤整體通過的時間,你會選哪個?

現在,我們暫時忘掉諸如巨集核心,微核心,程序隔離,程序切換,cache重新整理,IPC等概念,這些概念對於我們理解事情的本質毫無幫助,相反,它們會阻礙我們建立新的認知。比如,無論你覺得微核心多麼好,總有人跳出來說IPC是微核心的瓶頸,當你提出一個類似頁表項交換等優化後,又會有人說程序切換刷cache,暫存器上下文save/restore的開銷也不小,然後你可能知道點 帶有程序PID鍵值的cache方案 ,吧啦吧啦,最後一個show me the code 讓你無言以對,一來二去,還沒有認識全貌,便已經陷入了細節。

所以,把這些忘掉,來看一個觀點:

對待必須序列訪問的共享資源,正確的做法是引入一個仲裁者排隊排程訪問者,而不是任由訪問者們去併發爭鎖!

所謂 作業系統 這個概念,本來就是莫須有的,你可以隨便叫它什麼,早期它叫 監視器 ,現在我們姑且就叫它作業系統吧,但這並不意味著這個概念有多麼神奇。

作業系統本就是用來協調多個程序(這也是個抽象後的概念,你叫它任務也可以,無所謂)對底層共享資源的 多對一訪問 的,最典型的資源恐怕就是CPU資源了,而幾乎所有人都知道,CPU資源是需要排程使用的,於是任務排程一直都是一個熱門話題。

你看, CPU就不是所有任務併發爭搶使用的,而是排程器讓誰用誰才能用 。排程,或者說仲裁,這是作業系統的精髓。

那麼對於系統中共享的檔案,socket,對於各種表比如路由表等資源,憑什麼要用併發爭搶的方式去使用?!所有的共享資源,都應該是被排程使用的,就像CPU資源一樣。

如果我們循著作業系統理應實現的最本質的功能去思考,而不是以Linux作為先入為主的標準去思考,會發現Linux核心處理併發明顯是一種錯誤的方式!

Linux核心大量使用了自旋鎖,這明顯是從單核向SMP進化時最最最簡單的方案,即 只要保證不出問題的方案!

也確實如此,單核上的自旋鎖並不能如其字面表達的那樣 自旋 , 在單核場景下,Linux的自旋鎖實現僅僅是 禁用了搶佔 。因為,這樣即可保證 不出問題 。

但到了必須要支援SMP的時候,簡單的禁用搶佔已經無法保證不出問題,所以 待在原地自旋等待持鎖者離開 便成了最顯而易見的方案。自旋鎖就這樣一直用到了現在。一直到今天,自旋鎖在不斷被優化,然而無論怎麼優化,它始終都是一個不合時宜的自旋鎖。

可見,Linux核心一開始就不是為SMP設計的,因此其併發模式是錯誤的,至少不是合適的。

有破就要有立,我下面將用一套使用者態的程式碼來模擬 無仲裁的巨集核心 以及 有仲裁的微核心分別是如何對待共享資源訪問的。程式碼比較簡單,所以我就沒加入太多的註釋。

以下的程式碼模擬巨集核心中訪問共享資源時的自旋鎖併發爭搶模式:

#include <pthread.h>#include <signal.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <errno.h> #include <sys/time.h>static int count = 0;static int curr = 0;static pthread_spinlock_t spin;long long end, start;int timer_start = 0;int timer = 0;long long gettime(){ struct timeb t; ftime(&t);return 1000 * t.time + t.millitm;} void print_result(){ printf("%d\\n", curr); exit(0);} struct node { struct node *next; void *data;}; void do_task(){ int i = 0, j = 2, k = 0; // 為了更加公平的對比,既然模擬微核心的程式碼使用了記憶體分配,這裡也fake一個。 struct node *tsk = (struct node*) malloc(sizeof(struct node)); pthread_spin_lock(&spin); // 鎖定整個訪問計算區間 if (timer && timer_start == 0) { struct itimerval tick = {0}; timer_start = 1; signal(SIGALRM, print_result); tick.it_value.tv_sec = 10; tick.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &tick, NULL); } if (!timer && curr == count) { end = gettime(); printf("%lld\\n", end - start); exit(0); } curr ++; for (i = 0; i < 0xff; i++) { // 做一些稍微耗時的計算,模擬類似socket操作。強度可以調整,比如0xff->0xffff,CPU比較猛比較多的機器上做測試,將其調強些,否則佇列開銷會淹沒模擬任務的開銷。 k += i/j; } pthread_spin_unlock(&spin); free(tsk);} void* func(void *arg){ while (1) { do_task(); }} int main(int argc, char **argv){ int err, i; int tcnt; pthread_t tid; count = atoi(argv[1]); tcnt = atoi(argv[2]); if (argc == 4) { timer = 1; } pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); start = gettime(); // 建立工作執行緒 for (i = 0; i < tcnt; i++) { err = pthread_create(&tid, NULL, func, NULL); if (err != 0) { exit(1); } } sleep(3600); return 0;}

相對的,微核心採用將請求通過IPC傳送到專門的服務程序,模擬程式碼如下:

#include <pthread.h>#include <signal.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>#include <errno.h>#include <sys/time.h>static int count = 0;static int curr = 0; long long end, start;int timer = 0;int timer_start = 0;static int total = 0; long long gettime(){ struct timeb t; ftime(&t); return 1000 * t.time + t.millitm;} struct node { struct node *next; void *data;}; void print_result(){ printf("%d\\n", total); exit(0);} struct node *head = NULL;struct node *current = NULL; void insert(struct node *node){ node->data = NULL; node->next = head; head = node;} struct node* delete(){ struct node *tempLink = head; head = head->next; return tempLink;} int empty(){ return head == NULL;} static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_spinlock_t spin; int add_task(){ struct node *tsk = (struct node*) malloc(sizeof(struct node)); pthread_spin_lock(&spin); if (timer || curr < count) { curr ++; insert(tsk); } pthread_spin_unlock(&spin); return curr;} // 強度可以調整,比如0xff->0xffff,CPU比較猛比較多的機器上做測試,將其調強些,否則佇列開銷會淹沒模擬任務的開銷。void do_task(){ int i = 0, j = 2, k = 0; for (i = 0; i < 0xff; i++) { k += i/j; }} void* func(void *arg){ int ret; while (1) { ret = add_task(); if (!timer && ret == count) { break; } }} void* server_func(void *arg){ while (timer || total != count) { struct node *tsk; pthread_spin_lock(&spin); if (empty()) { pthread_spin_unlock(&spin); continue; } if (timer && timer_start == 0) { struct itimerval tick = {0}; timer_start = 1; signal(SIGALRM, print_result); tick.it_value.tv_sec = 10; tick.it_value.tv_usec = 0; setitimer(ITIMER_REAL, &tick, NULL); } tsk = delete(); pthread_spin_unlock(&spin); do_task(); free(tsk); total++; } end = gettime(); printf("%lld %d\\n", end - start, total); exit(0);} int main(int argc, char **argv){ int err, i; int tcnt; pthread_t tid, stid; count = atoi(argv[1]); tcnt = atoi(argv[2]); if (argc == 4) { timer = 1; } pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE); // 建立服務執行緒 err = pthread_create(&stid, NULL, server_func, NULL); if (err != 0) { exit(1); } start = gettime(); // 建立工作執行緒 for (i = 0; i < tcnt; i++) { err = pthread_create(&tid, NULL, func, NULL); if (err != 0) { exit(1); } } sleep(3600); return 0;}

我們對比一下執行同樣多的任務,在不同的執行緒數的約束下,兩種模式的時間開銷對比圖:

我們看到,在模擬微核心的程式碼中,用多執行緒執行並行訪問共享資料curr時,開銷不會隨著執行緒數量的變化而變化,而模擬巨集核心的程式碼中,總時間隨著執行緒數的增加而線性增加,顯然,這部分開銷是自旋鎖的開銷。當今流行的CPU cache結構已經排隊自旋鎖的開銷符合這種線性增長。

那麼為什麼微核心的模擬程式碼中的鎖開銷沒有隨著執行緒數量的增加而增加呢?

因為在類似巨集核心的同步任務中,由於併發上下文的相互隔離,整個任務必須被一個鎖保護,比如 Linux核心的tcp_v4_rcv 裡面的:

bh_lock_sock_nested(sk); // 這部分耗時時間不確定,因此CPU空轉率不確定,低效,浪費! ret = 0; if (!sock_owned_by_user(sk)) { if (!tcp_prequeue(sk, skb)) ret = tcp_v4_do_rcv(sk, skb); } else if (unlikely(sk_add_backlog(sk, skb, sk->sk_rcvbuf + sk->sk_sndbuf))) { bh_unlock_sock(sk); NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP); goto discard_and_relse; } bh_unlock_sock(sk);

然而,在微核心的程式碼中,類似上面的任務被打包統一交給單獨的服務執行緒去 排程執行 了,大大減少了鎖區裡的延時。

巨集核心的隔離上下文併發搶鎖場景需要鎖整個任務,造成搶鎖開銷巨大,而微核心只要鎖任務佇列的入隊出隊操作即可,這部分開銷和具體任務無關,完全可預期的開銷。

接下來讓我們對比一下執行同樣的任務,在不同CPU數量的約束下,兩種模式的時間開銷對比圖:

可見,隨著CPU數量的增加,模擬巨集核心的程式碼鎖開銷大致線上性增加,而模擬微核心的程式碼,鎖開銷雖然也有所增加,但顯然並不明顯。

為什麼會這樣?請看下面巨集核心和微核心的對比圖,先看巨集核心:

再看微核心:

這顯然是一種更加 現代 的方式,不光是減小了鎖的開銷提高了效能,更重要的是大大減少了CPU的空轉,提高了CPU的利用率。

我們先看一下模擬巨集核心的程式碼在執行10秒時的CPU利用率:

觀察下熱點,可以猜測就是spinlock:

顯然,CPU利用率那麼高,並非真的在執行有用的task,而是在spin空轉。

我們再看下模擬微核心的程式碼在同樣情況下的表現:

看下熱點:

顯然,仍然有個spinlock的熱點,但顯然降低了很多。在更高執行效率的保證下,CPU並沒有那麼高,剩餘的空閒時間可以再去執行更多有意義的工作程序。

本文只是展示一個定性的效果,實際中,微核心服務程序的任務佇列的管理效率會更高。甚至可以硬體實現。【參見交換機背板的交換網路實現。】

說了這麼多,也許有人會說, NO,你這兩個比對的case不嚴謹,你只模擬了訪問共享的資料,如果是真的可並行執行的程式碼用微核心的方案豈不是要降低效能嗎?平白自廢武功,將並行改成序列!確實如此,但是 核心本身就是共享的。 作業系統本身就是協呼叫戶程序對底層共享資源訪問的。

所以真並行需要程式設計師自己來 設計可並行的應用程式。

核心本身就是共享的。共享資源的多執行緒訪問就應該嚴格序列化,併發爭鎖是一種最無序的方式,而最有效的方式則是統一仲裁排程。

在我們日常生活中,我們顯然能看到和理解為什麼排隊上車比擁擠著上車更加高效。在計算機系統領域,同樣的事情我們也見於交換式乙太網和PCIe,相比CSMA/CD的共享式乙太網,交換機就是一個仲裁排程器,PCIe的訊息hub也是扮演著同樣的角色。

其實,即便是巨集核心,在訪問共享資源時,也並不是全都是併發爭鎖的方式,對於敏感度比較高的資源,比如時延要求很高的硬體資源,系統底層也是仲裁排程實現的,比如網絡卡上層發包的佇列排程程式,此外對於磁碟IO也有對應的磁碟排程程式。

然而對於巨集核心,更加上層的邏輯資源,比如VFS檔案物件,socket物件,各種佇列等等卻沒有采用仲裁排程的方式去訪問,當它們由多個執行緒併發訪問時,採用了令人遺憾的併發爭鎖模式,這也是不得已而為之,因為沒有哪個實體可以完成仲裁,畢竟訪問它們的上下文是隔離的。

來個插敘。當進行Linux系統調優時,瞄準這些方面相關的熱點基本就夠了。大量熱點問題都是這種引起的,open/close同一個檔案,程序上下文和軟中斷同時操作同一個socket,收包時多個CPU上的軟中斷上下文將包排入同一個佇列,諸如此類。\\ 如果你不準備去調優Linux,或許你已經知道Linux核心在SMP環境下的根本缺陷,調它作甚。多看看外面的世界,搞不好比你眼前唯一的那個要好。

當我們評價傳統UNIX以及Linux這種作業系統核心時,應該更多的去看它們缺失了什麼,而不是一味的覺得它們就是對的。【你認為它是的,可能僅僅因為它是你第一個見到並且唯一見過的】

如果非要說下概念,那就有必要說說 現代作業系統 的虛擬機器抽象。

對於我們經常說的 現代作業系統 而言,按照最初的馮諾伊曼結構,只有 “CPU和記憶體” 在 多處理(包括所有的多程序,多執行緒等機制) 機制中被抽象了出來,而對於檔案系統,網路協議棧等等卻沒有進行多處理抽象。換句話說,現代作業系統為程序提供了 獨佔的虛擬機器抽象,該虛擬機器僅僅包括CPU和記憶體:

時間片排程讓程序認為自己獨佔了CPU。虛擬記憶體讓程序認為自己獨享了記憶體。再無其它虛擬機器抽象。

在程序使用這些抽象資源時,現代作業系統無疑採用了仲裁排程機制:

作業系統提供任務排程器仲裁CPU的分時複用(典型的是多級反饋優先順序佇列演算法),為程序/執行緒統一分配物理CPU的時間片資源。作業系統提供記憶體分配演算法仲裁實體記憶體空間的分配(典型的是夥伴系統演算法),為程序/執行緒統一分配實體記憶體對映給虛擬記憶體。

顯然,正如本文開頭說過的,作業系統並未任由程序們去併發爭搶CPU和記憶體資源,然而對於其它幾乎所有資源,作業系統並未做任何嚴格的規定。作業系統以兩種態度對待它們:

認為其它資源並非作業系統核心的一部分,於是微核心,使用者態驅動等等就形成了概念。認為其它底層的資源也是作業系統核心的一部分,這就是巨集核心比如Linux的態度。

態度如何,這並不重要,巨集核心,微核心,使用者態,核心態,這些也只是概念而已,沒有什麼大不了的。關鍵的問題乃是:

如何協調共享資源的分配。 或空間資源,或時間資源的或併發爭鎖,或仲裁排程的方式分配。

無疑,最大的爭議就在CPU/記憶體之外如何協調非程序虛擬化的檔案系統的訪問和網路協議棧的訪問。但無論它們倆的哪一個,目前無論是巨集核心還是微核心都有非常非常棒的方案。遺憾的是,這些很棒的方案都不是Linux核心所採用的方案。

哦,對了Nginx便採取了類似微核心,交換機,PCIe的方法,Apache卻不是。還有很多別的例子,不再一一贅述,只是想說一點,作業系統領域,核心的東西都是大象無形的,而不是那些形形色色的概念。

摘錄一段王垠聊微核心時的一段話:

跟有些人聊作業系統是件鬧心的事,因為我往往會拋棄一些術語和概念,從零開始討論。我試圖從“計算本質”的出發點來理解這類事物,理解它們的起因,發展,現狀和可能的改進。我所關心的往往是“這個事物應該是什麼樣子”,“它還可以是什麼(也許更好的)樣子”,而不只是“它現在是什麼樣子”。不明白我的這一特性,又自恃懂點東西的人,往往會誤以為我連基本的術語都不明白。於是天就這樣被他們聊死了。

這其實也是我想說的。

so,忘掉微核心,巨集核心,忘掉核心態,使用者態,忘掉真實模式,保護模式,這樣你會更深刻地理解如何仲裁共享資源的訪問的本質。

最新評論
  • 1 #

    不對的。不能拿應用程式的執行來模擬核心設計,核心是一個被動體,在管理好資源的同時,最小程度的減小效能噪音是核心的。另外,linux中的spinlock,你還真可以好好看看它的實現,人家是排隊的,而且設計的非常精緻,而且高度適應smp。即便是談本質,也不能太想當然。

  • 2 #

    資訊產業部,必須牽頭搞一次大論證。從歷史角度來看,現在的作業系統才剛剛發展,我們從0和1開始寫起,還來得及,百年後千年後,子孫會記住你們的

  • 3 #

    都是些設計哲學的事情,在一個時代的設計哲學必然在一個時代的背景下產生的。而作業系統的壟斷性和行業性必須保證廠商的最小成本。所以設計哲學和設計實現之間有延時是正常的。而且linux因為開源,所以有些地方其實做的還是不錯的。當然作者的思考本身也是基於自身的侷限的,算是好的思考吧

  • 4 #

    呵呵…我一個朋友搞了一臺96核的機器(2塊48核的Xeon處理器),結果Win10死活用不了,然後詢問供應商說,Win10帶不動那麼多核,只能上Linux…

  • 5 #

    多CPU、多核的伺服器為什麼都選linux?

  • 6 #

    你的出發點過於理想化和簡單化,實際上,併發操作的行為,尤其是對共享資源的競爭行為是非常複雜的,不能用一個簡單模型去研究和討論。特別是不能和乙太網的包通訊模型去對比。

  • 7 #

    MacOS X好像是微核心的

  • 8 #

    Unix和linux體現了什麼叫系統哲學之美。

  • 9 #

    Liux有很多不足然而優點是開源,你有本事你就可以自己完善不足,完善不了Win這些不停優化的系統是你的選擇然而缺陷是你要把你的祕密公開給微軟

  • 10 #

    微核心難道不是一個訊息系統嗎?它並不負責排程和仲裁。

  • 11 #

    Linux 免費開源 接受全球的優秀程式設計師對其進行開發升級 這一點難能可貴 沒有完美的系統 相信Linux的擁護者會繼續努力

  • 12 #

    感覺Linux做到中規中矩就是最棒啦

  • 13 #

    好文章,多看幾遍,看能不能看懂點。。

  • 14 #

    debian有hurd版,功能都不全,而且開發了很多年了仍然不能運用。

  • 15 #

    進入佇列的時候也是無序競爭。

  • 16 #

    win再好,可惜都被綁架了

  • 17 #

    作為高手,你應該改進而不是吐槽

  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 萬字長文利用ELK分析Nginx日誌生產實戰(高清多圖)