大家一定听过这样一段话:
调用fork(),创建子进程;
调用pthread_create()创建子线程;
如果你要创建一个立刻执行exec()函数的进程,就使用vfork()。
说法没有错误,在我平时编写多进程/多线程项目时的确是这样使用的。那请问:为什么?它们有什么区别?既然在Linux系统上有进程、内核线程和用户线程之分,那调用不同的函数,分别创建的是什么?如果你能回答这些问题,本文章不是为您准备的。
下来的分析会借助fork的源码,但只会贴出重要的代码,如果你需要源码,可以去官网自行下载(http://kernel.org)
fork()、pthread_create()、vfork()的系统调用函数分别是sys_fork()、sys_clone()、sys_vfork(),他们底层调用的都是do_fork(),只是传递给do_fork()参数和clone_flags不同,不同的参数决定父子进程之间共享哪些数据结构和信息,即确定此时创建的是进程、内核线程或用户线程。
当调用fork()时,产生0x80号中断,进程从用户模式切换到内核模式,查系统调用表sys_call_table,当前进程要调用的函数是sys_fork()。
1.sys_fork()底层调用的是do_fork()函数,do_fork()先对传进来的参数做检测,判断参数是否合法,接着调用copy_process()【copy_process()是do_fork函数的核心函数,创建进程都是在这个函数中实现的】
long do_fork(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr)
{
struct task_struct *p;
int trace = 0
/*通过查找pidmap_array位图,为子进程分配新的pid参数*/
long pid = alloc_pidmap();
if (pid < 0)
return -EAGAIN;
/*如果父进程正在被跟踪,就检查debugger程序是否想跟踪子进程.并且子进程不是内核进程(CLONE_UNTRACED未设置),那么就设置CLONE_PTRACE标志。*/
if (unlikely(current->ptrace)) {
trace = fork_traceflag (clone_flags);
if (trace)
clone_flags |= CLONE_PTRACE;
}
/* copy_process复制进程描述符.如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址,这是创建进程的关键步骤。*/
p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);
//在copy_processs之前的代码都是检验参数合法性
2.copy__process()开始也会检查参数的合法性,接着调用dup_task_struct()【这个函数是为进程创建task_struct结构的】。task_thread是每个进程都有的,task_thread结构体中有thread_info结构体,thread_info保存的是进程上下文的信息,所以fork的子进程应该有自己的thread_info结构。
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
{
int retval;
struct task_struct *p = NULL;
/*检查clone_flags所传标志的一致性。*/
/*如果CLONE_NEWNS和CLONE_FS标志都被设置,返回错误*/
if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS))
return ERR_PTR(-EINVAL);
/* CLONE_THREAD标志被设置,并且CLONE_SIGHAND没有设置。(同一线程组中的轻量级进程必须共享信号)*/
if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND))
return ERR_PTR(-EINVAL);
/*CLONE_SIGHAND被设置,但是CLONE_VM没有设置。 (共享信号处理程序的轻量级进程也必须共享内存描述符)*/
if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM))
return ERR_PTR(-EINVAL);
/* 通过调用security_task_create以及稍后调用security_task_alloc执行所有附加的安全检查。 LINUX2.6提供扩展安全性的钩子函数,与传统unix相比,它具有更加强壮的安全模型。*/
retval = security_task_create(clone_flags);
if (retval)
goto fork_out;
retval = -ENOMEM;
/**
* 调用dup_task_struct为子进程获取进程描述符。
*/
p = dup_task_struct(current);
if (!p)
goto fork_out;
//在调用dup_task_struct之前的代码都是做参数检验
......
}
dup_task_struct()函数为子进程申请一个thread_info结构体,并用父进程的thread_info初始化子进程的thread_info结构体,用父进程的task_struct初始化子进程的task_struct,并将子进程task_struct中的thread_info指针指向自己的thread_info结构,由于thread_info结构体中有指向当前进程task_struct的指针,所以也要修改这个指针的指向,让其指向当前进程(子进程)的task_struct结构体。【这就是dup_task_struct函数的作用】
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
};
.......//保存进程上下文的其他信息
static struct task_struct *dup_task_struct(struct task_struct *orig)
{
struct task_struct *tsk;
struct thread_info *ti;
......//初始化信息
/**重点!!!
* 将current进程描述符的内容复制到tsk所指向的task_struct结构中,然后把tsk_thread_info置为ti
* 将current进程的thread_info内容复制给ti指向的结构中,并将ti_task置为tsk.
*/
*ti = *orig->thread_info;
*tsk = *orig;
tsk->thread_info = ti;
ti->task = tsk;
......//其他设置信息
return tsk;
3.调用dup_task_struct函数返回后,子进程的task_struct已经创建完成。现在要进行信号量/文件描述符/文件上下文/信号处理函数/信号/虚拟内存等的拷贝,这些操作是通过copy_semundo,copy_files,copy_fs,copy_sighand,copy_signal
,copy_mm创建新的数据结构,并把父进程相应数据结构的值复制到新数据结构中。
如果clone_flag参数指出它们有不同的值,会选择父子进程共享某些数据结构,fork()、clone()、vfork()的不同也是在这儿体现的。其中最主要的是copy_mm(),它拷贝的是进程4G虚拟内存空间【copy_mm()实现稍后介绍】。
static task_t *copy_process(unsigned long clone_flags,
unsigned long stack_start,
struct pt_regs *regs,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
int pid)
{
......//参数判断
p = dup_task_struct(current);
if (!p)
goto fork_out;
......//参数设置
if ((retval = copy_semundo(clone_flags, p)))
goto bad_fork_cleanup_audit;
if ((retval = copy_files(clone_flags, p)))
goto bad_fork_cleanup_semundo;
if ((retval = copy_fs(clone_flags, p)))
goto bad_fork_cleanup_files;
if ((retval = copy_sighand(clone_flags, p)))
goto bad_fork_cleanup_fs;
if ((retval = copy_signal(clone_flags, p)))
goto bad_fork_cleanup_sighand;
if ((retval = copy_mm(clone_flags, p)))
goto bad_fork_cleanup_signal;
if ((retval = copy_keys(clone_flags, p)))
goto bad_fork_cleanup_mm;
if ((retval = copy_namespace(clone_flags, p)))
goto bad_fork_cleanup_keys;
拷贝完数据结构后,进程的task_struct结构体已经创建完了,但是还没有复制父进程的系统堆栈,copy_thread就是复制系统调用堆栈的。但是,如果是完全复制,父子进程就没办法区分了,copy_thread函数将子进程的eax寄存器强行设置为0,在调用完成后,当子进程受调度恢复运行时,这就是返回值。这也解释了为什么fork在子进程中返回0。
retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs);
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
struct task_struct *tsk;
int err;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p->thread_info)) - 1;
*childregs = *regs;
//子进程的eax寄存器设置为0
childregs->eax = 0;
childregs->esp = esp;
p->thread.esp = (unsigned long) childregs;
p->thread.esp0 = (unsigned long) (childregs+1);
p->thread.eip = (unsigned long) ret_from_fork;
savesegment(fs,p->thread.fs);
savesegment(gs,p->thread.gs);
tsk = current;
if (unlikely(NULL != tsk->thread.io_bitmap_ptr)) {
p->thread.io_bitmap_ptr = kmalloc(IO_BITMAP_BYTES, GFP_KERNEL);
if (!p->thread.io_bitmap_ptr) {
p->thread.io_bitmap_max = 0;
return -ENOMEM;
}
memcpy(p->thread.io_bitmap_ptr, tsk->thread.io_bitmap_ptr,
IO_BITMAP_BYTES);
}
if (clone_flags & CLONE_SETTLS) {
struct desc_struct *desc;
struct user_desc info;
int idx;
err = -EFAULT;
if (copy_from_user(&info, (void __user *)childregs->esi, sizeof(info)))
goto out;
err = -EINVAL;
if (LDT_empty(&info))
goto out;
idx = info.entry_number;
if (idx < GDT_ENTRY_TLS_MIN || idx > GDT_ENTRY_TLS_MAX)
goto out;
desc = p->thread.tls_array + idx - GDT_ENTRY_TLS_MIN;
desc->a = LDT_entry_a(&info);
desc->b = LDT_entry_b(&info);
}
err = 0;
out:
if (err && p->thread.io_bitmap_ptr) {
kfree(p->thread.io_bitmap_ptr);
p->thread.io_bitmap_max = 0;
}
return err;
}
4.copy_process函数完成了它的使命,成功创建完成子进程需要的所有东西。copy_process函数完成后,判断当前进程是否设置了CLONE_STOPPED,如果没有设置,就调用wake_up_new_task,将子进程插入父进程的运行队列,并且将子进程插入父进程之前,先运行子进程。原因是:如果子进程在创建之后执行exec替换新进程,就可以避免写时拷贝机制执行不必要的页面复制。
if (!(clone_flags & CLONE_STOPPED))
wake_up_new_task(p, clone_flags);
else//如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。
p->state = TASK_STOPPED;
copy_mm()是复制进程虚拟空间的函数,其中dup_mmap是最重要的函数,dup_mmap函数里复制了父进程的线性区链表、红黑树、页目录和页表,为了实现写时拷贝机制,会将私有的、可写的页面权限设置只读,父子进程共享同一个物理页面,只有任意一个进程给页面上写数据的时候,发生异常,才会为进程分配独立页面,并修改该页的权限。
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;
tsk->mm = NULL;
tsk->active_mm = NULL;
oldmm = current->mm;
//指定了CLONE_VM标志,表示创建线程。
if (clone_flags & CLONE_VM)
{
//新线程共享父进程的地址空间,所以需要将mm_users加一
atomic_inc(&oldmm->mm_users);
mm = oldmm;
return;
}
//必须要有地址空间,即使此时并没有分配内存。
retval = -ENOMEM;
//分配一个新的内存描述符。把它的地址存放在新进程的mm中。
mm = allocate_mm();
//从当前进程mm的内容复制给mm
memcpy(mm, oldmm, sizeof(*mm));
//dup_mmap不但复制了线程区和页表,也设置了mm的一些属性。它也会改变父进程的私有,可写的页为只读的,以使写时复制机制生效。
retval = dup_mmap(mm, oldmm);
mm->hiwater_rss = mm->rss;
mm->hiwater_vm = mm->total_vm;
return retval;
}
static inline 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;
//复制父进程的每一个vm_area_struct线性区描述符,并把复制品插入到子进程的线性区链表和红黑树中。
for (mpnt = current->mm->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 int len = (mpnt->vm_end - mpnt->vm_start) >> PAGE_SHIFT;
charge = len;
}
tmp = kmem_cache_alloc(vm_area_cachep, SLAB_KERNEL);
*tmp = *mpnt;
pol = mpol_copy(vma_policy(mpnt));
retval = PTR_ERR(pol);
vma_set_policy(tmp, pol);
tmp->vm_flags &= ~VM_LOCKED;
tmp->vm_mm = mm;
tmp->vm_next = NULL;
anon_vma_link(tmp);
file = tmp->vm_file;
if (file)
{
struct inode *inode = file->f_dentry->d_inode;
get_file(file);
if (tmp->vm_flags & VM_DENYWRITE)
atomic_dec(&inode->i_writecount);
spin_lock(&file->f_mapping->i_mmap_lock);
tmp->vm_truncate_count = mpnt->vm_truncate_count;
flush_dcache_mmap_lock(file->f_mapping);
vma_prio_tree_add(tmp, mpnt);
flush_dcache_mmap_unlock(file->f_mapping);
spin_unlock(&file->f_mapping->i_mmap_lock);
}
spin_lock(&mm->page_table_lock);
*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->map_count++;
//copy_page_range创建必要的页表来映射线性区所包含的一组页。并且初始化新页表的表项。将私有、可写的页对父子进程都标记为只读的,为写时复制进行处理。
retval = copy_page_range(mm, current->mm, tmp);
spin_unlock(&mm->page_table_lock);
if (tmp->vm_ops && tmp->vm_ops->open)
tmp->vm_ops->open(tmp);
}
}
如果是clone函数,do_fork会根据传进来的参数的值,决定哪些共享,哪些复制。共享的知识改变task_struct结构体中指针的指向,让其指向父进程的结构。此时创建的是内核线程。
如果是vforkh函数,do_fork的clone_flags标识都是1,除task_struct结构外,所有的数据结构都是共享的。并且CLONE_VM设置为0,此时创建的是一个用户线程。
至此,fork执行完毕!
一张图帮你梳理fork
源码剖析过程参考《Linux内核源代码情景分析》,如有不对,欢迎指正。