作業系統-linux0.11程序排程函式分析
0 起源
程序是作業系統分配資源的最小單位;執行緒是程式執行的最小單位。對於只有少數幾個CPU的計算機,其上執行著幾十上百個程式,對於每個程式而言,他們都是獨享CPU的,作業系統製造了這一有多個CPU的假象。這一假象得以維持的基礎就在於程序之間的切換,而程序切換則需要用到程序排程,具體的程序排程內容可以看之前的博文:作業系統-程序排程。
但是空說無憑,理論還是需要結合實際,這篇博文將從linux0.11入手,看一個實際的排程函式。
1 原始碼分析
1.1 時間片輪轉
在BIOS引導進入系統後,會執行系統的main函式(init/main.c):
void main(void) /* This really IS void, no error here. */ { /* The startup routine assumes (well, ...) this */ /* * Interrupts are still disabled. Do necessary setups, then * enable them */ ... sched_init(); ... move_to_user_mode(); if (!fork()) { /* we count on this going ok */ init(); } for(;;) pause(); }
其中進行了很多的初始化操作,包括 sched_init
,這便是核心排程程式的初始化子程式(kernel/sched.c),其定義如下:
void sched_init(void)
{
...
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
...
}
sched_init
初始化了8253定時器(微機原理與介面技術學過的),8253每10ms發出一箇中斷請求訊號。然後設定了一箇中斷服務程式 timer_interrupt
timer_interrupt
。
timer_interrupt
在kernel/system_call.s中定義,為一段彙編程式:
.align 2 timer_interrupt: push %ds # save ds,es and put kernel data space push %es # into them. %fs is used by _system_call push %fs pushl %edx # we save %eax,%ecx,%edx as gcc doesn't pushl %ecx # save those across function calls. %ebx pushl %ebx # is saved as we use that in ret_sys_call pushl %eax movl $0x10,%eax mov %ax,%ds mov %ax,%es movl $0x17,%eax mov %ax,%fs incl jiffies movb $0x20,%al # EOI to interrupt controller #1 outb %al,$0x20 movl CS(%esp),%eax andl $3,%eax # %eax is CPL (0 or 3, 0=supervisor) pushl %eax call do_timer # 'do_timer(long CPL)' does everything from addl $4,%esp # task switching to accounting ... jmp ret_from_sys_call
可以發現,其核心為一句 call do_timer
,即呼叫 do_timer
函式。
do_timer
函式在kernel/sched.c中定義,
void do_timer(long cpl)
{
...
if ((--current->counter)>0) return;
current->counter=0;
// 核心中不排程
if (!cpl) return;
schedule();
}
其呼叫了一個 schedule()
, 這個 schedule()
函式選出下一個要執行的程序,並且切換到它,這是今天的主角!
通過上面的分析可以發現,counter
扮演了時間片的角色,每一次8253產生中斷會呼叫中斷服務程式,使 counter
減1,減到0則呼叫排程函式 schedule()
,這是一個十分明顯的round robin(時間片輪轉)策略。
1.2 優先順序排程
先看看 schedule()
函式的全貌吧,只看片段容易盲人摸象:
/*
* 'schedule()' is the scheduler function. This is GOOD CODE! There
* probably won't be any reason to change this, as it should work well
* in all circumstances (ie gives IO-bound processes good response etc).
* The one thing you might take a look at is the signal-handler code here.
*
* NOTE!! Task 0 is the 'idle' task, which gets called when no other
* tasks can run. It can not be killed, and it cannot sleep. The 'state'
* information in task[0] is never used.
*/
void schedule(void)
{
int i,next,c;
struct task_struct ** p;
/* check alarm, wake up any interruptible tasks that have got a signal */
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p) {
if ((*p)->alarm && (*p)->alarm < jiffies) {
(*p)->signal |= (1<<(SIGALRM-1));
(*p)->alarm = 0;
}
if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) &&
(*p)->state==TASK_INTERRUPTIBLE) {
(*p)->state=TASK_RUNNING;
}
}
/* this is the scheduler proper: */
while (1) {
c = -1;
next = 0;
i = NR_TASKS;
p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) +
(*p)->priority;
}
switch_to(next);
}
schedule()
函式中,第一個for迴圈處理了很多訊號的響應,這不是排程的重點(畢竟next代表選出的下一個要執行的程序,這一段還沒有next相關的操作呢。。。)。
重點聚焦於 while (1)
迴圈,畢竟註釋也寫了,這是排程程式的主要部分。
下面的程式碼展示了排程策略,從任務陣列的最後一個任務開始迴圈處理,跳過不含任務的陣列槽。選擇就緒任務中 counter
值最大的任務(說明),若有 counter
值不為0的結果或系統沒有一個可執行任務(此時next為0)存在,則選擇next對應程序進行切換。
c = -1, next = 0, i = NR_TASKS, p = &task[NR_TASKS];
while (--i) {
if (!*--p)
continue;
if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
c = (*p)->counter, next = i;
}
if (c) break;
若就緒任務中 counter
值全為0,則根據每個任務的優先權值更新每一個任務(全部任務,包含阻塞的)的 counter
值。
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
從上面的式子也可以看出來,若某些任務一直執行 counter
調整,其 counter
值是趨向於 2 * (*p)->priority
的(級數,自己可以算一下)。
綜上,schedule()
函式的行為為:
- 找
counter
值最大的任務排程,counter
表示了優先順序。 counter
代表的優先順序可以動態調整。
因為2的存在,阻塞的程序再就緒後,其優先順序會高於非阻塞程序。阻塞是因為發生了I/O,而I/O則是前臺程序的特徵,所以該排程策略照顧了前臺程序。
2 總結
正如Linus Torvalds所說,這確實是個GOOD CODE!!!
短短的一點程式碼實現的一個簡單的演算法,包含了優先順序、時間片輪轉等多種演算法,解決了大多數任務的需求,大佬牛逼,給大佬打call!!!
3 參考文獻
[1] Linux核心完全註釋:基於0.11核心 · 趙烔
[2] 哈工大作業系統課程 · 李治軍