在上一篇文章中简单分析了fork、pause等系统调用的实现,怀着对fork在父子进程中返回不同值的好奇,本文中将深入分析fork的执行过程以及如何实现在父子进程中返回不一样的值(父进程---子进程ID,子进程----0)。
为了分析fork,可以从它定义处开始一步一步的分析它执行的过程以及堆栈内容的变化。下面从syscall0(int,fork)展开后的结果:
static inline int fork(void)
{
long __res;
__asm__ volatile ("int $0x80" \ //调用系统中断0x80
: "=a" (__res) \ //__res用来承载中断返回值
: "0" (__NR_fork)); \ //输入为系统中断调用号__NR_fork ( = 2)
if (__res >= 0) \
return (int) __res; \ //如果返回值>=0,则直接返回该值。
errno = -__res; \ //否则置出错号
return -1; \ //并返回-1
}
从上面第2行代码可知,fork执行过程的起点为“int $0x80” ,通过调用系统中断0x80从而跳转到_system_call中去执行。返回值__res从eax寄存器中得到,当__res >= 0时返回__res值,否则报错并返回-1 。在调用系统中断0x80时,CPU保存现场,自动把一些寄存器的值按顺序压入栈,此时栈内的内容如下图所示,把ss、esp、eflags、cs寄存器入栈,在调用system_call函数时把返回程序的入口地址也入栈。
接下来,程序跳转进入/kernel/system_call.S文件中system_call系统调用入口函数_system_call处执行。
.align 2
reschedule:
pushl $ret_from_sys_call
jmp _schedule
.align 2
_system_call:
cmpl $nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx # push %ebx,%ecx,%edx as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call _sys_call_table(,%eax,4)
pushl %eax //%eax刚好是_sys_fork:中call _copy_process 的返回值----last_pid,即 子进程号
movl _current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl _current,%eax # task[0] cannot have signals
cmpl _task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call _do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
在__system_call中,先把ds,es,fs,edx,ecx,ebx压入栈,再把call _sys_call_table(,%eax,4)指令的返回入口地址入栈,然后调转到sys_fork函数处执行,此时栈内的内容为:
.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
addl $20,%esp
1: ret
在sys_fork中,首先调用find_empty_process函数取得不重复的进程号,其返回值在eax寄存器中;然后把gs,esi,edi,ebp,eax寄存器的值压栈,然后,以前面压入栈内的ss到eax(nr)这些值为参数调用copy_process函数,此时栈内的内容为:
在copy_process函数中有如下两行代码对于fork生成的子进程很关键:
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)
执行完copy_process函数后,返回addl $20,%esp指令处,把栈顶指针esp上移20(栈内的5个项* 4 字节 = 20),刚好把gs,esi,edi,ebp,eax(nr)的空间忽略掉。然后使用ret指令返回eip(none)中的地址,即 __system_call函数中的pushl %eax指令处,把eax寄存器的值(为copy_process 返回的子进程号)入栈。此时栈内的内容变为:
此时,程序返回到_system_call中,接下来判断是否进行进程调度:
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 ,其过程如下:
.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值
————————————————
版权声明:本文为CSDN博主「u010132427」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010132427/article/details/52157430