知识都是相通的--学无止境
分类: LINUX
2015-07-01 14:12:03
原文地址:linux进程地址空间 作者:雾琰
开门见山,在arch/arm/kernel/sys_arm.c文件中,有这样三个函数:sys_fork、sys_vfork、sys_clone,它们都是在创建进程,分别对应系统调用fork()、vfork()、clone(),下面是它们在arm中的函数实现:
顺便说一下,系统调用是如何对应到内核接口函数的(fork()->sys_fork),在之后会有文章专门研讨,这里重点讨论的是进程地址空间的问题;
asmlinkage int sys_fork(struct pt_regs *regs)
{
#ifdef CONFIG_MMU
return do_fork(SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
#else
/* can not support in nommu mode */
return(-EINVAL);
#endif
}
/* Clone a task - this clones the calling program thread.
* This is called indirectly via a small wrapper
*/
asmlinkage int sys_clone(unsigned long clone_flags, unsigned long newsp,
int __user *parent_tidptr, int tls_val,
int __user *child_tidptr, struct pt_regs *regs)
{
if (!newsp)
newsp = regs->ARM_sp;
return do_fork(clone_flags, newsp, regs, 0, parent_tidptr, child_tidptr);
}
asmlinkage int sys_vfork(struct pt_regs *regs)
{
return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs->ARM_sp, regs, 0, NULL, NULL);
}
它们都是调用函数do_fork(),其中第一参数不同,fork的是SIGCHLD,vfork的是CLONE_VFORK | CLONE_VM | SIGCHLD,clone的由调用者决定;
函数do_fork是一个很复杂很复杂的过程,这里仅仅讨论关于进程地址空间的内容,直接它调用函数copy_process,函数copy_process将完成操作系统创建进程的各个方面的事务,函数庞大而复杂,本文只关心进程地址空间方面,直接看它又调用函数copy_mm;
这个函数很重要,用于决定子进程的地址空间情况,先把task_struct的mm和active_mm初始化为NULL,同时取得父进程的mm即current->mm,然后是一个重要内容,根据参数clone_flags是否具有CLONE_VM标志界定所创建的进程是否和父进程共享一个地址空间,比如调用vfork时,或者调用clone时使用CLONE_VM标志的情况下,子进程将和父进程共有同一地址空间,具体来说就是父进程的mm就是子进程的mm,父进程的mm的mm_users成员加一意为这一mm的使用者加一,子进程的task->mm和task->active_mm均指向父进程的mm;如果没有CLONE_VM标志,那么子进程就得自己创建自己独立的进程地址空间,调用函数dup_mm;整个copy_mm函数的源码如下:
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..
*/
/*取得当前进程的mm,当前进程正在创建新进程*/
oldmm = current->mm;
if (!oldmm)
return 0;
/*一旦有标志CLONE_VM,说明父子进程共享同一地址空间,
这样新进程的mm就是父进程的mm,只需增加父进程的地址空间引用计数即可*/
if (clone_flags & CLONE_VM) {
atomic_inc(&oldmm->mm_users);
mm = oldmm;
goto good_mm;
}
/*没有CLONE_VM标志,说明父子进程不要共享同一地址空间,
换句话说就是需要子进程自己创建自己的地址空间*/
retval = -ENOMEM;
mm = dup_mm(tsk);
if (!mm)
goto fail_nomem;
good_mm:
/* Initializing for Swap token stuff */
mm->token_priority = 0;
mm->last_interval = 0;
/*设置task的mm,active_mm字段*/
tsk->mm = mm;
tsk->active_mm = mm;
return 0;
fail_nomem:
return retval;
}
可见如果是与父进程共享进程地址空间的话是非常简单的,它无需创建子进程的进程地址空间;同时可以发现,vfork其实是clone的一种实现,对于vfork,子进程将阻塞父进程的运行直到子进程退出或转而执行其他应用程序为止;
父子进程虽然共享同一进程地址空间,但实际上运行互不影响,这是COW即写时复制实现这一点,父子进程任何一方向写共享页时,会触发缺页异常的COW,这时会从buddy申请一个新页给触发者,并把那个共享页的内容复制到这个新页,需要注意共享页本身依然是写保护,只是当另外一方也要写共享页时,linux可以判断出只有这一个进程使用该页,就会使该页属性变为可写,具体过程在后面会详述。
下面详细描述需要子进程创建自己的mm的情况,关注函数dup_mm,在这里能看到子进程的mm_struct变量的创建和初步初始化及调用重点函数dup_mmap,dup_mm的目的就是创建它的mm并把父进程的mm尤其它的vma、二级页表映射的页都放进子进程的mm,源码如下:
struct mm_struct *dup_mm(struct task_struct *tsk)
{
struct mm_struct *mm, *oldmm = current->mm;
int err;
/*每个进程都是由另一个父进程创建的子进程,init 1进程是所有进程的祖先
如果没有父进程,那么失败返回NULL*/
if (!oldmm)
return NULL;
/*由slab分配一个mm_struct结构,作为子进程的mm
并把父进程的内容全都拷贝进去*/
mm = allocate_mm();
if (!mm)
goto fail_nomem;
memcpy(mm, oldmm, sizeof(*mm));
/* Initializing for Swap token stuff */
/*与交换(swap)相关的初始化*/
mm->token_priority = 0;
mm->last_interval = 0;
/*初始化新进程的mm*/
if (!mm_init(mm, tsk))
goto fail_nomem;
/*对于arm为空函数*/
if (init_new_context(tsk, mm))
goto fail_nocontext;
dup_mm_exe_file(oldmm, mm);
/*继承父进程的vma和vma内虚拟地址对物理地址的映射*/
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;
}
首先通过slab分配一个mm,然后把父进程的mm的全部内容拷贝到子进程的mm,这是一个明显的继承动作,然后调用函数mm_init对子进程的mm再进行一些初始化,最后调用函数dup_mmap把父进程的vma和二级页表映射的页全都继承过来;
先看下函数mm_init,它首先初始化mm的mm_users成员和mm_count成员为1,mm_users的意思是这个mm被多少个进程引用,比如父进程用一个mm,这时有个子进程与父进程共享进程地址空间,那么mm_users就要加一;
mm_count的意思比较乱一些,对linux来说,用户进程和内核线程都是一个个的task_struct的实例,唯一的区别是内核线程是没有进程地址空间的,也没有mm,所以内核线程的tsk->mm域是NULL(只在内核初始化时以init_mm作为内核的mm);而在切换上下文时,会根据tsk->mm判断即将调度的是用户进程还是内核线程,虽然内核线程不会去访问用户进程地址空间,但它仍然需要页表来访问它自己的空间即内核空间,不过对于任何用户进程来说,他们的内核空间都是完全相同的,所以内核可以借用上一个被调用的用户进程的mm中的页表来访问内核地址,这个mm就记录在mm的active_mm成员;
简而言之就是,对于内核线程,tsk->mm == NULL表示自己内核线程的身份,而tsk->active_mm是借用上一个用户进程的mm,用这个mm的页表来访问内核空间;对于用户进程,tsk->mm == tsk->active_mm;
为了这个原因,mm_struct里面引入了另外一个counter,mm_count;刚才说过mm_users表示这个进程地址空间被多少进程共享,则mm_count表示这个地址空间被内核线程引用的次数,比如一个进程A有3个线程,那么这个A的mm_struct的mm_users值为3,mm_count为1,mm_count是process level的counter;
维护2个计数的用处呢?考虑这样的情况,内核调度完用户进程A以后,切换到内核内核线程B,B 借用A的mm描述符以访问内核空间,这时mm_count变成了2,同时另外一个cpu core调度了A并且进程A exit,这个时候mm_users变为了0,mm_count变为了1,但是内核不会因为mm_users==0而销毁这个原先用户进程A的mm_struct,因为内核线程B还在使用!只会当mm_count==0的时候才会释放这个mm_struct,因为这个时候既没有用户进程使用这个地址空间,也没有内核线程引用这个地址空间。
回到函数mm_init,注意下这个初始化,“set_mm_counter(mm, file_rss, 0);和set_mm_counter(mm, anon_rss, 0);”,这是说这个mm的匿名映射和文件映射的个数,所谓匿名映射是相对于文件映射说的,简单的说就是不是文件映射的就是匿名映射,本质都是对vma线性区的映射;然后注意下free_area_cache成员和cached_hole_size的初始化,这两个成员是用于查找空闲的vma地址空间用的,后面会看到它们的用途;
最后看一下mm_init的最重要的初始化,mm_alloc_pgd,这是创建子进程的内存页表,我们知道内核有一个内存页表,确切的说是一个一级页表,用户进程也一样,每个进程都有一个自己的内存一级页表,函数调用顺序是:mm_alloc_pgd->pgd_alloc->get_pgd_slow,看下函数get_pgd_slow,只需看该函数的前几行即可:
pgd_t *get_pgd_slow(struct mm_struct *mm)
{
pgd_t *new_pgd, *init_pgd;
pmd_t *new_pmd, *init_pmd;
pte_t *new_pte, *init_pte;
/*从buddy获取4个连续的物理页,返回虚拟起始地址new_pgd作为一级页表条目*/
new_pgd = (pgd_t *)__get_free_pages(GFP_KERNEL, 2);
if (!new_pgd)
goto no_pgd;
memset(new_pgd, 0, FIRST_KERNEL_PGD_NR * sizeof(pgd_t));
……………………….
}
调用函数__get_free_pages从buddy获取4页连续物理内存,这就是子进程的内存一级页表,4页内存共16K空间,每个一级页表条目占8字节,这样就是2048个条目,每个一级页表条目可寻址2MB,所以一共可以寻址4G的空间,这就是子进程的内存页表。页表的地址将存在子进程mm的pgd成员。
回到dup_mm,现在已经拷贝了父进程的mm的全部内容并又初始化了一部分内容,接下来调用函数dup_mmap,把父进程的vma和里边映射的物理页都继承下来,这是最重要的内容,这个函数的本质内容是:
1、 父进程的mm有多少个vma,子进程就创建多少个vma;
2、 并把父进程的所有vma管理结构变量的内容都全盘复制到子进程的所有vma;
3、 父进程的每个vma都映射了哪些物理页,子进程的vma也映射哪些物理页;
4、 父进程和子进程对这些物理页的二级映射都设置为写保护,这是为了实现写时复制COW(后面会看到);
5、 此外子进程的vma相关成员需要做一些初始化;
dup_mmap函数描述见下一篇......
接上一篇,dup_mmap函数源码如下:
static int dup_mmap(struct mm_struct *mm, struct mm_struct *oldmm)
{
struct vm_area_struct *mpnt, *tmp, **pprev;
struct rb_node **rb_link, *rb_parent;
int retval;
unsigned long charge;
struct mempolicy *pol;
down_write(&oldmm->mmap_sem);
flush_cache_dup_mm(oldmm);
/*
* 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;
/*遍历父进程的每个vma,准备复制*/
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
struct file *file;
/*不让拷贝的vma,跳过*/
if (mpnt->vm_flags & VM_DONTCOPY) {
long pages = vma_pages(mpnt);
mm->total_vm -= pages;
vm_stat_account(mm, mpnt->vm_flags, mpnt->vm_file,
-pages);
continue;
}
charge = 0;
if (mpnt->vm_flags & VM_ACCOUNT) {
unsigned int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
if (security_vm_enough_memory(len))
goto fail_nomem;
charge = len;
}
/*注意,这里从slab创建vma,这是给新进程用的*/
tmp = kmem_cache_alloc(vm_area_cachep, GFP_KERNEL);
if (!tmp)
goto fail_nomem;
/*把父进程的vma内容拷贝到这个vma*/
*tmp = *mpnt;
pol = mpol_dup(vma_policy(mpnt));
retval = PTR_ERR(pol);
if (IS_ERR(pol))
goto fail_nomem_policy;
vma_set_policy(tmp, pol);
/*不锁定*/
tmp->vm_flags &= ~VM_LOCKED;
/*回指自己(新进程)的mm*/
tmp->vm_mm = mm;
tmp->vm_next = NULL;
anon_vma_link(tmp);
/*看看这个vma是不是文件映射的*/
file = tmp->vm_file;
if (file) {
struct inode *inode = file->f_path.dentry->d_inode;
struct address_space *mapping = file->f_mapping;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
spin_lock(&mapping->i_mmap_lock);
if (tmp->vm_flags & VM_SHARED)
mapping->i_mmap_writable++;
tmp->vm_truncate_count = mpnt->vm_truncate_count;
flush_dcache_mmap_lock(mapping);
/* insert tmp into the share list, just after mpnt */
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(mapping);
spin_unlock(&mapping->i_mmap_lock);
}
/*
* Clear hugetlb-related page reserves for children. This only
* affects MAP_PRIVATE mappings. Faults generated by the child
* are not guaranteed to succeed, even if read-only
*/
if (is_vm_hugetlb_page(tmp))
reset_vma_resv_huge_pages(tmp);
/*
* Link in the new vma and copy the page table entries.
*/
/*更新新进程的vma链表节点指针为这个tmp的vma,并且指定好pprev准备下一个节点*/
*pprev = tmp;
pprev = &tmp->vm_next;
/*插入红黑树*/
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb;
/*更新新进程的mm的vma个数*/
mm->map_count++;
/*参数分别为: 子进程的mm、父进程的mm、父进程的某个vma
复制mpnt地址空间部分的页表项到新进程的mm*/
retval = copy_page_range(mm, oldmm, mpnt);
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
if (retval)
goto out;
}
/* a new mm has just been created */
arch_dup_mmap(oldmm, mm);
retval = 0;
out:
up_write(&mm->mmap_sem);
flush_tlb_mm(oldmm);
up_write(&oldmm->mmap_sem);
return retval;
fail_nomem_policy:
kmem_cache_free(vm_area_cachep, tmp);
fail_nomem:
retval = -ENOMEM;
vm_unacct_memory(charge);
goto out;
}
对于内核源码的很多部分,一时间可能很难完全搞清搞透传,但重在理解它的意图和把握一个脉络,很多结构体成员众多,短时间内很难全部搞清,重点不要放在这里;
这里多数加了注释的就是我目前感觉比较有用的,或者说能看懂的,它们也比较清晰,for循环拷贝了父进程的所有vma给子进程,子进程又做了相应初始化,现在重点看下函数copy_page_range,它的功能是把父进程的页映射关系也复制给子进程,这里涉及了内存页表的知识,不熟悉的或忘了的可从这篇文章往前找到arm-linux内存页表创建(http://blog.csdn.net/u010246947/article/details/9837147),里边描述的比较清楚,也可以直接看下边copy_page_range:
int copy_page_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
struct vm_area_struct *vma)
{
pgd_t *src_pgd, *dst_pgd;
unsigned long next;
/*注意参数vma是父进程的vma,所以这里的addr和end也是这个父进程的vma的首尾地址*/
unsigned long addr = vma->vm_start;
unsigned long end = vma->vm_end;
int ret;
/*
* Don't copy ptes where a page fault will fill them correctly.
* Fork becomes much lighter when there are big shared or private
* readonly mappings. The tradeoff is that copy_page_range is more
* efficient than faulting.
*/
if (!(vma->vm_flags &
(VM_HUGETLB|VM_NONLINEAR|VM_PFNMAP|VM_INSERTPAGE))) {
if (!vma->anon_vma)
return 0;
}
if (is_vm_hugetlb_page(vma))
return copy_hugetlb_page_range(dst_mm, src_mm, vma);
if (unlikely(is_pfn_mapping(vma))) {
/*
* We do not free on error cases below as remove_vma
* gets called on error from higher level routine
*/
ret = track_pfn_vma_copy(vma);
if (ret)
return ret;
}
/*
* We need to invalidate the secondary MMU mappings only when
* there could be a permission downgrade on the ptes of the
* parent mm. And a permission downgrade will only happen if
* is_cow_mapping() returns true.
*/
if (is_cow_mapping(vma->vm_flags))
/*空函数*/
mmu_notifier_invalidate_range_start(src_mm, addr, end);
ret = 0;
/*先后是子进程、父进程的mm的页表,注意父子进程的pgd是不同的(即mm->pgd)*/
dst_pgd = pgd_offset(dst_mm, addr);
src_pgd = pgd_offset(src_mm, addr);
/*循环次数未知,得看addr和end相差多少个2MB*/
do {
/*对于arm,2MB为单位,一段一段来*/
next = pgd_addr_end(addr, end);
/*对于arm,底下的if默认为0不会进入*/
if (pgd_none_or_clear_bad(src_pgd))
continue;
if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
if (is_cow_mapping(vma->vm_flags))
/*空函数*/
mmu_notifier_invalidate_range_end(src_mm,
vma->vm_start, end);
return ret;
}
这个函数的重点首先看两个变量addr和end,分别是这个父进程的这个vma线性区的起始和结尾虚拟地址,后面就是把这个区间的虚拟物理映射复制到子进程的vma线性区,先看下边这个:
/*先后是子进程、父进程的mm的页表,注意父子进程的pgd是不同的(即mm->pgd)*/
dst_pgd = pgd_offset(dst_mm, addr);
src_pgd = pgd_offset(src_mm, addr);
如上面注释所说,是父子各自mm的pgd成员,即各自进程的一级页表,然后是下面的循环:
/*循环次数未知,得看addr和end相差多少个2MB*/
do {
/*对于arm,2MB为单位,一段一段来*/
next = pgd_addr_end(addr, end);
/*对于arm,底下的if默认为0不会进入*/
if (pgd_none_or_clear_bad(src_pgd))
continue;
if (unlikely(copy_pud_range(dst_mm, src_mm, dst_pgd, src_pgd,
vma, addr, next))) {
ret = -ENOMEM;
break;
}
} while (dst_pgd++, src_pgd++, addr = next, addr != end);
熟悉内存页表的肯定理解这是在干什么,这就是从addr到end这个区间,以2MB为单位不断调用函数copy_pud_range,所以说循环次数未知;对于函数copy_pud_range,它就是实际的拷贝映射关系,linux四级映射在arm上结合为两级映射,所以接下来调用的函数copy_pud_range和copy_pmd_range实际上相当于重复执行一遍,对结果没有影响,直到函数copy_pte_range,这时函数copy_pte_range的参数依然是父子mm、父子pgd、父vma、vma的首尾地址,源码如下:
static int copy_pte_range(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pmd_t *dst_pmd, pmd_t *src_pmd, struct vm_area_struct *vma,
unsigned long addr, unsigned long end)
{
pte_t *orig_src_pte, *orig_dst_pte;
pte_t *src_pte, *dst_pte;
spinlock_t *src_ptl, *dst_ptl;
int progress = 0;
int rss[2];
again:
rss[1] = rss[0] = 0;
/*这就是给子进程的创建二级页表,再次证明一级页表常驻内存,二级页表要靠分配*/
dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
if (!dst_pte)
return -ENOMEM;
/*得出对于当前的虚拟地址,父进程的二级页表条目*/
src_pte = pte_offset_map_nested(src_pmd, addr);
src_ptl = pte_lockptr(src_mm, src_pmd);
spin_lock_nested(src_ptl, SINGLE_DEPTH_NESTING);
orig_src_pte = src_pte;
orig_dst_pte = dst_pte;
arch_enter_lazy_mmu_mode();
/*以addr与end差值为2MB的正常情况下,将循环2MB/4KB=512次,
每次copy_one_pte将把父进程的二级页表映射内容拷贝给子进程二级页表条目,对应复制4KB空间*/
do {
/*
* We are holding two locks at this point - either of them
* could generate latencies in another task on another CPU.
*/
if (progress >= 32) {
progress = 0;
if (need_resched() ||
spin_needbreak(src_ptl) || spin_needbreak(dst_ptl))
break;
}
/*父进程二级页表映射内容不存在时,进行下一次循环*/
if (pte_none(*src_pte)) {
progress++;
continue;
}
copy_one_pte(dst_mm, src_mm, dst_pte, src_pte, vma, addr, rss);
progress += 8;
} while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end);
arch_leave_lazy_mmu_mode();
spin_unlock(src_ptl);
pte_unmap_nested(orig_src_pte);
add_mm_rss(dst_mm, rss[0], rss[1]);
pte_unmap_unlock(orig_dst_pte, dst_ptl);
cond_resched();
if (addr != end)
goto again;
return 0;
}
首先看如下片段:
/*这就是给子进程的创建二级页表,再次证明一级页表常驻内存,二级页表要靠分配*/
dst_pte = pte_alloc_map_lock(dst_mm, dst_pmd, addr, &dst_ptl);
这就是在给子进程创建一个二级页表,然后看下面的片段;
/*得出对于当前的虚拟地址,父进程的二级页表条目*/
src_pte = pte_offset_map_nested(src_pmd, addr);
这是给后面的把父进程的二级页表条目加上写保护属性做准备,先把父进程的二级页表条目获取到,在下一步将增加写保护的属性值;接下来是循环调用函数copy_one_pte,之所以会序号512次,是因为从上面调用到这里,addr和end都是2MB的间隔,这里每次调用copy_one_pte写一个二级页表条目,对应4KB,所以需要调用512次,函数copy_one_pte源码如下:
static inline void
copy_one_pte(struct mm_struct *dst_mm, struct mm_struct *src_mm,
pte_t *dst_pte, pte_t *src_pte, struct vm_area_struct *vma,
unsigned long addr, int *rss)
{
unsigned long vm_flags = vma->vm_flags;
pte_t pte = *src_pte;
struct page *page;
/* pte contains position in swap or file, so copy. */
/*(!pte_present(pte))为1,说明父进程的这个二级页表映射的内容,不在物理内存*/
if (unlikely(!pte_present(pte))) {
/*如果是因为被交换到磁盘(外存),那么把old_pte在swap file中的入口地址,将old_pte复制到内存中*/
if (!pte_file(pte)) {
swp_entry_t entry = pte_to_swp_entry(pte);
swap_duplicate(entry);
/* make sure dst_mm is on swapoff's mmlist. */
if (unlikely(list_empty(&dst_mm->mmlist))) {
spin_lock(&mmlist_lock);
if (list_empty(&dst_mm->mmlist))
list_add(&dst_mm->mmlist,
&src_mm->mmlist);
spin_unlock(&mmlist_lock);
}
if (is_write_migration_entry(entry) &&
is_cow_mapping(vm_flags)) {
/*
* COW mappings require pages in both parent
* and child to be set to read.
*/
make_migration_entry_read(&entry);
pte = swp_entry_to_pte(entry);
set_pte_at(src_mm, addr, src_pte, pte);
}
}
goto out_set_pte;
}
/*
* If it's a COW mapping, write protect it both
* in the parent and the child
*/
/*这一步很重要,这是在fork时,当子进程拷贝父进程的页表时,将这一页置为写保护,导致父子任何一方再要改动这页内容时不能写入,将触发COW*/
if (is_cow_mapping(vm_flags)) {
/*父进程的二级页表的该页条目设置为写保护*/
ptep_set_wrprotect(src_mm, addr, src_pte);
/*子进程的二级页表的该页条目也设置为写保护*/
pte = pte_wrprotect(pte);
}
/*
* If it's a shared mapping, mark it clean in
* the child
*/
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte);
/*根据二级页表映射的内容,找出是哪一物理页,并返回其页描述符,
如果是零页(zero_pfn)返回NULL*/
page = vm_normal_page(vma, addr, pte);
/*找到该物理页描述符的目的是,让其成员_count和_mapcount均加1,意即该页的使用进程个数*/
if (page) {
get_page(page);
page_dup_rmap(page);
rss[PageAnon(page)]++;
}
out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte);
}
这个函数直到最后才调用set_pte_at实际的写子进程的二级页表条目,前面主要完成以下功能:
1、 置该物理页对于父子进程的二级页表条目的属性均为写保护:
/*这一步很重要,这是在fork时,当子进程拷贝父进程的页表时,
将这一页置为写保护,导致父子任何一方再要改动这页内容时不能写入,将触发COW*/
if (is_cow_mapping(vm_flags)) {
/*父进程的二级页表的该页条目设置为写保护*/
ptep_set_wrprotect(src_mm, addr, src_pte);
/*子进程的二级页表的该页条目也设置为写保护*/
pte = pte_wrprotect(pte);
}
2、 更新该页的页描述符的一些成员:
/*根据二级页表映射的内容,找出是哪一物理页,并返回其页描述符,
如果是零页(zero_pfn)返回NULL*/
page = vm_normal_page(vma, addr, pte);
/*找到该物理页描述符的目的是,让其成员_count和_mapcount均加1,意即该页的使用进程个数*/
if (page) {
get_page(page);
page_dup_rmap(page);
rss[PageAnon(page)]++;
}
这里还需要看下函数vm_normal_page,它的功能是:根据二级页表映射的内容,找出是哪一物理页,并返回其页描述符,如果是零页(zero_pfn)返回NULL;也许会奇怪为什么还有可能是零页,后面就会发现原因和应用场合;
3、写子进程的二级页表:set_pte_at(dst_mm, addr, dst_pte, pte);
上面就是在函数dup_mmap中,for循环调用函数copy_page_range,把父进程的所有vma内的物理页映射都拷贝给子进程的所有vma中的全过程,回到函数dup_mmap,还应注意些小细节,诸如子进程的mm的插入红黑树和vma双向链表、更新vma个数(_mapcount成员)等等,有了印象后对后面其他内容的熟悉有好处,更有助于全面理解进程地址空间。
细心的你会发现,父子进程的每个vma的起始结尾虚拟地址值,都是一样的,同时,这些虚拟地址空间对应的物理地址也都是一样的;只是,父子任何一方试图写这些物理页时,MMU会阻止写操作,就会触发写时复制COW的缺页异常,因为这些物理页是写保护的。
所以,有没有CLONE_VM标志的fork/vfork/clone的区别在于,有CLONE_VM标志的情况下,子进程没有自己的mm也没有自己的vma,而有CLONE_VM的情况是有自己的mm和vma的;但虚拟空间的物理地址都是没有额外映射,这样做的好处是节省了物理内存,其实就如ULK所讲,在运行一段时间后,父子进程就会有完全不一样的地址空间了。