linux進程調度

爲何要有進程優先級?這彷佛不用過多的解釋,畢竟自從多任務操做系統誕生以來,進程執行佔用cpu的能力就是一個必需要能夠人爲控制的事情。由於有的進程相對重要,而有的進程則沒那麼重要。php

本文做者:鄒立巍python

Linux系統技術專家。目前在騰訊SNG社交網絡運營部 計算資源平臺組,負責內部私有云平臺的建設和架構規劃設計。linux

曾任新浪動態應用平臺系統架構師,負責微博、新浪博客等重點業務的內部私有云平臺架構設計和運維管理工做。nginx

進程優先級起做用的方式從發明以來基本沒有什麼變化,不管是隻有一個cpu的時代,仍是多核cpu時代,都是經過控制進程佔用cpu時間的長短來實現的。就是說在同一個調度週期中,優先級高的進程佔用的時間長些,而優先級低的進程佔用的短些。算法

從這個角度看,進程優先級其實也跟cgroup的cpu限制同樣,都是一種針對cpu佔用的QOS機制。我曾經一直很困惑一點,爲何已經有了優先級,還要再設計一個針對cpu的cgroup?獲得的答案大概是由於,優先級這個值不能很直觀的反饋出資源分配的比例吧?shell

不過這不重要,實際上從內核目前的進程調度器cfs的角度說,同時實現cpushare方式的cgroup和優先級這兩個機制徹底是相同的概念,並不會由於增長一個機制而提升什麼實現成本。既然如此,而cgroup又顯得那麼酷,那麼何樂而不爲呢?編程

在系統上咱們最熟悉的優先級設置方式是nice和renice命令。那麼咱們首先解釋一個概念,什麼是:vim

 

NICE值

nice值應該是熟悉Linux/UNIX的人很瞭解的概念了,咱們都知它是反應一個進程「優先級」狀態的值,其取值範圍是-20至19,一共40個級別。這個值越小,表示進程」優先級」越高,而值越大「優先級」越低。咱們能夠經過nice命令來對一個將要執行的命令進行nice值設置,方法是:數組

  1. [root@zorrozou-pc0 zorro]#nice-10bash

這樣我就又打開了一個bash,而且其nice值設置爲10,而默認狀況下,進程的優先級應該是從父進程繼承來的,這個值通常是0。咱們能夠經過nice命令直接查看到當前shell的nice值bash

  1. [root@zorrozou-pc0 zorro]#nice
  2. 10

對比一下正常狀況:

  1. [root@zorrozou-pc0 zorro]#exit

推出當前nice值爲10的bash,打開一個正常的bash:

  1. [root@zorrozou-pc0 zorro]#bash
  2. [root@zorrozou-pc0 zorro]#nice
  3. 0

另外,使用renice命令能夠對一個正在運行的進程進行nice值的調整,咱們也可使用好比top、ps等命令查看進程的nice值,具體方法我就很少說了,你們能夠參閱相關manpage。

須要你們注意的是,我在這裏都在使用nice值這一稱謂,而非優先級(priority)這個說法。固然,nice和renice的man手冊中, 也說的是priority這個概念,可是要強調一下,請你們真的不要混淆了系統中的這兩個概念,一個是nice值,一個是priority值,他們有着千 絲萬縷的關係,但對於當前的Linux系統來講,它們並非同一個概念。

咱們看這個命令:

  1. [root@zorrozou-pc0 zorro]#ps-l
  2. F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
  3. 4 S 0692457760800-17952 poll_s pts/500:00:00sudo
  4. 4 S 0692569240800-4435 wait pts/500:00:00bash
  5. 0 R 01297169250800-8514- pts/500:00:00ps

你們是否真的明白其中PRI列和NI列的具體含義有什麼區別?一樣的,若是是top命令:

  1. Tasks:1587 total,7 running,1570 sleeping,0 stopped,10 zombie
  2. Cpu(s):13.0%us,6.9%sy,0.0%ni,78.6%id,0.0%wa,0.0%hi,1.5%si,0.0%st
  3. Mem:132256952k total,107483920k used,24773032k free,2264772k buffers
  4. Swap:2101192k total,508k used,2100684k free,88594404k cached
  5. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  6. 3001 root 200232m21m4500 S 12.90.00:15.09 python
  7. 11541 root 200174562400888 R 7.40.00:00.06top

你們是否搞清楚了這其中PR值和NI值的差異?若是沒有,那麼咱們能夠首先搞清楚什麼是nice值。

nice值雖然不是priority,可是它確實能夠影響進程的優先級。

在英語中,若是咱們形容一我的nice,那通常說明這我的的人緣比較好。什麼樣的人人緣好?每每是謙讓、有禮貌的人。好比,你跟一個nice的人一 起去吃午餐,點了兩個同樣的飯,先上了一份後,nice的那位通常都會說:「你先吃你先吃!」,這就是人緣好,這人nice!可是若是另外一份上的很晚,那 麼這位nice的人就要餓着了。這說明什麼?越nice的人搶佔資源的能力就越差,而越不nice的人搶佔能力就越強。這就是nice值大小的含義,nice值越低,說明進程越不nice,搶佔cpu的能力就越強,優先級就越高。在原來使用O1調度的Linux上,咱們還會把nice值叫作靜態優先級,這也基本符合nice值的特色,就是��nice值設定好了以後,除非咱們用renice去改它,不然它是不變的。而priority的值在以前內核的O1調度器上表現是會變化的,因此也叫作動態優先級

 

優先級和實時進程

簡單瞭解nice值的概念以後,咱們再來看看什麼是priority值,就是ps命令中看到的PRI值或者top命令中看到的PR值。本文爲了區分這些概念,之後統一用nice值表示NI值,或者叫作靜態優先級,也就是用nice和renice命令來調整的優先級;而使用priority值表示PRI和PR值,或者叫動態優先級。咱們也統一將「優先級」這個詞的概念規定爲表示priority值的意思。

在內核中,進程優先級的取值範圍是經過一個宏定義的,這個宏的名稱是MAX_PRIO,它的值爲140。而這個值又是由另外兩個值相加組成的,一個 是表明nice值取值範圍的NICE_WIDTH宏,另外一個是表明實時進程(realtime)優先級範圍的MAX_RT_PRIO宏。說白了就 是,Linux實際上實現了140個優先級範圍,取值範圍是從0-139,這個值越小,優先級越高。nice值的-20到19,映射到實際的優先級範圍是 100-139。新產生進程的默認優先級被定義爲:

  1. #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH /2)

實際上對應的就是nice值的0。正常狀況下,任何一個進程的優先級都是這個值,即便咱們經過nice和renice命令調整了進程的優先級,它的 取值範圍也不會超出100-139的範圍,除非這個進程是一個實時進程,那麼它的優先級取值纔會變成0-99這個範圍中的一個。這裏隱含了一個信息,就是 說當前的Linux是一種已經支持實時進程的操做系統。

什麼是實時操做系統,咱們就再也不這裏詳細解釋其含義以及在工業領域的應用了,有興趣的能夠參考一下實時操做系統的維基百科。簡單來講,實時操做系統須要保證相關的實時進程在較短的時間內響應,不會有較長的延時,而且要求最小的中斷延時和進程切換延時。對於這樣的需求,通常的進程調度算法,不管是O1仍是CFS都是沒法知足的,因此內核在設計的時候,將實時進程單獨映射了100個優先級,這些優先級都要高與正常進程的優先級(nice值),而實時進程的調度算法也不一樣,它們採用更簡單的調度算法來減小調度開銷。總的來講,Linux系統中運行的進程能夠分紅兩類:

  1. 實時進程
  2. 非實時進程

它們的主要區別就是經過優先級來區分的。全部優先級值在0-99範圍內的,都是實時進程,因此這個優先級範圍也能夠叫作實時進程優先級,而 100-139範圍內的是非實時進程。在系統中可使用chrt命令來查看、設置一個進程的實時優先級狀態。咱們能夠先來看一下chrt命令的使用:

  1. [root@zorrozou-pc0 zorro]# chrt
  2. Showor change the real-time scheduling attributes of a process.
  3. Set policy:
  4. chrt [options]<priority><command>[<arg>...]
  5. chrt [options]-<priority><pid>
  6. Get policy:
  7. chrt [options]-<pid>
  8. Policy options:
  9. -b,--batch set policy to SCHED_OTHER
  10. -f,--fifo set policy to SCHED_FIFO
  11. -i,--idle set policy to SCHED_IDLE
  12. -o,--other set policy to SCHED_OTHER
  13. -r,--rr set policy to SCHED_RR (default)
  14. Scheduling flag:
  15. -R,--reset-on-fork set SCHED_RESET_ON_FORK for FIFO or RR
  16. Other options:
  17. -a,--all-tasks operate on all the tasks (threads)for a given pid
  18. -m,--max show min and max valid priorities
  19. -p,--pid operate on existing given pid
  20. -v,--verbose display status information
  21. -h,--help display this help andexit
  22. -V,--version output version information andexit
  23. Formore details see chrt(1).

咱們先來關注顯示出的Policy options部分,會發現系統給個種進程提供了5種調度策略。可是這裏並無說明的是,這五種調度策略是分別給兩種進程用的,對於實時進程能夠用的調度 策略是:SCHED_FIFO、SCHED_RR,而對於非實時進程則是:SCHED_OTHER、SCHED_OTHER、SCHED_IDLE。

系統的總體優先級策略是:若是系統中存在須要執行的實時進程,則優先執行實時進程。直到實時進程退出或者主動讓出CPU時,纔會調度執行非實時進程。實時進程能夠指定的優先級範圍爲1-99,將一個要執行的程序以實時方式執行的方法爲:

  1. [root@zorrozou-pc0 zorro]# chrt 10bash
  2. [root@zorrozou-pc0 zorro]# chrt -p $$
  3. pid 14840's current scheduling policy: SCHED_RR
  4. pid 14840's current scheduling priority:10

能夠看到,新打開的bash已是實時進程,默認調度策略爲SCHED_RR,優先級爲10。若是想修改調度策略,就加個參數:

  1. [root@zorrozou-pc0 zorro]# chrt -10bash
  2. [root@zorrozou-pc0 zorro]# chrt -p $$
  3. pid 14843's current scheduling policy: SCHED_FIFO
  4. pid 14843's current scheduling priority:10

剛纔說過,SCHED_RR和SCHED_FIFO都是實時調度策略,只能給實時進程設置。對於全部實時進程來講,優先級高的(就是 priority數字小的)進程必定會保證先於優先級低的進程執行。SCHED_RR和SCHED_FIFO的調度策略只有當兩個實時進程的優先級同樣的 時候纔會發生做用,其區別也是顧名思義:

SCHED_FIFO:以先進先出的隊列方式進行調度,在優先級同樣的狀況下,誰先執行的就先調度誰,除非它退出或者主動釋放CPU。

SCHED_RR:以時間片輪轉的方式對相同優先級的多個進程進行處理。時間片長度爲100ms。

這就是Linux對於實時進程的優先級和相關調度算法的描述。總體很簡單,也很實用。而相對更麻煩的是非實時進程,它們纔是Linux上進程的主要分類。對於非實時進程優先級的處理,咱們首先仍是要來介紹一下它們相關的調度算法:O1和CFS。

 

O1調度

O1調度算法是在Linux 2.6開始引入的,到Linux 2.6.23以後內核將調度算法替換成了CFS。雖然O1算法已經不是當前內核所默認使用的調度算法了,可是因爲大量線上的服務器可能使用的Linux版 本仍是老版本,因此我相信不少服務器仍是在使用着O1調度器,那麼費一點口舌簡單交代一下這個調度器也是有意義的。這個調度器的名字之因此叫作O1,主要 是由於其算法的時間複雜度是O1。

O1調度器仍然是根據經典的時間片分配的思路來進行總體設計的。簡單來講,時間片的思路就是將CPU的執行時間 分紅一小段一小段的,假如是5ms一段。因而多個進程若是要「同時」執行,實際上就是每一個進程輪流佔用5ms的cpu時間,而從1s的時間尺度上看,這些 進程就是在「同時」執行的。固然,對於多核系統來講,就是把每一個核心都這樣作就好了。而在這種狀況下,如何支持優先級呢?實際上就是將時間片分配成大小不 等的若干種,優先級高的進程使用大的時間片,優先級小的進程使用小的時間片。這樣在一個週期結速後,優先級大的進程就會佔用更多的時間而所以獲得特殊待 遇。O1算法還有一個比較特殊的地方是,即便是相同的nice值的進程,也會再根據其CPU的佔用狀況將其分紅兩種類型:CPU消耗型和IO消耗性。 典型的CPU消耗型的進程的特色是,它老是要一直佔用CPU進行運算,分給它的時間片老是會被耗盡以後,程序纔可能發生調度。好比常見的各類算數運算程 序。而IO消耗型的特色是,它常常時間片沒有耗盡就本身主動先釋放CPU了,好比vi,emacs這樣的編輯器就是典型的IO消耗型進程。

爲何要這樣區分呢?由於IO消耗型的進程常常是跟人交互的進程,好比shell、編輯器等。當系統中既有這種進程,又有CPU消耗型進程存在,並 且其nice值同樣時,假設給它們分的時間片長度是同樣的,都是500ms,那麼人的操做可能會由於CPU消耗型的進程一直佔用CPU而變的卡頓。能夠想 象,當bash在等待人輸入的時候,是不佔CPU的,此時CPU消耗的程序會一直運算,假設每次都分到500ms的時間片,此時人在bash上敲入一個字 符的時候,那麼bash極可能要等個幾百ms才能給出響應,由於在人敲入字符的時候,別的進程的時間片極可能並無耗盡,因此係統不會調度bash程度進 行處理。爲了提升IO消耗型進程的響應速度,系統將區分這兩類進程,並動態調整CPU消耗的進程將其優先級下降,而IO消耗型的將其優先級變高,以下降 CPU消耗進程的時間片的實際長度。已知nice值的範圍是-20 - 19,其對應priority值的範圍是100-139,對於一個默認nice值爲0的進程來講,其初始priority值應該是120,隨着其不斷執 行,內核會觀察進程的CPU消耗狀態,並動態調整priority值,可調整的範圍是+-5。就是說,最��其優先級能夠唄自動調整到115,最低到 125。這也是爲何nice值叫作靜態優先級而priority值叫作動態優先級的緣由。不過這個動態調整的功能在調度器換成CFS以後就不須要了,因 爲CFS換了另一種CPU時間分配方式,這個咱們後面再說。

再簡單瞭解了O1算法按時間片分配CPU的思路以後,咱們再來結合進程的狀態簡單看看其算法描述。咱們都知道進程有5種狀態:

  • S(Interruptible sleep):可中斷休眠狀態。
  • D(Uninterruptible sleep):不可中斷休眠狀態。
  • R(Running or runnable):執行或者在可執行隊列中。
  • Z(Zombie process):殭屍。
  • T(Stopped):暫停。

在CPU調度時,主要只關心R狀態進程,由於其餘狀態進程並不會被放倒調度隊列中進行調度。調度隊列中的進程通常主要有兩種狀況,一種是進程已經被 調度到CPU上執行,另外一種是進程正在等待被調度。出現這兩種狀態的緣由應該好理解,由於須要執行的進程數可能多於硬件的CPU核心數,好比須要執行的進 程有8個而CPU核心只有4個,此時cpu滿載的時候,必定會有4個進程處在「等待」狀態,由於此時有另外四個進程正在佔用CPU執行。

根據以上狀況咱們能夠理解,系統當下須要同時進行調度處理的進程數(R狀態進程數)和系統CPU的比值,能夠必定程度的反應系統的「繁忙」程度。需 要調度的進程越多,核心越少,則意味着系統越繁忙。除了進程執行自己須要佔用CPU之外,多個進程的調度切換也會讓系統繁忙程度增長的更多。因此,咱們往 往會發現,R狀態進程數量在增加的狀況下,系統的性能表現會降低。系統中可使用uptime命令查看系統平均負載指數(load average):

  1. [zorro@zorrozou-pc0 ~]uptime
  2. 16:40:56 up 2:12,1 user, load average:0.05,0.11,0.16

其中load average中分別顯示的是1分鐘,5分鐘,15分鐘以內的平均負載指數(能夠簡單認爲是相映時間範圍內的R狀態進程個數)。可是這個命令顯示的數字是 絕對個數,並無表示出不一樣CPU核心數的實際狀況。好比,若是咱們的1分鐘load average爲16,而CPU核心數爲32的話,那麼這個系統的其實並不繁忙。可是若是CPU個數是8的話,那可能就意味着比較忙了。可是實際狀況每每 可能比這更加複雜,好比進程消耗類型也會對這個數字的解讀有影響。總之,這個值的絕對高低並不能直觀的反饋出來當前系統的繁忙程度,還須要根據系統的其它 指標綜合考慮。

O1調度器在處理流程上大概是這樣進行調度的:

  1. 首先,進程產生(fork)的時候會給一個進程分配一個時間片長度。這個新進程的時間片通常是父進程的一半,而父進程也會所以減小它的時間片長度 爲原來的一半。就是說,若是一個進程產生了子進程,那麼它們將會平分當前時間片長度。好比,若是父進程時間片還剩100ms,那麼一個fork產生一個子 進程以後,子進程的時間片是50ms,父進程剩餘的時間片是也是50ms。這樣設計的目的是,爲了防止進程經過fork的方式讓本身所處理的任務一直有時 間片。不過這樣作也會帶來少量的不公平,由於先產生的子進程得到的時間片將會比後產生的長,第一個子進程分到父進程的一半,那麼第二個子進程就只能分到 1/4。對於一個長期工做的進程組來講,這種影響能夠忽略,由於第一輪時間片在耗盡後,系統會在給它們分配長度至關的時間片。
  2. 針對全部R狀態進程,O1算法使用兩個隊列組織進程,其中一個叫作活動隊列,另外一個叫作過時隊列。活動隊列中放的都是時間片未被耗盡的進程,而過時隊列中放時間片被耗盡的進程。
  3. 如1所述,新產生的進程都會先得到一個時間片,進入活動隊列等待調度到CPU執行。而內核會在每一個tick間隔期間對正在CPU上執行的進程進行 檢查。通常的tick間隔時間就是cpu時鐘中斷間隔,每秒鐘會有1000個,即頻率爲1000HZ。每一個tick間隔週期主要檢查兩個內容:一、當前正 在佔用CPU的進程是否是時間片已經耗盡了?二、是否是有更高優先級的進程在活動隊列中等待調度?若是任何一種狀況成立,就把則當前進程的執行狀態終止, 放到等待隊列中,換當前在等待隊列中優先級最高的那個進程執行。

以上就是O1調度的基本調度思路,固然實際狀況是,還要加上SMP(對稱多處理)的邏輯,以知足多核CPU的需求。目前在個人archlinux上能夠用如下命令查看內核HZ的配置:

  1. [zorro@zorrozou-pc0 ~]$ zgrep CONFIG_HZ /proc/config.gz
  2. # CONFIG_HZ_PERIODIC isnotset
  3. # CONFIG_HZ_100 isnotset
  4. # CONFIG_HZ_250 isnotset
  5. CONFIG_HZ_300=y
  6. # CONFIG_HZ_1000 isnotset
  7. CONFIG_HZ=300

咱們發現我當前系統的HZ配置爲300,而不是通常狀況下的1000。你們也能夠思考一下,配置成不一樣的數字(100、250、300、1000),對系統的性能到底會有什麼影響?

 

CFS徹底公平調度

O1已是上一代調度器了,因爲其對多核、多CPU系統的支持性能並很差,而且內核功能上要加入cgroup等因素,Linux在2.6.23以後 開始啓用CFS做爲對通常優先級(SCHED_OTHER)進程調度方法。在這個從新設計的調度器中,時間片,動態、靜態優先級以及IO消耗,CPU消耗 的概念都再也不重要。CFS採用了一種全新的方式,對上述功能進行了比較完善的支持。

其設計的基本思路是,咱們想要實現一個對全部進程徹底公平的調度器。又是那個老問題:如何作到徹底公平?答案跟上一篇IO調度中CFQ的思路相似: 若是當前有n個進程須要調度執行,那麼調度器應該再一個比較小的時間範圍內,把這n個進程全都調度執行一遍,而且它們平分cpu時間,這樣就能夠作到全部 進程的公平調度。那麼這個比較小的時間就是任意一個R狀態進程被調度的最大延時時間,即:任意一個R狀態進程,都必定會在這個時間範圍內被調度相應。這個 時間也能夠叫作調度週期,其英文名字叫作:sched_latency_ns。進程越多,每一個進程在週期內被執行的時間就會被平分的越小。調度器只須要對 全部進程維護一個累積佔用CPU時間數,就能夠衡量出每一個進程目前佔用的CPU時間總量是否是過大或者太小,這個數字記錄在每一個進程的vruntime 中。全部待執行進程都以vruntime爲key放到一個由紅黑樹組成的隊列中,每次被調度執行的進程,都是這個紅黑樹的最左子樹上的那個進程,即 vruntime時間最少的進程,這樣就保證了全部進程的相對公平。

在基本驅動機制上CFS跟O1同樣,每次時鐘中斷來臨的時候,都會進行隊列調度檢查,判斷是否要進程調度。固然還有別的時機須要調度檢查,發生調度的時機能夠總結爲這樣幾個:

  1. 當前進程的狀態轉換時。主要是指當前進程終止退出或者進程休眠的時候。
  2. 當前進程主動放棄CPU時。狀態變爲sleep也能夠理解爲主動放棄CPU,可是當前內核給了一個方法,可使用sched_yield()在不發生狀態切換的狀況下主動讓出CPU。
  3. 當前進程的vruntime時間大於每一個進程的理想佔用時間時(delta_exec > ideal_runtime)。這裏的ideal_runtime實際上就是上文說的sched_latency_ns/進程數n。固然這個值並非必定 這樣得出,下文會有更詳細解釋。
  4. 當進程從中斷、異常或系統調用返回時,會發生調度檢查。好比時鐘中斷。

 

CFS的優先級

固然,CFS中還須要支持優先級。在新的體系中,優先級是以時間消耗(vruntime增加)的快慢來決定的。就是說,對於CFS來講,衡量的時間 累積的絕對值都是同樣紀錄在vruntime中的,可是不一樣優先級的進程時間增加的比率是不一樣的,高優先級進程時間增加的慢,低優先級時間增加的快。比 如,優先級爲19的進程,實際佔用cpu爲1秒,那麼在vruntime中就記錄1s。可是若是是-20優先級的進程,那麼它極可能實際佔CPU用 10s,在vruntime中才會紀錄1s。CFS真實實現的不一樣nice值的cpu消耗時間比例在內核中是按照「每差一級cpu佔用時間差10%左右」 這個原則來設定的。這裏的大概意思是說,若是有兩個nice值爲0的進程同時佔用cpu,那麼它們應該每人佔50%的cpu,若是將其中一個進程的 nice值調整爲1的話,那麼此時應保證優先級高的進程比低的多佔用10%的cpu,就是nice值爲0的佔55%,nice值爲1的佔45%。那麼它們 佔用cpu時間的比例爲55:45。這個值的比例約爲1.25。就是說,相鄰的兩個nice值之間的cpu佔用時間比例的差異應該大約爲1.25。根據這 個原則,內核對40個nice值作了時間計算比例的對應關係,它在內核中以一個數組存在:

  1. staticconstint prio_to_weight[40]={
  2. /* -20 */88761,71755,56483,46273,36291,
  3. /* -15 */29154,23254,18705,14949,11916,
  4. /* -10 */9548,7620,6100,4904,3906,
  5. /* -5 */3121,2501,1991,1586,1277,
  6. /* 0 */1024,820,655,526,423,
  7. /* 5 */335,272,215,172,137,
  8. /* 10 */110,87,70,56,45,
  9. /* 15 */36,29,23,18,15,
  10. };

咱們看到,實際上nice值的最高優先級和最低優先級的時間比例差距仍是很大的,毫不僅僅是例子中的十倍。由此咱們也能夠推導出每個nice值級別計算vruntime的公式爲:

  1. delta vruntime  delta Time*1024/ load

這個公式的意思是說,在nice值爲0的時候(對應的比例值爲1024),計算這個進程vruntime的實際增加時間值(delta vruntime)爲:CPU佔用時間(delta Time)* 1024 / load。在這個公式中load表明當前sched_entity的值,其實就能夠理解爲須要調度的進程(R狀態進程)個數。load越大,那麼每一個進程 所能分到的時間就越少。CPU調度是內核中會頻繁進行處理的一個時間,因而上面的delta vruntime的運算會被頻繁計算。除法運算會佔用更多的cpu時間,因此內核編程中的一個原則就是,儘量的不用除法。內核中要用除法的地方,基本都 用乘法和位移運算來代替,因此上面這個公式就會變成:

  1. delta vruntime  delta time*1024*(2^32/(load *2^32))=(delta time*1024*Inverseload))>>32

內核中爲了方便不一樣nice值的Inverse(load)的相關計算,對作好了一個跟prio_to_weight數組一一對應的數組,在計算中能夠直接拿來使用,減小計算時的CPU消耗:

  1. staticconst u32 prio_to_wmult[40]={
  2. /* -20 */48388,59856,76040,92818,118348,
  3. /* -15 */147320,184698,229616,287308,360437,
  4. /* -10 */449829,563644,704093,875809,1099582,
  5. /* -5 */1376151,1717300,2157191,2708050,3363326,
  6. /* 0 */4194304,5237765,6557202,8165337,10153587,
  7. /* 5 */12820798,15790321,19976592,24970740,31350126,
  8. /* 10 */39045157,49367440,61356676,76695844,95443717,
  9. /* 15 */119304647,148102320,186737708,238609294,286331153,
  10. };

具體計算細節不在這裏細解釋了,有興趣的能夠自行閱讀代碼:kernel/shced/fair.c(Linux 4.4)中的__calc_delta()函數實現。

根據CFS的特性,咱們知道調度器老是選擇vruntime最小的進程進行調度。那麼若是有兩個進程的初始化vruntime時間同樣時,一個進程 被選擇進行調度處理,那麼只要一進行處理,它的vruntime時間就會大於另外一個進程,CFS難道要立刻換另外一個進程處理麼?出於減小頻繁切換進程所帶 來的成本考慮,顯然並不該該這樣。CFS設計了一個sched_min_granularity_ns參數,用來設定進程被調度執行以後的最小CPU佔用 時間。

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_min_granularity_ns
  2. 2250000

一個進程被調度執行後至少要被執行這麼長時間纔會發生調度切換。

咱們知道不管到少個進程要執行,它們都有一個預期延遲時間,即:sched_latency_ns,系統中能夠經過以下命令來查看這個時間:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_latency_ns
  2. 18000000

在這種狀況下,若是須要調度的進程個數爲n,那麼平均每一個進程佔用的CPU時間爲sched_latency_ns/n。顯然,每一個進程實際佔用的 CPU時間會由於n的增大而減少。可是實現上不可能讓它無限的變小,因此sched_min_granularity_ns的值也限定了每一個進程能夠得到 的執行時間週期的最小值。當進程不少,致使使用了sched_min_granularity_ns做爲最小調度週期時,對應的調度延時也就不在遵循 sched_latency_ns的限制,而是以實際的須要調度的進程個數n * sched_min_granularity_ns進行計算。固然,咱們也能夠把這理解爲CFS的」時間片」,不過咱們仍是要強調,CFS是沒有跟O1類 似的「時間片「的概念的,具體區別你們能夠本身琢磨一下。

 

新進程的VRUNTIME值

CFS是經過vruntime最小值來選擇須要調度的進程的,那麼能夠想象,在一個已經有多個進程執行了相對較長的系統中,這個隊列中的 vruntime時間紀錄的數值都會比較長。若是新產生的進程直接將本身的vruntime值設置爲0的話,那麼它將在執行開始的時間內搶佔不少的CPU 時間,直到本身的vruntime追遇上其餘進程後纔可能調度其餘進程,這種狀況顯然是不公平的。因此CFS對每一個CPU的執行隊列都維護一個 min_vruntime值,這個值紀錄了這個CPU執行隊列中vruntime的最小值,當隊列中出現一個新建的進程時,它的初始化vruntime將 不會被設置爲0,而是根據min_vruntime的值爲基礎來設置。這樣就保證了新建進程的vruntime與老進程的差距在必定範圍內,不會由於 vruntime設置爲0而在進程開始的時候佔用過多的CPU。

新建進程得到的實際vruntime值跟一些設置有關,好比:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_child_runs_first
  2. 0

這個文件是fork以後是否讓子進程優先於父進程執行的開關。0爲關閉,1爲打開。若是這個開關打開,就意味着子進程建立後,保證子進程在父進程之 前被調度。另外,在源代碼目錄下的kernel/sched/features.h文件中,還規定了一系列調度器屬性開關。而其中:

  1. /*
  2. * Place new tasks ahead so that they do not starve already running
  3. * tasks
  4. */
  5. SCHED_FEAT(START_DEBIT,true)

這個參數規定了新進程啓動以後第一次運行會有延時。這意味着新進程的vruntime設置要比默認值大一些,這樣作的目的是防止應用經過不停的 fork來儘量多的得到執行時間。子進程在建立的時候,vruntime的定義的步驟以下,首先vruntime被設置爲min_vruntime。然 後判斷START_DEBIT位是否被值爲true,若是是則會在min_vruntime的基礎上增大一些,增大的時間實際上就是一個進程的調度延時時 間,即上面描述過的calc_delta_fair()函數獲得的結果。這個時間設置完畢以後,就檢查sched_child_runs_first開關 是否打開,若是打開(值被設置爲1),就比較新進程的vruntime和父進程的vruntime哪一個更小,並將新進程的vruntime設置爲更小的那 個值,而父進程的vruntime設置爲更大的那個值,以此保證子進程必定在父進程以前被調度。

 

IO消耗型進程的處理

根據前文,咱們知道除了可能會一直佔用CPU時間的CPU消耗型進程之外,還有一類叫作IO消耗類型的進程,它們的特色是基本不佔用CPU,主要行 爲是在S狀態等待響應。這類進程典型的是vim,bash等跟人交互的進程,以及一些壓力不大的,使用了多進程(線程)的或select、poll、 epoll的網絡代理程序。若是CFS採用默認的策略處理這些程序的話,相比CPU消耗程序來講,這些應用因爲絕大多數時間都處在sleep狀態,它們的 vruntime時間基本是不變的,一旦它們進入了調度隊列,將會很快被選擇調度執行。對比O1調度算法,這種行爲至關於天然的提升了這些IO消耗型進程 的優先級,因而就不須要特殊對它們的優先級進行「動態調整」了。

但這樣的默認策略也是有問題的,有時CPU消耗型和IO消耗型進程的區分不是那麼明顯,有些進程可能會等一會,而後調度以後也會長時間佔用CPU。 這種狀況下,若是休眠的時候進程的vruntime保持不變,那麼等到休眠被喚醒以後,這個進程的vruntime時間就可能會比別人小不少,從而致使不 公平。因此對於這樣的進程,CFS也會對其進行時間補償。補償方式爲,若是進程是從sleep狀態被喚醒的,並且 GENTLE_FAIR_SLEEPERS屬性的值爲true,則vruntime被設置爲sched_latency_ns的一半和當前進程的 vruntime值中比較大的那個。sched_latency_ns的值能夠在這個文件中進行設置:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_latency_ns
  2. 18000000

由於系統中這種調度補償的存在,IO消耗型的進程老是能夠更快的得到響應速度。這是CFS處理與人交互的進程時的策略,即:經過提升響應速度讓人的 操做感覺更好。可是有時候也會由於這樣的策略致使總體性能受損。在不少使用了多進程(線程)或select、poll、epoll的網絡代理程序,通常是 由多個進程組成的進程組進行工做,典型的如apche、nginx和php-fpm這樣的處理程序。它們每每都是由一個或者多個進程使用 nanosleep()進行週期性的檢查是否有新任務,若是有責喚醒一個子進程進行處理,子進程的處理可能會消耗CPU,而父進程則主要是sleep等待 喚醒。這個時候,因爲系統對sleep進程的補償策略的存在,新喚醒的進程就可能會打斷正在處理的子進程的過程,搶佔CPU進行處理。當這種打斷不少很頻 繁的時候,CPU處理的過程就會由於頻繁的進程上下文切換而變的很低效,從而使系統總體吞吐量降低。此時咱們可使用開關禁止喚醒搶佔的特性。

  1. [root@zorrozou-pc0 zorro]#cat/sys/kernel/debug/sched_features
  2. GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY WAKEUP_PREEMPTION NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE RT_PUSH_IPI NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD

上面顯示的這個文件的內容就是系統中用來控制kernel/sched/features.h這個文件所列內容的開關文件,其中WAKEUP_PREEMPTION表示:目前的系統狀態是打開sleep喚醒進程的搶佔屬性的。可使用以下命令關閉這個屬性:

  1. [root@zorrozou-pc0 zorro]#echo NO_WAKEUP_PREEMPTION >/sys/kernel/debug/sched_features
  2. [root@zorrozou-pc0 zorro]#cat/sys/kernel/debug/sched_features
  3. GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY NO_WAKEUP_PREEMPTION NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE RT_PUSH_IPI NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD

其餘相關參數的調整也是相似這樣的方式。其餘我沒講到的屬性的含義,你們能夠看kernel/sched/features.h文件中的註釋。

系統中還提供了一個sched_wakeup_granularity_ns配置文件,這個文件的值決定了喚醒進程是否能夠搶佔的一個時間粒度條 件。默認CFS的調度策略是,若是喚醒的進程vruntime小於當前正在執行的進程,那麼就會發生喚醒進程搶佔的狀況。而 sched_wakeup_granularity_ns這個參數是說,只有在當前進程的vruntime時間減喚醒進程的vruntime時間所得的差 大於sched_wakeup_granularity_ns時,纔回發生搶佔。就是說sched_wakeup_granularity_ns的值越 大,越不容易發生搶佔。

 

CFS和其餘調度策略

SCHED_BATCH

在上文中咱們說過,CFS調度策略主要是針對chrt命令顯示的SCHED_OTHER範圍的進程,實際上就是通常的非實時進程。咱們也已經知道, 這樣的通常進程還包括另外兩種:SCHED_BATCH和SCHED_IDLE。在CFS的實現中,集成了對SCHED_BATCH策略的支持,而且其功 能和SCHED_OTHER策略幾乎是一致的。惟一的區別在於,若是一個進程被用chrt命令標記成SCHED_OTHER策略的話,CFS將永遠認爲這 個進程是CPU消耗型的進程,不會對其進行IO消耗進程的時間補償。這樣作的惟一目的是,能夠在確認進程是CPU消耗型的進程的前提下,對其儘量的進行 批處理方式調度(batch),以減小進程切換帶來的損耗,提升吞度量。實際上這個策略的做用並不大,內核中真正的處理區別只是在標記爲 SCHED_BATCH時進程在sched_yield主動讓出cpu的行爲發生是不去更新cfs的隊列時間,這樣就讓這些進程在主動讓出CPU的時候 (執行sched_yield)不會紀錄其vruntime的更新,從而能夠繼續優先被調度到。對於其餘行爲,並沒有不一樣。

SCHED_IDLE

若是一個進程被標記成了SCHED_IDLE策略,調度器將認爲這個優先級是很低很低的,比nice值爲19的優先級還要低。系統將只在CPU空閒的時候纔會對這樣的進程進行調度執行。若果存在多個這樣的進程,它們之間的調度方式跟正常的CFS相同。

SCHED_DEADLINE

最新的Linux內核還實現了一個最新的調度方式叫作SCHED_DEADLINE。跟IO調度相似,這個算法也是要實現一個能夠在最終期限到達前讓進程能夠調度執行的方法,保證進程不會餓死。目前大多數系統上的chrt還沒給配置接口,暫且不作深刻分析。

另外要注意的是,SCHED_BATCH和SCHED_IDLE同樣,只能對靜態優先級(即nice值)爲0的進程設置。操做命令以下:

  1. [zorro@zorrozou-pc0 ~]$ chrt -0bash
  2. [zorro@zorrozou-pc0 ~]$ chrt -p $$
  3. pid 5478's current scheduling policy: SCHED_IDLE
  4. pid 5478's current scheduling priority:0
  5. [zorro@zorrozou-pc0 ~]$ chrt -0bash
  6. [zorro@zorrozou-pc0 ~]$ chrt -p $$
  7. pid 5502's current scheduling policy: SCHED_BATCH
  8. pid 5502's current scheduling priority:0

 

多CPU的CFS調度

在上面的敘述中,咱們能夠認爲系統中只有一個CPU,那麼相關的調度隊列只有一個。實際狀況是系統是有多核甚至多個CPU的,CFS從一開始就考慮 了這種狀況,它對每一個CPU核心都維護一個調度隊列,這樣每一個CPU都對本身的隊列進程調度便可。這也是CFS比O1調度算法更高效的根本緣由:每一個 CPU一個隊列,就能夠避免對全局隊列使用大內核鎖,從而提升了並行效率。固然,這樣最直接的影響就是CPU之間的負載可能不均,爲了維持CPU之間的負 載均衡,CFS要按期對全部CPU進行load balance操做,因而就有可能發生進程在不一樣CPU的調度隊列上切換的行爲。這種操做的過程也須要對相關的CPU隊列進行鎖操做,從而下降了多個運行 隊列帶來的並行性。不過總的來講,CFS的並行隊列方式仍是要比O1的全局隊列方式要高效。尤爲是在CPU核心愈來愈多的狀況下,全局鎖的效率降低顯著增 加。

CFS對多個CPU進行負載均衡的行爲是idle_balance()函數實現的,這個函數會在CPU空閒的時候由schedule()進行調用, 讓空閒的CPU從其餘繁忙的CPU隊列中取進程來執行。咱們能夠經過查看/proc/sched_debug的信息來查看全部CPU的調度隊列狀態信息以 及系統中全部進程的調度信息。內容較多,我就不在這裏一一列出了,有興趣的同窗能夠本身根據相關參考資料(最好的資料就是內核源碼)瞭解其中顯示的相關內 容分別是什麼意思。

在CFS對不一樣CPU的調度隊列作均衡的時候,可能會將某個進程切換到另外一個CPU上執行。此時,CFS會在將這個進程出隊的時候將 vruntime減去當前隊列的min_vruntime,其差值做爲結果會在入隊另外一個隊列的時候再加上所入隊列的min_vruntime,以此來保 持隊列切換後CPU隊列的相對公平。

 

最後

本文的目的是從Linux系統進程的優先級爲出發點,經過了解相關的知識點,但願你們對系統的進程調度有個總體的瞭解。其中咱們也對CFS調度算法 進行了比較深刻的分析。在個人經驗來看,這些知識對咱們在觀察系統的狀態和相關優化的時候都是很是有用的。好比在使用top命令的時候,NI和PR值到底 是什麼意思?相似的地方還有ps命令中的NI和PRI值、ulimit命令-e和-r參數的區別等等。固然,但願看完本文後,能讓你們對這些命令顯示的了 解更加深刻。除此以外,咱們還會發現,雖然top命令中的PR值和ps -l命令中的PRI值的含義是同樣的,可是在優先級相同的狀況下,它們顯示的值確不同。那麼你知道爲何它們顯示會有區別嗎?這個問題的答案留給你們自 己去尋找吧。

相關文章
相關標籤/搜索