2.8.3 任务切换
任务切换是任务处理的始端,通常是对任务的地址空间、栈、寄存器等资源进行切换,它涉及到机器的底层,几乎全部由汇编指令完成。任务切换的效率,直接影响到系统的调度延迟,并且,并且与寄存器的使用情况有关。在XtratuM移植过程中,由于笔者就被该问题困扰,因此在这段内容详细介绍一下任务切换,或者是域切换。
正如前面所述,每一个域有一个域控制块(DCB,Domain Control Block),它保存者域的基本信息,为了清晰的对域进行了解,让我们首先看看域控制块的定义。
typedef struct domain_struct {
// Supervisor stack
unsigned long *sstack; /* HARDCODED, don't change it */
// Domain's page directory
unsigned long pd; /* HARDCODED, don't change it */
unsigned long *sstack_st;
char *name;
int id;
unsigned int priority;
unsigned long flags;
event_handling_t *events;
unsigned long events_user_addr;
time_inf_t time;
unsigned long *heap;
mm_page_t *mm_page_list;
struct domain_struct *prev, *next;
} domain_t;
结构体中,与任务切换的变量有两个,一个是sstack,另外一个是pd,从作者的注释中或许你能够想到它们的重要性。下面就详细讲解这两个变量,以及它们在任务切换中所扮演的角色。一个域具有两个很特别的属性,一个是页表,页表是用来保存获取内存物理地址的信息,并且从2.5节已经讲述了虚拟内存到物理内存的三级映射机制,读者应该清楚CR3这一关键寄存器,它保存了最高级页
表的物理地址,因此,对于每一个域,都要准备一个保存该数据的区域,而PD参数恰好就是用来保存这个最高级的页表的物理地址的。域的另外一个特别属性是栈空间,它是程序运行中数据的存储区域。栈空间的访问是从高地址到低地址,但是内存的分配是从低地址到高地址,sstack_st和sstack就分别用来保存这两个数值。sstack始终指向栈顶,但是,其值是动态变化的,而sstack_st指向栈对应内存的地址,是静态的。了解了这一点后,下面将根据XtratuM的代码讲述任务的切换过程。
#define PAGE_OFFSET_STR "0xC0000000"
#define CHANGE_DOMAIN(new_domain, current_domain) /
__asm__ __volatile__("movl (%%ebx), %%edx/n/t" /;将current_domain的DCB地址放入edx寄存器
"pushl $1f/n/t" /;将标记‘1’处地址放入栈中
"movl %%esp, (%%edx)/n/t" /;将当前的esp放入current_domain的DCB的第一个变量里面,即sstack里面
"movl (%%ecx), %%esp/n/t" /;将new_domain的DCB的第一个变量的值放入esp里面,即将sstack值放入esp,完成栈切换
"movl 4(%%ecx), %%eax/n/t" /;将new_domain的DCB的第二个变量的值放入eax里面,即pd值放入eax里面
"subl $"PAGE_OFFSET_STR", %%eax/n/t" /;将eax的值减去0xC0000000,从而获取内存物理地址,低效?
"movl %%eax, %%cr3/n/t" /;将PD对应的物理地址放入CR3寄存器,完成页表切换,
"movl %%ecx, (%%ebx)/n/t" /;将new_current域的DCB地址放入current_domain变量
"ret/n/t" /;跳转,由"pushl $1f"可以判断跳转到‘1’处,但是是new_domain空间的‘1’标记处
"1:/n/t" : :"c" (new_domain), /
"b" (current_domain))
#define domain_context_switch(new_domain, current_domain) /
PUSH_REGISTERS(); /
PUSH_FP(); /
CHANGE_DOMAIN(new_domain, current_domain); /
POP_FP(); /
POP_REGISTERS();
任务切换从domain_context_switch(new_domain, current_domain)这个宏开始,其中,new_domain是指向新域DCB的指针,而(*current_domain)则是指
向当前域DCB的指针,注意,current_domain是一个指向指针的指针。前面两个PUSH是用来保存通用寄存器和浮点运算相关寄存器,这里不过多介绍。下面是CHANGE_DOMAIN宏,首先,它将new_domain的值放入ecx寄存器,而current_domain的值放入ebx寄存器,系统继续执行。由上面的注释可以很清晰的看清系统的执行和切换过程,但是CHANGE_DOMAIN()仅仅是切换了页表和栈,对于寄存器的内容恢复还没有完成,而标记为‘1’的地方又恰好是POP_*操作,因此当两个POP_*操作完成后,整个任务切换也就完成了。但是由于现在依然是在内核态,所以还没有进入新任务的执行空间,当系统从内核态跳出时,才是真正进入新任务的执行空间。
有的读者可能注意到,只有当任务被切换出去的时候,才会有保存寄存器的PUSH_*操作,那么如果是一个新任务呢?在XtratuM中,系统的加载是通过wrapper程序进行的,也就是当任务首次被调度时,先进入一个wrapper程序,并且程序的真正入口地址__main()是作为wrapper的参数的。因此,现在的问题就变成了系统如何进入wrapper,并且将新域的入口函数地址传递给wrapper,并且还有绕过POP_*操作。在XtratuM系统中,认为的改变了pushl 1f操作,从上面的分析中可以知道,CHANGE_DOMAIN最后调用ret的时候将会跳到‘1’的地址出,现在就是人工设置,域在加载的时候,将__main,0,wrapper地址分别放在新建域的栈中:
#define init_sstack(stack, __main, wrapper) {/
*--(stack) = (unsigned long) __main; /
*--(stack) = (unsigned long) 0; /
*--(stack) = (unsigned long) wrapper; /
}
通过这种方式,当完成栈和页表切换后,系统继续执行RET操作,将会使wrapper和0分别被送入IP和CS寄存器。那么__main作为参数则被传递给wrapper,但是,问题在于系统传递参数的方式,如果是用栈的话,这样没有问题,但是如果是用寄存器,这里将会有问题。移植中,在高版本的Linux系统中,就是通过寄存器。为了满足这种需求,在移植过程中,我们对CHANGE_DOMAIN做了下面的修改,这里就不叙述详细原因了,可以参考系统调用一节内容。
#define CHANGE_DOMAIN(new_domain, current_domain) /
__asm__ __volatile__("movl (%%ebx), %%edx/n/t" /
"pushl $1f/n/t" /
"movl %%esp, (%%edx)/n/t" /
"movl (%%ecx), %%esp/n/t" /
"movl 4(%%ecx), %%eax/n/t" /
"subl $"PAGE_OFFSET_STR", %%eax/n/t" /
"movl %%eax, %%cr3/n/t" /
"movl %%ecx, (%%ebx)/n/t" /
"movl 8(%%esp), (%%eax)/n/t" /
"ret/n/t" /
"1:/n/t" : :"c" (new_domain), /
"b" (current_domain))
嵌入式实时Hypervisor:XtratuM (15)
最新推荐文章于 2021-01-05 14:02:19 发布