这一章,与前面提到的特权级转移以及中断处理机制相关的知识关系很大。
首先画个图了解下这几个概念的关系以及相互作用:
以前一直很想知道系统调用是怎么实现的,看完书才明白系统调用跟普通的中断处理程序实现基本是相似的,可见中断处理程序是本章重点中的重点。
梳理一下书中的思路:
1.首先实现一个简单的进程,注册一个时钟中断处理程序,在进程执行的过程中发生时钟中断,执行中断处理程序,返回进程被中断的地方继续执行。
2.在1的基础上,再增加一个进程,每次发生中断,切换到另一个进程执行,而且保证进程能正确的在被中断的地方恢复执行。
3.总结一下增加一个进程需要做哪些修改。
3.实现一个简单的系统调用,进程调用系统调用后,能正确返回进程并继续执行。
4.修改一下时钟中断,对进程增加类似优先级的标识,根据进程的优先级,实现简单的调度程序。
1. 简单的进程
大概的执行过程为:执行进程,进行特权级转移,切换到中断处理程序,恢复进程。这里的关键点就是,怎么保存进程的状态,以及怎么恢复进程继续执行。
这里记录进程信息的结构为:
typedef struct s_stackframe { /* proc_ptr points here
u32 gs;
u32 fs;
u32 es;
u32 ds;
u32 edi;
u32 esi;
u32 ebp;
u32 kernel_esp;
u32 ebx;
u32 edx;
u32 ecx;
u32 eax;
u32 retaddr;
u32 eip;
u32 cs;
u32 eflags;
u32 esp;
u32 ss;
}STACK_FRAME;
typedef struct s_proc {
STACK_FRAME regs;
u16 ldt_sel;
DESCRIPTOR ldts[LDT_SIZE];
u32 pid;
char p_name[16];
}PROCESS;
画个图,有个形象的了解:
注意图中的”STACK_FRAME A”部分和”STACK_FRAME B部分” 。
进程执行时,发生时钟中断,”STACK_FRAME B”部分被push进内核栈,开始执行中断处理程序,而”STACK_FRAME A”部分则是中断处理程序需要保存到进程头部结构的部分。当中断处理程序处理结束,想返回进程时,则需要找到进程的头部结构,然后跳到”STACK_FRAME B”部分,iretd完成进程的恢复。
这里最需要关注的就是栈的切换,进程执行->中断->恢复进程执行 这个循环过程,栈的变化如下图所示:
2. 多进程
从上面简单的进程实现中可以看到,执行一个进程最重要的,就是要找到这个进程的头部结构。在时钟中断处理程序中,在保存好之前进程的状态后,就可以根据自定义一些策略,选择接下来执行哪个进程。
在书中的例子中,在全局定义了一个变量 p_proc_ready,指向接下来要执行的进程的结构 头部。有了这个指针,如果要从内核切换到进程,可以直接把esp移动到STACK_FRAME B的部分,执行一系列push操作,把STACK_FRAME B入栈,执行iretd执行完成从内核到进程的跳转。然后设置好tss,使发生时钟中断时,esp指向被中断的进程的STACK_FRAME B部分,然后执行一系列push操作把STACK_FRAME A部分保存到进程的结构中,这样就保存了进程的状态。接着修改p_proc_ready使其指向不同的进程结构的头部,重复上面的过程,即实现多进程的切换。
3. 系统调用
这个与上述的时钟中断有些类似,只不过这里时自定义一个中断号,在IDT中注册一个handler,进程如果需要执行系统调用,这执行 int n进行。书中的系统调用,参数使用eax传递,调用的返回结果,也是通过eax传递。
4. 进程调度
这个是个比较大的话题,以后再做详细的研究。书中的调度相对简单,每个进程的结构,增加一个优先级属性,根据优先级属性分配不同的时间片。
遇到的问题
1. 在进程的结构中, 中间的kernel_esp,popad为什么会忽略掉?
好吧,popad指令就是这么设计的,为了防止修改当前的esp。
IF OperandSize = 32 (* instruction = POPAD *)
THEN
EDI ¬ Pop();
ESI ¬ Pop();
EBP ¬ Pop();
increment ESP by 4 (* skip next 4 bytes of stack *)
EBX ¬ Pop();
EDX ¬ Pop();
ECX ¬ Pop();
EAX ¬ Pop();
ELSE (* OperandSize = 16, instruction = POPA *)
DI ¬ Pop();
SI ¬ Pop();
BP ¬ Pop();
increment ESP by 2 (* skip next 2 bytes of stack *)
BX ¬ Pop();
DX ¬ Pop();
CX ¬ Pop();
AX ¬ Pop();
FI;
也就是说,popad指令,会忽略esp的恢复。
2. 内核中写的一些c代码,局部变量放在哪里?
代码跟踪的结果显示,C语言的局部变量,是放在进程的栈里。
3. leave指令
通常在进入函数中时有两条命令,如下:
push ebp ; 保存上一个函数的栈帧基地址
mov ebp,esp ; 设置新的函数栈帧基地址
在返回函数前通常有如下两条指令:
mov esp,ebp ; 将当前函数栈帧基地址保存到esp中
pop ebp ; 恢复上一个函数的栈帧基地址
Intel又设计了两条指令来简化上面的两个步骤,那就是ENTER和LEAVE指令。
leave指令就相当于 mov esp,ebp 和 pop ebp 两条指令的执行效果。而ENTER指令要麻烦一点。enter指令也有一个先天上的不足,那就是速度慢,这里不去了解,一般编译器生成代码时很少使用enter指令,倒是LEAVE指令经常被用到。