当前位置: 首页 > ai >正文

【Linux内核设计与实现】第三章——进程管理03

文章目录

    • 7.6. 实现 fork() 写时拷贝的关键所在 copy_process() 中的 copy_mm()
      • 7.6.1. copy_mm 函数
        • 7.6.1.1. copy_mm 函数的关键代码段
      • 7.6.2. dup_mm 函数
        • 7.6.2.1. 复制父进程的 `mm_struct`
        • 7.6.2.2. 初始化新的 `mm_struct`
        • 7.6.2.3. 复制虚拟内存区域(VMA)
      • 7.6.3. dup_mmap 函数
        • 7.6.3.1. 复制 `mm_struct` 的元数据
        • 7.6.3.2. 复制虚拟内存区域(VMA)映射树
        • 7.6.3.3. 复制页表项(写时拷贝)
      • 7.6.4. 写时拷贝的体现
    • 7.7. copy_process() 的核心函数 dup_task_struct()
      • 7.7.1. 参数及返回值
      • 7.7.2. 确定 NUMA 节点
      • 7.7.3. 分配 `task_struct`
      • 7.7.4. 复制原始任务结构
      • 7.7.5. 分配线程栈
      • 7.7.6. 初始化安全上下文切换栈
    • 7.8. dup_task_struct() 的核心函数 alloc_task_struct_node()
      • 7.8.1. 调用 kmem_cache_alloc_node
      • 7.8.2. kmem_cache_alloc_node 函数分析
      • 7.8.3. kmem_cache_alloc_node_noprof 函数分析
      • 7.8.3. slab_alloc_node 函数分析
      • 7.8.4. __slab_alloc_node 函数分析
    • 7.9. fork() 函数的简要总体框图
  • #上一篇
  • #下一篇

[注]:本篇文章与上一篇紧密相关,若未阅读上一篇请移步上一篇阅读,在文末可以找到上一篇链接。

7.6. 实现 fork() 写时拷贝的关键所在 copy_process() 中的 copy_mm()

  在上文中介绍进程创建时,很是强调 fork() 函数的一大特点,即写时拷贝,由于将具体拷贝页面的任务更变为,当新进程需要修改时才发生实际拷贝,这样能够加快进程的执行速度,也不会浪费额外内存空间(进程无需修改时便不需要拷贝页面)。那么这么有意思的机制是如何被实现的,这里就来一探究竟。

  在执行 copy_process() 函数中会调用到 copy_mm() 函数,按照道理,该函数就是为新进程拷贝父进程的内存空间,那么现在就具体来看看其中发生了什么。

7.6.1. copy_mm 函数

首先看 copy_mm 函数实现如下:

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
{struct mm_struct *mm, *oldmm;/** 初始化子进程的内存统计字段:* 	* min_flt: 次要缺页错误数。* 	* maj_flt: 主要缺页错误数* 	* nvcsw: 自愿上下文切换次数。* 	* nivcsw: 非自愿上下文切换次数。*/tsk->min_flt = tsk->maj_flt = 0;tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASKtsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;tsk->last_switch_time = 0;
#endif/** 初始化新进程的 mm 和 active_mm* 	* mm:进程的内存描述符* 	* active_mm:当前活动的内存描述符。*/tsk->mm = NULL;tsk->active_mm = NULL;/** Are we cloning a kernel thread?** We need to steal a active VM for that..*/// 检查父进程是否有 mm_struct// 如果父进程没有 mm_struct(例如内核线程),直接返回 0oldmm = current->mm;if (!oldmm)return 0;if (clone_flags & CLONE_VM) {mmget(oldmm);mm = oldmm;} else {mm = dup_mm(tsk, current->mm);if (!mm)return -ENOMEM;}// 将子进程的 mm 和 active_mm 设置为新创建或共享的 mm_struct。tsk->mm = mm;tsk->active_mm = mm;// 调用调度器相关的函数,处理与内存上下文 ID(CID)相关的初始化。sched_mm_cid_fork(tsk);return 0;
}
7.6.1.1. copy_mm 函数的关键代码段
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_mm()
if (clone_flags & CLONE_VM) {mmget(oldmm);mm = oldmm;
}
  • 如果设置了 CLONE_VM 标志:
    • 调用 mmget 增加父进程的 mm_struct 的引用计数。
    • 子进程直接共享父进程的 mm_struct
  • CLONE_VM 的作用:
    • 表示子进程与父进程共享内存地址空间。即子进程无需单独创建内存空间。
    • 通常用于线程(如 pthread),因为线程需要共享全局变量和堆等内存区域。
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: copy_mm()
mm = dup_mm(tsk, current->mm);
if (!mm)return -ENOMEM;
  • 如果没有设置 CLONE_VM 标志,调用 dup_mm 为子进程创建一个新的 mm_struct
  • dup_mm 的作用:
    • 复制父进程的 mm_struct 和虚拟内存区域(VMA)。
    • 实现写时拷贝(Copy-On-Write, COW)机制。

7.6.2. dup_mm 函数

  较为简单内容笔者以注释的方式标注在代码中,比较关键和不易理解的代码将详细摘出分析。

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static struct mm_struct *dup_mm(struct task_struct *tsk,struct mm_struct *oldmm)
{struct mm_struct *mm;int err;// 调用 allocate_mm 函数分配一个新的 mm_struct。mm = allocate_mm();if (!mm)goto fail_nomem;// 使用 memcpy 将父进程的 mm_struct 内容复制到新分配的 mm_struct 中。memcpy(mm, oldmm, sizeof(*mm));if (!mm_init(mm, tsk, mm->user_ns))goto fail_nomem;uprobe_start_dup_mmap();err = dup_mmap(mm, oldmm);if (err)goto free_pt;uprobe_end_dup_mmap();mm->hiwater_rss = get_mm_rss(mm);mm->hiwater_vm = mm->total_vm;if (mm->binfmt && !try_module_get(mm->binfmt->module))goto free_pt;return mm;free_pt:/* don't put binfmt in mmput, we haven't got module yet */mm->binfmt = NULL;mm_init_owner(mm, NULL);mmput(mm);if (err)uprobe_end_dup_mmap();fail_nomem:return NULL;
}
7.6.2.1. 复制父进程的 mm_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
memcpy(mm, oldmm, sizeof(*mm));
  • 使用 memcpy 将父进程的 mm_struct 内容复制到新分配的 mm_struct 中。
  • 这会复制父进程的内存布局、虚拟内存区域(VMA)等元数据。
7.6.2.2. 初始化新的 mm_struct
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
if (!mm_init(mm, tsk, mm->user_ns))goto fail_nomem;
  • 调用 mm_init 函数初始化新的 mm_struct
  • 主要完成以下任务:
    • 初始化 mm_struct 的引用计数。
    • 初始化页表锁、内存映射锁等。
    • 设置新的 mm_struct 的用户命名空间。
7.6.2.3. 复制虚拟内存区域(VMA)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function:dup_mm()
uprobe_start_dup_mmap();
err = dup_mmap(mm, oldmm);
if (err)goto free_pt;
uprobe_end_dup_mmap();
  • 调用 dup_mmap 函数复制父进程的虚拟内存区域(VMA)。
  • dup_mmap 是实现写时拷贝(Copy-On-Write, COW)的关键函数:
    • 遍历父进程的 VMA
    • 为子进程创建对应的 VMA
    • 不直接复制物理页面,而是通过写时拷贝机制延迟实际的页面分配。
  • 如果 dup_mmap 失败,释放已分配的资源并返回错误。

  接着更新内存统计信息,最终返回拷贝完成的新 mm_struct

7.6.3. dup_mmap 函数

  dup_mmap 函数是 dup_mm 函数处理过程中复制父进程虚拟内存的具体业务函数,先来看下该函数的实现。

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static __latent_entropy int dup_mmap(struct mm_struct *mm,struct mm_struct *oldmm)
{struct vm_area_struct *mpnt, *tmp;int retval;unsigned long charge = 0;LIST_HEAD(uf);VMA_ITERATOR(vmi, mm, 0);// 对父进程的 mm_struct 加写锁,确保在复制过程中不会有其他线程修改内存布局。if (mmap_write_lock_killable(oldmm))return -EINTR;// 调用 flush_cache_dup_mm 刷新父进程的缓存。flush_cache_dup_mm(oldmm);// 调用 uprobe_dup_mmap 处理用户探针相关的内存映射。uprobe_dup_mmap(oldmm, mm);/** Not linked in yet - no deadlock potential:*/mmap_write_lock_nested(mm, SINGLE_DEPTH_NESTING);/* No ordering required: file already has been exposed. */dup_mm_exe_file(mm, oldmm);mm->total_vm = oldmm->total_vm;mm->data_vm = oldmm->data_vm;mm->exec_vm = oldmm->exec_vm;mm->stack_vm = oldmm->stack_vm;/* Use __mt_dup() to efficiently build an identical maple tree. */retval = __mt_dup(&oldmm->mm_mt, &mm->mm_mt, GFP_KERNEL);if (unlikely(retval))goto out;mt_clear_in_rcu(vmi.mas.tree);for_each_vma(vmi, mpnt) {struct file *file;vma_start_write(mpnt);if (mpnt->vm_flags & VM_DONTCOPY) {retval = vma_iter_clear_gfp(&vmi, mpnt->vm_start,mpnt->vm_end, GFP_KERNEL);if (retval)goto loop_out;vm_stat_account(mm, mpnt->vm_flags, -vma_pages(mpnt));continue;}charge = 0;/** Don't duplicate many vmas if we've been oom-killed (for* example)*/if (fatal_signal_pending(current)) {retval = -EINTR;goto loop_out;}if (mpnt->vm_flags & VM_ACCOUNT) {unsigned long len = vma_pages(mpnt);if (security_vm_enough_memory_mm(oldmm, len)) /* sic */goto fail_nomem;charge = len;}tmp = vm_area_dup(mpnt);if (!tmp)goto fail_nomem;retval = vma_dup_policy(mpnt, tmp);if (retval)goto fail_nomem_policy;tmp->vm_mm = mm;retval = dup_userfaultfd(tmp, &uf);if (retval)goto fail_nomem_anon_vma_fork;// 如果 VMA 标记为 VM_WIPEONFORK,则不复制匿名内存。if (tmp->vm_flags & VM_WIPEONFORK) {/** VM_WIPEONFORK gets a clean slate in the child.* Don't prepare anon_vma until fault since we don't* copy page for current vma.*/tmp->anon_vma = NULL;} // 否则,调用 anon_vma_fork 共享匿名内存的 anon_vma。else if (anon_vma_fork(tmp, mpnt))goto fail_nomem_anon_vma_fork;vm_flags_clear(tmp, VM_LOCKED_MASK);/** Copy/update hugetlb private vma information.*/if (is_vm_hugetlb_page(tmp))hugetlb_dup_vma_private(tmp);/** Link the vma into the MT. After using __mt_dup(), memory* allocation is not necessary here, so it cannot fail.*/vma_iter_bulk_store(&vmi, tmp);mm->map_count++;if (tmp->vm_ops && tmp->vm_ops->open)tmp->vm_ops->open(tmp);// 如果 VMA 关联了文件,增加文件的引用计数,并将新的 VMA 插入文件的映射树。file = tmp->vm_file;if (file) {struct address_space *mapping = file->f_mapping;get_file(file);i_mmap_lock_write(mapping);if (vma_is_shared_maywrite(tmp))mapping_allow_writable(mapping);flush_dcache_mmap_lock(mapping);/* insert tmp into the share list, just after mpnt */vma_interval_tree_insert_after(tmp, mpnt,&mapping->i_mmap);flush_dcache_mmap_unlock(mapping);i_mmap_unlock_write(mapping);}if (!(tmp->vm_flags & VM_WIPEONFORK))retval = copy_page_range(tmp, mpnt);if (retval) {mpnt = vma_next(&vmi);goto loop_out;}}/* a new mm has just been created */retval = arch_dup_mmap(oldmm, mm);
loop_out:vma_iter_free(&vmi);if (!retval) {mt_set_in_rcu(vmi.mas.tree);ksm_fork(mm, oldmm);khugepaged_fork(mm, oldmm);} else {/** The entire maple tree has already been duplicated. If the* mmap duplication fails, mark the failure point with* XA_ZERO_ENTRY. In exit_mmap(), if this marker is encountered,* stop releasing VMAs that have not been duplicated after this* point.*/if (mpnt) {mas_set_range(&vmi.mas, mpnt->vm_start, mpnt->vm_end - 1);mas_store(&vmi.mas, XA_ZERO_ENTRY);/* Avoid OOM iterating a broken tree */set_bit(MMF_OOM_SKIP, &mm->flags);}/** The mm_struct is going to exit, but the locks will be dropped* first.  Set the mm_struct as unstable is advisable as it is* not fully initialised.*/set_bit(MMF_UNSTABLE, &mm->flags);}
out:mmap_write_unlock(mm);flush_tlb_mm(oldmm);mmap_write_unlock(oldmm);if (!retval)dup_userfaultfd_complete(&uf);elsedup_userfaultfd_fail(&uf);return retval;fail_nomem_anon_vma_fork:mpol_put(vma_policy(tmp));
fail_nomem_policy:vm_area_free(tmp);
fail_nomem:// 如果在复制过程中发生错误,释放已分配的资源并返回错误码。retval = -ENOMEM;vm_unacct_memory(charge);goto loop_out;
}
7.6.3.1. 复制 mm_struct 的元数据
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
dup_mm_exe_file(mm, oldmm);mm->total_vm = oldmm->total_vm;
mm->data_vm = oldmm->data_vm;
mm->exec_vm = oldmm->exec_vm;
mm->stack_vm = oldmm->stack_vm;
  • 复制父进程的 mm_struct 元数据,包括虚拟内存大小、数据段大小、代码段大小和栈大小。
7.6.3.2. 复制虚拟内存区域(VMA)映射树
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
retval = __mt_dup(&oldmm->mm_mt, &mm->mm_mt, GFP_KERNEL);
if (unlikely(retval))goto out;...for_each_vma(vmi, mpnt) {...tmp = vm_area_dup(mpnt);if (!tmp)goto fail_nomem;retval = vma_dup_policy(mpnt, tmp);if (retval)goto fail_nomem_policy;tmp->vm_mm = mm;...
}
  • 调用 __mt_dup 复制父进程的虚拟内存区域(VMA)映射树。
  • 遍历父进程的每个 VMA
  • 调用 vm_area_dup 复制 VMA 的元数据。
  • 调用 vma_dup_policy 复制 VMA 的内存策略。
7.6.3.3. 复制页表项(写时拷贝)
// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_mmap()
if (!(tmp->vm_flags & VM_WIPEONFORK))retval = copy_page_range(tmp, mpnt);
  • 调用 copy_page_range 实现写时拷贝:
    • 页面表不会被直接复制,而是标记为只读。
    • 当父进程或子进程尝试写入这些页面时,会触发页面错误(Page Fault),然后内核为写入的进程分配新的物理页面。

  本文由于篇幅关系这边暂时不过多分析 copy_page_range 函数实现,此处简要描述该函数实现写时拷贝(COW)这一机制的原理:

    1. 拷贝页表项:
    • copy_page_range 会遍历父进程的虚拟内存区域(VMA)对应的页表,将父进程的页表项复制到子进程的页表中。(按照 x86_64 的五级页表结构 PGDP4DPUDPMDPTE 顺序逐级递归复制页表项)
    • 页表项中会标记页面为只读(通过设置页表项的权限位)。
    1. 实现写时拷贝(COW):
    • 页面被标记为只读后,父进程和子进程共享相同的物理页面。
    • 当父进程或子进程尝试写入这些页面时,会触发页面错误(Page Fault)。
    • 页面错误处理程序会为写入的进程分配一个新的物理页面,并将原页面的内容复制到新页面中。
    1. 延迟实际的页面分配:
    • copy_page_range 不会立即分配新的物理页面,而是通过写时拷贝机制延迟页面分配,从而提高 fork 的性能。

  到这里也就实现了 fork() 函数的写时拷贝(COW)机制。

在这里插入图片描述

7.6.4. 写时拷贝的体现

  最终调用到 copy_page_range 函数只会复制父进程的页表项,并将子进程的页表项标记为只读权限,而不直接拷贝物理页面给子进程。当子进程尝试写入页面时,由于页面被标记为只读,从而触发 Page Fault,这时陷入页面错误处理程序,才开始为子进程分配新的物理页面,并将之前的只读页面内容复制到新页面中。也就是说无需再创建过程中拷贝内存(物理页面的信息),通过 copy_mm,子进程能够继承或共享父进程的内存布局,从而实现高效的进程创建。将实际拷贝推迟到真正发生写入时,这也便完成了写时拷贝这一机制。下图为 x86_64 下的 5 级页表转换查找物理地址示意图,在子进程与父进程共享内存时,将会查找到同一块物理页面,只不过该段内存子进程被标记为只读权限。

在这里插入图片描述

[注]:原画较大,点击图片查看高清图。

7.7. copy_process() 的核心函数 dup_task_struct()

  dup_task_struct 函数作为 copy_process() 中一个核心函数,用来为新进程分配并初始化一个新的 task_struct(任务结构)。

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.cstatic struct task_struct *dup_task_struct(struct task_struct *orig, int node)
{struct task_struct *tsk;int err;if (node == NUMA_NO_NODE)node = tsk_fork_get_node(orig);tsk = alloc_task_struct_node(node);if (!tsk)return NULL;err = arch_dup_task_struct(tsk, orig);if (err)goto free_tsk;err = alloc_thread_stack_node(tsk, node);if (err)goto free_tsk;// 如果启用了 CONFIG_THREAD_INFO_IN_TASK 配置,初始化线程栈的引用计数为 1。
#ifdef CONFIG_THREAD_INFO_IN_TASKrefcount_set(&tsk->stack_refcount, 1);
#endif// 调用 account_kernel_stack 记录新任务的内核栈内存使用情况。account_kernel_stack(tsk, 1);err = scs_prepare(tsk, node);if (err)goto free_stack;#ifdef CONFIG_SECCOMP/** We must handle setting up seccomp filters once we're under* the sighand lock in case orig has changed between now and* then. Until then, filter must be NULL to avoid messing up* the usage counts on the error path calling free_task.*/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);// 清除系统调用用户调度标志。 clear_syscall_work_syscall_user_dispatch(tsk);// 如果启用了栈保护(CONFIG_STACKPROTECTOR),为新任务生成一个随机的栈保护值(stack_canary)。
#ifdef CONFIG_STACKPROTECTORtsk->stack_canary = get_random_canary();
#endif// 如果原任务的 CPU 掩码指针指向其自身的 cpus_mask,则新任务的指针也指向自身的 cpus_mask。if (orig->cpus_ptr == &orig->cpus_mask)tsk->cpus_ptr = &tsk->cpus_mask;// 调用 dup_user_cpus_ptr 复制用户定义的 CPU 掩码。dup_user_cpus_ptr(tsk, orig, node);/** One for the user space visible state that goes away when reaped.* One for the scheduler.*//** 初始化新任务的引用计数:* 	rcu_users:RCU(Read-Copy-Update)用户计数,初始值为 2。* 	usage:任务的使用计数,初始值为 1。*/refcount_set(&tsk->rcu_users, 2);/* One for the rcu users */refcount_set(&tsk->usage, 1);
#ifdef CONFIG_BLK_DEV_IO_TRACEtsk->btrace_seq = 0;
#endiftsk->splice_pipe = NULL;tsk->task_frag.page = NULL;tsk->wake_q.next = NULL;tsk->worker_private = NULL;// 初始化内核代码覆盖率跟踪(kcov)。kcov_task_init(tsk);// 初始化内核内存安全分析(kmsan)。kmsan_task_create(tsk);// 初始化局部内核映射。kmap_local_fork(tsk);#ifdef CONFIG_FAULT_INJECTIONtsk->fail_nth = 0;
#endif#ifdef CONFIG_BLK_CGROUPtsk->throttle_disk = NULL;tsk->use_memdelay = 0;
#endif#ifdef CONFIG_ARCH_HAS_CPU_PASIDtsk->pasid_activated = 0;
#endif#ifdef CONFIG_MEMCGtsk->active_memcg = NULL;
#endif#ifdef CONFIG_X86_BUS_LOCK_DETECTtsk->reported_split_lock = 0;
#endif#ifdef CONFIG_SCHED_MM_CIDtsk->mm_cid = -1;tsk->last_mm_cid = -1;tsk->mm_cid_active = 0;tsk->migrate_from_cpu = -1;
#endif// 如果所有初始化成功,返回新分配的 task_struct。return tsk;// 错误处理部分
free_stack:// 调用 exit_task_stack_account 和 free_thread_stack 释放线程栈exit_task_stack_account(tsk);free_thread_stack(tsk);
free_tsk:// 调用 free_task_struct 释放任务结构。free_task_struct(tsk);return NULL;
}

7.7.1. 参数及返回值

  • 参数

    • orig:指向当前进程的 task_struct,用于作为模板复制。
    • nodeNUMA 节点,用于指定在哪个内存节点上分配新任务结构。
  • 返回值

    • 成功时返回新分配的 task_struct 指针。
    • 失败时返回 NULL

7.7.2. 确定 NUMA 节点

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
if (node == NUMA_NO_NODE)node = tsk_fork_get_node(orig);
  • 作用
    • 如果未指定 NUMA 节点(node == NUMA_NO_NODE),调用 tsk_fork_get_node 根据当前进程的 NUMA 策略选择一个节点。

7.7.3. 分配 task_struct

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
tsk = alloc_task_struct_node(node);
if (!tsk)return NULL;
  • 作用
    • 调用 alloc_task_struct_node 在指定的 NUMA 节点上分配一个新的 task_struct
    • 如果分配失败,返回 NULL

7.7.4. 复制原始任务结构

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = arch_dup_task_struct(tsk, orig);
if (err)goto free_tsk;
  • 作用
    • 调用 arch_dup_task_struct 将原始任务结构 orig 的内容复制到新分配的 tsk 中。
    • 这是一个架构相关的函数,会根据具体的硬件架构执行额外的初始化。
    • 如果复制失败,跳转到 free_tsk 标签释放已分配的资源。

7.7.5. 分配线程栈

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = alloc_thread_stack_node(tsk, node);
if (err)goto free_tsk;
  • 作用
    • 调用 alloc_thread_stack_node 为新任务分配线程栈。
    • 如果分配失败,跳转到 free_tsk 标签释放已分配的资源。

7.7.6. 初始化安全上下文切换栈

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
// Function: dup_task_struct()
err = scs_prepare(tsk, node);
if (err)goto free_stack;
  • 作用
    • 调用 scs_prepare 初始化安全上下文切换栈(SCSSafe Context Switching)。
    • 如果初始化失败,跳转到 free_stack 标签释放线程栈。

  接下来初始化新任务的各种字段和子系统(如栈保护、CPU 掩码、引用计数等)。若所有步骤都成功完成则最终返回申请并初始化的 task_struct

7.8. dup_task_struct() 的核心函数 alloc_task_struct_node()

  这里 alloc_task_struct_node() 作为 dup_task_struct() 的核心部分用于从内核的 slab 缓存上分配一个内存来创建新的 task_struct(进程描述符)。 alloc_task_struct_node 是一个内联函数,实现如下:

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.c
static inline struct task_struct *alloc_task_struct_node(int node)
{return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}

  alloc_task_struct_node 函数只有一个参数 node,用于指定从 NUMA 节点,即从指定的 NUMA 节点分配内存。接下来该函数直接调用一个 kmem_cache_alloc_nodeslab 缓冲池中的内存分配函数就返回退出。那么接下来去看看 kmem_cache_alloc_node 函数的实现过程。

7.8.1. 调用 kmem_cache_alloc_node

参数描述:

  • kmem_cache_alloc_node 是内核提供的一个函数,用于从指定的 slab 缓存中分配内存。
  • 参数说明:
    • task_struct_cachep:
      • 这是一个全局变量,表示 task_structslab 缓存池。
      • 它在内核初始化时通过 kmem_cache_create_usercopy 创建。
    • GFP_KERNEL:
      • 分配内存时使用的标志,表示分配内存时可以睡眠。
      • 这是内核中最常用的分配标志,适用于大多数场景。
    • node:
      • 指定从哪个 NUMA 节点分配内存。
      • 如果系统支持 NUMA(非一致性内存访问),可以通过该参数优化内存分配的局部性。

  这里简单解释一下全局变量 task_struct_cachep 的初始化过程,该全局变量用于管理 task_structslab 缓存。在内核启动的早期阶段的初始化进程管理子系统时调用 kmem_cache_create_usercopy 函数创建了该 slab 缓存,具体调用流程如下图。

在这里插入图片描述

fork_init 函数中通过 kmem_cache_create_usercopy 初始化:

// Linux Kernel 6.15.0-rc2
// PATH: kernel/fork.ctask_struct_cachep = kmem_cache_create_usercopy("task_struct",arch_task_struct_size, align,SLAB_PANIC|SLAB_ACCOUNT,useroffset, usersize, NULL);

参数说明:

  • "task_struct": 缓存的名称。
  • arch_task_struct_size: task_struct 的大小,由架构定义。
  • align: 对齐要求,通常是 L1_CACHE_BYTES
  • SLAB_PANIC|SLAB_ACCOUNT: 分配标志,表示分配失败时触发 panic,并启用内存计费。
  • useroffsetusersize: 用于用户空间访问的偏移和大小。

7.8.2. kmem_cache_alloc_node 函数分析

函数定义:

// Linux Kernel 6.15.0-rc2
// PATH: include/linux/slab.h
void *kmem_cache_alloc_node_noprof(struct kmem_cache *s, gfp_t flags, int node)__assume_slab_alignment __malloc;#define kmem_cache_alloc_node(...) alloc_hooks(kmem_cache_alloc_node_noprof(__VA_ARGS__))
  • kmem_cache_alloc_node 是一个宏,最终调用的是 kmem_cache_alloc_node_noprof
  • kmem_cache_alloc_node_noprof 是实际的实现函数。
  • alloc_hooks 是一个宏,用于在内存分配函数的调用中插入额外的钩子逻辑。这些钩子通常用于调试、性能分析或其他扩展功能。

7.8.3. kmem_cache_alloc_node_noprof 函数分析

// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
void *kmem_cache_alloc_node_noprof(struct kmem_cache *s, gfp_t gfpflags, int node)
{// slab_alloc_node 是核心分配函数,负责从指定的 kmem_cache 中分配对象。void *ret = slab_alloc_node(s, NULL, gfpflags, node, _RET_IP_, s->object_size);// 调用 trace_kmem_cache_alloc 跟踪分配事件。该跟踪函数用于调试和性能分析,帮助开发者了解分配行为。trace_kmem_cache_alloc(_RET_IP_, ret, s, gfpflags, node);return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc_node_noprof);

  该函数的核心业务是调用 slab_alloc_node 函数从 kmem_cache 即这里的 *s 中分配内存,然后调用 trace_kmem_cache_alloc 跟踪分配事件后返回分配的内存对象指针。

7.8.3. slab_alloc_node 函数分析

// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s, struct list_lru *lru,gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{void *object;bool init = false;// 检查分配前的条件,例如是否需要触发内存分配失败注入(should_failslab)。s = slab_pre_alloc_hook(s, gfpflags);if (unlikely(!s))return NULL;// 调用 kfence_alloc 检查是否需要从 KFENCE(Kernel Electric Fence)分配内存。object = kfence_alloc(s, orig_size, gfpflags);if (unlikely(object))goto out;// 进入核心分配逻辑,尝试从指定的 slab 缓存中分配对象。object = __slab_alloc_node(s, gfpflags, node, addr, orig_size);// 如果启用了内存初始化选项(如 slab_want_init_on_free),清除对象中的空闲指针以防止数据泄露。maybe_wipe_obj_freeptr(s, object);// 检查是否需要初始化分配的对象init = slab_want_init_on_alloc(gfpflags, s);out:/** When init equals 'true', like for kzalloc() family, only* @orig_size bytes might be zeroed instead of s->object_size* In case this fails due to memcg_slab_post_alloc_hook(),* object is set to NULL*//** 执行分配后的钩子操作,包括:* 	* 内存初始化(如清零)。* 	* 调用内存控制组(memcg)相关的分配钩子。* 	* 调用 KASAN(Kernel Address Sanitizer)相关的分配钩子。*/slab_post_alloc_hook(s, lru, gfpflags, 1, &object, init, orig_size);// 返回分配的对象指针。return object;
}
// Linux Kernel 6.15.0-rc2
// PATH: mm/slub.c
// Function: kmem_cache_alloc_node_noprof()
void *ret = slab_alloc_node(s, NULL, gfpflags, node, _RET_IP_, s->object_size);

传入参数说明:

  • s: 指向分配对象的 kmem_cache
  • NULL: 传递的 list_lru 参数为 NULL,表示不涉及 LRU 列表;
  • gfpflags: 分配标志,控制分配行为(如是否允许阻塞、是否从高优先级内存分配等);
  • node: 指定的 NUMA 节点;
  • _RET_IP_: 调用者的返回地址,用于跟踪分配来源;
  • s->object_size: 分配对象的大小;

  在当前函数中的核心业务是调用 __slab_alloc_node 函数从指定的 slab 缓存中分配对象。

7.8.4. __slab_alloc_node 函数分析


static __always_inline void *__slab_alloc_node(struct kmem_cache *s,gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{struct kmem_cache_cpu *c;struct slab *slab;unsigned long tid;void *object;redo:// 获取当前 CPU 的 kmem_cache_cpu 结构体。c = raw_cpu_ptr(s->cpu_slab);// tid 是事务 ID,用于确保分配操作的原子性。tid = READ_ONCE(c->tid);barrier();// 快速路径尝试从当前 CPU 的 slab 缓存中分配对象。object = c->freelist;slab = c->slab;#ifdef CONFIG_NUMAif (static_branch_unlikely(&strict_numa) &&node == NUMA_NO_NODE) {struct mempolicy *mpol = current->mempolicy;if (mpol) {/** Special BIND rule support. If existing slab* is in permitted set then do not redirect* to a particular node.* Otherwise we apply the memory policy to get* the node we need to allocate on.*/if (mpol->mode != MPOL_BIND || !slab ||!node_isset(slab_nid(slab), mpol->nodes))node = mempolicy_slab_node();}}
#endif// 如果 freelist 和 slab 都非空,则尝试分配。// 并且如果当前 slab 不匹配指定的 NUMA 节点,或者 freelist 为空,则进入慢速路径。if (!USE_LOCKLESS_FAST_PATH() ||unlikely(!object || !slab || !node_match(slab, node))) {// 调用 __slab_alloc 处理慢速路径分配。object = __slab_alloc(s, gfpflags, node, addr, c, orig_size);} else {// 使用 get_freepointer_safe 获取下一个空闲对象。void *next_object = get_freepointer_safe(s, object);/** The cmpxchg will only match if there was no additional* operation and if we are on the right processor.** The cmpxchg does the following atomically (without lock* semantics!)* 1. Relocate first pointer to the current per cpu area.* 2. Verify that tid and freelist have not been changed* 3. If they were not changed replace tid and freelist** Since this is without lock semantics the protection is only* against code executing on this cpu *not* from access by* other cpus.*/// 使用 cmpxchg 原子操作更新 freelist 和 tid。if (unlikely(!__update_cpu_freelist_fast(s, object, next_object, tid))) {// 如果 cmpxchg 失败,说明发生了竞争,重新尝试分配。note_cmpxchg_failure("slab_alloc", s, tid);goto redo;}prefetch_freepointer(s, next_object);stat(s, ALLOC_FASTPATH);}// 如果分配成功,返回分配的对象指针。return object;
}

  __slab_alloc_nodeSLUB 分配器的核心函数之一,用于从指定的 slab 缓存中分配对象。支持 NUMA 节点感知,并结合快速路径慢速路径优化分配性能。

参数说明:

  • s: 指向分配对象的 kmem_cache
  • gfpflags: 分配标志,控制分配行为(如是否允许阻塞、是否从高优先级内存分配等)。
  • node: 指定的 NUMA 节点。
  • addr: 调用者的返回地址,用于跟踪分配来源。
  • orig_size: 原始分配大小。

实现细节

    1. 获取当前 CPU 的 kmem_cache_cpu 结构体;
    1. 检查快速路径,如果 freelistslab 都非空,并 slab 匹配指定的 NUMA 节点,则进入快速路径分配;
    1. 否则调用 __slab_alloc 处理慢速路径分配;
    1. 快速路径:使用 get_freepointer_safe 直接获取下一个 slab 缓冲池中空闲对象(获取失败则尝试重新分配);
    1. 慢速路径:调用 __slab_alloc 找到合适的 slab(若没有则新分配一个 slab 添加进当前 CPUslab 中)并返回;
    1. 返回分配的内存对象指针。

  到此 dup_task_struct 函数就获得了一个新的 task_struct 内存对象。在创建新进程中分配新的 task_struct 内存空间的工作流程就如下图所示。

在这里插入图片描述

7.9. fork() 函数的简要总体框图

  通过上文的分析,到这里我们也大致掌握了 fork() 函数的工作流程,也就是说你已经清楚了一个进程是如何被创建出来的,笔者将其关键流程简要画了出来以供大家梳理。

在这里插入图片描述

#上一篇

【Linux内核设计与实现】第三章——进程管理02

#下一篇

【Linux内核设计与实现】第三章——进程管理04

http://www.xdnf.cn/news/867.html

相关文章:

  • Python accumulate 函数详解
  • (二十九)安卓开发中DataBinding 和 ViewBinding详解
  • 线性代数-矩阵的秩
  • Vue---vue2和vue3的生命周期
  • 数字ic后端设计从入门到精通2(含fusion compiler, tcl教学)
  • 2025最新︱中国信通院静态应用程序安全测试(SAST)工具能力评估,悬镜安全灵脉AI通过评估!
  • 高保真动态项目管理图表集
  • 批量导出多个文件和文件夹名称与路径信息到Excel表格的详细方法
  • pytest基础-new
  • CSS基础-即学即用 -- 笔记1
  • Synopsys:printvar命令和puts/echo命令的区别
  • 15 - VDMA之SD卡读BMP图片显示实验
  • Unity中的数字孪生项目:两种输入方式对观察物体的实现
  • Linux系统安全及应用
  • android studio sdk unavailable和Android 安装时报错:SDK emulator directory is missing
  • Office文件内容提取 | 获取Word文件内容 |Javascript提取PDF文字内容 |PPT文档文字内容提取
  • 边缘计算场景下的GPU虚拟化实践(基于vGPU的QoS保障与算力隔离方案)
  • ‌信号调制与解调技术基础解析
  • Docker 集成KingBase
  • 瑞吉外卖-分页功能开发中的两个问题
  • 【分布式理论17】分布式调度3:分布式架构-从中央式调度到共享状态调度
  • 8.1 线性变换的思想
  • 基于遗传算法的智能组卷系统设计与实现(springboot+ssm+React+mysql)含万字详细文档
  • Elasticsearch中的_source字段讲解
  • hadoop与spark的区别和联系
  • 大模型面经 | 春招、秋招算法面试常考八股文附答案(三)
  • 主流大模型(如OpenAI、阿里云通义千问、Anthropic、Hugging Face等)调用不同API的参数说明及对比总结
  • 53、Spring Boot 详细讲义(十)(Spring Boot 高级主题)
  • Python自动化selenium-一直卡着不打开浏览器怎么办?
  • 2025.4.21总结