进程切换
一、进程调度
1、调度时机
1)自愿的调度随时可以进行
##内核空间:一个进程可以通过schedule()启动一次调度/在调用该函数之前,将本进程的状态设置为TASK_INTERRUPIBLE 或 TASK_UNINTERRUPTIBLE,暂时放弃运行进入睡眠;为自愿的暂时放弃运行加上时间限制的系统调用:schedule_timeout()
##用户空间:通过系统调用 pause();为自愿的暂时放弃运行加上时间限制的系统调用:nanosleep()
两者的区别:自愿放弃运行在用户空间可见,在内核空间不可见
2)非自愿:这种调度发生在进程从系统空间返回到用户空间的前夕
二、进程切换
1、硬件支持:Intel在i386系统结构中增设任务状态段TSS,保存要切换的进程大的所有信息,计划为每一个进程准备一个TSS,将它存放在TR寄存器中;但是Linux内核并没有这么做,每一个CPU拥有一个TSS,一经装入就不再变了,原因在于:改变TSS中的SS0和ESP0所花的开销比通过装入TR以更换一个TSS要小得多。因为不是为每个进程分配TSS,所以配替换的进程的硬件上下文是保存在进程描述符中的thread_struct类型的字段。
TSS段的结构在Processor.h (include\asm-i386) 中,如下定义:
struct tss_struct {
unsigned short back_link,__blh;
unsigned long esp0;
unsigned short ss0,__ss0h;
unsigned long esp1;
unsigned short ss1,__ss1h; /* ss1 is used to cache MSR_IA32_SYSENTER_CS */
unsigned long esp2;
unsigned short ss2,__ss2h;
unsigned long __cr3;
unsigned long eip;
unsigned long eflags;
unsigned long eax,ecx,edx,ebx;
unsigned long esp;
unsigned long ebp;
unsigned long esi;
unsigned long edi;
unsigned short es, __esh;
unsigned short cs, __csh;
unsigned short ss, __ssh;
unsigned short ds, __dsh;
unsigned short fs, __fsh;
unsigned short gs, __gsh;
unsigned short ldt, __ldth;
unsigned short trace, io_bitmap_base;
/*
* The extra 1 is there because the CPU will access an
* additional byte beyond the end of the IO permission
* bitmap. The extra byte must be all 1 bits, and must
* be within the limit.
*/
unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
/*
* Cache the current maximum and the last task that used the bitmap:
*/
unsigned long io_bitmap_max;
struct thread_struct *io_bitmap_owner;
/*
* pads the TSS to be cacheline-aligned (size is 0x100)
*/
unsigned long __cacheline_filler[35];
/*
* .. and then another 0x100 bytes for emergency kernel stack
*/
unsigned long stack[64];
} __attribute__((packed));
在文件Processor.h (include\asm-i386) 中给出了TSS定义
#define INIT_TSS { \
.esp0 = sizeof(init_stack) + (long)&init_stack, \
.ss0 = __KERNEL_DS, \
.ss1 = __KERNEL_CS, \
.ldt = GDT_ENTRY_LDT, \
.io_bitmap_base = INVALID_IO_BITMAP_OFFSET, \
.io_bitmap = { [ 0 ... IO_BITMAP_LONGS] = ~0 }, \
}
2、执行进程切换
进程切换由schedule()函数执行的,主要分为两步
1) 切换页全局目录(Page Global Directory)来加载一个新的地址空间,实际上就是加载新进程的cr3寄存器值。
2) 切换内核堆栈和硬件上下文,这些包含了内核执行一个新进程的所有信息,包含了CPU寄存器。
3、linux进程的切换过程
最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程
- 正在运行的用户态进程X
- 发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).
- SAVE_ALL //保存现场
- 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换
- 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)
- restore_all //恢复现场
- iret - pop cs:eip/ss:esp/eflags from kernel stack
- 继续运行用户态进程Y
4、源码分析
在schedule()函数中,真正担当起进程切换的函数是context_switch(),在该函数中,它使用了一个很重要的宏函数switch_to()来实现进程切换,原来进程切换说到底关键在switch_to()上,看看swicth_to()的真面目:
#define switch_to(prev,next,last) do { \
unsigned long esi,edi; \
/**
* 在真正执行汇编代码前,已经将prev存入eax,next存入edx中了。
*/
/**
* 保存eflags和ebp到内核栈中。必须保存是因为编译器认为在switch_to结束前,
* 它们的值应当保持不变。
*/
asm volatile("pushfl\n\t" \
"pushl %