我是如何学习写一个操作系统(六):进程的调度

前言

既然引进了多进程,其实也就是在进程之间来回切换,那么就会有进程之间的调度问题。实则是在可运行进程之间分配有限的处理器时间资源的内核子系统。

几个简单的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,也就是运行时间最少的一个进程,切换到它的进程

  • 当执行完一回的轮转就会重新分配一次时间片,这时候对于已经轮转过的进程,时间片将会被设置为初值,但是对于那些阻塞的进程,时间片将会增加,也就是进行了一次折中的调度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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定时器

  • 最后是设置时钟中断和系统调用中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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其实就是根据描述符各个位的作用来进行设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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)) \
)

设置中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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里的调度算法,十分的简洁,但是又照顾了响应时间,又照顾了周转时间。

然后提了一下内核调度程序的初始化子程序,其实就是根据之前说的设置一些描述符和中断处理