1. 程式人生 > >07-時間片輪轉排程

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。計算方法是下面這樣的:

counter=p+p2+p4++p2n+=2p

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,請不要忘記在部落格後面評論。

到此為止,執行緒切換與執行緒排程的框架我們就完成了,不過還有很多有關執行緒的東西都沒有做,比如執行緒的同步與互斥,還有更多的執行緒狀態啊這些。不過我相信,在這些程式碼的基礎上,應該還是可以實現這些功能的。