浏览创建进程的相关关键代码
看一下do_fork /linux-3.18.6/kernel/fork.c#do_fork
1651 p = copy_process(clone_flags, stack_start, stack_size, // 创建进程的主要代码 1652 child_tidptr, NULL, trace);
看一下copye_process /linux-3.18.6/kernel/fork.c#copy_process
1240 p = dup_task_struct(current); // 复制PCB
看一下dup_task_struct /linux-3.18.6/kernel/fork.c#dup_task_struct
320 err = arch_dup_task_struct(tsk, orig); // 执行复制,orig 当前进程
316 ti = alloc_thread_info_node(tsk, node); // 实际就是alloc一个内核堆栈
324 tsk->stack = ti; // 把alloc后返回的地址赋给stack
看一下arch_dup_task_struct /linux-3.18.6/kernel/fork.c#arch_dup_task_struct
290int __weak arch_dup_task_struct(struct task_struct *dst, 291 struct task_struct *src) 292{ 293 *dst = *src; // 就是把数据结构加*,原来它是数据结构的指针,加*,表示它的值 294 return 0; 295}
看一下alloc_thread_info_node /linux-3.18.6/kernel/fork.c#alloc_thread_info_node
150static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, 151 int node) 152{ 153 struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP, 154 THREAD_SIZE_ORDER); 155 156 return page ? page_address(page) : NULL; 157}
做了实际分配内核堆栈空间的效果,实际的代码是alloc_kmem_pages_node,创建了一定大小的页面,页面有一部分用来存放thread_info,另一部分从高地址向低地址就是内核堆栈
回到dup_task_struct,现在已经把父进程的PCB,也就是task_struct数据结构复制过来了,也就是由p所指向的子进程的PCB(进程描述符)
1240 p = dup_task_struct(current); // 复制PCB
往后的代码,有大量地修改子进程内容的代码,做初始化,这些都可以抽象掉
1375 retval = copy_files(clone_flags, p); 1378 retval = copy_fs(clone_flags, p); // 初始化文件系统 1381 retval = copy_sighand(clone_flags, p); 1384 retval = copy_signal(clone_flags, p); // 初始化信号 1387 retval = copy_mm(clone_flags, p); // 初始化内存 1390 retval = copy_namespaces(clone_flags, p); 1393 retval = copy_io(clone_flags, p); // 初始化IO
1396 retval = copy_thread(clone_flags, stack_start, stack_size, p); // 关键的内容
看一下copy_thread /linux-3.18.6/arch/x86/kernel/process_32.c#132
135 struct pt_regs *childregs = task_pt_regs(p);
从这里可以看到,从子进程的pid,也就是内核堆栈的位置,找到了栈空间,SAVE_ALL的一些内容,SAVE_ALL的地址
139 p->thread.sp = (unsigned long) childregs; // 调度到子进程时的内核栈底
把栈底赋上
拷贝内核堆栈数据和指定新进程的第一条指令地址
159 *childregs = *current_pt_regs(); // 复制内核堆栈
当前进程,也就是父进程,因为我们这个执行过程还在父进程的执行上下文当中。父进程的内核堆栈的栈底,也就是SAVE_ALL的内容,把它拷贝过来,这个地方实际就是做内核堆栈里已有数据的拷贝
值得注意的是:在复制内核堆栈的时候,只复制了与SAVE_ALL相关的那一部分,只复制了struct pt_regs
看一下struct pt_regs数据结构的内容 /linux-3.18.6/arch/x86/include/asm/ptrace.h
9#ifdef __i386__ 10 11struct pt_regs {
// SAVE_ALL压到内核堆栈里的内容
12 unsigned long bx; 13 unsigned long cx; 14 unsigned long dx; 15 unsigned long si; 16 unsigned long di; 17 unsigned long bp; 18 unsigned long ax; // 传递的系统调用号 19 unsigned long ds; 20 unsigned long es; 21 unsigned long fs; 22 unsigned long gs; 23 unsigned long orig_ax; // 原来的eax
// 执行int 0x80指令的时候,CPU自动压到内核堆栈里面的内容
24 unsigned long ip; 25 unsigned long cs; 26 unsigned long flags; 27 unsigned long sp; 28 unsigned long ss; 29}; 30 31#else /* __i386__ */
在复制内核堆栈的时候,i386只复制了内核堆栈最栈底的那一部分内容,也就是系统调用压栈的过程,int 0x80指令(CPU自动)和SAVE_ALL压到内核堆栈里的内容
160 childregs->ax = 0; // 为什么子进程的fork返回0,这里就是原因!
返回值存放在eax,pid=0就是在这赋值的。因为子进程的返回值是0,所以拷贝完还需要修改一下内核堆栈里压入的返回值
161 if (sp) 162 childregs->sp = sp; // sp是传递给copy_thread的第二个参数stack_start
包括栈底的数据
164 p->thread.ip = (unsigned long) ret_from_fork; // 调度到子进程时的第一条指令地址
赋值thread.ip的内容为ret_from_fork,子进程得到进程调度,得到CPU的时候,是从这个位置开始执行的
看一下entry_32.S /linux-3.18.6/arch/x86/kernel/entry_32.S
系统调用总控程序,找到ret_from_fork
290ENTRY(ret_from_fork) 291 CFI_STARTPROC 292 pushl_cfi %eax 293 call schedule_tail 294 GET_THREAD_INFO(%ebp) 295 popl_cfi %eax 296 pushl_cfi $0x0202 # Reset kernel eflags 297 popfl_cfi 298 jmp syscall_exit // 在这里会跳转到syscall_exit 299 CFI_ENDPROC 300END(ret_from_fork)
syscall_exit 在哪个地方呢?
490ENTRY(system_call) 493 pushl_cfi %eax # save orig_eax 494 SAVE_ALL // 这里进行SAVE_ALL 501syscall_call: // 这里进行system_call 502 call *sys_call_table(,%eax,4)
503syscall_after_call: // 这里call返回了,返回到内核堆栈 // 也就是内核堆栈怎么压栈,它就又怎么出来了
504 movl %eax,PT_EAX(%esp) # store the return value 505syscall_exit:
call返回,返回到内核堆栈,也就是内核堆栈怎么压栈,它就又怎么出来了,所以到syscall_exit的时候,实际上和sys_call之前它的堆栈状态是一样的
所以ret_from_fork跳到syscall_exit来,就可以继续往下执行,就可以正常地返回到用户态
也就是说当子进程获得CPU控制权,开始运行的时候,它的ret_from_fork可以把后面的堆栈出栈出栈,从iret返回到用户态,这时候返回到用户态,就不是原来父进程的进程空间了,而是子进程的进程空间了
(下篇完)