结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

fork系统调用

fork系统调用主要是通过_do_fork来完成的。
_do_fork主要代码片段如下

long _do_fork(struct kernel_clone_args *args)
{
	u64 clone_flags = args->flags;
	struct completion vfork;
	struct pid *pid;
	struct task_struct *p;
	int trace = 0;
	long nr;

	if (!(clone_flags & CLONE_UNTRACED)) {
		if (clone_flags & CLONE_VFORK)
			trace = PTRACE_EVENT_VFORK;
		else if (args->exit_signal != SIGCHLD)
			trace = PTRACE_EVENT_CLONE;
		else
			trace = PTRACE_EVENT_FORK;

		if (likely(!ptrace_event_enabled(current, trace)))
			trace = 0;
	}
      // 复制进程描述符
	p = copy_process(NULL, trace, NUMA_NO_NODE, args);
	add_latent_entropy();

	if (IS_ERR(p))
		return PTR_ERR(p);

	trace_sched_process_fork(current, p);

	pid = get_task_pid(p, PIDTYPE_PID);
	nr = pid_vnr(pid);

	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, args->parent_tid);

	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}
      // 将子进程插入到就绪队列
	wake_up_new_task(p);

	if (unlikely(trace))
		ptrace_event_pid(trace, pid);

	if (clone_flags & CLONE_VFORK) {
		if (!wait_for_vfork_done(p, &vfork))
			ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
	}

	put_pid(pid);
	return nr;
}

其中_do_fork完成的最主要的功能是利用函数copy_process()来创建进程描述符以及子进程所需要的所有其他内核数据结构。
然后再利用wake_up_new_task()函数,调整子进程和父进程的参数,然后检查子进程与父进程,如果它们在同一CPU上且不共享同一组页表,就把子进程插入到父进程所在的运行队列。并且恰好插在父进程的前面。如果它们不再同一CPU上或不共享同一组页表,就将子进程插在父进程所在运行队列的队尾。
当_do_fork()结束之后,调度程序会将子进程内核态堆栈的地址装入esp寄存器,把ret_from_fork()的地址装入eip寄存器。然后当fork系统调用执行结束时,新进程将开始执行,系统调用的返回值存放在eax中,其中子进程的返回值为0,父进程的返回值为子进程的pid.

execeve系统调用

execve系统调⽤对应的内核处理函数为__x64_sys_execve().该函数最终通过__do_execve_file()来具体执⾏加载可执⾏⽂件的⼯作。
整体的调用关系为如下
__x64_sys_execve - > do_execve() - > do_execveat_common() - > __do_execve_file - > exec_binprm() - > search_binary_handler() - > load_elf_binary() - > start_thread()

static int __do_execve_file(int fd, struct filename *filename,
			    struct user_arg_ptr argv,
			    struct user_arg_ptr envp,
			    int flags, struct file *file)
{
	char *pathbuf = NULL;
	struct linux_binprm *bprm;
	struct files_struct *displaced;
	int retval;

	if (IS_ERR(filename))
		return PTR_ERR(filename);

	if ((current->flags & PF_NPROC_EXCEEDED) &&
	    atomic_read(¤t_user()->processes) > rlimit(RLIMIT_NPROC)) {
		retval = -EAGAIN;
		goto out_ret;
	}

	current->flags &= ~PF_NPROC_EXCEEDED;

	retval = unshare_files(&displaced);
	if (retval)
		goto out_ret;

	retval = -ENOMEM;
	bprm = kzalloc(sizeof(*bprm), GFP_KERNEL);
	if (!bprm)
		goto out_files;

	retval = prepare_bprm_creds(bprm);
	if (retval)
		goto out_free;

	check_unsafe_exec(bprm);
	current->in_execve = 1;

	if (!file)
		file = do_open_execat(fd, filename, flags);
	retval = PTR_ERR(file);
	if (IS_ERR(file))
		goto out_unmark;

	sched_exec();

	bprm->file = file;
	if (!filename) {
		bprm->filename = "none";
	} else if (fd == AT_FDCWD || filename->name[0] == '/') {
		bprm->filename = filename->name;
	} else {
		if (filename->name[0] == '\0')
			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d", fd);
		else
			pathbuf = kasprintf(GFP_KERNEL, "/dev/fd/%d/%s",
					    fd, filename->name);
		if (!pathbuf) {
			retval = -ENOMEM;
			goto out_unmark;
		}

		if (close_on_exec(fd, rcu_dereference_raw(current->files->fdt)))
			bprm->interp_flags |= BINPRM_FLAGS_PATH_INACCESSIBLE;
		bprm->filename = pathbuf;
	}
	bprm->interp = bprm->filename;

	retval = bprm_mm_init(bprm);
	if (retval)
		goto out_unmark;

	retval = prepare_arg_pages(bprm, argv, envp);
	if (retval < 0)
		goto out;

	retval = prepare_binprm(bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings_kernel(1, &bprm->filename, bprm);
	if (retval < 0)
		goto out;

	bprm->exec = bprm->p;
	retval = copy_strings(bprm->envc, envp, bprm);
	if (retval < 0)
		goto out;

	retval = copy_strings(bprm->argc, argv, bprm);
	if (retval < 0)
		goto out;

	would_dump(bprm, bprm->file);

	retval = exec_binprm(bprm);
	if (retval < 0)
		goto out;

	current->fs->in_exec = 0;
	current->in_execve = 0;
	rseq_execve(current);
	acct_update_integrals(current);
	task_numa_free(current, false);
	free_bprm(bprm);
	kfree(pathbuf);
	if (filename)
		putname(filename);
	if (displaced)
		put_files_struct(displaced);
	return retval;

out_free:
	free_bprm(bprm);
	kfree(pathbuf);

out_files:
	if (displaced)
		reset_files_struct(displaced);
}

在 __do_execve_file()函数中,主要是创建了一个bprm的数据结构用来表示可执行文件,这里会根据可执行文件对bprm做一些填充,然后再调用search_binary_handler()函数来扫描能够处理相应可执行文件格式的处理器,调用相对应的load_binary()函数
在load_binary()函数中主要完成以下工作

  • 校验文件
  • 加载文件到内存中并根据ELF文件中的Program header table和Section head table映射到进程的地址空间
  • 判断是否需要动态链接
  • 配置进程上下文启动环境start_thread
void start_thread(struct pt_regs *regs, unsigned int pc, unsigned long usp)
{
	/*
	 * The binfmt loader will setup a "full" stack, but the C6X
	 * operates an "empty" stack. So we adjust the usp so that
	 * argc doesn't get destroyed if an interrupt is taken before
	 * it is read from the stack.
	 *
	 * NB: Library startup code needs to match this.
	 */
	usp -= 8;

	regs->pc  = pc;
	regs->sp  = usp;
	regs->tsr |= 0x40; /* set user mode */
	current->thread.usp = usp;
}

start_thraed()修改保存在内核态堆栈的eip和esp的值,使它们分别指向动态链接程序的入口点和新的用户态堆栈的栈顶。
所以当execve()系统调用返回后,返回的已经是新的程序的起点了。

一般执行过程

  1. 正在运⾏的⽤户态进程X。
  2. 发⽣中断(包括异常、系统调⽤等),硬件完成
    • 当前CPU上下文压入用户态进程X的内核堆栈。
    • 加载当前进程内核堆栈相关信息,跳转到中断处理程序,即中断执行路径的起点。
  3. 保存现场,完成中断上下文切换,从进程X的用户态到进程X的内核态
  4. 中断处理过程中或中断返回前调⽤了schedule函数,其中完成了进程调度算法选择next进程、进程地址空间切换、关键的进程上下⽂切换等。
  5. switch_to调⽤了__switch_to_asm汇编代码做了关键的进程上下⽂切换。将当前进程X的内核堆栈切换到进程调度算法选出来的next进程(本例假定为进程Y)的内核堆栈,并完成了进程上下⽂所需的指令指针寄存器状态切换。之后开始运⾏进程Y。
  6. 中断上下⽂恢复,与(3)中断上下⽂切换相对应。注意这⾥是进程Y的中断处理过程中,⽽(3)中断上下⽂切换是在进程X的中断处理过程中,因为内核堆栈从进程X切换到进程Y了。
  7. iret pop cs:rip/ss:rsp/rflags,从Y进程的内核堆栈中弹出(3)中对应的压栈内容。此时完成了中断上下⽂的切换,即从进程Y的内核态返回到进程Y的⽤户态。
  8. 继续运⾏⽤户态进程Y。

ffork子进程启动执行时进程上下文的特殊之处

fork一个子进程时,子进程不是从switch_to下一行代码或者label1的位置开始执行的,而是从ret_from_fork开始执行的

execve系统调用中断上下文的特殊之处

execve系统调用加载新的可执行程序,在execve系统调用处理过程中修改了触发该系统调用保存的中断上下文,使得返回到用户态的位置修改为新程序的elf_entry或者ld动态连接器的起点地址

参考资料《深入理解Linux内核(第三版)》

你可能感兴趣的:(结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程)