一、前言?
Linux調(diào)度器神秘而充滿誘惑,每個(gè)Linux工程師都想深入其內(nèi)部一探究竟。不過中國有一句古話叫做“相由心生”,一個(gè)模塊精巧的內(nèi)部邏輯(也就是所謂的“心”)其外延就是簡潔而優(yōu)雅的接口(我稱之為“相”)。通過外部接口的定義,其實(shí)我們也可以收獲百分之六七十的該模塊的內(nèi)部信息。因此,本文主要描述Linux調(diào)度器開放給用戶空間的接口,希望可以通過用戶空間的調(diào)度器接口來理解Linux調(diào)度器的行為。
二、nice函數(shù)
nice函數(shù)用來修改調(diào)用進(jìn)程的nice value,其接口定義如下:
#include?
?????? int nice(int inc);
為了方便說明該接口的作用,我們還是舉實(shí)際的例子說明。程序調(diào)用nice(3),則將當(dāng)前進(jìn)程的nice value增加3,這也就是意味著該進(jìn)程的優(yōu)先級(jí)降低3個(gè)level(提升nice value也就是對(duì)別人更加nice,自己的優(yōu)先級(jí)就會(huì)低)。如果程序調(diào)用nice(-5),則將當(dāng)前進(jìn)程的nice value減去5,這也就是意味著該進(jìn)程的優(yōu)先級(jí)提升5個(gè)level。當(dāng)調(diào)用錯(cuò)誤的時(shí)候返回-1,調(diào)用成功會(huì)稍微有一些歧義。POSIX標(biāo)準(zhǔn)規(guī)定了nice函數(shù)返回新的nice value,但是linux的系統(tǒng)調(diào)用和c庫都是采用了操作成功返回0的方式。這樣的處理方式使得在調(diào)用nice函數(shù)的時(shí)候無法得到當(dāng)前的優(yōu)先級(jí),如果想要得到當(dāng)前優(yōu)先級(jí),需要調(diào)用getpriority函數(shù),我們?cè)谙乱恍」?jié)描述。
雖然說nice函數(shù)是用來調(diào)整優(yōu)先級(jí),實(shí)際上調(diào)整nice value就是調(diào)整調(diào)度器分配給該進(jìn)程的CPU時(shí)間,具體是如何影響cpu time的呢?我們?cè)诤竺婷枋鰞?nèi)核代碼的時(shí)候再詳聊。此外,需要注意的是:根據(jù)POSIX標(biāo)準(zhǔn),nice value是一個(gè)per process的設(shè)定,但是在linux中,nice value沒有遵從這個(gè)標(biāo)準(zhǔn),它是per-thread的一個(gè)屬性。
三、getpriority/setpriority函數(shù)
從上節(jié)的描述中,我們了解到了nice的函數(shù)的限制,例如只能修改自己的nice value,無法獲取當(dāng)前的nice value值等,為此我們給出加強(qiáng)版本的nice接口,也就是getpriority/setpriority函數(shù)了。getpriority/setpriority函數(shù)定義如下:
#include?
#include?
int getpriority(int which, int who);?
int setpriority(int which, int who, int prio);
你說接口增加功能是好事,怎么就把名字也改了呢?為何不是getnice/setnice呢?其實(shí)從上節(jié)的描述也看出稍許端倪,我們并沒有區(qū)分調(diào)度優(yōu)先級(jí)和nice value這兩個(gè)值,歷史上,首先被使用的是nice value,很快大家覺得這個(gè)詞不是那么好理解,特別是對(duì)于初學(xué)者,因此改成優(yōu)先級(jí)(priority)這樣的名詞可以讓用戶更好的理解這個(gè)API的作用,當(dāng)然,事實(shí)證明這個(gè)改動(dòng)并不是非常理想,我們后面會(huì)描述。
getpriority/setpriority功能比較強(qiáng)大,能處理多種請(qǐng)求,不同的請(qǐng)求通過which和who這兩個(gè)參數(shù)來制定。當(dāng)which等于PRIO_PROCESS的時(shí)候,who需要傳入一個(gè)process id的參數(shù),getpriority將返回指定進(jìn)程的nice value。當(dāng)which等于PRIO_PGRP的時(shí)候,who需要傳入一個(gè)process group id的參數(shù),此時(shí)getpriority將返回指定進(jìn)程組中優(yōu)先級(jí)最高的那個(gè)(BTW,nice value是最小的)。當(dāng)which等于PRIO_USER的時(shí)候,who需要user id的信息,這時(shí)候,getpriority將返回屬于該user的所有進(jìn)程中nice value最小的那個(gè)。who等于0說明要get或者set的對(duì)象是當(dāng)前進(jìn)程(或者當(dāng)前進(jìn)程組,或者當(dāng)前的user)。
setpriority類似與nice,當(dāng)然功能要強(qiáng)那么一點(diǎn)點(diǎn),因?yàn)樗梢越邮誔RIO_PROCESS,PRIO_PGRP或者PRIO_USER參數(shù)用來設(shè)定一組進(jìn)程的nice value。setpriority的返回值和其他函數(shù)類似,0表示成功,-1表示操作失敗,不過getpriority就稍微有一點(diǎn)繞了。作為linux程序員,我們都知道的nice value是[-20, 19],如果getpriority返回這個(gè)范圍,那么這里的-1優(yōu)先級(jí)就有點(diǎn)尷尬了,因?yàn)橐话愕膌inux c庫接口函數(shù)返回-1表示調(diào)用錯(cuò)誤,我們是如何區(qū)分-1調(diào)用錯(cuò)誤的返回還是優(yōu)先級(jí)-1的返回值呢?getpriority是少數(shù)返回-1也是有可能正確的接口函數(shù):在調(diào)用getpriority之前,我們需要首先將errno清零,調(diào)用getpriority之后,如果返回-1,我們需要看看errno是否還是保持0值,如果是,那么說明返回的是優(yōu)先級(jí)-1,否則說明發(fā)生了錯(cuò)誤。
四、操作rt priority的接口
傳統(tǒng)的類unix內(nèi)核,調(diào)度器是采用round-robin time-sharing的算法:如果有若干個(gè)進(jìn)程是runnable的,那么不著急,大家排排隊(duì)、吃果果,每個(gè)進(jìn)程分配一個(gè)cpu時(shí)間片,大家輪流按照分配的時(shí)間片來獲取cpu資源,所有的時(shí)間片用完,那么就重新一輪的分配。在這樣的模型下面,間接影響cpu時(shí)間片的nice接口函數(shù)就夠用了。當(dāng)然,分配了更多的時(shí)間片也就是意味著有更高的優(yōu)先級(jí),因此nice vlaue也被稱為進(jìn)程的優(yōu)先級(jí)。
但是,新的需求層出不窮(人類的欲望是無窮D),特別是實(shí)時(shí)性方面的需求,因此,POSIX標(biāo)準(zhǔn)(2008版本)增加了實(shí)時(shí)調(diào)度的內(nèi)容,并且提供了POSIX realtime scheduling API來讓用戶空間來修改調(diào)度策略和調(diào)度優(yōu)先級(jí)。這下子有點(diǎn)尷尬了,原來的nice value大家已經(jīng)習(xí)慣稱之為進(jìn)程優(yōu)先級(jí)了,現(xiàn)在真正的進(jìn)程優(yōu)先級(jí)登場了,怎么區(qū)分?為了解決這個(gè)問題,我們引入一個(gè)新的名詞叫做調(diào)度策略(scheduling policy)。調(diào)度器在運(yùn)作的時(shí)候往往設(shè)定一組規(guī)則來決定何時(shí),選擇哪一個(gè)進(jìn)程進(jìn)入執(zhí)行狀態(tài),執(zhí)行多長的時(shí)間。那些“規(guī)則”就是調(diào)度策略。
好的調(diào)度策略依賴于對(duì)進(jìn)程的分類,有一類進(jìn)程是大家都灰常的熟悉了就是普通進(jìn)程,使用時(shí)間片輪轉(zhuǎn)算法的那些進(jìn)程。當(dāng)然這類進(jìn)程還可以細(xì)分,例如運(yùn)算密集型進(jìn)程(SCHED_BATCH,調(diào)度器最好不要太經(jīng)常的喚醒這種進(jìn)程),例如idle類進(jìn)程(SCHED_IDLE),idle類進(jìn)程優(yōu)先級(jí)非常低,也就是說如果系統(tǒng)有其他事情要處理就去干別的事情(調(diào)度其他進(jìn)程執(zhí)行),實(shí)在沒有活干了,再考慮IDLE類型的進(jìn)程。不論哪一種普通進(jìn)程,其優(yōu)先級(jí)使用nice value這樣一個(gè)調(diào)度參數(shù)來描述就OK了。
除了普通進(jìn)程,還有一類是嚴(yán)格按照優(yōu)先級(jí)來調(diào)度的進(jìn)程,如果熟悉RTOS的話,對(duì)priority-base的調(diào)度器應(yīng)該不會(huì)陌生,官大一級(jí)壓死人,只要優(yōu)先級(jí)高的進(jìn)程是runnable的,那么優(yōu)先級(jí)低的進(jìn)程是根本沒有機(jī)會(huì)執(zhí)行的。這里的優(yōu)先級(jí)才是真正意義的優(yōu)先級(jí),但是nice value已經(jīng)被稱為進(jìn)程優(yōu)先級(jí)了,因此這里的優(yōu)先級(jí)被叫做rt priority。rt進(jìn)程的調(diào)度又被細(xì)分成兩類:SCHED_FIFO和SCHED_RR。這兩種調(diào)度策略在相同rt priority的時(shí)候稍有差別,SCHED_FIFO是誰先到誰先獲取cpu資源,并且一直占用,直到主動(dòng)讓出cpu或者退出,相同rt priority的進(jìn)程才有機(jī)會(huì)執(zhí)行。SCHED_RR稍微人性化了一點(diǎn),相同rt priority的進(jìn)程有時(shí)間片,大家輪流執(zhí)行。對(duì)于實(shí)時(shí)進(jìn)程而言,rt priority這個(gè)調(diào)度參數(shù)就描述了全部。
介紹到這里,是時(shí)候總結(jié)一下了:進(jìn)程優(yōu)先級(jí)有兩個(gè)范圍,一個(gè)是nice value,用前兩個(gè)小節(jié)的API來set或者get。另外一個(gè)優(yōu)先級(jí)是rt priority,完全碾壓nice value這種優(yōu)先級(jí),操作rt priority的接口就在這一小節(jié)描述。
OK,經(jīng)過漫長的鋪墊過程,我們終于可以介紹realtime process scheduling API了,具體API定義如下:
#include
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
int sched_getscheduler(pid_t pid);
int sched_get_priority_max(int policy);--返回指定policy的最大的rt priority?
int sched_get_priority_min(int policy);--返回指定policy的最小的rt priority
int sched_setparam(pid_t pid, const struct sched_param *param);?
int sched_getparam(pid_t pid, struct sched_param *param);
sched_get_priority_max和sched_get_priority_min分別返回了指定調(diào)度策略的最大和最小的rt priority,不同的操作系統(tǒng)實(shí)現(xiàn)不同的優(yōu)先級(jí)數(shù)量。在linux中,實(shí)時(shí)進(jìn)程(SCHED_FIFO和SCHED_RR)的rt priority共計(jì)99個(gè)level,最小是1,最大是99。對(duì)于其他的調(diào)度策略,這些函數(shù)返回0。
sched_getscheduler函數(shù)可以獲取指定進(jìn)程的scheduling policy(如果pid等于0,那么是獲取調(diào)用進(jìn)程的調(diào)度策略)。sched_setscheduler函數(shù)是用來設(shè)定指定進(jìn)程的scheduling policy,對(duì)于實(shí)時(shí)進(jìn)程,該接口函數(shù)還可以設(shè)定rt priority。如果設(shè)定進(jìn)程的調(diào)度策略是非實(shí)時(shí)的調(diào)度策略的時(shí)候(例如SCHED_NORMAL),那么param參數(shù)是沒有意義的,其sched_priority成員必須設(shè)定為0。sched_setparam/sched_getparam非常簡單,大家自己看man page好了。
五、一統(tǒng)江湖的接口
看起來前面小節(jié)描述的API已經(jīng)夠用了,然而,故事并未結(jié)束。經(jīng)過前面關(guān)于調(diào)度接口的討論,基本上我們對(duì)調(diào)度器的行為也已經(jīng)有了了解:調(diào)度器就是按照優(yōu)先級(jí)(指rt priority)來工作,優(yōu)先級(jí)高的永遠(yuǎn)是優(yōu)先調(diào)度。范圍落在[1,99]的rt priority是實(shí)時(shí)進(jìn)程,而rt priority等于0的是普通進(jìn)程。對(duì)于普通進(jìn)程,調(diào)度器還要根據(jù)nice value(這個(gè)也曾經(jīng)被稱為優(yōu)先級(jí),不要和rt priority弄混了)來進(jìn)行調(diào)整。用戶空間的進(jìn)程可以通過各種前面描述的接口API來修改調(diào)度策略、nice value以及rt priority。一切看上去已經(jīng)完美,CFS類型的調(diào)度器處理普通的運(yùn)算密集形(例如編譯內(nèi)核)和用戶交互形的應(yīng)用(例如vi編輯文件)。如果有應(yīng)用有實(shí)時(shí)需求,可以考慮讓rt類型的調(diào)度器來運(yùn)籌帷幄。但是,如何混合了一些realtime的應(yīng)用以及有一些timing要求的應(yīng)用的時(shí)候,SCHED_FIFO和SCHED_RR并不能解決問題,因?yàn)樵谶@種調(diào)度策略下,高優(yōu)先級(jí)的任務(wù)會(huì)永遠(yuǎn)的delay低優(yōu)先級(jí)的任務(wù),如果低優(yōu)先級(jí)的任務(wù)有一些timing的需求,這時(shí)候,你根本控制不了調(diào)度延遲時(shí)間。
為了解決上一節(jié)中描述的問題,一類新的進(jìn)程被定義出來,這類進(jìn)程的優(yōu)先級(jí)比實(shí)時(shí)進(jìn)程和普通進(jìn)程的優(yōu)先級(jí)都要高,這類進(jìn)行有自己的特點(diǎn),參考下圖:
這類進(jìn)程的特點(diǎn)就是每隔固定的周期都會(huì)起來干活,需要一定的時(shí)間來處理事務(wù)。這類進(jìn)程很牛,一上來就告訴調(diào)度器,我可是有點(diǎn)脾氣的進(jìn)程,和其他的那些妖艷的進(jìn)程不一樣的,我每隔一段時(shí)間(period)你就得固定分配給我一定的cpu資源(computer time),當(dāng)然,分配的cpu time必須在該周期內(nèi)執(zhí)行完畢,因此就有deadline的概念。為了應(yīng)對(duì)這種需求,3.14內(nèi)核引入了一類新的進(jìn)程叫做deadline進(jìn)程,這類進(jìn)程的調(diào)度策略是SCHED_DEADLINE。調(diào)度器對(duì)這類進(jìn)程也會(huì)高看一眼,每當(dāng)一個(gè)周期的開始時(shí)間到來的時(shí)候(也就是該deadline進(jìn)程被喚醒的時(shí)間),調(diào)度器要優(yōu)先處理這個(gè)deadline進(jìn)程對(duì)cpu timer的需求,并且在某個(gè)指定的deadline時(shí)間內(nèi)調(diào)度該進(jìn)程執(zhí)行。執(zhí)行了指定的cpu time后,可以考慮調(diào)度走該進(jìn)行,不過,當(dāng)下一個(gè)周期到來的時(shí)候,調(diào)度器仍然要奮不顧身的在deadline時(shí)間內(nèi),再次調(diào)度該deadline進(jìn)程執(zhí)行。
雖然deadline進(jìn)程優(yōu)先級(jí)高于其他兩類進(jìn)程,但是用“優(yōu)先級(jí)”來描述這類進(jìn)程當(dāng)然是不合理的,應(yīng)該使用下面的三個(gè)參數(shù)來描述:
(1)周期時(shí)間(上圖中的period)
(2)deadline時(shí)間(上圖中的relative deadline)
(3)一次調(diào)度周期內(nèi)分配多少的cpu時(shí)間(上圖中的comp. time)
至此,估計(jì)您也已經(jīng)發(fā)現(xiàn),前面描述的接口其實(shí)都是不適合設(shè)定這些參數(shù)的,因此,GNU/linux操作系統(tǒng)中增加了下面的接口API:
#include
int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags);?
int sched_getattr(pid_t pid, const struct sched_attr *attr, unsigned int size, unsigned int flags);
attr這個(gè)參數(shù)的數(shù)據(jù)類型是struct sched_attr,這個(gè)數(shù)據(jù)結(jié)構(gòu)囊括了一切你想要的關(guān)于調(diào)度的控制參數(shù):policy,nice value,rt priority,period,deadline等等。用這個(gè)接口可以完成所有前面幾個(gè)小節(jié)描述API能完成的任務(wù),唯一的不好的地方就是這個(gè)接口是linux特有的,不是posix標(biāo)準(zhǔn),是否應(yīng)用這個(gè)接口就是見仁見智了。更細(xì)節(jié)的知識(shí)這里就不描述了,大家還是參考man page好了。
六、其他
上面描述的接口API都是和調(diào)度器參數(shù)相關(guān),其實(shí)Linux調(diào)度器還有兩類接口。一個(gè)是sched_getaffinity和sched_setaffinity,用于操作一個(gè)線程的CPU affinity。另外一個(gè)接口是sched_yield,該接口可以讓出CPU資源,讓Linux調(diào)度器選擇一個(gè)合適的線程執(zhí)行。這些接口很簡單,大家仔細(xì)學(xué)習(xí)就OK了。
參考文檔:
1、POSIX標(biāo)準(zhǔn)2008
2、linux下的各種man page
3、linux 4.4.6內(nèi)核源代碼
評(píng)論