- 进程调度的实现
- 调度程序在内核中就是一个函数,为了讨论方便,我们同样对其进行了简化,略其对SMP的实现部分。
- asmlinkage void schedule(void)
- {
- struct task_struct *prev, *next, *p; /* prev表示调度之前的进程,
- next表示调度之后的进程 */
- struct list_head *tmp;
- int this_cpu, c;
- if (!current->active_mm) BUG();/*如果当前进程的的active_mm为空,出错*/
- need_resched_back:
- prev = current; /*让prev成为当前进程 */
- this_cpu = prev->processor;
- if (in_interrupt()) {/*如果schedule是在中断服务程序内部执行,
- 就说明发生了错误*/
- printk("Scheduling in interrupt\n");
- BUG();
- }
- release_kernel_lock(prev, this_cpu); /*释放全局内核锁,
- 并开this_cpu的中断*/
- spin_lock_irq(&runqueue_lock); /*锁住运行队列,并且同时关中断*/
- if (prev->policy == SCHED_RR) /*将一个时间片用完的SCHED_RR实时
- goto move_rr_last; 进程放到队列的末尾 */
- move_rr_back:
- switch (prev->state) { /*根据prev的状态做相应的处理*/
- case TASK_INTERRUPTIBLE: /*此状态表明该进程可以被信号中断*/
- if (signal_pending(prev)) { /*如果该进程有未处理的
- 信号,则让其变为可运行状态*/
- prev->state = TASK_RUNNING;
- break;
- }
- default: /*如果为可中断的等待状态或僵死状态*/
- del_from_runqueue(prev); /*从运行队列中删除*/
- case TASK_RUNNING:;/*如果为可运行状态,继续处理*/
- }
- prev->need_resched = 0;
- /*下面是调度程序的正文 */
- repeat_schedule: /*真正开始选择值得运行的进程*/
- next = idle_task(this_cpu); /*缺省选择空闲进程*/
- c = -1000;
- if (prev->state == TASK_RUNNING)
- goto still_running;
- still_running_back:
- list_for_each(tmp, &runqueue_head) { /*遍历运行队列*/
- p = list_entry(tmp, struct task_struct, run_list);
- if (can_schedule(p, this_cpu)) { /*单CPU中,该函数总返回1*/ int weight = goodness(p, this_cpu, prev->active_mm);
- if (weight > c)
- c = weight, next = p;
- }
- }
-
- /* 如果c为0,说明运行队列中所有进程的权值都为0,也就是分配给各个进程的
- 时间片都已用完,需重新计算各个进程的时间片 */
- if (!c) {
- struct task_struct *p;
- spin_unlock_irq(&runqueue_lock);/*锁住运行队列*/
- read_lock(&tasklist_lock); /* 锁住进程的双向链表*/
- for_each_task(p) /* 对系统中的每个进程*/
- p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
- read_unlock(&tasklist_lock);
- spin_lock_irq(&runqueue_lock);
- goto repeat_schedule;
- }
- spin_unlock_irq(&runqueue_lock);/*对运行队列解锁,并开中断*/
- if (prev == next) { /*如果选中的进程就是原来的进程*/
- prev->policy &= ~SCHED_YIELD;
- goto same_process;
- }
- /* 下面开始进行进程切换*/
- kstat.context_swtch++; /*统计上下文切换的次数*/
-
- {
- struct mm_struct *mm = next->mm;
- struct mm_struct *oldmm = prev->active_mm;
- if (!mm) { /*如果是内核线程,则借用prev的地址空间*/
- if (next->active_mm) BUG();
- next->active_mm = oldmm;
-
- } else { /*如果是一般进程,则切换到next的用户空间*/
- if (next->active_mm != mm) BUG();
- switch_mm(oldmm, mm, next, this_cpu);
- }
- if (!prev->mm) { /*如果切换出去的是内核线程*/
- prev->active_mm = NULL;/*归还它所借用的地址空间*/
- mmdrop(oldmm); /*mm_struct中的共享计数减1*/
- }
- }
-
- switch_to(prev, next, prev); /*进程的真正切换,即堆栈的切换*/
- __schedule_tail(prev); /*置prev->policy的SCHED_YIELD为0 */
- same_process:
- reacquire_kernel_lock(current);/*针对SMP*/
- if (current->need_resched) /*如果调度标志被置位*/
- goto need_resched_back; /*重新开始调度*/
- return;
- }
- 以上就是调度程序的主要内容,为了对该程序形成一个清晰的思路,我们对其再给出进一步的解释:
- · 如果当前进程既没有自己的地址空间,也没有向别的进程借用地址空间,那肯定出错。另外, 如果schedule()在中断服务程序内部执行,那也出错.
- · 对当前进程做相关处理,为选择下一个进程做好准备。当前进程就是正在运行着的进程,可是,当进入schedule()时,其状态却不一定是TASK_RUNNIG,例如,在exit()系统调用中,当前进程的状态可能已被改为TASK_ZOMBE;又例如,在wait4()系统调用中,当前进程的状态可能被置为TASK_INTERRUPTIBLE。因此,如果当前进程处于这些状态中的一种,就要把它从运行队列中删除。
- · 从运行队列中选择最值得运行的进程,也就是权值最大的进程。
- · 如果已经选择的进程其权值为0,说明运行队列中所有进程的时间片都用完了(队列中肯定没有实时进程,因为其最小权值为1000),因此,重新计算所有进程的时间片,其中宏操作NICE_TO_TICKS就是把优先级nice转换为时钟滴答。
- · 进程地址空间的切换。如果新进程有自己的用户空间,也就是说,如果next->mm与next->active_mm相同,那么,switch_mm( )函数就把该进程从内核空间切换到用户空间,也就是加载next的页目录。如果新进程无用户空间(next->mm为空),也就是说,如果它是一个内核线程,那它就要在内核空间运行,因此,需要借用前一个进程(prev)的地址空间,因为所有进程的内核空间都是共享的,因此,这种借用是有效的。
- · 用宏switch_to()进行真正的进程切换,后面将详细描述。
在中断描述符表(IDT)中,除中断门、陷阱门和调用门外,还有一种“任务们”。任务门中包含有TSS段的选择符。当CPU因中断而穿过一个任务门时,就会将任务门中的段选择符自动装入TR寄存器,使TR指向新的TSS,并完成任务切换。CPU还可以通过JMP或CALL指令实现任务切换,当跳转或调用的目标段(代码段)实际上指向GDT表中的一个TSS描述符项时,就会引起一次任务切换。
Intel的这种设计确实很周到,也为任务切换提供了一个非常简洁的机制。但是,由于i386的系统结构基本上是CISC的,通过JMP指令或CALL(或中断)完成任务的过程实际上是“复杂指令”的执行过程,其执行过程长达300多个CPU周期(一个POP指令占12个CPU周期),因此,Linux内核并不完全使用i386CPU提供的任务切换机制。
由于i386CPU要求软件设置TR及TSS,Linux内核只不过“走过场”地设置TR及TSS,以满足CPU的要求。但是,内核并不使用任务门,也不使用JMP或CALL指令实施任务切换。内核只是在初始化阶段设置TR,使之指向一个TSS,从此以后再不改变TR的内容了。也就是说,每个CPU(如果有多个CPU)在初始化以后的全部运行过程中永远使用那个初始的TSS。同时,内核也不完全依靠TSS保存每个进程切换时的寄存器副本,而是将这些寄存器副本保存在各个进程自己的内核栈中(参见上一章task_struct结构的存放)。
这样以来,TSS中的绝大部分内容就失去了原来的意义。那么,当进行任务切换时,怎样自动更换堆栈?我们知道,新任务的内核栈指针(SS0和ESP0)应当取自当前任务的TSS,可是,Linux中并不是每个任务就有一个TSS,而是每个CPU只有一个TSS。Intel原来的意图是让TR的内容(即TSS)随着任务的切换而走马灯似地换,而在Linux内核中却成了只更换TSS中的SS0和ESP0,而不更换TSS本身,也就是根本不更换TR的内容。这是因为,改变TSS中SS0和ESP0所化的开销比通过装入TR以更换一个TSS要小得多。因此,在Linux内核中,TSS并不是属于某个进程的资源,而是全局性的公共资源。在多处理机的情况下,尽管内核中确实有多个TSS,但是每个CPU仍旧只有一个TSS。
- fastcall对函数的调用不同于一般函数的调用,因为__switch_to()从寄存器(如表5.1)取参数,而不像一般函数那样从堆栈取参数,也就是说,通过寄存器eax和edx把prev和next 参数传递给__switch_to()函数。
- void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
- {
- struct thread_struct *prev = &prev_p->thread,
- *next = &next_p->thread;
- struct tss_struct *tss = init_tss + smp_processor_id();
- unlazy_fpu(prev_p);/* 如果数学处理器工作,则保存其寄存器的值*/
- /* 将TSS中的内核级(0级)堆栈指针换成next->esp0,这就是next 进程在内核
- 栈的指针
-
- tss->esp0 = next->esp0;
- /* 保存fs和gs,但无需保存es和ds,因为当处于内核时,内核段
- 总是保持不变*/
- asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
- asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
- /*恢复next进程的fs和gs */
- loadsegment(fs, next->fs);
- loadsegment(gs, next->gs);
- /* 如果next挂起时使用了调试寄存器,则装载0~7个寄存器中的6个寄存器,其中第4、5个寄存器没有使用 */
- if (next->debugreg[7]){
- loaddebug(next, 0);
- loaddebug(next, 1);
- loaddebug(next, 2);
- loaddebug(next, 3);
- /* no 4 and 5 */
- loaddebug(next, 6);
- loaddebug(next, 7);
- }
- if (prev->ioperm || next->ioperm) {
- if (next->ioperm) {
-
- /*把next进程的I/O操作权限位图拷贝到TSS中 */
- memcpy(tss->io_bitmap, next->io_bitmap,
- IO_BITMAP_SIZE*sizeof(unsigned long));
- /* 把io_bitmap在tss中的偏移量赋给tss->bitmap */
- tss->bitmap = IO_BITMAP_OFFSET;
- } else
-
- /*如果一个进程要使用I/O指令,但是,若位图的偏移量超出TSS的范围,
- * 就会产生一个可控制的SIGSEGV信号。第一次对sys_ioperm()的调用会
- * 建立起适当的位图 */
-
- tss->bitmap = INVALID_IO_BITMAP_OFFSET;
- }
- }
- 从上面的描述我们看到,尽管Intel本身为操作系统中的进程(任务)切换提供了硬件支持,但是Linux内核的设计者并没有完全采用这种思想,而是用软件实现了进程切换,而且,软件实现比硬件实现的效率更高,灵活性更大。
阅读(2086) | 评论(0) | 转发(0) |