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,第二讲)继续执行。
相关文章推荐
- php下使用curl进行多种数据编码方式的POST请求
- 快速幂
- [数据结构]Priority_queue(优先级队列)
- 初识spring mvc + mybatis
- 多线程中调用run()方法和start()方法的简单区别
- HDU 5676 ztr loves lucky numbers(dfs+离线)——BestCoder Round #82(div.1 div.2)
- [数据结构]Radix_sort(MSD)
- 关于i2c_register_board_info()函数
- nodejs包高效升级插件npm-check-updates
- MOOC的Python笔记(三)基本算术、逻辑操作符
- couchbase的简单介绍
- Oracle简单易用的表结构导出方法
- [POJ 2886] Who Gets the Most Candies? (Joseph环问题 + 树状数组)
- Codeforces 665A - Buses Between Cities
- HTML5培训第10节课堂笔记(盒子模型、行内与块级、float、定位、html5布局)
- Linq 语法举例
- JAVA为什么要配置环境变量,怎样配置
- 《Nodejs开发加密货币》之三:Nodejs让您的前端开发像子弹飞一样
- VS---“重新生成解决方案”和"生成解决方案"的学习
- 设计模式学习笔记——解释器模式