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

进程是计算机中已运行程序的实体。在面向线程设计的系统(Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

UglyYouth
, in 10 April 2015

此文仅用于MOOCLinux内核分析作业

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


task_struct数据结构

根据wiki的定义,进程是计算机中已运行程序的实体。在面向线程设计的系统(Linux 2.6及更新的版本)中,进程本身不是基本运行单位,而是线程的容器。

A computer program is a passive collection of instructions; a process is the actual execution of those instructions. Several processes may be associated with the same program; for example, opening up several instances of the same program often means more than one process is being executed.

在Linux中,task_struct其实就是通常所说的PCB。该结构定义位于:

/include/linux/sched.h

task_struct比较庞大,大致可以分为几个部分:

其中比较重要的几个参数:

分析内核处理过程sys_clone

fork、vfork和clone三个系统调用实际上都是通过do_fork来实现进程的创建. 见如下语句:

return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);

而do_fork函数真正实现复制是copy_process

long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
{

	...
	
	p = copy_process(clone_flags, stack_start, stack_size,
	         child_tidptr, NULL, trace);
  ...
}

copy_process()主要完成进程数据结构,各种资源的初始化。

p = dup_task_struct(current);
  1. (省略的IF语句)检查clone_flags参数,防止无效的组合进入
  2. p = dup_task_struct(current);调用dup_task_struct()为新进程创建一个内核栈
  3. 判断权限及允许范围的代码
  4. 对子进程的描述符初始化和复制父进程的资源给子进程 - retval = sched_fork(clone_flags, p);完成调度相关的设置,将这个task分配给CPU - if (retval)语句群,复制共享进程的的各个部分 - retval = copy_thread(clone_flags, stack_start, stack_size, p);复制父进程堆栈的内容到子进程的堆栈中去.这其中,copy_thread()函数中的语句p->thread.ip = (unsigned long) ret_from_fork;决定了新进程的第一条指令地址.
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;
	
	tsk = alloc_task_struct_node(node);
	if (!tsk)
	    return NULL;
	
	ti = alloc_thread_info_node(tsk, node);
	if (!ti)
	    goto free_tsk;
	
	err = arch_dup_task_struct(tsk, orig);
	if (err)
	    goto free_ti;
	
	tsk->stack = ti;
# ifdef CONFIG_SECCOMP

	tsk->seccomp.filter = NULL;
# endif

	setup_thread_stack(tsk, orig);
	clear_user_return_notifier(tsk);
	clear_tsk_need_resched(tsk);
	set_task_stack_end_magic(tsk);

# ifdef CONFIG_CC_STACKPROTECTOR
	tsk->stack_canary = get_random_int();
# endif

	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;
}
  1. tsk = alloc_task_struct_node(node);为task_struct开辟内存
  2. ti = alloc_thread_info_node(tsk, node);ti指向thread_info的首地址,同时也是系统为新进程分配的两个连续页面的首地址。
  3. err = arch_dup_task_struct(tsk, orig);复制父进程的task_struct信息到新的task_struct里, (dst = src;)
  4. tsk->stack = ti;task的对应栈
  5. setup_thread_stack(tsk, orig);初始化thread info结构
  6. set_task_stack_end_magic(tsk);栈结束的地址设置数据为栈结束标示(for overflow detection)

gdb跟踪sys_clone

新进程是从哪里开始执行的?

在之前的分析中,谈到copy_process中的copy_thread()函数,正是这个函数决定了子进程从系统调用中返回后的执行.

int copy_thread(unsigned long clone_flags, unsigned long sp,
	unsigned long arg, struct task_struct *p)
{
	...
	
	*childregs = *current_pt_regs();
	childregs->ax = 0;
	if (sp)
	    childregs->sp = sp;
	
	p->thread.ip = (unsigned long) ret_from_fork;
	
	...
}

子进程执行ret_from_fork

ENTRY(ret_from_fork)
	CFI_STARTPROC
	pushl_cfi %eax
	call schedule_tail
	GET_THREAD_INFO(%ebp)
	popl_cfi %eax
	pushl_cfi $0x0202       # Reset kernel eflags
	popfl_cfi
	jmp syscall_exit
	CFI_ENDPROC
END(ret_from_fork)

执行起点与内核堆栈如何保证一致?

  1. 在ret_from_fork之前,也就是在copy_thread()函数中*childregs = *current_pt_regs();该句将父进程的regs参数赋值到子进程的内核堆栈,
  2. *childregs的类型为pt_regs,里面存放了SAVE ALL中压入栈的参数
  3. 故在之后的RESTORE ALL中能顺利执行下去.

总结

  1. Linux通过复制父进程来创建一个新进程,通过调用do_fork来实现
  2. Linux为每个新创建的进程动态地分配一个task_struct结构.
  3. 为了把内核中的所有进程组织起来,Linux提供了几种组织方式,其中哈希表和双向循环链表方式是针对系统中的所有进程(包括内核线程),而运行队列和等待队列是把处于同一状态的进程组织起来
  4. fork()函数被调用一次,但返回两次

参考

  1. dup_task_struct
  2. Linux进程切换以及内核线程的返回值
  3. kernel-book