深入分析fork的执行过程(Linux-0.11内核)

        在上一篇文章中简单分析了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值。

  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值