【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
)这一机制的原理:
-
- 拷贝页表项:
copy_page_range
会遍历父进程的虚拟内存区域(VMA
)对应的页表,将父进程的页表项复制到子进程的页表中。(按照x86_64
的五级页表结构PGD
、P4D
、PUD
、PMD
、PTE
顺序逐级递归复制页表项)- 页表项中会标记页面为只读(通过设置页表项的权限位)。
-
- 实现写时拷贝(
COW
):
- 页面被标记为只读后,父进程和子进程共享相同的物理页面。
- 当父进程或子进程尝试写入这些页面时,会触发页面错误(
Page Fault
)。 - 页面错误处理程序会为写入的进程分配一个新的物理页面,并将原页面的内容复制到新页面中。
- 实现写时拷贝(
-
- 延迟实际的页面分配:
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
,用于作为模板复制。node
:NUMA
节点,用于指定在哪个内存节点上分配新任务结构。
-
返回值:
- 成功时返回新分配的
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
初始化安全上下文切换栈(SCS
,Safe 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_node
从 slab
缓冲池中的内存分配函数就返回退出。那么接下来去看看 kmem_cache_alloc_node
函数的实现过程。
7.8.1. 调用 kmem_cache_alloc_node
参数描述:
kmem_cache_alloc_node
是内核提供的一个函数,用于从指定的slab
缓存中分配内存。- 参数说明:
task_struct_cachep
:- 这是一个全局变量,表示
task_struct
的slab
缓存池。 - 它在内核初始化时通过
kmem_cache_create_usercopy
创建。
- 这是一个全局变量,表示
GFP_KERNEL
:- 分配内存时使用的标志,表示分配内存时可以睡眠。
- 这是内核中最常用的分配标志,适用于大多数场景。
node
:- 指定从哪个
NUMA
节点分配内存。 - 如果系统支持
NUMA
(非一致性内存访问),可以通过该参数优化内存分配的局部性。
- 指定从哪个
这里简单解释一下全局变量 task_struct_cachep 的初始化过程,该全局变量用于管理 task_struct
的 slab
缓存。在内核启动的早期阶段的初始化进程管理子系统时调用 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
,并启用内存计费。useroffset
和usersize
: 用于用户空间访问的偏移和大小。
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_node
是 SLUB
分配器的核心函数之一,用于从指定的 slab
缓存中分配对象。支持 NUMA
节点感知,并结合快速路径和慢速路径优化分配性能。
参数说明:
s
: 指向分配对象的kmem_cache
。gfpflags
: 分配标志,控制分配行为(如是否允许阻塞、是否从高优先级内存分配等)。node
: 指定的 NUMA 节点。addr
: 调用者的返回地址,用于跟踪分配来源。orig_size
: 原始分配大小。
实现细节
-
- 获取当前 CPU 的
kmem_cache_cpu
结构体;
- 获取当前 CPU 的
-
- 检查快速路径,如果
freelist
和slab
都非空,并slab
匹配指定的NUMA
节点,则进入快速路径分配;
- 检查快速路径,如果
-
- 否则调用
__slab_alloc
处理慢速路径分配;
- 否则调用
-
- 快速路径:使用
get_freepointer_safe
直接获取下一个slab
缓冲池中空闲对象(获取失败则尝试重新分配);
- 快速路径:使用
-
- 慢速路径:调用
__slab_alloc
找到合适的slab
(若没有则新分配一个slab
添加进当前CPU
的slab
中)并返回;
- 慢速路径:调用
-
- 返回分配的内存对象指针。
到此 dup_task_struct
函数就获得了一个新的 task_struct
内存对象。在创建新进程中分配新的 task_struct
内存空间的工作流程就如下图所示。
7.9. fork() 函数的简要总体框图
通过上文的分析,到这里我们也大致掌握了 fork()
函数的工作流程,也就是说你已经清楚了一个进程是如何被创建出来的,笔者将其关键流程简要画了出来以供大家梳理。
#上一篇
【Linux内核设计与实现】第三章——进程管理02
#下一篇
【Linux内核设计与实现】第三章——进程管理04