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
------------------------------------------------------(1)
if (!oldmm) //父进程如果内核线程,直接退出
return 0;
-----------------------------------------------------(2)
if (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钟情况:
- 如果是内核线程,mm指针为null,直接退出,每次调度到内核线程时,会借用上一个进程的mm结构,放在active_mm中
- 如果不是内核线程,并且设置了CLONE_VM flag,则说明是个用户线程,共享父进程的运行空间,所以把父进程的mm赋值给子线程
- 如果以上情况都不是,那么新建的肯定是个进程,有独立的运行空间,所以需要新建自己的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
-------------------------------------------------------(1)
if (!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() 源码 )