Linux系统创建一个新进程(下)

浏览创建进程的相关关键代码

看一下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返回到用户态,这时候返回到用户态,就不是原来父进程的进程空间了,而是子进程的进程空间了


(下篇完)

你可能感兴趣的:(Linux系统创建一个新进程(下))