move to user mode
#define move_to_user_mode() \
__asm__ ("movl %%esp,%%eax\n\t" \ 保存堆栈指针 esp 到 eax 寄存器中
"pushl $0x17\n\t" \ 首先将堆栈段选择符(SS)入栈。
"pushl %%eax\n\t" \ 然后将保存的堆栈指针值(esp)入栈。
"pushfl\n\t" \ 将标志寄存器(eflags)内容入栈
"pushl $0x0f\n\t" \ 将内核代码段选择符(cs)入栈
"pushl $1f\n\t" \ 将下面标号 1 的偏移地址(eip)入栈
"iret\n" \ 执行中断返回指令,则会跳转到下面标号 1 处
"1:\tmovl $0x17,%%eax\n\t" \ 此时开始执行任务 0
"movw %%ax,%%ds\n\t" \ 初始化段寄存器指向本局部表的数据段
"movw %%ax,%%es\n\t" \
"movw %%ax,%%fs\n\t" \
"movw %%ax,%%gs" \
:::"ax")
首先解释指令iret.
iret 指令(interrupt return)中断返回,终端服务程序的最后一条指令。iret指令将推入堆栈的段地址和偏移地址弹出,使程序返回到原来中断发生的地方。它将产生以下三点效应:
1.恢复IP(instruction pointer):(IP)←((SP)+1:(SP)),
2.恢复CS(code segment):(CS)←(SP)←(SP)+2
3.恢复中断前的PSW(program status word),即恢复中断前的标志寄存器的状态。
以上操作按顺序进行。
当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。
当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。
因此,这段程序的大概意思是先将任务0所需要的各个寄存器的值压栈,压栈后执行IRET指令,利用该中断返回指令将各个寄存器设置为我们所理想的值。
但是,程序中压入了几个常数,0x17,0x0f,1f是什么意思呢。
分两类,0x17,0x0f是段选择子。段选择子用于保护模式下的寻址。0-1位表示请求的特权级,0表示系统级,3表示用户级。2位用于选择全局描述符表还是局部描述符表。
3-15位是描述符表项索引。
1f表示将下面标号1的程序段的偏移地址入栈。
fork 函数
main.c 中定义的fork应用层函数
static inline _syscall0(int,fork)
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \ 系统调用进入sys_fork
: "=a" (__res) \ __res 中断返回值
: "0" (__NR_##name)); \ //输入系统中断调用号__NR_fork (=2)
if (__res >= 0) \
return (type) __res; \ //如果返回值>=0,则直接返回该值
errno = -__res; \ //否则出错号
return -1; \
}
在进入系统调用给的时候cpu会自动保存一些寄存器的值
接下来,程序跳转进入/kernel/system_call.S文件中system_call系统调用入口函数_system_call处执行。
关于系统调用这块代码请参考:系统调用代码流程
或者 深入分析fork的执行过程(Linux-0.11内核)
.align 2
_sys_fork:
call _find_empty_process
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call _copy_process # 调用 C 函数 copy_process()(kernel/fork.c,68)
addl $20,%esp # 丢弃这里所有压栈内容
1: ret
在sys_fork中,首先调用find_empty_process函数取得不重复的进程号,其返回值在eax寄存器中;然后把gs,esi,edi,ebp,eax寄存器的值压栈,然后,以前面压入栈内的ss到eax(nr)。这些值为参数调用copy_process函数.
int copy_process(int nr,long ebp,long edi,long esi,long gs,long none,
long ebx,long ecx,long edx,
long fs,long es,long ds,
long eip,long cs,long eflags,long esp,long ss)
{
struct task_struct *p;
int i;
struct file *f;
p = (struct task_struct *) get_free_page(); // 为新任务数据结构分配内存
if (!p)
return -EAGAIN;
task[nr] = p; // 将新任务结构指针放入任务数组中。
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
/* 注意!这样做不会复制超级用户的堆栈 */(只复制当前进程内容)
p->state = TASK_UNINTERRUPTIBLE; // 将新进程的状态先置为不可中断等待状态
p->pid = last_pid; // 新进程号。由前面调用 find_empty_process()得到
p->father = current->pid; // 设置父进程号
p->counter = p->priority;
p->signal = 0; // 信号位图置 0。
p->alarm = 0;
p->leader = 0; /* process leadership doesn't inherit */
p->utime = p->stime = 0;
p->cutime = p->cstime = 0;
p->start_time = jiffies;
// 以下设置任务状态段 TSS 所需的数据(参见列表后说明)
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p; // 堆栈指针(由于是给任务结构 p 分配了 1 页
// 新内存,所以此时 esp0 正好指向该页顶端)
p->tss.ss0 = 0x10; // 堆栈段选择符(内核数据段)[??]。
p->tss.eip = eip; // 指令代码指针。
p->tss.eflags = eflags; // 标志寄存器。
p->tss.eax = 0;
p->tss.ecx = ecx;
p->tss.edx = edx;
p->tss.ebx = ebx;
p->tss.esp = esp;
p->tss.ebp = ebp;
p->tss.esi = esi;
p->tss.edi = edi;
p->tss.es = es & 0xffff; // 段寄存器仅 16 位有效
p->tss.cs = cs & 0xffff;
p->tss.ss = ss & 0xffff;
p->tss.ds = ds & 0xffff;
p->tss.fs = fs & 0xffff;
p->tss.gs = gs & 0xffff;
p->tss.ldt = _LDT(nr); // 该新任务 nr 的局部描述符表选择符(LDT 的描述符在 GDT 中)
p->tss.trace_bitmap = 0x80000000;
// 如果当前任务使用了协处理器,就保存其上下文。
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
// 设置新任务的代码和数据段基址、限长并复制页表。如果出错(返回值不是 0),则复位任务数组中 相应项并释放为该新任务分配的内存页。
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
// 如果父进程中有文件是打开的,则将对应文件的打开次数增 1。
for (i=0; i<NR_OPEN;i++)
if (f=p->filp[i])
f->f_count++;
// 将当前进程(父进程)的 pwd, root 和 executable 引用次数均增 1
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
// 设置新任务的 TSS 和 LDT 描述符项(在 GDT 中),数据从 task 结构中取
// 在任务切换时,任务寄存器 tr 由 CPU 自动加载。局部描述符表寄存器 ldtr 已在 task0 时加载。
set_tss_desc(gdt+(nr<<1)+FIRST_TSS_ENTRY,&(p->tss));
set_ldt_desc(gdt+(nr<<1)+FIRST_LDT_ENTRY,&(p->ldt));
p->state = TASK_RUNNING; /* do this last, just in case */
return last_pid;
}
p->tss.eip = eip; //新的进程b的TSS里头的eip指向 syscall0中的 (if (__res >= 0) return (int) __res;)指令
…
p->tss.eax = 0; //新的进程b的TSS里头的eax赋值为0,当调度新进程运行时,新进程的syscall0中返回值__res = p->tss.eax = 0(即在新进程中fork返回的进程号为0)
int copy_mem(int nr,struct task_struct * p)
{
unsigned long old_data_base,new_data_base,data_limit;
unsigned long old_code_base,new_code_base,code_limit;
code_limit=get_limit(0x0f);// 取局部描述符表中代码段描述符项中段限长
data_limit=get_limit(0x17); // 取局部描述符表中数据段描述符项中段限长
old_code_base = get_base(current->ldt[1]); // 取原代码段基址。
old_data_base = get_base(current->ldt[2]); // 取原数据段基址
if (old_data_base != old_code_base) // 0.11 版不支持代码和数据段分立的情况。
panic("We don't support separate I&D");
if (data_limit < code_limit) // 如果数据段长度 < 代码段长度也不对。
panic("Bad data_limit");
new_data_base = new_code_base = nr * 0x4000000; // 新基址=任务号*64Mb(任务大小)。
p->start_code = new_code_base;
set_base(p->ldt[1],new_code_base); // 设置代码段描述符中基址域
set_base(p->ldt[2],new_data_base); // 设置数据段描述符中基址域
if (copy_page_tables(old_data_base,new_data_base,data_limit)) { // 复制代码和数据段
free_page_tables(new_data_base,data_limit); // 如果出错则释放申请的内存。
return -ENOMEM;
}
return 0;
}
容易忽略的地方:
movl _current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
这些代码的意思是:先比较当前current,即子进程的状态是否可以运行,如果当前进程不再就绪状态就去执行调度程序,如果该任务在就绪状态但时间片用完了,也就执行调度程序。所有后续的情况是,我们无法确定进程0或者进程1先执行,但是返回值已经明显确定了。
a. 对于进程0(父进程)而言,接下来会执行ret_from_sys_call后的指令,进行system_call系统调用的退出和信号处理,其中call _do_signal后面的popl %eax 表示把 子进程号 出栈存到eax中,返回到syscall0时传递给__res,表示进程0(父进程)的fork返回的子进程号。
b. 对于进程1(子进程)而言,在schedule函数中调度到子进程运行时,由前面copy_process函数可知,子进程会返回到syscall0中if(__res >= 0) return (int) __res;)指令处,__res为 0,即子进程的fork返回0 ,其过程如下:
eax返回值一直为0.
.align 2
reschedule:
pushl $ret_from_sys_call
jmp _schedule
先把_ret_from_sys_call的地址压入栈,再跳转到schedule函数执行,在schedule函数最后会调用switch_to宏:
#define switch_to(n) {\
struct {long a,b;} __tmp; \
__asm__("cmpl %%ecx,_current\n\t" \ // 比较当前任务current和要切换到的任务task[n]
"je 1f\n\t" \ // 如果要切换到的任务是当前任务,则跳到标号1,即结束,什么也不做,否则继续执行下面的代码
"movw %%dx,%1\n\t" \ // 把新任务的TSS选择符_TSS(n) 赋值给 __tmp.b的低16位
"xchgl %%ecx,_current\n\t" \ // 交换两个操作数的值,相当于C代码的:current = task[n] ,ecx = 被切换出去的任务(原任务);
"ljmp %0\n\t" \ // 长跳转到地址&__tmp.a中包含的48bit逻辑地址处:__tmp.a即为该逻辑地址的offset部分,
// __tmp.b的低16bit为seg_selector(高16bit无用)部分, 即切换到选择符_TSS(n)指定的的任务
"cmpl %%ecx,_last_task_used_math\n\t" \ // 返回原进程后开始执行指令的地方。
"jne 1f\n\t" \
"clts\n" \
"1:" \ // 返回_ret_from_sys_call处
::"m" (*&__tmp.a),"m" (*&__tmp.b), \
"d" (_TSS(n)),"c" ((long) task[n])); \
}
switch_to宏的核心是"ljmp %0\n\t"指令,它实现任务的切换。当它切换到子进程时,由于子进程的tss.eip指向syscall0中if(__res >= 0) return (int) __res;)指令处,且tss.eax = 0,所以子进程中fork会返回0值。