Chinaunix首页 | 论坛 | 博客
  • 博客访问: 103330
  • 博文数量: 33
  • 博客积分: 101
  • 博客等级: 民兵
  • 技术积分: 185
  • 用 户 组: 普通用户
  • 注册时间: 2011-11-17 22:42
文章分类
文章存档

2013年(1)

2012年(16)

2011年(16)

我的朋友

分类:

2012-01-20 23:59:02

原文地址:Linux进程切换 作者:platinaluo

从用户的角度来理解,进程就是程序的执行过程,例如我们在shell下敲入一个nonbuild-in的命令,我们的shell就会fork出一个进程来执行这个任务。

(1) 什么是多任务

地球人都知道,我们现在使用的Linux都是多任务的操作系统。下面就唠一唠多任务的概念。

什么是多任务?执行完一个任务再接着执行另一个任务(传说中石器时代,计算机还真是这样处理任务的)就是多任务? NO,必须多个任务“同时”执行才能算是多任务操作系统。此处的“同时”只是指在用户角度感觉是同时执行。如果只有一个CPU,显然它是没有办法同时执行两个任务的。因此,让用户感到是多个任务在同时执行才是多任务操作系统的目的。

(2)从kernel看进程

如何才能够在单个CPU上运行多个进程呢?很自然的想法就是保存老进程的状态,然后加载新进程的状态。对,Linux就是这样子实现的。那么老进程需要保存那些信息呢?让我们来看看Linux是如何完成的。

在Linux下,进程切换通常存在以下情况:

(1)进程主动放弃继续执行,例如yield系统调用和一些阻塞型的系统调用会引发进程切换,某些异常的发生也会导致进程切换的发生。

(2)由内核主动暂停进程的执行,转而执行别的进程或中断。例如中断的发生和更高优先级进程的唤醒,或者进程消耗完时间片等等因素,都会导致内核暂停动迁进程的执行。

Linux 2.4(i386平台)使如下代码进行进程切换:

//sched.c schedule()函数的部分代码

{
prepare_to_switch();   //在i386体系下该函数什么也不执行
{
   struct mm_struct *mm = next->mm;
   struct mm_struct *oldmm = prev->active_mm;
   if (!mm) {
    BUG_ON(next->active_mm);
    next->active_mm = oldmm;
    atomic_inc(&oldmm->mm_count);
    enter_lazy_tlb(oldmm, next, this_cpu);
   } else {
    BUG_ON(next->active_mm != mm);
    switch_mm(oldmm, mm, next, this_cpu);
   }

   if (!prev->mm) {
    prev->active_mm = NULL;
    mmdrop(oldmm);
   }
}
switch_to(prev, next, prev);
__schedule_tail(prev);
}

此处应注意对内核线程的处理方法。判断线程是不是内核线程的有效方法就是判断PCB中的内存描述符是否为空。如果内存描述符为空,则就是内核线程;否则就是普通线程。下面函数为页表切换的程序:

static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
{
if (prev != next) {
   /* stop flush ipis for the previous mm */
   clear_bit(cpu, &prev->cpu_vm_mask);
#ifdef CONFIG_SMP
   cpu_tlbstate[cpu].state = TLBSTATE_OK;
   cpu_tlbstate[cpu].active_mm = next;
#endif
   set_bit(cpu, &next->cpu_vm_mask);
   /* Re-load page tables */
   load_cr3(next->pgd);
/* load_LDT, if either the previous or next thread
   * has a non-default LDT.
   */
   if (next->context.size+prev->context.size)
    load_LDT(&next->context);
}
#ifdef CONFIG_SMP
else {
   cpu_tlbstate[cpu].state = TLBSTATE_OK;
   if(cpu_tlbstate[cpu].active_mm != next)
    out_of_line_bug();
   if(!test_and_set_bit(cpu, &next->cpu_vm_mask)) {
    /* We were in lazy tlb mode and leave_mm disabled 
    * tlb flush IPI delivery. We must reload %cr3.
    */
    load_cr3(next->pgd);
    load_LDT(&next->context);
   }
}
#endif
}

以上应注意对于具有相同内存描述符的管理措施,具有相同描述符只有两种情况:1. 至少其中之一是内核线程;2. 或者两个共享相同页表的普通进程。如果内存描述符相同,则页表切换不会发生。页表切换的代价是相当巨大的,TLB中的内容全部失效,这意味着更多次的访问内存甚至硬盘。
此处也给我们较好的提示:在应用程序开发中,能用线程则尽量用线程来解决,这样就能将不同执行流程之间的切换代价降至最小。

以下代码为386平台下的切换代码:

#define switch_to(prev,next,last) do {      \
asm volatile("pushl %%esi\n\t"      \
       "pushl %%edi\n\t"      \
       "pushl %%ebp\n\t"      \
       "movl %%esp,%0\n\t" /* save ESP */   \
       "movl %3,%%esp\n\t" /* restore ESP */ \
       "movl $1f,%1\n\t"   /* save EIP */   \
       "pushl %4\n\t"   /* restore EIP */ \         //将返回地址压栈,__switch_to函数返回会自动执行1后的代码
       "jmp __switch_to\n"     \
       "1:\t"       \            "popl %%ebp\n\t"      \
       "popl %%edi\n\t"      \
       "popl %%esi\n\t"      \
       :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
        "=b" (last)      \
       :"m" (next->thread.esp),"m" (next->thread.eip), \
        "a" (prev), "d" (next),     \
        "b" (prev));      \
} while (0)

// 进程切换,在Linux2.4中每个CPU只使用了一个TSS,因此当进程切换时,需要频繁的更改TSS。
void fastcall __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); //保存FPU,MMX,XMM寄存器

/*
* Reload esp0, LDT and the page table pointer:
*/
tss->esp0 = next->esp0;

/*
* Save away %fs and %gs. No need to save %es and %ds, as
* those are always kernel segments while inside the kernel.
*/
asm volatile("mov %%fs,%0":"=m" (prev->fs));
asm volatile("mov %%gs,%0":"=m" (prev->gs));

/*
* Restore %fs and %gs.
*/
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);

/*
* Now maybe reload the debug registers
*/
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) {
    /*
    * 4 cachelines copy ... not good, but not that
    * bad either. Anyone got something better?
    * This only affects processes which use ioperm().
    * [Putting the TSSs into 4k-tlb mapped regions
    * and playing VM tricks to switch the IO bitmap
    * is not really acceptable.]
    */
    memcpy(tss->io_bitmap, next->io_bitmap,
     IO_BITMAP_BYTES);
    tss->bitmap = IO_BITMAP_OFFSET;
   } else
    /*
    * a bitmap offset pointing outside of the TSS limit
    * causes a nicely controllable SIGSEGV if a process
    * tries to use a port IO instruction. The first
    * sys_ioperm() call sets up the bitmap properly.
    */
    tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}
此处会保存FPU,MMX,XMM等寄存器,内核堆栈切换终于发生。状态段加载IO允许位图。

参考文献:
1. 《Understanding Linux Kernel》
2. Linux 2.4 部分源码

阅读(1482) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~