关于idle进程
也就是pid=0的进程。它是内核完成初始化后所创建的第一个进程,在系统空闲时执行。它的代码很简单:
for(;;) pause();
强调一下,idle进程是用户态进程。那么问题来了,内核从启动到初始化过程总都处在内核态,那么内核是怎么创 建idle并且切换到用户态呢?
一种很直接简单的想法是,内核直接调用用户空间的代码实现内核态到用户态的转换,但是这是不可能的,因为规
不能这么做。那怎么办呢?这就是这篇文章要讲的问题。
进程相关的结构
进程是一个动态的概念,要管理进程首先我们需要将这个动态的概念用一些静态数据结构抽象化。这个结构就是经 常所说的task_struct。里面存放了保护模式下进程的信息(比如ldt和tss等)。关于保护模式相关知识请参考 《linux0.11内核完全注释》前几章。所以我们要创建idle,首先就要先准备好相应的task_struct。在linux0.11中
是直接初始化的:
static union task_union init_task = {INIT_TASK,};
#define INIT_TASK \
/* state etc */ { 0,15,15, \
/* signals */ 0,{{},},0, \
/* ec,brk... */ 0,0,0,0,0,0, \
/* pid etc.. */ 0,-1,0,0,0, \
/* uid etc */ 0,0,0,0,0,0, \
/* alarm */ 0,0,0,0,0,0, \
/* math */ 0, \
/* fs info */ -1,0022,NULL,NULL,NULL,0, \
/* filp */ {NULL,}, \
{ \
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
/*tss*/ {0,PAGE_SIZE+(long)&init_task,0x10,0,0,0,0,(long)&pg_dir,\
0,0,0,0,0,0,0,0, \
0,0,0x17,0x17,0x17,0x17,0x17,0x17, \
_LDT(0),0x80000000, \
{} \
}, \
}
然后把idle任务的LDT和TSS放在全局描述符表GDT中:
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
内核态--->用户态
进程相关的信息准备好了,接下来就是状态的切换了。
直接调用显然是不能改变特权级的,但是我们知道,中断处理是可以在不同的特权级之间切换的。所以内核采用 了一种“模拟中断返回”的方式。
先看看CPU处理中断的时候是怎么做的:
其中上半部分着色的“原ss,原esp,原flags,原cs,原eip”,是指被中断的程序ss、esp、flags、cs、eip, 这些寄存器的入栈和出栈(由iret指令完成)都是由CPU自动完成,而且其他的寄存器出入栈全部由程序员自己处 理。提醒一下,这里的cs和eip是用在保护模式下的,所以cs是在GDT中寻址相应的代码段(idle的代码段和数据段已经在前一节里面准备好放在GDT中了。)
接下来看linux0.11是怎么模仿中断返回的:
#define move_to_user_mode() \//切换到用户态
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \//压入原ss(指向idle的代码段,低两位代表cpl=3,代表用户
态)
"pushl %%eax\n\t" \//压入原esp
"pushfl\n\t" \//压入原flags
"pushl $0x0f\n\t" \ //压入原cs(指向idle的代码段,低两位代表cpl=3,代表用户态)
"pushl $1f\n\t" \//压入原eip,指向iret指令后面的代码
/*以上的入栈操作本来都应该在程序被中断是由CPU自动完成,这里是手动压入,制造被中断的假象。以便马 上调用iret,由CPU自动完成这些寄存器的出栈操作,完成内核态到用户态的切换*/
"iret\n" \
//中断返回,由CPU恢复先前压入的寄存器。
"1:\tmovl $0x17,%%eax\n\t" \
"movw %%ax,%%ds\n\t" \
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")