Linux kernel Process Management 2.2(x86)——Creation and Switch

Author: Harold Wang 
http://blog.csdn.net/hero7935

 

进程切换时需要保存《进程上下文》

 

 ——用户地址空间

         包括程序代码,数据,用户堆栈等

——控制信息

         进程描述符,内核堆栈等

——硬件上下文

         包括通用寄存器以及一些系统寄存器

         通用寄存器如eax,bx等;系统寄存器如eip,esp,cr3等等

 

Linux将硬件上下文保存在如下thread_struct结构中:

 

423 struct thread_struct {
424
425         struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];  TLS?
426         unsigned long   esp0;
427         unsigned long   sysenter_cs;
428         unsigned long   eip;
429         unsigned long   esp;
430         unsigned long   fs;
431         unsigned long   gs;
432
433         unsigned long   debugreg[8];   调试相关的寄存器内容
434
435         unsigned long   cr2, trap_no, error_code;
436
437         union i387_union        i387; 保存数学协处理器相关寄存器的内容
438
439         struct vm86_struct __user * vm86_info;
440         unsigned long           screen_bitmap;
441         unsigned long           v86flags, v86mask, saved_esp0;
442         unsigned int             saved_fs, saved_gs;
443
444         unsigned long   *io_bitmap_ptr; 保存当前进程的I/O权限位图
445
446         unsigned long   io_bitmap_max;
447 };

上下文切换

switch_to宏执行进程切换
——schedule()函数调用这个宏
——调用一个新的进程在cpu上运行
switch_to利用了prev和next两个参数
——prev:指向当前进程
——next:指向被调度的进程

shedule是一个严格的机器依赖函数,如下所示:

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过程:

其中,在sched.h中对调度策略定义如下:

#define SCHED_OTHER 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_YIELD 0x10

如果p->mm为空,则意味着该进程无用户空间(例如内核线程),则无需切换到用户空间。如果p->mm=this_mm,则说明该进程的用 户空间就是当前进程的用户空间,该进程完全有可能再次得到运行。对于以上两种情况,都给其权值加1。

· 如果当前进程既没有自己的地址空间,也没有向别的进程借用地址空间,那肯定出错。另外, 如果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宏,这个宏实现了进程之间的真正切换:
看这段代码之前,请先回顾本节前面的thread_struct结构,因为这里涉及到的堆栈指针切换就是thread_struct。
1 #define switch_to(prev,next,last) do { /
2 asm volatile("pushl %%esi/n/t" /
3 "pushl %%edi/n/t" /
4 "pushl %%ebp/n/t" /
5 "movl %%esp,%0/n/t" /* save ESP */ /
6 "movl %3,%%esp/n/t" /* restore ESP */ /
7 "movl $1f,%1/n/t" /* save EIP */ /
8 "pushl %4/n/t" /* restore EIP */ /
9 "jmp __switch_to/n" /
10 "1:/t" /
11 "popl %%ebp/n/t" /
12 "popl %%edi/n/t" /
13 "popl %%esi/n/t" /
14 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), /    //这里就是thread_struct的两个数据成员
15 "=b" (last) /
16 :"m" (next->thread.esp),"m" (next->thread.eip), /
17 "a" (prev), "d" (next), /
18 "b" (prev)); /
19 } while (0)

输出参数有三个,表示这段代码执行后有三项数据会有变化,它们与变量及寄存器的对应关系如下:
0%与prev->thread.esp对应,1%与prev->thread.eip对应,这两个参数都存放在内存,而2%与ebx寄存器对应,同时说明last参数存放在ebx寄存器中。
 输入参数有五个,其对应关系如下:
3%与next->thread.esp对应,4%与next->thread.eip对应,这两个参数都存放在内存,而5%,6%和7%分 别与eax,edx及ebx相对应,同时说明prev,next以及prev三个参数分别放在这三个寄存器中。表5.1列出了这几种对应关系:

· 第2~4行就是在当前进程prev的内核栈中保存esi,edi及ebp寄存器的内容。
· 第5行将prev的内核堆栈指针ebp存入prev->thread.esp中。
· 第6行把将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。从现在开始,内核对next的内核栈进行操作,因 此,这条指令执行从prev到next真正的上下文切换,因为进程描述符的地址与其内核栈的地址紧紧地联系在一起,因此,改变内核栈就意味 着改变当前进程。如果此处引用current的话,那就已经指向next的task_struct结构了。从这个意义上说,进程的切换在这一行指令执行完 以后就已经完成。但是,构成一个进程的另一个要素是程序的执行,这方面的切换尚未完成。
· 第7行将标号“1”所在的地址,也就是第一条popl指令(第11行)所在的地址保存在prev->thread.eip中,这个地址就是prev下一次被调度运行而切入时的“返回”地址。
· 第8行将next->thread.eip压入next的内核栈。那么,next->thread.eip究竟指向那个地址?实际上,它就是 next上一次被调离时通过第7行保存的地址,也就是第11行popl指令的地址。因为,每个进程被调离时都要执行这里的第7行,这就决定了每个进程(除 了新创建的进程)在受到调度而恢复执行时都从这里的第11行开始。
· 第9行通过jump指令(而不是 call指令)转入一个函数__switch_to()。这个函数的具体实现将在下面介绍。当CPU执行到__switch_to()函数的ret指令 时,最后进入堆栈的next->thread.eip就变成了返回地址,这就是标号“1”的地址。
· 第11~13行恢复next上次被调离时推进堆栈的内容。从现在开始,next进程就成为当前进程而真正开始执行。

Author: Harold Wang 
http://blog.csdn.net/hero7935

 

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;
}
}

Author: Harold Wang 
http://blog.csdn.net/hero7935

 

总结:
整个进程切换过程,关键就是做好以下几步:
1.根据调度策略找到下一个应当运行的进程next;
2.保存当前进程current的进程上下文,硬件上下文保存在current的thread_struct结构中,其他信息保存在内核态堆栈中;
3.保存被切换下的进程current的返回地址;
4.将进程next的进程上下文恢复到机器的相关寄存器、堆栈;
5.自此系统已经在next的上下文环境中运行,即完成了切换

Reference:
1.  http://www.eefocus.com/article/09-06/74893s.html
2. Robert Love. Linux Kernel Development 3rd Edition [M]. US: Addison-Wesley 2010
3. lecture_02.1 ppt (used in class, not convenient to upload ,email me)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值