分析Linux内核创建一个新进程的过程

杨明辉+ 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

一、实验过程

1. 打开终端输入cd Linux进入Linux目录下,然后输入rm menu -rf命令删除menu,然后输入命令git clone https://github.com/mengning/menu.git克隆一个新的menu;然后输入命令mv test_fork.c test.c覆盖test.c的内容;最后输入sudo make rootfs进行编译,我们可以看到MENUOS中多了fork命令;实验结果如图1,图2所示。

图1

图2
2. 与前几次实验一样打开gdb,输入命令file linux-3.18.6/vmlinux加载符号表,然后输入target remote:1234来连接MENUOS,并在sys_clone、do_fork、dup_task_struct、copy_process、copy_thread、ret_from_fork处设置断点。实验结果如图3所示。

图3
3. 然后利用gdb中的c、n、s和finish等命令进行跟踪调试,实验结果如图4、图5、图6所示。

图4
              继续:

图5
               继续:

图6

二、创建新进程过程分析

1.进程的创建
       在用户态调用fork()函数来创建一个子进程, fork系统调用在父进程和子进程都会返回一次,在父进程的返回值是子进程中的进程id号,在子进程的返回值是0,在shell命令行下执行函数调用后,函数中的if和else子句中的程序都得到了执行,但是if和else子句的执行分别是在不同的进程中执行的,一个在父进程、一个在子进程中执行;其中子进程复制了父进程所有的信息。子进程从fork()函数后开始执行,与父进程的程序逻辑相同。

      fork、vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_fork来实现进程的创建。创建新进程是通过复制当前进程的PCB(task_struct)来实现的,然后进行相应的初始化。

2. task_struct的关键数据结构


struct task_struct {
	volatile long state;	/* -1 不可执行, 0 可执行, >0 停止 */
	void *stack;
	atomic_t usage;
	unsigned int flags;	/* 每一个进程的标识符,是唯一的 */
	unsigned int ptrace;
#ifdef CONFIG_SMP
	struct llist_node wake_entry;
	int on_cpu;
	struct task_struct *last_wakee;
	unsigned long wakee_flips;
	unsigned long wakee_flip_decay_ts;
	int prio, static_prio, normal_prio;
	unsigned int rt_priority;
	const struct sched_class *sched_class;
	struct sched_entity se;
	struct sched_rt_entity rt;
#ifdef CONFIG_TREE_PREEMPT_RCU
	struct rcu_node *rcu_blocked_node;
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */
#ifdef CONFIG_TASKS_RCU
	unsigned long rcu_tasks_nvcsw;
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
	struct sched_info sched_info;
	struct list_head tasks;
	struct plist_node pushable_tasks;
	struct rb_node pushable_dl_tasks;
#endif
	struct mm_struct *mm, *active_mm;
#ifdef CONFIG_COMPAT_BRK
unsigned brk_randomized:1;
#endif
	u32 vmacache_seqnum;
	struct vm_area_struct *vmacache[VMACACHE_SIZE];
#if defined(SPLIT_RSS_COUNTING)
	struct task_rss_stat	rss_stat;
};

3.进程执行的状态及状态转化过程如图7所示。


图7

4. 创建进程的大致流程。

1. fork函数通过ox80中断(系统调用)来陷入内核,然后进入系统提供的相应系统调用来完成进程的创建过程。

2. fork、vfork、和clone三个系统调用都可以创建一个新的进程,而且都是通过do_fork来实现进程的创建。

3. 在do_fork中首先调用copy_process为子进程复制一份进程信息。

4. 调用dup_task_struct复制当前的task_struct。

5. 检查进程数是否超过限制。

6. 初始化自旋锁、挂起信号、CPU定时器等。

7. 调用sched_fork初始化进程数据结构,并把进程状态设置为TASK_RUNNING.

8. 复制所以进程信息,包括文件系统、信号处理函数、信号、内存管理等。

9. 调用copy_thread初始化进程内核栈。

10. 为新进程分配并设置新的pid

5. do_fork关键代码及分析。



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;
	
		//……
		
		//复制进程描述符,copy_process()的返回值是一个 task_struct 指针。
		p = copy_process(clone_flags, stack_start, stack_size,
			 child_tidptr, NULL, trace);

		if (!IS_ERR(p)) {
			struct completion vfork;
			struct pid *pid;

			trace_sched_process_fork(current, p);
			
			//得到新创建的进程描述符中的pid
			pid = get_task_pid(p, PIDTYPE_PID);
			nr = pid_vnr(pid);

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

			//如果调用的 vfork()方法,初始化 vfork 完成处理信息。
			if (clone_flags & CLONE_VFORK) {
				p->vfork_done = &vfork;
				init_completion(&vfork);
				get_task_struct(p);
			}
			
			//将子进程加入到调度器中,为其分配 CPU,准备执行
			wake_up_new_task(p);

			//fork 完成,子进程即将开始运行
			if (unlikely(trace))
				ptrace_event_pid(trace, pid);
			
			//如果是 vfork,将父进程加入至等待队列,等待子进程完成
			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;
}

6.dup_task_struct的源码及分析

static struct task_struct *dup_task_struct(struct task_struct *orig)
{
	struct task_struct *tsk;
	struct thread_info *ti;
	int node = tsk_fork_get_node(orig);
	int err;

	//分配一个 task_struct 节点
	tsk = alloc_task_struct_node(node);
	if (!tsk)
		return NULL;

	//分配一个 thread_info 节点,包含进程的内核栈,ti 为栈底
	ti = alloc_thread_info_node(tsk, node);
	if (!ti)
		goto free_tsk;

	//将栈底的值赋给新节点的栈
	tsk->stack = ti;

	//……

	return tsk;

}



7. 堆栈状态执行流程图如图8、图9所示。


图8


图9

三、总结

1. 新进程的执行源于以下原因:
    1.  dup_task_struct中为其分配了新的堆栈。
    2. 调用了sched_fork,将其置为TASK_RUNNING。
    3. copy_thread中将父进程的寄存器上下文复制给子进程,保证了父子进程的堆栈信息是一致的。
    4. 将ret_from_fork的地址设置为eip寄存器的值。
2.  最终子进程从ret_from_fork开始执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值