fork实现原理
clone,fork,vfork系统调用的区别
这几个系统调用最终都会调用_do_fork内核函数
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
fork
SYSCALL_DEFINE0(fork)
{
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
只使用SIGCHLD标志位,子进程采用写时复制技术,只复制父进程的页表,不会复制页面内容。当子进程需要写入新内容时才触发写时复制机制,并为子进程创建一个副本。
vfork
SYSCALL_DEFINE0(vfork)
{
return _do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
0, NULL, NULL, 0);
}
CLONE_VM表示父、子进程执行在相同的进程地址空间中。另外,通过vfork()可以避免复制父进程的页表项。
SYSCALL_DEFINE5(clone, unsigned long, clone_flags,
unsigned long, newsp,
int __user *, parent_tidptr,
int __user *, child_tidptr,
unsigned long, tls)
{
return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr, tls);
}
clone()函数功能强大,可以传递众多参数,可以有选择地继承父进程的资源。如可以和vfork()一样,与父进程共享一个进程地址空间,从而创建线程;也可以不和父进程共享进程地址空间,甚至可以创建兄弟关系进程。
copy on write写时复制的实现
在dup_mm函数中,首先直接memcpy复制父进程的mm_struct结构体,然后调用dup_mmap函数
static struct mm_struct *dup_mm(struct task_struct *tsk)
{
......
mm = allocate_mm();
memcpy(mm, oldmm, sizeof(*mm));
err = dup_mmap(mm, oldmm);
return mm;
......
}
dup_mmap函数
<kernel/fork.c>
static __latent_entropy int dup_mmap(struct mm_struct *mm,
struct mm_struct *oldmm)
{
down_write_killable(&oldmm->mmap_sem)
//遍历父进程所有的vma
for (mpnt = oldmm->mmap; mpnt; mpnt = mpnt->vm_next) {
tmp = vm_area_dup(mpnt); //从slab系统申请一个vm_are_struct,并将父进程的vma逐字节拷贝过去
tmp->vm_mm = mm;
anon_vma_fork(tmp, mpnt)
__vma_link_rb(mm, tmp, rb_link, rb_parent);
rb_link = &tmp->vm_rb.rb_right;
rb_parent = &tmp->vm_rb; //将vma插入mm红黑树中
copy_page_range(mm, oldmm, mpnt);
}
up_write(&mm->mmap_sem);
}
其中anon_vma_fork的作用如下图所示,分配avc枢纽,avc枢纽对应的av是父进程的av,对应的vma是子进程的vma
copy_page_range()函数是复制父进程的进程地址空间相应页表的核心实现函数。它会沿着页表中的PGD、P4D、PUD、PMD以及PTE的方向查询和遍历页表(子进程如果不存在会申请)。遍历PTE的函数为copy_pte_range(),下面是该函数实现的代码片段。
mm/memory.c>
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)
{
do {
entry.val = copy_one_pte(dst_mm, src_mm, dst_pte, src_pte,
vma, addr, rss);
} while (dst_pte++, src_pte++, addr += PAGE_SIZE, addr != end);
return 0;
}
写时复制技术的精华就在copy_one_pte()函数中
<mm/memory.c>
static inline unsigned long
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)
{
if (unlikely(!pte_present(pte))) {
swp_entry_t entry = pte_to_swp_entry(pte);
goto out_set_pte;
}
if (is_cow_mapping(vm_flags) && pte_write(pte)) {
ptep_set_wrprotect(src_mm, addr, src_pte);
pte = pte_wrprotect(pte);
}
if (vm_flags & VM_SHARED)
pte = pte_mkclean(pte);
pte = pte_mkold(pte);
page = vm_normal_page(vma, addr, pte);
if (page) {
get_page(page);
rss[mm_counter(page)]++;
}
out_set_pte:
set_pte_at(dst_mm, addr, dst_pte, pte);
return 0;
}
static inline bool is_cow_mapping(vm_flags_t flags)
{
return (flags & (VM_SHARED | VM_MAYWRITE)) == VM_MAYWRITE;
}
总的来说就是将子进程对应页表里的pte设置为只读,这样对子进程vma写操作的时候就会触发缺页异常,copy on write