linux do_fork详解

Posted 贺二公子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux do_fork详解相关的知识,希望对你有一定的参考价值。

原文地址:https://blog.csdn.net/oqqYuJi12345678/article/details/102828714


文章目录


当内核调用kernel_thread函数创建内核线程或者应用程序系统调用fork创建进程以及使用pthread_create创建线程的时候,其在内核中最终调用的函数就是do_fork。

do_fork这个函数非常复杂,这边只介绍里面的两个子函数copy_mm和copy_thread。

1 copy_mm

do_fork()copy_mm()流程如下

do_fork
  --> copy_process
      --> copy_mm
//tsk参数是新建的子进程/线程的task结构
static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)

	struct mm_struct *mm, *oldmm;
	int retval;
 
	tsk->min_flt = tsk->maj_flt = 0;
	tsk->nvcsw = tsk->nivcsw = 0;
#ifdef CONFIG_DETECT_HUNG_TASK
	tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
#endif
 
	tsk->mm = NULL; 
	tsk->active_mm = NULL; 
 
	/*
	 * Are we cloning a kernel thread?
	 *
	 * We need to steal a active VM for that..
	 */
	oldmm = current->mm;  //获取父进程的mm结构,如果没有mm结构,则说明父进程是个内核线程,父进程的active_mm借用的是上个进程的mm
------------------------------------------------------1if (!oldmm) //父进程如果内核线程,直接退出
		return 0;
-----------------------------------------------------2if (clone_flags & CLONE_VM)   //如果设置了CLONE_VM,则说明新建的是个用户线程,共享父进程的运行空间
		atomic_inc(&oldmm->mm_users);
		mm = oldmm;
		goto good_mm;
	
 
	retval = -ENOMEM;
-----------------------------------------------------3)
	mm = dup_mm(tsk);//复制页表,新建运行空间
	if (!mm)
		goto fail_nomem;
 
good_mm:
	tsk->mm = mm;//为新建task设置mm
	tsk->active_mm = mm;
	return 0;
 
fail_nomem:
	return retval;

上面的copy_mm可以分为3钟情况:

  1. 如果是内核线程,mm指针为null,直接退出,每次调度到内核线程时,会借用上一个进程的mm结构,放在active_mm中
  2. 如果不是内核线程,并且设置了CLONE_VM flag,则说明是个用户线程,共享父进程的运行空间,所以把父进程的mm赋值给子线程
  3. 如果以上情况都不是,那么新建的肯定是个进程,有独立的运行空间,所以需要新建自己的mm_struct结构,linux基于写时复制的原则,先复制父进程的页表。

下面主要分析第三种情况,看一下子进程是如何复制父进程的页表的。

基本逻辑是linux中所有的进程和线程的内核空间都是一样的,所以内核页表必然都是相同的,那不同的就是用户空间部分。新建用户子进程的时候,子进程的用户空间页表会直接复制父进程的,在复制的时候,会把该页表指向的页面设置为写保护,也就说一开始父子进程用户空间也是相同的,等到父子进程真正要去读写这块空间的时候,触发页面读写中断,在中断函数里面去处理重新分配页面,这个时候父子进程的用户空间就真的分道扬镳了。

struct mm_struct *dup_mm(struct task_struct *tsk)

	struct mm_struct *mm, *oldmm = current->mm;
	int err;
 
	if (!oldmm)
		return NULL;
 
	mm = allocate_mm(); //分配一个mm_struct结构
	if (!mm)
		goto fail_nomem;
 
	memcpy(mm, oldmm, sizeof(*mm));//把父进程mm中的信息拷贝过来
	mm_init_cpumask(mm);
 
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
	mm->pmd_huge_pte = NULL;
#endif
#ifdef CONFIG_NUMA_BALANCING
	mm->first_nid = NUMA_PTE_SCAN_INIT;
#endif
-------------------------------------------------------1if (!mm_init(mm, tsk))  //复制父进程内核页表
		goto fail_nomem;
 
	if (init_new_context(tsk, mm))
		goto fail_nocontext;
 
	dup_mm_exe_file(oldmm, mm);
--------------------------------------------------------2)
	err = dup_mmap(mm, oldmm); //复制父进程用户空间页表
	if (err)
		goto free_pt;
 
	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;
	mmput(mm);
 
fail_nomem:
	return NULL;
 
fail_nocontext:
	/*
	 * If init_new_context() failed, we cannot use mmput() to free the mm
	 * because it calls destroy_context()
	 */
	mm_free_pgd(mm);
	free_mm(mm);
	return NULL;

上面两个比较重要的函数是mm_init和dup_mmap,下面分别说明。

1.1 mm_init

mm_init函数的主要作用就是复制父进程的内核页表。

static struct mm_struct *mm_init(struct mm_struct *mm, struct task_struct *p)

	atomic_set(&mm->mm_users, 1);
	atomic_set(&mm->mm_count, 1);
	init_rwsem(&mm->mmap_sem);
	INIT_LIST_HEAD(&mm->mmlist);
	mm->flags = (current->mm) ?
		(current->mm->flags & MMF_INIT_MASK) : default_dump_filter;
	mm->core_state = NULL;
	mm->nr_ptes = 0;
	memset(&mm->rss_stat, 0, sizeof(mm->rss_stat));
	spin_lock_init(&mm->page_table_lock);
	mm->free_area_cache = TASK_UNMAPPED_BASE;
	mm->cached_hole_size = ~0UL;
	mm_init_aio(mm);
	mm_init_owner(mm, p);
 
	if (likely(!mm_alloc_pgd(mm)))  //这个函数负责内核页表的复制
		mm->def_flags = 0;
		mmu_notifier_mm_init(mm);
		return mm;
	
 
	free_mm(mm);
	return NULL;

mm_init() -> mm_alloc_pgd()

static inline int mm_alloc_pgd(struct mm_struct *mm)

	mm->pgd = pgd_alloc(mm);//把页表基地址赋值给pgd
	if (unlikely(!mm->pgd))
		return -ENOMEM;
	return 0;

pgd_alloc函数完成了页表页目录空间的分配以及内核页表的复制

mm_init() -> mm_alloc_pgd() -> pgd_alloc()

#define __pgd_alloc()	(pgd_t *)__get_free_pages(GFP_KERNEL, 2)
pgd_t *pgd_alloc(struct mm_struct *mm)

	pgd_t *new_pgd, *init_pgd;
	pud_t *new_pud, *init_pud;
	pmd_t *new_pmd, *init_pmd;
	pte_t *new_pte, *init_pte;
 
	new_pgd = __pgd_alloc();//分配页目录空间,order为2,所以为16K,每个页目录映射1M空间,总共4096个页目录,刚好对应4G的空间
	if (!new_pgd)
		goto no_pgd;
//把内核空间0xc0000000向下偏移16M 的用户空间页表先全部清零
	memset(new_pgd, 0, USER_PTRS_PER_PGD * sizeof(pgd_t));
 
	/*
	 * Copy over the kernel and IO PGD entries
	 */
	init_pgd = pgd_offset_k(0);//取得init_mm记录的页目录的基地址,init_mm是一开始系统启动的时候给init task用的,因为所有的进程的内核页表都相同,这边为了方便直接取init进程的
//对内核页目录进行复制,起始地址为内核空间0xc0000000向下偏移16M,end 地址为0xffffffff
	memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD,
		       (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
 
	clean_dcache_area(new_pgd, PTRS_PER_PGD * sizeof(pgd_t));
 
#ifdef CONFIG_ARM_LPAE //这个宏没定义
	/*
	 * Allocate PMD table for modules and pkmap mappings.
	 */
	new_pud = pud_alloc(mm, new_pgd + pgd_index(MODULES_VADDR),
			    MODULES_VADDR);
	if (!new_pud)
		goto no_pud;
 
	new_pmd = pmd_alloc(mm, new_pud, 0);
	if (!new_pmd)
		goto no_pmd;
#endif
 
	if (!vectors_high())  //异常向量放在高地址,这个分支不会走
		/*
		 * On ARM, first page must always be allocated since it
		 * contains the machine vectors. The vectors are always high
		 * with LPAE.
		 */
		new_pud = pud_alloc(mm, new_pgd, 0);
		if (!new_pud)
			goto no_pud;
 
		new_pmd = pmd_alloc(mm, new_pud, 0);
		if (!new_pmd)
			goto no_pmd;
 
		new_pte = pte_alloc_map(mm, NULL, new_pmd, 0);
		if (!new_pte)
			goto no_pte;
 
		init_pud = pud_offset(init_pgd, 0);
		init_pmd = pmd_offset(init_pud, 0);
		init_pte = pte_offset_map(init_pmd, 0);
		set_pte_ext(new_pte, *init_pte, 0);
		pte_unmap(init_pte);
		pte_unmap(new_pte);
	
 
	return new_pgd;
 
no_pte:
	pmd_free(mm, new_pmd);
no_pmd:
	pud_free(mm, new_pud);
no_pud:
	__pgd_free(new_pgd);
no_pgd:
	return NULL;

具体的解释已经在代码中给出,到这边一开始我就有一个疑问,arm内存映射有两种方式,一种是段映射,一种是页表映射,系统初始化的时候,其物理内存段采用的是段映射(包含代码段),但是其异常向量表以及寄存器的映射,其虚拟地址虽然都是在内核空间,但是采用的是2级页表映射,上面的复制只是复制了第一级页表,也就是页目录而已,不会有问题么?

对于物理内存,采用段映射,只有一级页表肯定没有问题。其实对于异常向量表等虽然只是复制了一级页目录,其实也是没有问题的!如果对内核页表的初始化不了解,可以先看这篇文章:linux3.10 paging_init页表初始化详解_oqqYuJi12345678的博客-CSDN博客

早期在初始化页表的时候有这么一个函数:

static pte_t * __init early_pte_alloc(pmd_t *pmd, unsigned long addr, unsigned long prot)

	if (pmd_none(*pmd)) 
		pte_t *pte = early_alloc(PTE_HWTABLE_OFF + PTE_HWTABLE_SIZE);
		__pmd_populate(pmd, __pa(pte), prot);
	
	BUG_ON(pmd_bad(*pmd));
	return pte_offset_kernel(pmd, addr);

当页目录为空的时候,为这个页目录分配二级页表,并把得到的二级页表的地址填到页目录中。二级页表的分配方式通过early_alloc,其分配的空间的地址是物理地址加上一个逻辑偏移,也就是其虚拟地址总是落在物理地址所对应的虚拟空间中,假设物理地址为0x30000000x34000000,其虚拟地址为0xc0000000xc4000000,这块地址是段映射。来模拟一下这个情景,假设异常中断到来,根据其虚拟地址,肯定能找到一级页目录,然后一级页目录指向的二级页表的虚拟地址肯定是在0xc000000~0xc4000000这个范围内,所以最终能找到其物理地址。

由此看,上面只复制一级页目录的行为看起来是没问题的,二级页表虚拟地址总是落在物理地址的逻辑地址空间中,而这块逻辑地址早就映射过了。

另一个需要注意的问题是,vmalloc分配的内存页表,其实不在普通进程的页表中,因为要实现所有的进程共享同一块啮合空间,所以vmalloc的内存分配的时候,页表是填写在init 进程的页表中:

inline int vmalloc_area_pages (unsigned long address, unsigned long size,
                               int gfp_mask, pgprot_t prot)

    pgd_t * dir;
    unsigned long end = address + size;
    int ret;
 
    dir = pgd_offset_k(address);         // 获取 address 地址在 init 进程对应的页目录项
    spin_lock(&init_mm.page_table_lock); // 对 init_mm 上锁
    do 
        pmd_t *pmd;
 
        pmd = pmd_alloc(&init_mm, dir, address);
        ret = -ENOMEM;
        if (!pmd)
            break;
 
        ret = -ENOMEM;
        if (alloc_area_pmd(pmd, address, end - address, gfp_mask, prot)) // 对页目录项进行映射
            break;
 
        address = (address + PGDIR_SIZE) & PGDIR_MASK;
        dir++;
 
        ret = 0;
     while (address && (address < end));
    spin_unlock(&init_mm.page_table_lock);
    return ret;

而当其他进程需要访问vmalloc的空间时,会产生一个缺页异常,在该缺页异常里面,复制init进程的页表,完成空间的共享:

	index = pgd_index(addr);
 
	pgd = cpu_get_pgd() + index; //从页表寄存器获取出错的页目录地址
	pgd_k = init_mm.pgd + index;//获取init进程的页目录地址
 
	if (pgd_none(*pgd_k))
		goto bad_area;
	if (!pgd_present(*pgd))
		set_pgd(pgd, *pgd_k);
 
	pud = pud_offset(pgd, addr);
	pud_k = pud_offset(pgd_k, addr);
 
	if (pud_none(*pud_k))
		goto bad_area;
	if (!pud_present(*pud))
		set_pud(pud, *pud_k);
 
	pmd = pmd_offset(pud, addr);
	pmd_k = pmd_offset(pud_k, addr);
 
#ifdef CONFIG_ARM_LPAE
	/*
	 * Only one hardware entry per PMD with LPAE.
	 */
	index = 0;
#else
	/*
	 * On ARM one Linux PGD entry contains two hardware entries (see page
	 * tables layout in pgtable.h). We normally guarantee that we always
	 * fill both L1 entries. But create_mapping() doesn't follow the rule.
	 * It can create inidividual L1 entries, so here we have to call
	 * pmd_none() check for the entry really corresponded to address, not
	 * for the first of pair.
	 */
	index = (addr >> SECTION_SHIFT) & 1;
#endif
	if (pmd_none(pmd_k[index]))
		goto bad_area;
 
	copy_pmd(pmd, pmd_k);

1.2 dup_mmap

该函数的主要作用是复制用户空间页表。

static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)

	struct vm_area_struct *mpnt, *tmp, *prev, **pprev;
	struct rb_node **rb_link, *rb_parent;
	int retval;
	unsigned long charge;
	struct mempolicy *pol;
 
	uprobe_start_dup_mmap();
	down_write(&oldmm->mmap_sem);
	flush_cache_dup_mm(oldmm);
	uprobe_dup_mmap(oldmm, mm);
	/*
	 * Not linked in yet - no deadlock potential:
	 */
	down_write_nested(&mm->mmap_sem, SINGLE_DEPTH_NESTING);
 
	mm->locked_vm = 0;
	mm->mmap = NULL;
	mm->mmap_cache = NULL;
	mm->free_area_cache = oldmm->mmap_base;
	mm->cached_hole_size = ~0UL;
	mm->map_count = 0;
	cpumask_clear(mm_cpumask(mm));
	mm->mm_rb = RB_ROOT;
	rb_link = &mm->mm_rb.rb_node;
	rb_parent = NULL;
	pprev = &mm->mmap;
	retval = ksm_fork(mm, oldmm);
	if (retval)
		goto out;
	retval = khugepaged_fork(mm, oldmm);
	if (retval)
		goto out;
 
	prev = NULL;
	for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next)  \\\\遍历父进程的虚存空间
		struct file *file;
 
		if (mpnt->vm_flags & VM_DONTCOPY) 
			vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
							-vma_pages(mpnt));
			continue;
		
		charge = 0;
		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 以上是关于linux do_fork详解的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核进程管理 ( 进程相关系统调用源码分析 | fork() 源码 | vfork() 源码 | clone() 源码 | _do_fork() 源码 | do_fork() 源码 )

do_fork源码阅读

CFS调度器——源码解析

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

Linux内核 fork 源码分析

do_fork函数