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


(下篇完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 此文仅用于MOOCLinux内核分析作业张依依+原创作品转载请注明出处+《Linux内核分析》MOOC课程http...
    uglyyouth阅读 2,345评论 0 4
  • 进程是怎么描述的?这是一个提纲挈领性的东西,它可以把内存管理,文件系统,信号,进程间通信等等全都串联起来 进程的描...
    那只大象阅读 3,620评论 0 6
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,191评论 0 23
  • 进程的描述 进程控制块PCB -- task_struct 操作系统的三大核心功能:1、进程管理2、内存管理3、文...
    _Iridescent阅读 2,131评论 0 1
  • 我相信爱是 萤火般的光芒 在黑夜里 迷路的人 才能找到方向 握住希望 把星星都装进行囊 就不会害怕 孤单流浪 我相...
    西域狂龙阅读 285评论 0 0