進程調度
- 1 策略
- I/O消耗型和處理器消耗型的進程
- 進程優先級
- 時間片
- 進程搶占
- 2 Linux調度算法
- 可執行隊列
- 優先級數組
- 重新計算時間片
- schedule()
- 計算優先級和時間片
- 睡眠和喚醒
- 負載平衡程序
- 3 搶占和上下文切換
- 用戶搶占
- 內核搶占
- 4 實時
- 5 與調度相關的系統調用
- 與調度策略和優先級相關的系統調用
- 與處理器綁定有關的系統調用
- 放棄處理器時間
- 6 調度程序小結
調度程序是內核的組成部分,它負責選擇下一個要運行的進程。多任務操作系統就是能 同時并發地執行 多個進程的操作系統。多任務系統可以分為兩類:非搶占式多任務和搶占式多任務。 Linux提供搶占式的多任務模式。在此模式下,由調度程序來決定什么時候 停止一個進程的運行以便其他進程能夠得到執行。這個 強制停止動作叫做搶占,注意這是強制停止,不是進程自己停止。進程在被搶占之前能夠運行的時間是預先設置好的,而且有一個專門的名字,叫進程的時間片。時間片實際上就是分配給每個 可運行進程的處理器時間段。
在非搶占式多任務模式下,除非進程自己主動停止運行,否則它會一直運行。進程主動掛起自己的操作稱為讓步。Unix從一開始就是搶占式的多任務。
1 策略
策略決定調度程序在何時讓什么進程運行
I/O消耗型和處理器消耗型的進程
I/O消耗型進程大部分時間用來提交I/O請求或是等待I/O請求。因此,這樣的進程經常處于可運行狀態,但通常都是運行短短的一會兒,因為I/O操作總會阻塞。
處理器消耗型把時間大多用在執行代碼上。對于這類處理器消耗型的進程,調度策略是盡量降低它們的運行頻率,對它們而言,延遲其運行時間會更合適些。
I/O消耗型是盡量提高其運行頻率,降低運行時間;處理器消耗型是降低其運行頻率,提高其運行時間。
Linux為了保證交互時應用,對進程的響應做了優化(縮短響應時間),更傾向于優先調度I/O消耗型進程。
進程優先級
調度算法中最基本的一類就是基于優先級的調度。優先級高的進程先運行,低的后運行,相同優先級的進程按輪轉方式進行調度(一個接一個,重復進行)。在包括Linux在內的某些系統中,優先級高的進程使用的時間片也較長。調度程序總是選擇時間片未用盡而且優先級最高的進程運行。
Linux根據以上思想實現了一種基于動態優先級的調度方法。一開始,該方法先設置基本的優先級,然后允許調度程序根據需要來加減優先級。
Linux內核提供了兩組獨立的優先級范圍。第一種是nice值,范圍是-20到+19,默認值是0。nice值越大優先級越低。另外nice值也用來決定分配給進程的時間片的長短,nice值為-20的進程可能獲得的時間片最長,nice值為19的進程獲得時間片可能最短。"可能"是因為時間片的長短還受其他因素影響。nice值是所有Unix系統都使用到的標準優先級范圍。
第二個是實時優先級,其值是可配置的,默認情況下它的變化范圍是從0到99,任何實時進程的優先級都高于普通的進程。Linux提供了對POSIX實時優先級的支持。
時間片
時間片是一個數值,它表明進程在被搶占前還能運行的時間。調度策略必須規定一個默認的時間片。
Linux調度程序提高交互式程序的優先級,讓它們運行更頻繁。于是,程序提供提供了較長默認時間片給交互程序。如下圖
當一個進程的時間片耗盡時,就認為進程到期了。沒有時間片的進程不會再投入運行,除非等他其他所有的進程都耗盡了它們的時間片,系統會重算所有進程的時間片,然后繼續運行。
進程搶占
Linux系統是搶占式的。當一個進程進入TASK_RUNNING狀態,內核會檢查它的優先級是否高于當前正在執行的進程。如果高于,調度程序會被喚醒,搶占當前正在運行的進程并運行高優先級的進程。此外,當一個進程的時間片變為0,調度程序被喚醒以選擇一個新的進程。
2 Linux調度算法
Linux的調度程序定義于kernel/sched.c中。調度程序的算法和相關支持代碼大部分都在2.5版內核的開發版中被重寫了。因此,現在的程序與以前版本內核的調度程序區別很大,幾乎是全新的。設計新的調度程序是為了實現以下目標:
- 充分實現0(1)調度。不管有多少進程,新調度程序采用的每個算法都能在恒定時間內完成。
- 全面實現SMP的可拓展性。每個處理器都擁有自己的鎖和自己的可執行隊列
- 強化SMP的可拓展性。盡量將相關的一組任務分配給一個CPU進行連續的執行。只有在需要平衡任務隊列的大小時才在CPU之間移動進程
- 加強交互性能。即使在系統處于相當負載的情況下,也能保證系統的響應,并立即調度交互式進程。
- 保證公平。在合理設定的時間范圍內,沒有進程會處于饑餓狀態。
可執行隊列
調度程序中最基本的數據結構是運行隊列。可執行隊列定義在kernel/sched.c中。由結構runqueue表示。可執行隊列是給定處理器上的可執行進程的鏈表,每個處理器一個。每個可投入運行的進程都唯一的歸屬于一個可執行隊列。此外,可執行隊列中還包含每個處理器的調度信息。
/** This is the main, per-CPU runqueue data structure.** Locking rule: those places that want to lock multiple runqueues* (such as the load balancing or the thread migration code), lock* acquire operations must be ordered by ascending &runqueue.*/
struct runqueue {spinlock_t lock; /* 保護運行隊列的自旋鎖 *//** nr_running and cpu_load should be in the same cacheline because* remote CPUs use both these fields when doing load calculation.*/unsigned long nr_running; /* 可運行任務的數目 */
#ifdef CONFIG_SMPunsigned long cpu_load;
#endifunsigned long long nr_switches; /* 上下文切換數目 *//** This is part of a global counter where only the total sum* over all CPUs matters. A task can increase this counter on* one CPU and if it got migrated afterwards it may decrease* it on another CPU. Always updated under the runqueue lock:*/unsigned long nr_uninterruptible; /* 處于不可中斷睡眠狀態的任務數目 */unsigned long expired_timestamp; /* 隊列最后被換出時間 */unsigned long long timestamp_last_tick; /* 最后一個調度程序的節拍 */task_t *curr, *idle; /* 當前運行任務、該處理器的空任務 */struct mm_struct *prev_mm; /* 最后運行任務的mm_struct結構體 */prio_array_t *active, *expired, arrays[2];/* 活動優先級隊列、超時優先級隊列、實際優先級數組 */int best_expired_prio;atomic_t nr_iowait; /* 等待I/O操作的任務數目 */#ifdef CONFIG_SMPstruct sched_domain *sd;/* For active balancing */int active_balance;int push_cpu;task_t *migration_thread; /* 移出線程 */struct list_head migration_queue; /* 移出隊列 */
#endif#ifdef CONFIG_SCHEDSTATS/* latency stats */struct sched_info rq_sched_info;/* sys_sched_yield() stats */unsigned long yld_exp_empty;unsigned long yld_act_empty;unsigned long yld_both_empty;unsigned long yld_cnt;/* schedule() stats */unsigned long sched_noswitch;unsigned long sched_switch;unsigned long sched_cnt;unsigned long sched_goidle;/* pull_task() stats */unsigned long pt_gained[MAX_IDLE_TYPES];unsigned long pt_lost[MAX_IDLE_TYPES];/* active_load_balance() stats */unsigned long alb_cnt;unsigned long alb_lost;unsigned long alb_gained;unsigned long alb_failed;/* try_to_wake_up() stats */unsigned long ttwu_cnt;unsigned long ttwu_attempts;unsigned long ttwu_moved;/* wake_up_new_task() stats */unsigned long wunt_cnt;unsigned long wunt_moved;/* sched_migrate_task() stats */unsigned long smt_cnt;/* sched_balance_exec() stats */unsigned long sbe_cnt;
#endif
};
由于可執行隊列是調度程序的核心數據結構體,所以有一組宏定義用于獲取與給定處理器或進程相關的可執行隊列:
- cup_rq(process):用于返回給定處理器上的可執行隊列的指針
- this_rq():返回當前處理器的可執行隊列
- task_rq(task):返回給定進程所在的隊列指針
在對可執行隊列操作之前,應該先鎖住可執行隊列,用來防止隊列被其他代碼改動。此時需要用到task_rq_lock()和task_re_unlock()函數:
struct runqueue *rq;
unsigned long flags;rq = task_rq_lock(task,&flags); /* task是用到可執行隊列的任務 */.../* 對任務的隊列rq進行操作 */...
task_rq_unlock(rq,&flags);
或者可以用this_rq_lock()來鎖住當前的可執行隊列,用rq_unlock(struct runqueue *rq)釋放給定隊列上的鎖。
為了避免死鎖,要鎖住多個運行隊列的代碼必須按照同樣的順序獲取這些鎖:可按照執行隊列地址從低到高的順序
/* 鎖定 */
if (rq1 == rq2)spin_lock(&rq1->lock)
else {if(re1 <rq2){spin_lock(&rq1->lock);spin_lock(&rq2->lock);}else{spin_lock(&rq2->lock);spin_lock(&rq1->lock);}
}/* 操作兩個運行隊列 *//* 釋放鎖 */
spin_unlock(&rq1->lock);
if(rq1 != rq2)spin_unlock(&rq2=>lock);
這些步驟能通過double_rq_lock()和double_rq_unlock()自動完成。上面的步驟會被簡化為:
double_rq_lock(rq1,rq2);/* 操作兩個運行隊列 */double_rq_unlock(rq1,rq2);
優先級數組
每個運行隊列都有兩個優先級數組,一個活躍的和一個過期的。優先級數組在kernel/sched.c文件中被定義,它是prio_array類型的結構體。
struct prio_array {unsigned int nr_active; /* 任務數目 */unsigned long bitmap[BITMAP_SIZE]; /* 優先級位圖 */struct list_head queue[MAX_PRIO]; /* 優先級隊列 */
};
優先級數組是一種能夠提供0(1)級算法復雜度的數據結構。優先級數組使可運行處理器的每一種優先級都包含一個相應的隊列,而這些隊列包含對應優先級上的可執行進程鏈表。優先級還擁有一個優先機位圖,當需要查找當前系統內擁有最高優先級的可執行進程時,它可以提高效率。
MAX_PRIO定義了系統擁有的優先級個數。默認是140,BITMAP_SIZE是優先級位圖數組的大小,類型為unsigned long長整數型,32位,每一位代表一個優先級,140個優先級需要5個長整型,bitmap就正好含有五個數組項,總共160位。一開始,所有的位都被置為0,當某個擁有一定優先級的進程開始執行時,位圖中相應的位就會被置為1.比如,如果一個優先級為7的進程變為可執行狀態,第7位就被置為1。這樣,查找系統中最高的優先級就變成了查找位圖中被設置的第一個位。Linux提供了sched_find_first_bit()函數,來快速查找位圖被設置的第一個位。
每個優先級還包含一個加做struct list_head的隊列,每個鏈表與一個給定的優先級相對應,每個鏈表都包含該處理器隊列上相應優先級的全部可運行進程。所以要找到下一個要運行的任務非常簡單,就像在鏈表中選擇下一個元素一樣,對于給定的優先級,按輪轉方式調度任務。
nr_active是計數器,表示該優先級數組內可執行進程的數目。
重新計算時間片
新的Linux為每個處理器維護兩個優先級數組:活動數組和過期數組。活動數組內的可執行隊列上的進程都還有時間片剩余,而過期數組內的可執行隊列上的進程都用完了時間片。當一個進程的時間片用完時,它會從活動數組移至到過期數組,在移動前,會重新計算時間片。
schedule()
選定下一個進程并讓該進程執行是通過schedule()函數實現的。當內核代碼想要休眠時,會直接調用該函數,另外,如果有那個進程將被搶占,那么該函數也會被喚起執行。
首先,該函數在活動優先級數組中找到第一個被設置的位。該位對應著優先級最高的可執行進程。然后,調度程序選擇這個級別鏈表里的頭一個進程。這就是系統中優先級最高的可執行程序,也是馬上會被調度執行的進程。
計算優先級和時間片
進程擁有一個初始的優先級,叫做nice值。該數值變化范圍是-20到+19,默認值是0。進程task_struct的static_prio變量存放著這個值。而調度程序要用到的動態優先級存放在prio變量里。動態優先級通過一個關于靜態優先級和進程交互性的函數關系計算而來。
effective_prio()函數可以返回一個進程的動態優先級。這個函數以nice值為基數,再加上-5到+5之間的進程交互性的獎勵或罰分。交互性很強的進程可以認為是I/O消耗型,加上的值為負數,優先級會變高,很弱可以認為是處理器消耗型,加上的值為正數,優先級會變高。調度程序根據進程休眠的時間長短來推斷進程是I/O消耗型還是處理器消耗型。
為了支持這種推斷機制,Linux用一個變量記錄一個進程用于休眠和用于執行的時間。該變量是task_struct的sleep_avg,它的范圍是0到MAX_SLEEP_AVG,默認值是10毫秒。當一個進程從休眠狀態恢復到執行狀態時,sleep_arg會根據它休眠時間的長短而增加,直到達到MAX_SLEEP_AVG為至。相反,進程每運行一個時鐘節拍,sleep_avg就做相應的遞減,到0為止。
sleep_avg的計算不僅僅根據休眠時間的長短,而且運行時間的長短也要被計算進去。如果一個進程開始占用大量的處理器時間,那么,它很快就會失去增加得到的優先級提升。由于獎勵和罰分都加在作為基數的nice值上,所以用戶還是可以通過改變進程的nice值來對調度程序施加影響。
在一個進程創建的時候,新建的子進程和父進程均分父進程剩余的進程時間片。這樣分配是為了防止不斷創建新進程來獲取時間片。當一個任務的時間用完之后,可以用task_timeslice()為給定任務計算一個新的時間。時間片的計算只需要把優先級按比例縮放。進程的優先級越高,它每次執行得到的時間片越長。
調度程序還提供了另外一種機制來支持交換進程:如果一個進程的交互性非常強,那么當它的時間片用完后,它會被再放置到活動數組而不是過期數組中。
一般進程在用完它們的時間片后,都會被移至過期數組,當活動數組沒有剩余進程的時候,這兩個數組就會被交換,活動數組變成過期數組,過期數組替代活動數組。這種操作提供了時間復雜度為0(1)的時間片重新計算。將 交互式強的進程重新插入到活動數組,該進程不會被立即執行,它會和優先級相同的進程輪流被調度和執行。將交互性非常強的進程插入到活動數組的邏輯過程在scheduler_tick ()中實現,該函數會被中斷定時器調用。
睡眠和喚醒
休眠(被阻塞)的進程處于一個特殊的不可執行狀態,為了等待一些事件的發生。
休眠有兩種相關的進程狀態:TASK_INTERRUPTIBLE和TASK_UNINTERRUPIBLE。它們唯一區別是處于TASK_UNINTERRUPIBLE的進程會忽略信號,而處于TASK_INTERRUPTIBLE狀態的進程如果接收到一個信號會被提前喚醒并響應該信號。兩種狀態的進程位于同一個等待隊列上,等待某些事件。
等待隊列是由等待某些事件發生的進程組成的簡單鏈表。內核用wake_queue_head_t來代表等待隊列。等待隊列可以通過DECLARE_WAITQUEUE()靜態創建,也可以用init_waitqueue_head()動態創建。進程把自己放入等待隊列中并設置成不可執行狀態。當與等待隊列相關的事件發生的時候,隊列上的進程會被喚醒。
進程通過執行下面幾個步驟將自己加入到一個等待隊列中:
- 調用DECLRE_WAITQUEUE()創建一個等待隊列的項
- 調用add_wait_queue()把自己將入到等待隊列中。該隊列會在進程等待的條件滿足時喚醒它。
- 將進程的狀態變更為TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
- 如果狀態被置為TASK_INTERRUPTIBLE,則信號滿足會喚醒進程。
- 檢查條件是否為真,如果為真的話,就沒必要休眠了。如果不為真,調用schedule()
- 當進程被喚醒的時候,它會再次檢查條件是否為真,如果是,他就退出循環,如果不是,它再次調用schedule()并一直重復這步操作。
- 當條件滿足后,進程將自己設置為TASK_RUNNING并調用remove_wait_queue()把自己移出等待隊列。
喚醒操作通過函數wake_up()進行,它會喚醒指定的等待隊列上的所有進程。它調用函數try_to_wake_up(),該函數負責將進程設置為TASK_RUNNING狀態,調用activate_task()將此進程放入可執行隊列,如果被喚醒的進程優先級比當前正在執行的進程的優先級高,還要設置need_resched標志。
關于休眠有一點要注意,存在虛假的喚醒。有時候進程被喚醒并不是因為它所等待的條件達成了,所以才需要一個循環處理來保證它等待的條件真正達成。
負載平衡程序
Linux的調度程序為對稱多處理系統的每個處理器準備了單獨的可執行隊列和鎖。每個處理器都有一個自己的進程鏈表,而它只對屬于自己的這些進程進行調度操作。當可執行隊列間出現負載不均衡的情況時,比如一個處理器的隊伍上有五個進程,而另外一個處理器的隊列上只有一個進程,這時候就需負載平衡程序解決,它負責保證可執行隊列之間的負載處于均衡狀態。負責平衡程序會拿當前處理器的可執行隊列和系統中的其他可執行隊列做比較。如果它發生了不均衡,就會把相對繁忙的隊列中的進程抽到當前的可執行隊列中來,使它們大致持平。
負載平衡程序由kernel/sched.c中的函數load_balance()來實現。它有兩種調用方法。在schedule()執行的時候,只要當前的可執行隊列為空,它就會被調用。此外,它還會被定時器調用:系統空閑時每隔一毫秒調用一次或者在其他情況下每隔200毫秒調用一次。在單處理器系統中,load_balance不會被調用,它甚至不會被編譯進內核。因為那里面只有一個可執行隊列,因此根本沒有平衡負載的必要。
負載平衡程序調用時需要鎖住當前處理器的可執行隊列并且屏蔽中斷,以避免可執行隊列被并發地訪問
load_balance()操作步驟:
/** Check this_cpu to ensure it is balanced within domain. Attempt to move* tasks if there is an imbalance.** Called with this_rq unlocked.*/
static int load_balance(int this_cpu, runqueue_t *this_rq,struct sched_domain *sd, enum idle_type idle)
{struct sched_group *group;runqueue_t *busiest;unsigned long imbalance;int nr_moved;spin_lock(&this_rq->lock);schedstat_inc(sd, lb_cnt[idle]);group = find_busiest_group(sd, this_cpu, &imbalance, idle);if (!group) {schedstat_inc(sd, lb_nobusyg[idle]);goto out_balanced;}busiest = find_busiest_queue(group);if (!busiest) {schedstat_inc(sd, lb_nobusyq[idle]);goto out_balanced;}/** This should be "impossible", but since load* balancing is inherently racy and statistical,* it could happen in theory.*/if (unlikely(busiest == this_rq)) {WARN_ON(1);goto out_balanced;}schedstat_add(sd, lb_imbalance[idle], imbalance);nr_moved = 0;if (busiest->nr_running > 1) {/** Attempt to move tasks. If find_busiest_group has found* an imbalance but busiest->nr_running <= 1, the group is* still unbalanced. nr_moved simply stays zero, so it is* correctly treated as an imbalance.*/double_lock_balance(this_rq, busiest);nr_moved = move_tasks(this_rq, this_cpu, busiest,imbalance, sd, idle);spin_unlock(&busiest->lock);}spin_unlock(&this_rq->lock);if (!nr_moved) {schedstat_inc(sd, lb_failed[idle]);sd->nr_balance_failed++;if (unlikely(sd->nr_balance_failed > sd->cache_nice_tries+2)) {int wake = 0;spin_lock(&busiest->lock);if (!busiest->active_balance) {busiest->active_balance = 1;busiest->push_cpu = this_cpu;wake = 1;}spin_unlock(&busiest->lock);if (wake)wake_up_process(busiest->migration_thread);/** We've kicked active balancing, reset the failure* counter.*/sd->nr_balance_failed = sd->cache_nice_tries;}/** We were unbalanced, but unsuccessful in move_tasks(),* so bump the balance_interval to lessen the lock contention.*/if (sd->balance_interval < sd->max_interval)sd->balance_interval++;} else {sd->nr_balance_failed = 0;/* We were unbalanced, so reset the balancing interval */sd->balance_interval = sd->min_interval;}return nr_moved;out_balanced:spin_unlock(&this_rq->lock);/* tune up the balancing interval */if (sd->balance_interval < sd->max_interval)sd->balance_interval *= 2;return 0;
}
- 首先,load_balance調用find_busiest_queue(),找到最繁忙的可執行隊列。如果沒有那個可執行隊列中進程的數目比當前隊列中的數目多個25%或25%以上,find_busiest_queue()返回NULL,并且load_balance()也返回。否則,最繁忙的可執行隊列就被返回。
- 其次,load_balance()從最繁忙的運行隊列中選擇一個優先級數組以便抽取進程,
- 接著,load_balance()尋找到含有進程并且優先級最高的鏈表,因為把優先級高的進程平均分散開來才是最重要的。
- 分析找到的所有這些優先級相同的進程,選擇一個不是正在執行,也不會因為處理器相關性不可移動,并且不在高速緩存中的進程,如果有進程滿足這些條件,調用pull_task()將其從最繁忙的隊列中抽取到當前隊列。
- 只要可執行隊列之間仍然不均衡,就重復上面步驟,繼續沖繁忙的隊列中抽取進程到當前隊列。這終將消除不平衡,此時,解除對當前運行隊列的鎖定,從load_balance返回。
3 搶占和上下文切換
上下文切換,也就是從一個可執行進程切換到另一個可執行進程,由定義在kernel/sched.c中的context_switch()函數負責處理。每當一個新的進程被選出來準備投入運行的時候,schedule()就會調用該函數。它完成了兩項基本的工作:
- 調用定義在include/asm-具體架構/mmu_context.h中的switch_mm(),該函數負責把虛擬內存從上一個進程映射切換到新進程中
- 調用定義在include/asm-具體架構/system.h中的switch_to(),該函數負責從上一個進程的處理器切換到新進程的處理器狀態。
內核必須知道什么時候調用schedule()。內核提供了一個need_resched標志來表明是否需要重新執行一次調度。
每個進程都包含一個need_resched標志,這是因為訪問進程描述符內的數值要比訪問一個全局變量快(因為current宏速度很快并且描述符通常都在高速緩存中)。在2.2以前的內核版本中,該標志是一個全局變量。2.2到2.4版本內核中它在task_struct中。而在2.6中,它被移到thread_info結構體中,用一個特別的標志變量中的一位來表示。
用戶搶占
用戶搶占就是一個運行在用戶空間的進程被另一個進程搶占執行。
內核即將返回用戶空間的時候,如果need_resched標志被設置,會導致schedule()被調用,此時就會發生用戶搶占。在內核返回用戶空間時,它知道自己是安全的,因為它可以去繼續去執行當前進程,那么它當然可以選擇一個新的進程去執行,所以,內核無論是在從中斷處理程序還是在系統調用后返回,都會檢查need_resched標志。
簡而言之,用戶搶占在以下情況時發生:
- 從系統調用返回用戶空間
- 從中斷處理程序返回用戶空間
內核搶占
內核搶占就是指一個在內核態運行的進程, 可能在執行內核函數期間被另一個進程取代。
在2.6版本的內核中,內核引入了搶占能力。現在,只要重新調度是安全的,那么內核就可以在任何時間搶占正在執行的任務。
什么時候重新調度才是安全的呢?只要沒有持有鎖,內核就可以進行搶占。為了支持內核搶占,每個進程的thread_info結構引入了preempt_count計數器。該計數器初始值為0,每當使用鎖的時候數值加1,釋放鎖的時候數值減一。當數值為0的時候,內核就可執行搶占。從中斷返回內核空間的時候,內核會檢查need_resched和preempt_count的值。如果need_resched被設置,并且preempt_count為0的話,這說明有一個更為重要的任務需要執行并且可以安全搶占,此時,調度程序就會被調用。
如果內核中的進程被阻塞了,或它顯示地調用了schedule(),內核搶占也會顯式的發生。這種形式的內核搶占從來都是受支持的,因為無需保證內核可以安全地被搶占。
內核搶占發生在:
- 當從中斷處理程序正在執行且返回內核空間之前
- 當內核代碼再一次具有可搶占性的時候
- 如果內核中的任務顯示的調用schedule()
- 如果內核中的任務阻塞
4 實時
Linux提供了兩種實時調度策略:SCHED_FIFO和SCHED_RR。而普通的,非實時的調度策略是SCHED_NORMAL。SCHED_FIFO實現了一種簡單的、先進先出的調度算法,它不使用時間片。SCHED_FIFO級的進程會比任何SCHED_NORMAL級的進程都先得到調度。 一旦一個SCHED_FIFO級進程處于可執行狀態,就會一直執行,直到它自己受阻塞或顯式釋放處理器或者由被其他進程搶占,只有較高優先級的SCHED_FIFO或SCHED_RR任務才能搶占SCHED_FIFO任務。
SCHED_RR與SCHED_FIFO大體相同,只是SCHED_RR級的進程在耗盡事先分配給他的時間后就不能接著執行了。也就說,SCHED_RR是有時間片的。當SCHED_RR任務耗盡它的時間片,在同一優先級的其他實時進程被輪流調度。
這兩種實時算法實現的都是靜態優先級。內核不為實時進程計算動態優先級。這能確保給定優先級別的實時進程總能搶占優先級比它低的進程。
實時優先級范圍是從0到MAX_RT_PRIO減1.默認情況下,MAX_RT_PRIO為100。SCHED_NORMAL級進程的nice值共享了這個取值空間,它的取值范圍是從MAX_RT_PRIO到(MAX_RT_PRIO+40)。也就說,在默認情況下,nice值從-20到+19直接對應的是從100到139的實時優先級范圍。
5 與調度相關的系統調用
Linux提供了一族系統調用,用于管理與調度程序相關的參數。這些系統調用可以用來操作和處理進程優先級、調度策略及處理器,同時還提供了顯式的將處理器交給其他進程的機制。我們在man幫助文檔上可以查找
與調度策略和優先級相關的系統調用
sched_setscheduler()和sched_getscheduler()分別用于設置和獲取進程的調度策略和實時優先級,它們的作用是讀取或改寫進程描述符task_struct的polucy和rt_priority的值。
sched_setparam()和sched_getparam()分別用于設置和獲取進程的實時優先級。這兩個系統調用獲取封裝在sched_param特殊結構體中的rt_priority。sched_get_prioruty_max()和sched_get_priority_min()分別用于返回給定調度策略的最大和最小優先級。
對于一個普通的進程,nice()函數可以將給定進程的靜態優先級增加一個給定的量。只有超級用戶才能調用它時使用負值,從而提高進程的優先級。nice()函數會調用內核的set_user_nice()函數,這個函數會設置進程的task_struct的static_prio和prio值。
與處理器綁定有關的系統調用
Linux調度程序提供強制的處理器綁定機制。也就是說,允許用戶強制指定“這個進程無論如何都必須在這些處理器上運行”。這種強制的親和性保存在進程task_struct的cpus_allowed這個位掩碼標志中。該掩碼標志的每一位對應一個系統可用的處理器。默認情況下,所有的位都被設置,進程可以在系統中所有可用的處理器上執行。用戶可以用sched_setaffinity函數設置一個不同一個或幾個位組合的位掩碼。而調用sched_getaffinity函數則返回當前的cpus_allowed位掩碼。
子進程繼承父進程進程描述符的cpus_allowed。如果父進程運行在指定的處理器上,子進程也會運行在該處理器上。
放棄處理器時間
Linux通過sched_yield函數系統調用,來將處理器讓給其他等待執行的進程。它是通過將進程從活動隊列移到過期隊列實現的。由于實時進程不會過期,所以屬于例外,實時進程只被移動到其優先級隊列的最后面(不會放到過期隊列中)。
內核代碼為了方便,可以直接調用yield(),它先要確定給定進程確實處于可執行狀態,然后再調用sched_yield函數。用戶空間的應用程序直接使用sched_yield()系統調用就可以了。
6 調度程序小結
本章介紹了進程調度所遵循的基本原理、具體實現、調度算法以及目前Linux內核使用的借口。