1. 程式人生 > 其它 >作業系統-linux0.11程序排程函式分析

作業系統-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

,即每10ms中斷一次,執行一次 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() 函式的行為為:

  1. counter 值最大的任務排程, counter 表示了優先順序。
  2. counter 代表的優先順序可以動態調整。

因為2的存在,阻塞的程序再就緒後,其優先順序會高於非阻塞程序。阻塞是因為發生了I/O,而I/O則是前臺程序的特徵,所以該排程策略照顧了前臺程序。

2 總結

正如Linus Torvalds所說,這確實是個GOOD CODE!!!

短短的一點程式碼實現的一個簡單的演算法,包含了優先順序、時間片輪轉等多種演算法,解決了大多數任務的需求,大佬牛逼,給大佬打call!!!

3 參考文獻

[1] Linux核心完全註釋:基於0.11核心 · 趙烔

[2] 哈工大作業系統課程 · 李治軍