07-時間片輪轉排程
終於寫到時間片輪轉排程了,相信大家一定很期待吧。在前面實驗中,執行緒都屬於主動切換,如果執行緒不主動切換,也就是說線上程過程函式中不呼叫 mysleep 函式,導致的結果就是此執行緒會一直霸佔 cpu 而不會離開,這樣就一直執行到該執行緒結束為止。為了解決此問題,電腦科學家們發明了時間片輪轉排程演算法。
1. 時間片的概念
時間片,是一個執行緒在被切換掉之前所能持續執行的最大 CPU 時間。特別注意的是,它的單位不是納秒、不是微秒也不是毫秒,而是嘀嗒數。在我們的實驗中,設定一個執行緒能持續霸佔 CPU 的時間片是 15 個嘀嗒數。注意,這個時間片是動態變化的,只是初始化為 15 個嘀嗒數。
1 個嘀嗒數,我們規定它為 10 ms,這個值你可以根據實際情況調整。
按照上面的換算規則,執行緒的一個時間片 = 15 嘀嗒數 = 150 ms.
2. 時間片輪轉的排程演算法
2.1 時鐘中斷處理函式
使用嘀嗒數的好處在於它把時間粗粒度化了,採用了計數的方式進行計時。系統會在每一個嘀嗒中產生一個時鐘中斷,並進入時鐘中斷函式 do_timer. 在時鐘中斷中,我們可以讓執行緒的時間片的值減 1,直到為 0. 當時間片的值為 0 的時候,執行 schedule 函式。
既然時間片是執行緒的東西,所以它應該新增到執行緒結構中,現線上程結構體更新為:
struct task_struct {
int id; // 執行緒 id
void (*th_fn)(); // 執行緒過程函式
int esp; // 棧頂指標
unsigned int wakeuptime; // 喚醒時間
int status; // 執行緒狀態
int counter; // 時間片
int priority; // 優先順序,後面講
int stack[STACK_SIZE]; // 執行緒執行棧
}
時鐘中斷函式名為 do_timer,此函式非常簡單:
// 時鐘中斷函式,每一個嘀嗒數自動執行一次
static void do_timer() {
// 將當前執行緒的時間片的值減 1. 如果減 1 後小於等於 0 則進行排程。
if (--current->counter > 0) return;
current->counter = 0;
schedule();
}
從 do_timer 函式中可以看出,如果執行緒不主動讓出 cpu,會一直將時間片的時間用完才會進行排程!!!
現在我們最關心的問題是如何讓 do_timer 函式每一個嘀嗒數被執行一次。答案很簡單,利用訊號機制,使用函式 setitimer 每隔 10 ms 傳送一次訊號 SIGALRM
,然後捕捉此訊號即可。有關訊號的知識,請參考《Linux 程式設計學習筆記》。在這裡,我們只要知道這樣做就可以每一個嘀嗒,系統就會進入 do_timer 函式就行了。
安裝時鐘中斷的完整程式如下:
__attribute__((constructor)) // 這一行表示被標記的函式在 main 函式前執行。
static void init() {
struct itimerval value;
value.it_value.tv_sec = 0;
value.it_value.tv_usec = 1000;
value.it_interval.tv_sec = 0;
value.it_interval.tv_usec = 1000*10; // 10 ms
if (setitimer(ITIMER_REAL, &value, NULL) < 0) {
perror("setitimer");
}
signal(SIGALRM, do_timer);
}
2.2 排程函式
每次執行排程函式的時候,會挑選一個時間片值最大的執行緒執行。也就是遍歷所有處理 THREAD_RUNNING
狀態的執行緒,找到時間片最大的那一個執行緒然後返回。
假設,所有 THREAD_RUNNING
的執行緒時間片都為 0 了。這時候會重新為所有執行緒調整時間片的值。具體看排程的程式碼,這段時間片輪轉演算法是 linus 大神寫的,膜拜一下吧:
static struct task_struct *pick() {
int i, next, c;
// 檢視有沒有睡眠的程序能夠被喚醒
for (i = 0; i < NR_TASKS; ++i) {
if (task[i] && task[i]->status != THREAD_EXIT
&& getmstime() > task[i]->wakeuptime) {
task[i]->status = THREAD_RUNNING;
}
}
// 基於優先順序的時間片輪轉
while(1) {
c = -1;
next = 0;
for (i = 0; i < NR_TASKS; ++i) {
if (!task[i]) continue;
if (task[i]->status == THREAD_RUNNING && task[i]->counter > c) {
c = task[i]->counter;
next = i;
}
}
if (c) break;
// 如果所有任務時間片都是 0,重新調整時間片的值
if (c == 0) {
for (i = 0; i < NR_TASKS; ++i) {
if(task[i]) {
// counter = counter / 2 + priority,注意此處的 priority 表示初始的優先順序,一開始預設值就等於 15.
task[i]->counter = task[i]->priority + (task[i]->counter >> 1);
}
}
}
}
return task[next];
}
上面的演算法的巧妙之處在於它照顧了狀態為 THREAD_SLEEP
的執行緒,因為這些執行緒睡眠時間很長,它們的 counter 值會越來越大,最後無限接近於 2*priority
。計算方法是下面這樣的:
3. 結果驗證
接下來在客戶端編寫一個執行緒過程函式,它不呼叫 mysleep,所以不會主動讓出 cpu。但是結果發現,所有的執行緒都可以正常排程。
void fun4() {
int i = 15;
int m;
int n;
while(i--) {
printf("hello, I'm fun4\n");
// sleep 會失效(因為有訊號產生),這裡用迴圈。
for (m = 0; m < 10000; ++m)
for (n = 0; n < 10000; ++n);
}
}
4. 總結
- 理解什麼是時間片
- 對於不主動讓出 cpu 的執行緒,是如何切換到其它程序的
重要提示:請不要忘記給專案點 star 啊!!!還有一點,程式碼中可能存在 bug,請不要忘記在部落格後面評論。
到此為止,執行緒切換與執行緒排程的框架我們就完成了,不過還有很多有關執行緒的東西都沒有做,比如執行緒的同步與互斥,還有更多的執行緒狀態啊這些。不過我相信,在這些程式碼的基礎上,應該還是可以實現這些功能的。