基础知识

thread_union

进程在内核态运行时需要自己的堆栈信息,linux内核为每个进程都提供了一个内核栈。对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中:

  • 一个是内核态的进程堆栈stack
  • 另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。

它们共同组成了thread_union

union thread_union
{
    struct thread_info thread_info;
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

task_struct包含了指向thread_info的字段,而thread_info通过task字段和thread_struct相互联系。

写时复制

  写时复制技术允许父子进程读相同的物理页,只要两者中有一个试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。这样做得目的是为了提高进程创建的效率,因为子进程全部拷贝父进程的地址空间非常慢且效率低,实际上,子进程几乎不必读或修改父进程拥有的所有资源,在很多情况下,子进程立即调用execve(),并清除父进程之前拷贝过来的地址空间。
  这里为什么要介绍写时复制呢?
  因为wake_up_new_task函数里会执行下列操作:如果子进程和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表,那么,就把子进程插入父进程运行队列,插入时让子进程在父进程前面执行,这里为什么要让子进程先执行呢?
  因为如果我们先让父进程运行,那么写时复制机制将会执行一系列不必要的页面复制。

代码分析

do_fork函数

  • 通过查找pidmap_array位图,为子进程分配新的pid
  • 检查父进程的ptrace字段
  • 调用copy_process()复制进程描述符
  • 调用wake_up_new_task()函数
  • 如果设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存地址空间。

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;
		}

从上面的代码可以看出系统调用clone()、fork()、和vfork()都是由do_fork()进行处理的。do_fork通过copy_process函数来创建进程描述符和子进程执行所需要的所有其他内核数据结构。

copy_process函数

  • 检查参数clone_flags所传递标志的一致性。
  • 通过调用security_task_create()以及稍后调用的security_task_alloc()执行所有附加的安全检查
  • 调用dup_task_struct()为子进程获取进程描述符
  • 检查系统中的进程数量(存放在NR_THREADS变量中)是否超过max_threads变量的值
  • 把tsk->did_exec字段初始化为0:它记录了进程发出的execve()系统调用的次数
  • 把新进程的pid存入tsk->pid字段
  • 初始化子进程描述符中的list_head数据结构和自旋锁,并为与挂起信号、定时器及时间统计表相关的几个字段赋初值
  • 调用copy_semundo(),copy_files(),copy_fs(),copy_sighand(),copy_signal(),copy_mm()和copy_namespace()来创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中。
  • 调用copy_thread(),将保存在父进程的内核栈中的CPU寄存器的值来初始化子进程的内核栈,将eax寄存器置0,子进程返回值为0,将ret_from_fork()的地址存放在thread.eip字段
  • 清除子进程thread_info结构的TIF_SYSCALL_TRACE标志,使ret_from_fork()函数不会把系统调用结束的消息通知给调试进程。
  • 调用sched_fork()完成对新进程调度程序数据结构的初始化,把新进程的状态设置为TASK_RUNNING,并把thread_info结构的preempt_count字段设置为1,从而禁止内核抢占
  • 初始化亲子关系字段
  • 将新进程pid插入散列表中
  • 递增nr_threads变量的值
  • 递增total_forks变量记录被创建的进程的数量

copy_thread函数

  • 将保存在父进程的内核栈中的CPU寄存器的值来初始化子进程的内核栈
  • 将eax寄存器置0,子进程返回值为0
  • 将ret_from_fork()的地址存放在thread.eip字段

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;
	//创建进程描述符对象
	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;
	//进程描述符stack指向thread_info
	tsk->stack = ti;
	……
	//使子进程thread_info内容与父进程一致但task指向子进程task_struct
	setup_thread_stack(tsk,orig);
	……
	return tsk;
	……
}

代码调试

forkAPI函数,会通过宏指令来跳转到相应的系统调用

forkAPI函数会通过SYS_clone宏指令,最终会调用do_fork函数:

调用copy_process函数

调用dup_task_struct函数

经过dup_task_struct和copy_thread等一系列操作后,子进程被创建,递增总进程数:
创建子进程之前total_forks值:

创建子进程之后total_forks值:

进程上下文的切换:

代码结构图