我是如何學習寫一個作業系統(六):程序的排程
前言
既然引進了多程序,其實也就是在程序之間來回切換,那麼就會有程序之間的排程問題。實則是在可執行程序之間分配有限的處理器時間資源的核心子系統。
幾個簡單的CPU排程演算法
First Come, First Served(FCFS)
其實就是一個先進先出隊列了,也就是說先申請的程序,先執行。當CPU空閒時,它會分配給位於佇列頭部的程序,並且這個執行程序從佇列中移去。FCFS排程程式碼編寫簡單並且理解容易。
但是對於一個需要和使用者進行互動的程序,這種排程演算法就會造成體驗非常不好,因為週轉時間需要完成一整個佇列的任務,非常的長
但FCFS排程演算法是非搶佔的。一旦 CPU 分配給了一個程序,該程序就會使用 CPU 直到釋放 CPU 為止,即程式終止或是請求I/O。
Shortest Job First(SJF)
SJF排程演算法就指對短作業或者短程序優先排程的演算法,將每個程序與其估計執行時間進行關聯選取估計計算時間最短的作業投入執行。這樣就可以縮短週轉時間
最短作業優先(SJF)排程演算法將每個程序與其下次CPU執行的長度關聯起來。當 CPU 變為空閒時,它會被賦給具有最短 CPU 執行的程序。如果兩個程序具有同樣長度的 CPU 執行那麼可以由FCFS來處理。
RR
該演算法中,將一個較小時間單元定義為時間量或時間片。時間片的大小通常為 10~100ms。就緒佇列作為迴圈佇列。CPU排程程式迴圈整個就緒佇列,為每個程序分配不超過一個時間片的CPU。
為了實現RR排程,我們再次將就緒佇列視為程序的 FIFO 佇列。新程序新增到就緒佇列的尾部。CPU 排程程式從就緒佇列中選擇第一個程序,將定時器設定在一個時間片後中斷,最後分派這個程序。
排程演算法的折中
在執行的許多程序裡,有的程序更關心響應時間,有的程序更關心週轉時間,所以排程演算法就需要折中,但是如何折中又是一個問題。
Linux0.11 排程演算法
schedule
schedule是Linux0.11裡最主要的排程演算法,但是十分簡潔
task_struct是用來描述一個程序的結構體
task_struct的counter是排程演算法實現折中的一個關鍵,它既用來表示分配的時間片,又用來表示程序的優先順序
首先從任務陣列中最後一個任務開始迴圈檢測一些域
如果設定過任務的定時值alarm,並且已經過期(alarm<jiffies),則在訊號點陣圖中置SIGALRM訊號,即向任務傳送SIGALARM訊號。然後清alarm。還有一些訊號量相關的會在後面再提
找到數值最大的一個couter,也就是執行時間最少的一個程序,切換到它的程序
當執行完一回的輪轉就會重新分配一次時間片,這時候對於已經輪轉過的程序,時間片將會被設定為初值,但是對於那些阻塞的程序,時間片將會增加,也就是進行了一次折中的排程。
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);
}
init
我們在順便來看一下sched_init,這個函式核心排程程式的初始化子程式,就是初始化一些中斷和描述符
首先呼叫set_tss_desc和set_ldt_desc設定程序0的tss和ldt
清任務陣列和描述符表項
之後初始化8253定時器
最後是設定時鐘中斷和系統呼叫中斷
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
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);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
設定描述符
_set_tssldt_desc其實就是根據描述符各個位的作用來進行設定
#define set_tss_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x89")
#define set_ldt_desc(n,addr) _set_tssldt_desc(((char *) (n)),((int)(addr)),"0x82")
#define _set_tssldt_desc(n,addr,type) \
__asm__ ("movw $104,%1\n\t" \
"movw %%ax,%2\n\t" \
"rorl $16,%%eax\n\t" \
"movb %%al,%3\n\t" \
"movb $" type ",%4\n\t" \
"movb $0x00,%5\n\t" \
"movb %%ah,%6\n\t" \
"rorl $16,%%eax" \
::"a" (addr), "m" (*(n)), "m" (*(n+2)), "m" (*(n+4)), \
"m" (*(n+5)), "m" (*(n+6)), "m" (*(n+7)) \
)
設定中斷
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
#define set_intr_gate(n,addr) \
_set_gate(&idt[n],14,0,addr)
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
小結
這一篇主要看了一下Linux0.11裡的排程演算法,十分的簡潔,但是又照顧了響應時間,又照顧了週轉時間。
然後提了一下核心排程程式的初始化子程式,其實就是根據之前說的設定一些描述符和中斷處