您的位置:首页 > 其它

3.fork()到底干了啥?

2016-04-30 22:16 453 查看

导语

很多童鞋有分析阅读Linux源代码的强烈愿望,可是Linux内核代码量庞大,大部分人不知道如何下手,以下是我分析Linux源代码的一些经验,仅供参考,有不实之处请大神指正!

1.要想阅读内核首先要进入内核,其中用户态程序进入内核态的主要方式是int 0x80中断,搞懂这条指令的执行过程是我们学习内核的第一步;

2.Linux中最重要的结构体莫过于task_struct,没错,这就是大名鼎鼎的进程描述符(PCB,process control block),task_struct是Linux这个大轮子能转起来的关键,对task_struct的掌握程度基本上反应了你对内核的掌握程度,task_struct中包含了内存管理,IO管理,文件系统等操作系统的基本模块。task_struct位于linux-3.18.6/include/linux/sched.h中,约400行。

3.读万卷书不如行万里路,光是读内核代码是不够的,有精力的童鞋可以试着打断点看看内核中一个函数是怎么执行的,而Linux下的调试神器就是gdb,在Linux下开发过应用程序的童鞋肯定或多或少用过gdb,经常使用图形化IDE调试工具的童鞋初涉gdb可能会有些不适应。具体怎么用gdb调试Linux内核,网上这方面的教程不少,请自行Google;

4.开gdb调试时我认为有一个很重要的方法就是搞懂函数栈,Linux内核中函数不停的调用和跳转,很容易让你迷失其中,调试时清楚知晓函数调用堆栈这点很重要~

5.打蛇打七寸,擒贼先擒王,Linux代码中有不少错误处理之类的分支,调试时千万不要陷入其中,陷进去往往不能自拔。我们要抓住主要矛盾,忽略次要矛盾。错误处理一般是Linux hacker关注的重点,hacker期望从错误处理中找到漏洞以便对内核发起攻击,而我们作为Linux 内核的reader看看函数实现就足矣;

正题

用户态创建进程的fork()、vfork()和clone()系统调用在内核中最终都是调用的do_fork(),故我们分析一下do_fork()的代码。我使用的是linux-3.18.6版本的内核~

在linux-3.18.6\kernel目录下打开fork.c,找到do_fork()函数:

/*
*  Ok, this is the main fork-routine.
*
* It copies the process, and if successful kick-starts
* it and waits for it to finish using the VM if required.
*/
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0;
long nr;

/*
* Determine whether and which event to report to ptracer.  When
* called from kernel_thread or CLONE_UNTRACED is explicitly
* requested, no event is reported; otherwise, report if the event
* for the type of forking is enabled.
*/
if (!(clone_flags & CLONE_UNTRACED)) {
if (clone_flags & CLONE_VFORK)
trace = PTRACE_EVENT_VFORK;
else if ((clone_flags & CSIGNAL) != SIGCHLD)
trace = PTRACE_EVENT_CLONE;
else
trace = PTRACE_EVENT_FORK;

if (likely(!ptrace_event_enabled(current, trace)))
trace = 0;
}

p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
/*
* Do this prior waking up the new thread - the thread pointer
* might get invalid after that point, if the thread exits quickly.
*/
if (!IS_ERR(p)) {
struct completion vfork;
struct pid *pid;

trace_sched_process_fork(current, p);

pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);

if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);

if (clone_flags & CLONE_VFORK) {
p->vfork_done = &vfork;
init_completion(&vfork);
get_task_struct(p);
}

wake_up_new_task(p);

/* forking complete and child started to run, tell ptracer */
if (unlikely(trace))
ptrace_event_pid(trace, pid);

if (clone_flags & CLONE_VFORK) {
if (!wait_for_vfork_done(p, &vfork))
ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
}

put_pid(pid);
} else {
nr = PTR_ERR(p);
}
return nr;
}


35行的copy_process()函数就是创建进程内容的主要代码,copy_process()位于linux-3.18.6\kernel\fork.c文件中。于是我们进入copy_process(),探究里面的奥秘。

注意,跳入copy_process()之后函数栈样子:do_fork() -> copy_process()。



/*
* This creates a new process as a copy of the old one,
* but does not actually start it yet.
*
* It copies the registers, and all the appropriate
* parts of the process environment (as per the clone
* flags). The actual kick-off is left to the caller.
*/
static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;

…… // 省略的部分是出错处理,不理会
p = dup_task_struct(current); // 复制父进程的PCB
if (!p)
goto fork_out;

…… // 后面的先不管
fork_out:
return ERR_PTR(retval);
}


20行的dup_task_struct()是复制父进程的PCB,current 是父进程的PCB指针,p是新创建的指向子进程的PCB指针,我们再深入到dup_task_struct()中一探究竟,dup_task_struct()位于linux-3.18.6\kernel\fork.c文件中。

注意,跳入dup_task_struct()后函数栈的样子为:do_fork() -> copy_process() -> dup_task_struct()。函数栈不断加深~



</pre><p><pre name="code" class="cpp">static struct task_struct *dup_task_struct(structtask_struct *orig)
{
structtask_struct *tsk;
structthread_info *ti;
int node= tsk_fork_get_node(orig);
int err;

tsk =alloc_task_struct_node(node);
if(!tsk)
returnNULL;

ti = alloc_thread_info_node(tsk, node);
if(!ti)
gotofree_tsk;

err = arch_dup_task_struct(tsk, orig);
if (err)
gotofree_ti;
tsk->stack= ti;
……………
setup_thread_stack(tsk,orig);
clear_user_return_notifier(tsk);
clear_tsk_need_resched(tsk);
set_task_stack_end_magic(tsk);
……………
free_ti:
free_thread_info(ti);
free_tsk:
free_task_struct(tsk);
returnNULL;
}


12行的alloc_thread_info_node()用于分配新进程的内核态堆栈,我们进入到alloc_thread_info_node()里面,看看究竟做了什么,alloc_thread_info_node()位于linux-3.18.6\kernel\fork.c文件中。

注意,跳入alloc_thread_info_node()后函数栈为:do_fork() -> copy_process() -> dup_task_struct() –> alloc_thread_info_node()。



static struct thread_info *alloc_thread_info_node(structtask_struct
*tsk,  intnode)
{
structpage *page = alloc_kmem_pages_node(node, THREADINFO_GFP,
THREAD_SIZE_ORDER);

returnpage ? page_address(page) : NULL;
}


alloc_kmem_pages_node()分配了一定大小的内存页面,其中一部分用来存放thread info,另一部分用来作为内核态堆栈。

看完alloc_thread_info_node(),dup_task_struct()接下来的16行就是从父进程复制出新进程结构体所调用的函数啦,我们进到arch_dup_task_struct(tsk, orig)中看看,arch_dup_task_struct()位于linux-3.18.6\kernel\fork.c文件中。

注意,跳入arch_dup_task_struct()后函数栈为:do_fork() -> copy_process() -> dup_task_struct() –> arch_dup_task_struct()。



int __weak arch_dup_task_struct(struct task_struct*dst,
struct task_struct *src)
{
*dst =*src;
return 0;
}


这段代码对于学过C/C++的人来说再简单不过,src是一个数据结构的指针,* src解除指针引用,表示父进程的结构体的值,复制给*dst,也就是新进程的结构体。

alloc_thread_info_node()执行完毕后新进程的内核堆栈有了,arch_dup_task_struct()执行完毕后新进程的结构体有了,arch_dup_task_struct()中的tsk->stack = ti;就是使新进程的内核态堆栈与结构体关联起来!

回到dup_task_struct()函数,接着来到setup_thread_stack(tsk, orig);跳进去看看setup_thread_stack(tsk, orig);做了啥,setup_thread_stack()位于linux-3.18.6\include\linux\sched.h中。

注意,跳入setup_thread_stack()后函数栈的样子:do_fork() -> copy_process() -> dup_task_struct()–> setup_thread_stack()。



static inline void setup_thread_stack(structtask_struct *p, struct task_struct *org)
{
*task_thread_info(p)= *task_thread_info(org);
task_thread_info(p)->task= p;
}


org是父进程PCB的指针,p是新进程的PCB指针,看到了吧,新进程的内核堆栈的内容也是由父进程的内核堆栈直接拷过来的!(当然,后面会做一些修改,不会是完全一样的啦~)。

看完以上内容,我们知道了dup_task_struct()、alloc_thread_info_node()、arch_dup_task_struct()、setup_thread_stack()这几个函数就是创建新进程的堆栈、结构体,复制父进程的堆栈到子进程中等等的操作,这些操作使得父子进程模样完全一样,仅仅是内存位置不同,显然我们的工作还没有完成,子进程必须有自己独特的个性,让我们把函数栈弹出,回到copy_process()中。

注意,我们的函数栈的样子是:do_fork() -> copy_process()。



static struct task_struct *copy_process(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace)
{
int retval;
struct task_struct *p;

……………

p = dup_task_struct(current);
if (!p)
goto fork_out;

……………

retval = copy_fs(clone_flags, p);
if (retval)
goto bad_fork_cleanup_files;
…………
retval = copy_mm(clone_flags, p);
if (retval)
goto bad_fork_cleanup_signal;
…………
retval = copy_io(clone_flags, p);
if (retval)
goto bad_fork_cleanup_namespaces;
retval = copy_thread(clone_flags, stack_start, stack_size, p);
if (retval)
goto bad_fork_cleanup_io;

………………

bad_fork_cleanup_io:
if (p->io_context)
exit_io_context(p);
fork_out:
return ERR_PTR(retval);
}


dup_task_struct()之后基本上就是一系列的初始化,实在是太多,就不一一贴出来了,讲讲几个主要的。

1. retval= copy_fs(clone_flags, p);初始化文件系统,retval是return value的意思,也就是返回值,每一个初始化都要检查返回值,如果初始化失败就要处理错误

2. retval= copy_mm(clone_flags, p); mm:memory manage初始化内存管理

3. retval= copy_io(clone_flags, p); 初始化io

注意第30行的copy_thread()是我们理解的关键,跳入到copy_thread()中一探究竟, copy_thread()位于linux-3.18.6\arch\x86\kernel\ process.c文件中。

每次进入一个函数都要清楚的知道函数栈是什么样子,不然我们自己都会跳晕的,跳入copy_thread()后函数栈的样子是:do_fork() -> copy_process() -> copy_thread()。



int copy_thread(unsigned long clone_flags,unsigned long sp,
unsignedlong arg, struct task_struct *p)
{
structpt_regs *childregs = task_pt_regs(p);
structtask_struct *tsk;
int err;

p->thread.sp= (unsigned long) childregs;
p->thread.sp0= (unsigned long) (childregs+1);
memset(p->thread.ptrace_bps,0, sizeof(p->thread.ptrace_bps));

if(unlikely(p->flags & PF_KTHREAD)) {
/*kernel thread */
memset(childregs,0, sizeof(struct pt_regs));
p->thread.ip= (unsigned long) ret_from_kernel_thread;
task_user_gs(p)= __KERNEL_STACK_CANARY;
childregs->ds= __USER_DS;
childregs->es= __USER_DS;
childregs->fs= __KERNEL_PERCPU;
childregs->bx= sp;  /* function */
childregs->bp= arg;
childregs->orig_ax= -1;
childregs->cs= __KERNEL_CS | get_kernel_rpl();
childregs->flags= X86_EFLAGS_IF | X86_EFLAGS_FIXED;
p->thread.io_bitmap_ptr= NULL;
return0;
}
*childregs= *current_pt_regs();
childregs->ax= 0;
if (sp)
childregs->sp= sp;

p->thread.ip= (unsigned long) ret_from_fork;
task_user_gs(p)= get_user_gs(current_pt_regs());

p->thread.io_bitmap_ptr= NULL;
tsk =current;
err =-ENOMEM;

if(unlikely(test_tsk_thread_flag(tsk, TIF_IO_BITMAP))) {
p->thread.io_bitmap_ptr= kmemdup(tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES,GFP_KERNEL);
if(!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max= 0;
return-ENOMEM;
}
set_tsk_thread_flag(p,TIF_IO_BITMAP);
}

err = 0;

/*
* Set a new TLS for the child thread?
*/
if(clone_flags & CLONE_SETTLS)
err = do_set_thread_area(p,-1,
(structuser_desc __user *)childregs->si, 0);

if (err&& p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max= 0;
}
returnerr;
}


父进程执行fork系统调用,CPU往父进程的内核堆栈压入了很多寄存器值,子进程复制了父进程的内核堆栈,现在父子进程的内核堆栈一模一样。struct pt_regs *childregs =task_pt_regs(p);就是从子进程内核堆栈中找到系统调用时SAVE_ALL宏(传送门:第一讲)压入内核栈的内容(其实是父进程压入内核堆栈的,只不过子进程复制了父进程的内核堆栈,导致父子进程内核堆栈一模一样),并用childregs指针指向该内容。

看看pt_regs结构体的内容(在linux-3.18.6\arch\x86\include\asm\ptrace.h中):

struct pt_regs {
unsignedlong r15;
unsignedlong r14;
unsignedlong r13;
unsignedlong r12;
unsignedlong bp;
unsignedlong bx;
/* arguments: non interrupts/non tracing syscallsonly save up to here*/
unsignedlong r11;
unsignedlong r10;
unsignedlong r9;
unsignedlong r8;
unsignedlong ax;
unsignedlong cx;
unsignedlong dx;
unsignedlong si;
unsignedlong di;
unsignedlong orig_ax;
/* end of arguments */
/* cpu exception frame or undefined */
unsignedlong ip;
unsignedlong cs;
unsignedlong flags;
unsignedlong sp;
unsignedlong ss;
/* top of stack page */
};


可以看到,pt_regs结构体中保存的是系统调用压栈的内容。其中

unsignedlong ip;
unsignedlong cs;
unsignedlong flags;
unsignedlong sp;
unsignedlong ss;


是int 0x80硬件中断时CPU自动压入内核堆栈的内容。

拿到childregs指针后我们就要着手修改子进程的内核堆栈,使之与父进程不一样,拥有自己的个性。

p->thread.sp = (unsigned long) childregs; 确定子进程的栈顶指针。

*childregs = *current_pt_regs();*current_pt_regs();当前进程(别忘了我们还在父进程中执行)SAVE_ALL的内容拷贝到子进程,注意SAVE_ALL压入内核堆栈的内容并不是父进程内核堆栈的所有内容。

childregs->ax = 0;子进程的返回值是0!这是返回到用户态后区分父子进程的关键!

系统调用通过eax寄存器保存返回值,fork()系统调用结束后从内核态返回两次,一次是父进程返回,一次是子进程返回,区分父子进程的方法就是看返回值是否为0,若为0,说明返回的是新进程,不为0返回的是父进程。

if (sp)

childregs->sp= sp; 复制栈顶数据

p->thread.ip = (unsigned long) ret_from_fork; 子进程的ip为ret_from_fork,子进程调度执行时就是从这里开始执行的!

系统调用的总控程序包含了ret_from_fork的代码,我们到系统调用的总控程序里面去看看(在linux-3.18.6\arch\x86\kernel\entry_32.S文件中)。

ENTRY(ret_from_fork)
CFI_STARTPROC
pushl_cfi%eax
callschedule_tail
GET_THREAD_INFO(%ebp)
popl_cfi%eax
pushl_cfi$0x0202    # Reset kernel eflags
popfl_cfi
jmpsyscall_exit
CFI_ENDPROC
END(ret_from_fork)


可以发现ret_from_fork一顿执行后来到jmp syscall_exit这句话,然后跳到syscall_exit处执行,syscall_exit也在linux-3.18.6\arch\x86\kernel\entry_32.S文件中。

# systemcall handler stub
ENTRY(system_call)
RING0_INT_FRAME          # can't unwind into user space anyway
ASM_CLAC
pushl_cfi%eax           # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
#system call tracing in operation / emulation
testl$_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
jnzsyscall_trace_entry
cmpl$(NR_syscalls), %eax
jaesyscall_badsys
syscall_call:
call*sys_call_table(,%eax,4)
syscall_after_call:
movl%eax,PT_EAX(%esp)      # store the returnvalue
<span style="color:#ff0000;">syscall_exit:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
#setting need_resched or sigpending
#between sampling and the iret</span>

………………………

ENDPROC(system_call)


子进程调度执行时先来到系统调用总控程序的ret_from_fork处执行,在ret_from_fork末尾跳到系统调用总控程序的syscall_exit处,此时子进程的内核堆栈状态和syscall_call这条指令之前的堆栈状态相同,于是可以RESOTR_ALL、iret,正常的返回到用户态执行,返回用户态之后就不是父进程的进程空间了,变成了子进程的进程空间。

fork与一般系统调用不同之处在于,fork进入内核态执行完毕之后两次返回,第一次返回到父进程的位置继续向下执行;第二次返回到子进程中特定的点(ret_from_fork,第二讲)继续执行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: