通常我们在代码中调用fork()来创建一个进程或者调用pthread_create()来创建一个线程,创建一个进程需要为其分配内存资源,文件资源,时间片资源等,在这里来描述一下linux进程的创建过程及写时复制技术。
一写时复制
子进程和父进程通常拥有着不同的进程内存空间(线程除外),传统的unix在创建子进程后,会复制父进程的地址空间的所有内容,这就十分的低效,因为经常子进程会立即执行exec操作,创建一个崭新的内存空间,另外像进程代码段这样的内存,父子进程只是读,而没有写操作,完全可以共享,而不用去复制,这样会节省大量的时间。fork(),vfork(),clone()三个系统调用最后都是使用sys_clone()服务例程来完成了系统调用,sys_clone()服务例程会去调用do_fork()函数,主要的处理流程就在do_fork()中。
3.1do_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(); //查看当前进程是否被trace,设置新进程的trace状态,只要进程不是内核线程就应该被trace if (unlikely(current->ptrace)) { trace = fork_traceflag (clone_flags); if (trace) clone_flags |= CLONE_PTRACE; } //将父进程的相关信息,如地址空间,信号等信息复制到新建进程描述符上面 p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid); if (!IS_ERR(p)) { struct completion vfork; //如果该进程是vfork出来的,需要等待子进程结束或退出后再执行父进程 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); } //如果该进程被追踪,或者设置了clone_stopped标记,给该进程发送STOP信号, 设置了CLONE_STOPPED标记的话,进程不能够立即执行,需要先stop下来,后面通过 向该进程发送SIG_CONT信号使其恢复执行 if ((p->ptrace & PT_PTRACED) || (clone_flags & CLONE_STOPPED)) { sigaddset(&p->pending.signal, SIGSTOP); set_tsk_thread_flag(p, TIF_SIGPENDING); } //未设置clone_stopped标记,唤醒新进程,此后新进程可以参与调度 if (!(clone_flags & CLONE_STOPPED)) wake_up_new_task(p, clone_flags); else p->state = TASK_STOPPED; ++total_forks; //需要trace的话,向debug进程发送一个trace消息 if (unlikely (trace)) { current->ptrace_message = pid; ptrace_notify ((trace << 8) | SIGTRAP); } //如果该进程是被vfork()出来的,父进程在此等待,等待子进程退出 if (clone_flags & CLONE_VFORK) { wait_for_completion(&vfork); if (unlikely (current->ptrace & PT_TRACE_VFORK_DONE)) ptrace_notify ((PTRACE_EVENT_VFORK_DONE << 8) | SIGTRAP); } } else { free_pidmap(pid); pid = PTR_ERR(p); } return pid; }
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; if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS)) return ERR_PTR(-EINVAL); //clone出一个线程,线程必须共享信号处理函数等 if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL); //共享信号处理,但不共享地址空间,clone_sighand表示子进程和父进程共享信号处理函数表,共享阻塞信号表以及挂起信号表 if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL); if (retval) goto fork_out; retval = -ENOMEM; //复制当前进程进程描述符 ,实质的工作就是把当前进程的task_struct, thread_info都复制到了子进程里面,在该函数里面也会分配进程描述符 p = dup_task_struct(current); if (!p) goto fork_out; retval = -EAGAIN; //检查是否进程数符合进程资源限制这个时候p->user其实和父进程中的p->user是一样的,因为在dup_task_struct里面对父进程的描述符进行了复制 if (atomic_read(&p->user->processes) >= p->signal->rlim[RLIMIT_NPROC].rlim_cur) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->user != &root_user) goto bad_fork_free; } //增加统计信息和进程所属用户的引用计数 atomic_inc(&p->user->__count); atomic_inc(&p->user->processes); //检查线程数是否超出了限制 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; //新进程尚未调用exec p->did_exec = 0; //设置新进程的flag,主要是去除PF_SUPERPRIV,设置PF_FORKNOEXEC,根据clone_falgs设置task的trace字段 copy_flags(clone_flags, p); p->pid = pid; retval = -EFAULT; if (clone_flags & CLONE_PARENT_SETTID) if (put_user(p->pid, parent_tidptr)) goto bad_fork_cleanup; p->proc_dentry = NULL; //初始化进程的子进程链表和挂入兄弟链表的节点 INIT_LIST_HEAD(&p->children); INIT_LIST_HEAD(&p->sibling); //初始化等待队列,在系统调用vfork()中会使用到 init_waitqueue_head(&p->wait_chldexit); p->vfork_done = NULL; spin_lock_init(&p->alloc_lock); spin_lock_init(&p->proc_lock); //为新进程清除掉有信号挂起的标记位,表明该进程现在没有信号尚未处理 clear_tsk_thread_flag(p, TIF_SIGPENDING); //清除掉新进程的挂起信号表,初始化相应的信号队列,从这里的代码看来,父进程未处理完毕的信号不会遗传给子进程,oh,yeah! init_sigpending(&p->pending); : //跳过了很多代码,这些代码我们不关注,不影响对主体流程的理解 : : //设置新进程的tgid,若该新进程是一个轻量级进程即线程的话,设置其tgid为父进程的tgid,否则其tgid和其pid是一样的 p->tgid = p->pid; if (clone_flags & CLONE_THREAD) p->tgid = current->tgid; //下面主要是根据clone_flags的标志来决定子进程是否来共用内存描述符,文件描述符等,信号处理函数,信号及阻塞码等,copy_thread()则会为新的子进程内核栈复制父进程内核栈的寄存器信息,设置进程描述符中thread中的相关寄存器信息,为后续的进程切换做好准备,3.3介绍了copy_thread()函数。 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; retval = copy_thread(0, clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_namespace; //清除掉p->thread的TIF_SYSCALL_TRACE的标志位,以使ret_from_fork不会把系统调用结束的消息发送给trace进程 clear_tsk_thread_flag(p, TIF_SYSCALL_TRACE); //设置exit signal,对于线程组来说,只有线程组的最后一个线程退出,整个线程组才退出,并向线程组的父进程发送child信号,exit_signal其实主要是用来表明该进程是否是线程组首进程:exit_signal!=-1 p->exit_signal = (clone_flags & CLONE_THREAD) ? -1 : (clone_flags & CSIGNAL); p->pdeath_signal = 0; p->exit_state = 0; //给新进程分配时间片,分配的额度是父进程的一半,同时禁止内核抢占 sched_fork(p); //设置进程的组长进程就是自己,这里的组长进程是指线程组的组长进程,在后面的处理中会把对轻量级进程的组长进程设为真正的线程组组长进程 p->group_leader = p; INIT_LIST_HEAD(&p->ptrace_children); INIT_LIST_HEAD(&p->ptrace_list); /* Need tasklist lock for parent etc handling! */ write_lock_irq(&tasklist_lock); //子进程继承父进程的cpus_allowed字段,cpus_allowed字段指明进程可以在哪些cpu上运行 p->cpus_allowed = current->cpus_allowed; //设置子进程的thread->cpu,当前正在执行sys_clone()服务例程的cpu set_task_cpu(p, smp_processor_id()); if (sigismember(¤t->pending.signal, SIGKILL)) { write_unlock_irq(&tasklist_lock); retval = -EINTR; goto bad_fork_cleanup_namespace; } //对于轻量级线程而言,新建子进程的parent,real_parent进程是父进程的父进程,即线程的父进程是fork()出线程组组长进程的进程 if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) p->real_parent = current->real_parent; else p->real_parent = current; p->parent = p->real_parent; if (clone_flags & CLONE_THREAD) { spin_lock(¤t->sighand->siglock); //对于线程而言,其线程组长进程就是其父进程的组长进程 p->group_leader = current->group_leader; spin_unlock(¤t->sighand->siglock); } //把新进程插入到进程列表中,是组长进程,才插入到全局的进程链表,即以init_task为进程链表头的链表,但还是会把该进程放入到PIDTYPE的hash表中 SET_LINKS(p); //如果进程必须被追踪,将该进程放入current的父进程即调试进程的跟踪列表中,同时子进程的父进程也被设置为调试进程 if (unlikely(p->ptrace & PT_PTRACED)) __ptrace_link(p, current->parent); //将进程插入到PIDTYPE_PID的hash表中 attach_pid(p, PIDTYPE_PID, p->pid); //将进程插入到PIDTYPE_TGID的hash标中 attach_pid(p, PIDTYPE_TGID, p->tgid); //该进程为线程组组长进程 if (thread_group_leader(p)) { //将该进程插入到PIDTYPE_PGID hash表中 attach_pid(p, PIDTYPE_PGID, process_group(p)); //将该进程插入到PIDTYPE_SID hash表中 attach_pid(p, PIDTYPE_SID, p->signal->session); if (p->pid) __get_cpu_var(process_counts)++; } nr_threads++; write_unlock_irq(&tasklist_lock); retval = 0; : : }
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,eax表示返回值 childregs->eax = 0; //childregs->esp中存放的是子进程用户态的栈地址 childregs->esp = esp; //thread.esp存放的是内核态的栈顶地址 p->thread.esp = (unsigned long) childregs; p->thread.esp0 = (unsigned long) (childregs+1); //thread.eip存放的是内核态的返回地址 p->thread.eip = (unsigned long) ret_from_fork; savesegment(fs,p->thread.fs); savesegment(gs,p->thread.gs); tsk = current; //copy父进程的io权限位图 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); } : : return err; }