进程源码分析-以fork函数为例分析
int main(int argc, char **argv)
{
A();
B();
return 0;
}
void A() {fork();}
前提知识点:
1、每次在执行一个函数内部功能前会把函数的下一条指令压栈
2、每个栈都与每一层的函数接口相对应
3、分析汇编代码,要时刻关注栈的内容变化
在执行到fork函数时
- 1、此时的用户栈的分布情况:
- exit -> B
解释:1、当进入到main函数内部时,会立马把main函数的下一条指令也就是exit压入到栈中;2、接着进入到A函数内部时,会立马把A函数的下一条指令也就是B压入到栈中。
在执行fork函数内部功能时
// linux-0.11/init/main.c
static inline _syscall0(int,fork);
// linux-0.11/include/unistd.h
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
1、从上面代码可以看出执行fork函数的顺序是先压栈fork后的一条指令,由于在A中fork后没有指令,所以不再压栈,继续执行fork函数内部代码。首先将寄存器eax填入__NR_fork表示的值。
2、接着执行中断指令int 0x80。在执行中断时,硬件立马将ss、sp、eflags、cs、ip依次入栈,此时cs、ip对应的地址存放的指令就是”mov__res
, %eax“。此时进入到中断内部了,所以会有另一个栈(内核栈)来存放内容,与用户栈区分开。此时的当前线程的内核栈分布情况如下:ss(2) -> sp(2) -> eflags(4) -> cs(2) -> ip(2),其中ss、sp为用户栈的栈段地址和栈偏移地址,efalgs为用户线程的寄存器状态。
3、判断__res的值按条件返回。
执行int 0x80的中断调用
- 进入中断时,由于系统初始化时下面set_system_gate的作用,将0x80的中断映射到system_call函数中执行。
// linux-0.11/kernel/sched.c
void sched_init(void)
{
...
set_system_gate(0x80,&system_call);
...
}
- system_call的前部分代码如下所示
// linux-0.11/kernel/system_call.s
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
...
- 执行完上面的代码后,此时的栈内容为:
- ss(2) -> sp(2) -> eflags(4) -> cs(2) -> ip(2) -> ds(2) -> es(2) -> fs(2) -> edx(4) -> ecx(4) -> ebx(4)
- 将ds设置为0x10,进入内核空间。
- system_call的后部分代码如下所示
// linux-0.11/kernel/system_call.s
system_call:
...
call sys_call_table(,%eax,4)
pushl %eax
...
1、
__NR_fork
的值就是此时eax寄存器中存放的值。
2、 执行系统调用表system_call_table时会找到sys_fork函数。
3、 在执行时call sys_call_table时,会进行压栈处理,此时的栈内容为: ss(2) -> sp(2) -> eflags(4) -> cs(2) -> ip(2) -> ds(2) -> es(2) -> fs(2) -> edx(4) -> ecx(4) -> ebx(4) -> ip(2)[pushl %eax]. 此处压入的ip地址对应的指令为“pushl %eax”
此时进入sys_fork函数
// linux-0.11/kernel/system_call.s
sys_fork:
call find_empty_process // the returned value is saved in eax.
testl %eax,%eax
js 1f
push %gs
pushl %esi
pushl %edi
pushl %ebp
pushl %eax
call copy_process
addl $20,%esp
1: ret
在执行到call copy_process时,系统会进行再次压栈处理,此时的栈内容为:ss(2) -> sp(2) -> eflags(4) -> cs(2) -> ip(2) -> ds(2) -> es(2) -> fs(2) -> edx(4) -> ecx(4) -> ebx(4) -> ip(2)[pushl %eax] -> ip(2)[addl $20, %esp]-> gs(2) -> esi(4) -> edi(4) -> ebp(4) -> eax(4)
下面会进入copy_process程序
// linux-0.11/kerrnel/fork.c
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; // the nr is the empty process id
*p = *current; /* NOTE! this doesn't copy the supervisor stack */
p->state = TASK_UNINTERRUPTIBLE;
p->pid = last_pid;
p->father = current->pid;
p->counter = p->priority;
p->signal = 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;
p->tss.back_link = 0;
p->tss.esp0 = PAGE_SIZE + (long) p;
p->tss.ss0 = 0x10;
p->tss.eip = eip; // set eip as the same with the old process for the new process
p->tss.eflags = eflags;
p->tss.eax = 0; // set eax as 0 for the new process
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;
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);
p->tss.trace_bitmap = 0x80000000;
if (last_task_used_math == current)
__asm__("clts ; fnsave %0"::"m" (p->tss.i387));
if (copy_mem(nr,p)) {
task[nr] = NULL;
free_page((long) p);
return -EAGAIN;
}
for (i=0; i<NR_OPEN;i++)
if ((f=p->filp[i]))
f->f_count++;
if (current->pwd)
current->pwd->i_count++;
if (current->root)
current->root->i_count++;
if (current->executable)
current->executable->i_count++;
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;
}
代码执行到此时已经创建一个新的进程,准备返回ret,出栈执行“addl $20, %esp”指令
执行完“addl $20, %esp”指令
1、此条指令的意思是将esp的指针向上偏移20个字节
2、这样栈中的内容就变为:
ss(2) -> sp(2) -> eflags(4) -> cs(2) -> ip(2) -> ds(2) -> es(2) -> fs(2) -> edx(4) -> ecx(4) -> ebx(4) -> ip(2)[pushl %eax]
3、然后执行ret出栈指令,执行pushl %eax,此时的eax里面存放的要么对应新进程0,要么对应旧进程非0。
此时的代码又回到system_call中
// linux-0.11/kernel/system_call.s
system_call:
...
movl current,%eax # 将当前的esp保存在eax中
cmpl $0,state(%eax) # 比较当前进程运行状态
jne reschedule # 不满足条件则进入进程调度,或许可以切换到新的进程中,一般情况下,父进程在这里堵塞,等待子进程返回后父进程再返回
cmpl $0,counter(%eax) # counter
je reschedule
1、如果返回0则是子进程,返回大于0则是父进程
2、返回值是通过eax传递的
3、进程如何调度的,请看下回分析