main.c/move_to_mode()
在做李治军操作系统实验课的时候,在进程运行轨迹与统计部分,有提到使用move_to_mode()函数,但是当时的实验并没有涉及到这个函数的具体内容,于是自己找了一些资料,捋了一下。
1.进程0的创建
move_to_user_mode()实现从内核模式切换到用户模式,执行进程0.
进程0则在经过sti()之前的语句,已经创建完毕。
在kernel/sched.c中:
static union task_union init_task = {INIT_TASK,};
//union task_union {
// struct task_struct task;
// char stack[PAGE_SIZE];
//};
其中INIT_TASK为宏定义,在include/linux/sched.h:
#define INIT_TASK \
/* state etc */ { 0,15,150, \
/* 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, \
{} \
}, \
}
这里我们看ldt的三个值:
{ \
{0,0}, \
/* ldt */ {0x9f,0xc0fa00}, \
{0x9f,0xc0f200}, \
}, \
ldt[0]是一个空描述符;ldt[1]是进程0的代码段描述符,ldt[2]是进程的数据段描述符。
段描述符的格式为:
关于段描述符的分析:段描述符分析
ldt[1]的值为0x0000 009f,0x00c0 fa00,其中第一个数在低32位,第二个数在高32位:
0x00c0 fa00
0x0000 009f
根据上图,可以知道段基地址为0,特权级DPL为3,
再查看gdt的代码段描述符号:
gdt: .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x00c09a0000000fff /* 16Mb */
.quad 0x00c0920000000fff /* 16Mb */
.quad 0x0000000000000000 /* TEMPORARY - don't use */
.fill 252,8,0 /* space for LDT's and TSS's etc */
第二项为代码段描述符:
0x00c0 9a00
0x0000 0fff
其基址也为0,DPL为1,所以进程0的代码段基址与内核段基址是相同的。
具体分析:进程0的LDT0分析
tss的值:
/*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, \
{} \
}
其中tss_struct的定义为:
struct tss_struct {
long back_link; /* 16 high bits zero */
long esp0;
long ss0; /* 16 high bits zero */
long esp1;
long ss1; /* 16 high bits zero */
long esp2;
long ss2; /* 16 high bits zero */
long cr3;
long eip;
long eflags;
long eax,ecx,edx,ebx;
long esp;
long ebp;
long esi;
long edi;
long es; /* 16 high bits zero */
long cs; /* 16 high bits zero */
long ss; /* 16 high bits zero */
long ds; /* 16 high bits zero */
long fs; /* 16 high bits zero */
long gs; /* 16 high bits zero */
long ldt; /* 16 high bits zero */
long trace_bitmap; /* bits: trace 0, bitmap 16-31 */
struct i387_struct i387;
};
第二项PAGE_SIZE+(long)&init_task
为init_task
联合体的最后一个字节,这是tss_struct中0特权esp的值。
每个task_union都是4096字节(一页)大小的。而前104个字节是一个task_struct结构,其布局大致是:
|---------------------------------------4096个字节----------------------------------------------------|
|-------task_struct----------|----------------------剩余未用-----------------------------------------|
esp0指向这个结构的最后一个字节,作为内核栈的栈顶。
为什么会有这么多的剩余未用空间呢?如果这部分是闲置不用的。对于追求最高效利用内存资源的OS来说,不太可能如此浪费资源。其实这个剩余未用部分,就作为一个个进程的0特权栈来使用。我们都晓得,进程在运行期间很有可能使用调用system call函数的,而一调用system call函数,CPL就会从原来的3特权,翻转到0特权。而翻转特权以后,原来3特权的栈将不得被0特权的内核使用,于是Intel要求每个进程在创建之初都得指定一个0特权的栈,用于将来进程进入0特权时使用。
并且让task[0]指向这个task_struct。
在sched.c中:
struct task_struct * task[NR_TASKS] = {&(init_task.task), };
进程0的东西准备完毕,就需要将进程的特权级由0转移到3,因为操作系统规定进程必须都运行在3特权上。
接下来就是move_to_user_mode()的工作了。
2.move_to_user_mode()
move_to_user_mode()是一个宏定义,定义在include/asm/system.h中:
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \
"pushl $0x17\n\t" \
"pushl %%eax\n\t" \
"pushfl\n\t" \
"pushl $0x0f\n\t" \
"pushl $1f\n\t" \
"iret\n" \
"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")
该函数利用iret模拟中断返回,继续执行进程0.
iret前面的指令,模拟int中断过程中需要一次将ss,esp,eflag,cs,eip压栈的过程:
movl %%esp,%%eax
将esp的值赋给eax,esp是当前栈的栈顶。当前栈为那个大小为一个页的user_stack的栈。
pushl $0x17
将0x17压栈。
pushl %%eax
将刚才保存的esp内容压栈。
pushfl
将eflag压栈。
pushl $0x0f
将0x0f压栈。
pushl $1f
将指令为1f的偏移压栈。
最后栈中的内容为:
通过iret弹出以后,cs为0x0f,这是一个段选择子,0x0f = 0x0000 1111,表示以DPL=3去选择LDT中的第2项,这一项正是前面INIT_TASK中ldt的第二项,即进程0的代码段。
ss为0x17 = 0x 0001 0111,表示以DPL=3去选择LDT中的第三项,也就是INIT_TASK中ldt的第三项。
iret之后,将会执行1f偏移的指令:
movl $0x17,%%eax
movw %%ax,%%ds
movw %%ax,%%es
movw %%ax,%%fs
movw %%ax,%%gs
将ds,es,fs,gs对齐到0x17选择子上。
自此,特权级就从0转为3了,开始执行 fork()创建进程1。