0 上下文切换
上下文切换(Context Switching)指的是切换处理器执行的任务时发生的一系列动作。
感性地说,从一个任务切换到另一个任务,过一段时间后又想切换回来,必然是需要保存一些东西的,这些东西就是某个任务的上下文。
因此,被要实现多任务的操作系统,或者简单的任务调度器,就肯定绕不过上下文切换。
而要知道上下文切换到底需要做什么,可以想看看描述一个任务及其状态到底需要哪些参数。
1 任务创建
先看看任务创建需要的所有信息——地址寄存器、一些特殊寄存器和返回地址。而所谓的上下文(context,避免读着别扭后面一直用context代上下文),也在介绍中断时作了定义啦。
对操作系统而言,创建任务等同于初始化用户提供的栈和入口地址(entry point)的上下文(context)。通常创建任务的函数都是没有返回值的。创建任务需要的参数包括入口地址、栈地址、栈大小和传给栈的参数。原型类似于下面这样。
void add_task(taskFunc *entry, void *stack, unsigned int stack_size, void *arg);
这个函数拿到这些参数信息之后,需要做两件事情——初始化任务堆栈、向操作系统或者运行时(runtime)注册任务,也就是告知系统该任务的信息。
2 注册任务
一般说来,操作系统或者系统内核会用一个叫任务控制块(Task Control Block,TCB)的数据结构来保存任务的信息。
对于简单的运行时系统而言,不需要保存详细的任务信息,只需要保存各任务堆栈的入口地址task_stacks[i]
、已经注册的任务数量allocated_tasks
,以及当前任务的标号current_task
(也可以是别的,主要是用来标记处理器现在在执行的的哪一个任务,即task_stacks[current_task]
。所以维护下面这几个全局变量就绰绰有余了。
unsigned int *task_stacks[MAXTASKS];
unsigned int allocated_tasks = 0;
int current_task = -1;
3 初始化任务栈
任务栈的初始化也是分两步走——第一步是计算任务栈栈顶(the top of the stack)地址,第二步是填充任务栈。
略有奇怪哈,要得到任务栈栈顶地址的话,直接用任务栈地址+栈长度不就好啦,怎么感觉还需要别的操作,单独成一步?答案是栈顶地址有字节对齐需求。
任务创建函数往往不会对堆栈地址和长度设置约束,但处理器可能需要堆栈的地址是 16、32 或者 64 的倍数,这就需要咱在计算栈顶地址时做一点处理。
sp = (unsigned int *)((int)(stack + stack_size) & 0xfffffff0);
像这样,把地址末尾不足 f 的值抹去,也就是对求和结果做一下向下取整,这样就能保证栈顶地址(sp)是16的倍数。
这里又联想到内存管理。内存管理算法也会刻意对申请地空间大小做这样的向下取整操作,以减少内存中的小碎片。这样横向比较一下,也就能理解malloc
函数返回实际分配的空间大小的意义了
参考
- Xtensa, programmers_guide.pdf, chapter 8 Context Switching