在<Linux内核之execve函数>中,我们知道execve是一个新程序运行的开始,那么很多要问fork呢?fork又有什么区别呢?fork通常是在编程中,为了创建多个进程而使用的API。他和execve的差别是:execve属于进程替换,通过<Linux内核之execve函数>分析,仔细的朋友应该发现了execve函数在启动新程序的时候,还是用的之前老程序的task_struct结构。因此,这样它的pid就不会变,只是需要从内存重新分配他需要的页表和内存。而fork则不一样,它会产生一个完全独立的新进程,新的task_struct结构,新的pid的,只是会有一个写时复制的功能(刚刚fork出来的进程和原进程一模一样,内存什么都一样,只有当前进程内存发生写访问的时候,会重建立新的映射表)。这里先不说映射表重映射的问题(COW机制),这个问题之后讲映射的时候会讲。
下面我们看看fork函数在内核的实现:
点击(此处)折叠或打开
- long do_fork(unsigned long clone_flags,
- unsigned long stack_start,
- unsigned long stack_size,
- int __user *parent_tidptr,
- int __user *child_tidptr)
- {
- // stack_start是新程序占地址,stack_size栈大小
- return _do_fork(clone_flags, stack_start, stack_size,
- parent_tidptr, child_tidptr, 0);
- }
- SYSCALL_DEFINE0(vfork)
- { // 可以看到vfork产生的新进程内存空间和原始进程一样CLONE_VM
- return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0, 0, NULL, NULL, 0);
- }
- SYSCALL_DEFINE0(fork)
- {
- return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
- }
- // 内核线程创建,也是调用的do_fork,只是参数不同
- pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
- {
- // 注意,内核线程在stack_start和stack_sz传入的是线程回调函数和线程传参
- return _do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
- (unsigned long)arg, NULL, NULL, 0);
- }
其中SYSCALL_DEFINE0是一个宏,表示这是一个不包含任何参数的系统调用,这个宏会产生一个sys_fork/sys_vfork函数。也就是当应用程序调用fork函数的时候,CPU产生系统调用中断,中断处理函数通过查表(sys_call_table),通过对应的系统调用号找到sys_fork函数并开始执行(R7/W8存放的系统调用号,参数以此类推),sys_fork函数到_do_fork并没有做什么操作,只是参数不同(fork:SIGCHLD, vfork:CLONE_VFORK),这里kernel_thread、线程功能也会混着一起讲,因此现在直接到_do_fork函数,调用栈如下:
点击(此处)折叠或打开
- fork (user space)
- ---|----------------------------------------------------------------------------
- v (kernel space)
- el0_sync (arm64同步异常中断处理函数)
- el0_svc (查找sys_call_table,获取到sys_execve函数地址,并运行)
- sys_fork
- do_fork
- _do_fork
_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,
- unsigned long tls)
- {
- struct task_struct *p;
- int trace = 0;
- long nr;
- // ptrace相关设置,主要判断进入do_fork是哪个系统调用引起的
- 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;
- }
- // 将当前进程的进程空间拷贝一份给p,p是新进程的task_struct
- p = copy_process(clone_flags, stack_start, stack_size,
- child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
- add_latent_entropy();
- // 如果进程子进程复制成功,主进程就进入if语句,子进程不会进入这里,子进程直接进入
- // ret_from_fork
- if (!IS_ERR(p)) {
- struct completion vfork;
- struct pid *pid;
- trace_sched_process_fork(current, p);
- // 父进程获取新进程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_done完成函数
- if (clone_flags & CLONE_VFORK) {
- p->vfork_done = &vfork;
- init_completion(&vfork);
- get_task_struct(p);
- }
- // 将新进程p加入到run queue即加入到ready调度队列,此后p就会开始后参与调度
- wake_up_new_task(p);
- /* forking complete and child started to run, tell ptracer */
- if (unlikely(trace))
- ptrace_event_pid(trace, pid);
- // 如果是vfork产生的进程,需要等待子进程先运行,直到子进程complete了父进程,
- // vfork函数才返回。因此,vfork产生的子进程,总是优先于父进程运行。
- // vfork保证子进程先运行,在它调用exec或exit之后父进程才能调度运行,也就是当子
- // 进程调用exec或者exit的时候, 会触发wake_up($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);
- }
- // 父进程在这里返回,返回子进程的pid号,这个pid会修改父进程返回的pt_regs[0]的值
- // 子进程的返回值在copy_thread_tls里面已经设置,子进程不会运行这里
- return nr;
- }
可以知道,fork是把当前进程复制一个一模一样的进程,除了栈、LR,其他内容都一样。
在do_fork函数中实现的wake_up_new_task函数在copy_process函数之后调用,主要是用于将子进程加入调度队列。它的实现如下:
点击(此处)折叠或打开
- void wake_up_new_task(struct task_struct *p)
- {
- struct rq_flags rf;
- struct rq *rq;
- raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
- // 设置新进程P的状态为TASK_RUNNING
- p->state = TASK_RUNNING;
- #ifdef CONFIG_SMP
- // 进程添加到CPU负载均衡
- __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
- #endif
- // 获取当前p进程所在CPU的运行队列rq
- rq = __task_rq_lock(p, &rf);
- post_init_entity_util_avg(&p->se);
- // 将进程p加入到rq队列,具体在以后进程调度章节说明(RUNNING状态)
- activate_task(rq, p, 0);
- p->on_rq = TASK_ON_RQ_QUEUED;
- trace_sched_wakeup_new(p);
- check_preempt_curr(rq, p, WF_FORK);
- #ifdef CONFIG_SMP
- // SMP相关,这里占时不知道干什么的
- if (p->sched_class->task_woken) {
- p->sched_class->task_woken(rq, p);
- }
- #endif
- task_rq_unlock(rq, p, &rf);
- }
- static __latent_entropy struct task_struct *copy_process(
- unsigned long clone_flags,
- unsigned long stack_start,
- unsigned long stack_size,
- int __user *child_tidptr,
- struct pid *pid,
- int trace,
- unsigned long tls,
- int node)
- {
- int retval;
- struct task_struct *p;
- // 这里省略掉一些参数检测
- // 将当前进程复制一份给新进程p
- retval = -ENOMEM;
- p = dup_task_struct(current, node);
- if (!p)
- goto fork_out;
- ftrace_graph_init_task(p);
- rt_mutex_init_task(p);
- // 省略一些无用,chinaunix总说是敏感词汇的代码。
- // 复制信用凭证
- retval = copy_creds(p, clone_flags);
- if (retval < 0)
- goto bad_fork_free;
- // 超过最大进程,返回
- retval = -EAGAIN;
- if (nr_threads >= max_threads)
- goto bad_fork_cleanup_count;
- delayacct_tsk_init(p); /* Must remain after dup_task_struct() */
- p->flags &= ~(PF_SUPERPRIV | PF_WQ_WORKER);
- p->flags |= PF_FORKNOEXEC;
- INIT_LIST_HEAD(&p->children);
- INIT_LIST_HEAD(&p->sibling);
- rcu_copy_process(p);
- p->vfork_done = NULL;
- spin_lock_init(&p->alloc_lock);
- // 初始化信号挂起pending
- init_sigpending(&p->pending);
- // 这里省略初始化和时间等相关的杂项
- // 初始化调度实体se,和调度器类,调度器章节将会描述
- // 如果存在p->sched_class->task_fork,就执行
- retval = sched_fork(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_policy;
- // perf相关,这里不描述
- retval = perf_event_init_task(p);
- if (retval)
- goto bad_fork_cleanup_policy;
- //审计相关,这里不描述
- retval = audit_alloc(p);
- if (retval)
- goto bad_fork_cleanup_perf;
- // 进程共享内存链表初始化
- shm_init_task(p);
- retval = copy_semundo(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_audit;
- // 将当前current进程的打开的fd描述复制一份到新进程->p->files
- retval = copy_files(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_semundo;
- // 将current进程的工作目录复制一份到新进程(pwd, /)-》p->fs
- retval = copy_fs(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_files;
- // 将Current进程的信号处理函数,赋值一份到新进程->p->sighand
- retval = copy_sighand(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_fs;
- // 通过current初始化一份信号共享结构(线程信号共享就用的这个结构,还有一些其他数据)
- // p->signal
- retval = copy_signal(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_sighand;
- // 将current的映射页表拷贝一份到新进程,并设置成只读,为什么这么设置,
- // 以后的虚拟内存章节会讲解。
- // 即这就是为什么刚fork后,两个进程内存里面的值是一样的
- retval = copy_mm(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_signal;
- // 命名空间继承current的,即同一个命名空间
- retval = copy_namespaces(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_mm;
- // 赋值IO空间,tsk->io_context或者new_ioc->ioprio
- retval = copy_io(clone_flags, p);
- if (retval)
- goto bad_fork_cleanup_namespaces;
- // 拷贝每个进程不同的部分,这里非常重要,稍后详细说明
- retval = copy_thread_tls(clone_flags, stack_start, stack_size, p, tls);
- if (retval)
- goto bad_fork_cleanup_io;
- // 如果线程不是init_struct_pid(idle进程pid),就从新申请一个Pid
- if (pid != &init_struct_pid) {
- pid = alloc_pid(p->nsproxy->pid_ns_for_children);
- if (IS_ERR(pid)) {
- retval = PTR_ERR(pid);
- goto bad_fork_cleanup_thread;
- }
- }
- //省略一些赋值
- // 如果是线程,则设置线程组组长为当前进程指向的组长,如果是进程则将组长设置成自己,
- // 并初始化退出码
- p->pid = pid_nr(pid);
- if (clone_flags & CLONE_THREAD) { // 线程运行这里
- p->exit_signal = -1;
- p->group_leader = current->group_leader;
- p->tgid = current->tgid; // 进程的tgid才是真的pid 即同一个线程组下的线程tgid相同,pid不同
- } else {
- if (clone_flags & CLONE_PARENT)
- p->exit_signal = current->group_leader->exit_signal;
- else
- p->exit_signal = (clone_flags & CSIGNAL);
- p->group_leader = p;
- p->tgid = p->pid; //没有线程时,tgid==pid
- }
- p->nr_dirtied = 0;
- p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
- p->dirty_paused_when = 0;
- p->pdeath_signal = 0;
- INIT_LIST_HEAD(&p->thread_group);
- p->task_works = NULL;
- // cgroup相关,这里不说明
- threadgroup_change_begin(current);
- retval = cgroup_can_fork(p);
- if (retval)
- goto bad_fork_free_pid;
- write_lock_irq(&tasklist_lock);
- // 设置父进程,如果是线程,父进程就是current的父进程。否则current就是新进程的父进程
- if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {
- p->real_parent = current->real_parent;
- p->parent_exec_id = current->parent_exec_id;
- } else {
- p->real_parent = current;
- p->parent_exec_id = current->self_exec_id;
- }
- // 省略一些与此无关的
- //
- if (likely(p->pid)) {
- ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace);
- //初始化pid结构到
- init_task_pid(p, PIDTYPE_PID, pid);
- if (thread_group_leader(p)) {//新进程是线程组组长,同样意味着它是进程 而不是线程
- init_task_pid(p, PIDTYPE_PGID, task_pgrp(current));//将pid赋值到task->pid[PGID].pid
- init_task_pid(p, PIDTYPE_SID, task_session(current));//将pid赋值到task->pid[SID].pid
- // 是否是进程收养者,是,就设置成当前Pid空间的收养者(do_exit函数里面会用到)
- if (is_child_reaper(pid)) {
- ns_of_pid(pid)->child_reaper = p;
- p->signal->flags |= SIGNAL_UNKILLABLE;
- }
- p->signal->leader_pid = pid;//因为是组长,因此将它的pid记录到leader_pid
- p->signal->tty = tty_kref_get(current->signal->tty);
- // 将进程p添加到它父进程的children链表
- list_add_tail(&p->sibling, &p->real_parent->children);
- // 将进程p添加到Init_task下面,即idle进程的tasks链表下面
- list_add_tail_rcu(&p->tasks, &init_task.tasks);
- // 将p添加到对应的pgid和sid链表(组,会话)
- attach_pid(p, PIDTYPE_PGID);
- attach_pid(p, PIDTYPE_SID);
- __this_cpu_inc(process_counts);
- } else {// 不是组长,说明是线程,增加线程数量
- current->signal->nr_threads++;
- atomic_inc(¤t->signal->live);//增加线程live数量
- atomic_inc(¤t->signal->sigcnt);
- list_add_tail_rcu(&p->thread_group, // 将线程p挂到组长的thread_group链表下面
- &p->group_leader->thread_group);
- list_add_tail_rcu(&p->thread_node, // 将线程p挂到共享信号的thread_head链表下面(信号使用)
- &p->signal->thread_head);
- }
- // 添加到pid空间,并增加线程数
- attach_pid(p, PIDTYPE_PID);
- nr_threads++;
- }
- total_forks++;
- //省略一些不重要的
- // 返回新进程task_struct
- return p;
- // 省略失败处理细节
- }
在copy_process函数中的dup_task_struct函数实现如下,该函数用于从当前进程current的task_struct结构中,复制一份一模一样的给新进程p:
点击(此处)折叠或打开
- static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
- {
- struct task_struct *tsk;
- unsigned long *stack;
- struct vm_struct *stack_vm_area;
- int err;
- // 这里传入的参数是NUMA_NO_NODE,表示申请内存的时候,从当前进程所在内存结点申请;内存结点将在伙伴子系统加以说明,这里不讲解;
- if (node == NUMA_NO_NODE)
- node = tsk_fork_get_node(orig);
- // 从内存node结点申请一段内存作为新进程的tsk结构
- tsk = alloc_task_struct_node(node);
- if (!tsk)
- return NULL;
- // 从node结点申请一个新的栈地址
- stack = alloc_thread_stack_node(tsk, node);
- if (!stack)
- goto free_tsk;
- // 得到一个stack的vm_struct结构,这里不讲解
- stack_vm_area = task_stack_vm_area(tsk);
- // 这个函数 其实就是tsk = orig,即将current进程memcpy到tsk。
- err = arch_dup_task_struct(tsk, orig);
- // 将stack地址设置到tsk的记录栈地址的位置,替换掉从current拷贝来的栈(这是内核栈,
- // 每个应用程序有两个栈,一个应用栈,一个内核栈;而内核线程只有内核栈.
- // 因此,这里可以统一申请内核栈)
- tsk->stack = stack;
- // 如果定义了VMAP_STACK,就将stack vm_struct结构赋值
- #ifdef CONFIG_VMAP_STACK
- tsk->stack_vm_area = stack_vm_area;
- #endif
- // 增加引用计数
- #ifdef CONFIG_THREAD_INFO_IN_TASK
- atomic_set(&tsk->stack_refcount, 1);
- #endif
- if (err)
- goto free_stack;
- #ifdef CONFIG_SECCOMP
- tsk->seccomp.filter = NULL;
- #endif
- // 将current的threadinfo结构拷贝一份到tsk,threadinfo待会儿会详细介绍。
- setup_thread_stack(tsk, orig);
- clear_user_return_notifier(tsk);
- clear_tsk_need_resched(tsk);
- // 设置栈的结束位置,用于检测栈溢出用的
- set_task_stack_end_magic(tsk);
- // 省略一些无关紧要的赋值
- account_kernel_stack(tsk, 1);
- kcov_task_init(tsk);
- // 返回新进程的task_struct结构
- return tsk;
- }
- static inline void setup_thread_stack(struct task_struct *p, struct task_struct *org)
- {
- // 赋值current的thread_info结构,然后将task指向新进程
- *task_thread_info(p) = *task_thread_info(org);
- task_thread_info(p)->task = p;
- }
在申请新的进程task_struct结束后,我这里有必要说说进程的thread_info结构。thread_info结构是存放到内核的SP栈中的,原本Linux是直接将task_struct存放到SP当中,但是后来随着task_struct越来越大,因此,添加了一个thread_info结构做中间代理结构,达到通过sp能够获取到对应进程的task_struct结构的目的。thread_info在栈的位置如图:
如上图, 通常内核的栈大小为2个page,即8K,并且内核栈严格按照8K对齐,因此当获取到SP的指针的时候(sp current),只需要与上8k取反的值(sp & (~8k))就可以得到thread_info结构。thread_info->task就指向该进程的task_struct结构。由于内核栈是向下增长,因此,如果栈越界,首先破坏的是自己的thread_info结构,然后崩溃(咋们内核中使用的current起始就是一个宏,利用当前SP和thread_info反向找到自己的task_struct结构的,有兴趣的可以看看这个宏)。
copy_process函数中实现的copy_files函数比较简单,如下:
点击(此处)折叠或打开
- static int copy_files(unsigned long clone_flags, struct task_struct *tsk)
- {
- struct files_struct *oldf, *newf;
- int error = 0;
- oldf = current->files;
- if (!oldf)
- goto out;
- if (clone_flags & CLONE_FILES) {
- atomic_inc(&oldf->count);
- goto out;
- }
- // 复制一份current的files成newf
- newf = dup_fd(oldf, &error);
- if (!newf)
- goto out;
- // 将复制出来的newf赋值给files
- tsk->files = newf;
- error = 0;
- out:
- return error;
- }
因此,从copy_files这一个简单的说明后,其他的copy_*函数,这里就不多做说明了, 有兴趣的,可以自己去看看,先主要说说copy_thread_tls函数。
copy_thread_tls是一个非常重要的函数,其主要是设置新进程的运行地址和寄存器值,实现如下:
点击(此处)折叠或打开
- struct cpu_context {
- unsigned long x19;
- unsigned long x20;
- unsigned long x21;
- unsigned long x22;
- unsigned long x23;
- unsigned long x24;
- unsigned long x25;
- unsigned long x26;
- unsigned long x27;
- unsigned long x28;
- unsigned long fp;
- unsigned long sp;
- unsigned long pc;
}; - asmlinkage void ret_from_fork(void) asm("ret_from_fork");
- // 这个函数是和体系结构强相关的,这里我看的是armv8处理器
- int copy_thread(unsigned long clone_flags, unsigned long stack_start,
- unsigned long stk_sz, struct task_struct *p)
- {
- // 获取pt_regs地址: task->stack + THREAD_START_SP -1
- // 其中task->stack起始存放的是thread_info,THREAD_START_SP = 16k - 16,
- // 即在栈的最高地址存放的是寄存器值, pt_regs。
- struct pt_regs *childregs = task_pt_regs(p);
- // 将新进程的内核态需要的寄存器信息清0
- memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));
- // 发现新进程是一个应用进程,进入if语句
- if (likely(!(p->flags & PF_KTHREAD))) {
- // 复制current进程的用户态寄存器到新进程
- *childregs = *current_pt_regs();
- // 将新进程的返回值设置成0,regs[0] 就是X0寄存器,在ARM中X0作为返回值寄存器使用
- // 因此当fork函数返回后,在子进程中,返回值是0
- childregs->regs[0] = 0;
- *task_user_tls(p) = read_sysreg(tpidr_el0);
- // 如果用户设置了栈的起始地址,就是设置用户栈的地址是stack_start
- // 用户进程有两个栈,用户态的栈和内核态的栈,内核态的栈前面dup_task_struct的
- // 时候申请了,这里是设置用户态的栈,由用户态指定
- if (stack_start) {
- if (is_compat_thread(task_thread_info(p)))
- childregs->compat_sp = stack_start;
- else
- childregs->sp = stack_start;
- }
- // TLS相关的
- if (clone_flags & CLONE_SETTLS)
- p->thread.tp_value = childregs->regs[3];
- } else { // 内核线程进入else语句
- // 内核线程将childregs的CPU寄存器请0
- memset(childregs, 0, sizeof(struct pt_regs));
- childregs->pstate = PSR_MODE_EL1h;//设置CPU工作模式是EL1模式(用户进程在EL0模式)
- if (IS_ENABLED(CONFIG_ARM64_UAO) &&
- cpus_have_cap(ARM64_HAS_UAO))
- childregs->pstate |= PSR_UAO_BIT;
- // X19存放stack_start,从kernel_thread传入参数可以知道stack_start是线程回调函数
- // X20存放内核线程传入参数(stk_sz:内核线程传入的线程函数传参)
- p->thread.cpu_context.x19 = stack_start;
- p->thread.cpu_context.x20 = stk_sz;
- }
- // 不管是内核线程还是应用线程,新进程执行的第一个函数时钟是ret_from_fork
- p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
- // 设置CPU的栈指向childregs结构,方便返回的时候,弹出到寄存器。
- p->thread.cpu_context.sp = (unsigned long)childregs;
- ptrace_hw_copy_thread(p);
- return 0;
- }
- static inline int copy_thread_tls(
- unsigned long clone_flags, unsigned long sp, unsigned long arg,
- struct task_struct *p, unsigned long tls)
- {
- return copy_thread(clone_flags, sp, arg, p);
- }
经过了copy_thread_tls函数之后,新进程的运行环境就已经准备就绪,最后只需要将新进程的task_struct结构加入到run queue调度队列里面即可(wake_up_new_task函数)。一旦调度到新的进程,新的进程就从ret_from_fork函数开始运行,其中ret_from_fork函数实现如下:
点击(此处)折叠或打开
- ret_to_user:
- disable_irq // 禁止中断
- ldr x1, [tsk, #TI_FLAGS] // 读取tsk结构的TI_FLAGS位到x1寄存器
- and x2, x1, #_TIF_WORK_MASK // 判断tsk的TI_FLAGS位是否置位
- cbnz x2, work_pending // 这里用于检查是否有工作挂起。
- finish_ret_to_user:
- enable_step_tsk x1, x2
- kernel_exit 0 // 退出内核态EL1到EL0
- ENDPROC(ret_to_user)
- ENTRY(ret_from_fork)
- bl schedule_tail // 进程调度的时候 会说明
- cbz x19, 1f //从copy_thread_tls函数知道x19如果不为0那么,此进程就为内核线程。X19存放的是内核线程的回调函数。
- mov x0, x20 //从copy_thread_tls函数知道,如果是内核线程,那么x20内核线程的回调函数的传入参数
- blr x19 // x0存放传入的第一个参数,然后跳转到内核线程回调函数去执行。
- 1: get_thread_info tsk // 用户线程运行这里,通过ret_to_user返回到用户空间
- b ret_to_user // 返回到应用空间
- ENDPROC(ret_from_fork)
应用程序fork的调用栈如下:
注:copy_mm函数会在以后讲解虚拟地址的时候详细讲解。