2021-2022-1 20212822《Linux内核原理与分析》第七周作业

  • 实验六:分析 Linux 内核创建一个新进程的过程

  • 实验要求

    • 阅读理解 task_struct 数据结构 https://github.com/torvalds/linux/blob/v3.18-rc6/include/linux/sched.h#L1235;
    • 分析 fork 函数对应的内核处理过程 sys_clone,理解创建一个新进程如何创建和修改 task_struct 数据结构;
    • 使用 gdb 跟踪分析一个 fork 系统调用内核处理函数 sys_clone ,验证您对 Linux 系统创建一个新进程的理解,推荐在实验楼 Linux 虚拟机环境下完成实验。 特别关注新进程是从哪里开始执行的?为什么从那里能顺利执行下去?即执行起点与内核堆栈如何保证一致。
  • 实验过程

    • 首先清空原来的menu文件夹,克隆一个新的menu

    • 在这里插入图片描述

    • 然后将test_fork.c文件改为test.c用以测试,再make一下,保证更新过

    • 在这里插入图片描述

    • 正常运行,输入help命令,可以使用fork

    • 在这里插入图片描述

    • 尝试调用一下fork

    • 在这里插入图片描述

    • 接下来把刚才的全关掉,再打开一个新的命令行,输入-S -s让MenuOs停下方便调试

    • 在这里插入图片描述

    • 再开一个终端,进入gdb,执行例行操作,然后设置六个断点

    • (gdb)b sys_clone

    • (gdb)b do_fork

    • (gdb)b dup_task_struct

    • (gdb)b copy_process

    • (gdb)b copy_thread

    • (gdb)b ret_from_fork

    • 在这里插入图片描述

    • 然后用c一个运行调试,这里只放出一个截图

    • 在这里插入图片描述

  • 实验分析

    • 创建进程的大致流程
      • 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

    • 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; //进程描述符结构体指针
	int trace = 0;
	long nr; //总的pid数量
 
	/*
	 * 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;
	}
 
	// 复制进程描述符,返回创建的task_struct的指针
	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);
 
		// 取出task结构体内的pid
		pid = get_task_pid(p, PIDTYPE_PID);
		nr = pid_vnr(pid);
 
		if (clone_flags & CLONE_PARENT_SETTID)
			put_user(nr, parent_tidptr);
 
		// 如果使用的是vfork,那么必须采用某种完成机制,确保父进程后运行
		if (clone_flags & CLONE_VFORK) {
			p->vfork_done = &vfork;
			init_completion(&vfork);
			get_task_struct(p);
		}
 
		// 将子进程添加到调度器的队列,使得子进程有机会获得CPU
		wake_up_new_task(p);
 
		/* forking complete and child started to run, tell ptracer */
		if (unlikely(trace))
			ptrace_event_pid(trace, pid);
 
		// 如果设置了 CLONE_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;
}
  • 然后再看看dup_task_struct
    • 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结点,其实内部分配了一个union,包含进程的内核栈
	// 此时ti的值为栈底,在x86下为union的高地址处。
	ti = alloc_thread_info_node(tsk, node);
	if (!ti)
		goto free_tsk;

	err = arch_dup_task_struct(tsk, orig);
	if (err)
		goto free_ti;

	// 将栈底的值赋给新结点的stack
	tsk->stack = ti; 
    ...
	/*
	 * One for us, one for whoever does the "release_task()" (usually
	 * parent)
	 */
	// 将进程描述符的使用计数器置为2
	atomic_set(&tsk->usage, 2);
#ifdef CONFIG_BLK_DEV_IO_TRACE
	tsk->btrace_seq = 0;
#endif
	tsk->splice_pipe = NULL;
	tsk->task_frag.page = NULL;

	account_kernel_stack(ti, 1);

	// 返回新申请的结点
	return tsk;

free_ti:
	free_thread_info(ti);
free_tsk:
	free_task_struct(tsk);
	return NULL;
}
  • 再看看copy_thread
    • copy_thread代码分析
// 初始化子进程的内核栈
int copy_thread(unsigned long clone_flags, unsigned long sp,
	unsigned long arg, struct task_struct *p)
{

	// 取出子进程的寄存器信息
	struct pt_regs *childregs = task_pt_regs(p);
	struct task_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;
		return 0;
	}

	// 将当前进程的寄存器信息复制给子进程
	*childregs = *current_pt_regs();
	// 子进程的eax置为0,所以fork的子进程返回值为0
	childregs->ax = 0;
	if (sp)
		childregs->sp = sp;

	// 子进程从ret_from_fork开始执行
	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;

	// 如果父进程使用IO权限位图,那么子进程获得该位图的一个拷贝
	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);
	}
	...
	return err;
}
  • 总结

    • do_fork() 函数
      • 在 Linux 内核中,供用户创建进程的系统调用fork()函数的响应函数是 sys_fork()、sys_clone()、sys_vfork()。这三个函数都是通过调用内核函数 do_fork() 来实现的。根据调用时所使用的 clone_flags 参数不同,do_fork() 函数完成的工作也各异。
      • 1、建立进程控制结构并赋初值,使其成为进程映像。
      • 2、必须为新进程的执行设置跟踪进程执行情况的相关内核数据结构。包括 任务数组、自由时间列表 tarray_freelist 以及 pidhash[] 数组。
      • 3、启动调度程序,使子进程获得运行的机会
    • 新进程是从哪里开始执行的?
      • 从ret_from_fork处开始执行。
      • dup_task_struct中为其分配了新的堆栈,copy_process中调用了sched_fork,将其置为TASK_RUNNING,copy_thread中将父进程的寄存器上下文复制给子进程,这是非常关键的一步,这里保证了父子进程的堆栈信息是一致的。将ret_from_fork的地址设置为eip寄存器的值,这是子进程的第一条指令。
  • 实验中遇到的问题及解决方案

    • 一开始menu那里不能正常make,检查一下是发现自己的路径和实验楼上的有区别,解决方法是进入menu中的makefile文件中修改路径,修改成自己的路径即可。
    • 用gdb调试时,vmlinux有时会显示(No debugging symbols),这个问题我还没有能解决…
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值