unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr) { struct task_struct *p; int trace = 0; long nr; 如果创建新的user namespace就需要具备管理员权限或者CAP_SETUID或者CAP_SETGID。 if (clone_flags & CLONE_NEWUSER) { if (clone_flags & CLONE_THREAD) return -EINVAL; if (!capable(CAP_SYS_ADMIN) || !capable(CAP_SETUID) || !capable(CAP_SETGID)) return -EPERM; } 检查是否跟踪子进程,如果是,就根据不同的跟踪行为发送不同的跟踪事件。 PTRACE_EVENT_VFORK: 使子进程下次调用 vfork() 时停止其执行,并自动跟踪开始执行时就已设置 SIGSTOP 信号的新进程。 PTRACE_EVENT_CLONE: 使子进程下次调用 clone() 时停止其执行,并自动跟踪开始执行时就已设置 SIGSTOP 信号的新进程。 PTRACE_EVENT_FORK: 使子进程下次调用 fork() 时停止其执行,并自动跟踪开始执行时就已设置 SIGSTOP 信号的新进程。 if (likely(user_mode(regs)) && !(clone_flags & CLONE_UNTRACED)) { if (clone_flags & CLONE_VFORK) trace = PTRACE_EVENT_VFORK; else if ((clone_flags & CSIGNAL) != SIGCHLD) trace = PTRACE_EVENT_CLONE; else trace = PTRACE_EVENT_FORK; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } 分配进程描述符task_struct和内核堆栈空间,通过父进程的相关信息初始化一些资源。这个函数在进程创建中做了很多重要工作后面将详细讲述。 p = copy_process(clone_flags, stack_start, regs, stack_size,child_tidptr, NULL, trace); if (!IS_ERR(p)) { struct completion vfork; 获取在当前进程的PID命名空间中的id号。 nr = task_pid_vnr(p); if (clone_flags & CLONE_PARENT_SETTID) put_user(nr, parent_tidptr); 如果是通过vfork创建进程则初始化完成量vfork,它保证了子进程先运行。 if (clone_flags & CLONE_VFORK) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } 将新进程插入调度队列,等待调度运行。后面详细分析。 wake_up_new_task(p); 发送跟踪事件 if (unlikely(trace)) ptrace_event(trace, nr); 如果是通过vfork创建进程则挂起当前进程等待子进程运行结束后再运行。 if (clone_flags & CLONE_VFORK) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event(PTRACE_EVENT_VFORK_DONE, nr); } } else { nr = PTR_ERR(p); } 返回到发出创建新进程系统调用的进程中。如fork系统调用会两次返回,一次返回到父进程,返回值为子进程的进程id,另一次返回到子进程,后面会讲到返回到子进程的情况。 return nr; } 上面有一些函数需要进一步分析如下: 【do_fork--->copy_process】 static struct task_struct *copy_process(unsigned long clone_flags,unsigned long stack_start, struct pt_regs *regs,unsigned long stack_size, int __user *child_tidptr,struct pid *pid, int trace) { int retval; struct task_struct *p; int cgroup_callbacks_done = 0; 如果创建新的命名空间就不能同时又拷贝父进程的文件系统信息。 if ((clone_flags & (CLONE_NEWNS|CLONE_FS)) == (CLONE_NEWNS|CLONE_FS)) return ERR_PTR(-EINVAL); 同一线程组中的进程必须共享信号 if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL); 共享信号处理方法就必须共享用户虚拟空间 if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL); if ((clone_flags & CLONE_PARENT) && current->signal->flags & SIGNAL_UNKILLABLE) return ERR_PTR(-EINVAL); 执行附加的安全检查。 retval = security_task_create(clone_flags); if (retval) goto fork_out; retval = -ENOMEM; 函数dup_task_struct要做的就是下面几件事: tsk = alloc_task_struct_node(node); 为新进程分配一个task_struct结构 ti = alloc_thread_info_node(tsk, node); 分配2页的内核栈空间并返回页地址 err = arch_dup_task_struct(tsk, orig); 将父进程的进程描述符结构task_struct的内容拷贝到子进程的进程描述符结构task_struct中 tsk->stack = ti;线程描述符结构thread_info存放在上面分配的两页栈空间的开始出,让tsk->stack指向线程描述符结构。 setup_thread_stack(tsk, orig);将父进程的线程描述符结构内容拷贝到子进程中。 p = dup_task_struct(current); if (!p) goto fork_out; ...... retval = -EAGAIN; 每个进程都属于某个用户,每个用户都有它的资源限制,所以一个用户所能创建的进程数不能超过他的限制,除非他具有管理员权限。 if (atomic_read(&p->real_cred->user->processes) >= task_rlimit(p, RLIMIT_NPROC)) { if (!capable(CAP_SYS_ADMIN) && !capable(CAP_SYS_RESOURCE) && p->real_cred->user != INIT_USER) goto bad_fork_free; } current->flags &= ~PF_NPROC_EXCEEDED; 结构体struct cred包含了uid、gid、user等安全权能相关的东西。在函数copy_creds中先判断是不是创建线程CLONE_THREAD,如果是就共享父进程的cred结构,如果不是就重新分配一个cred结构,并用父进程中cred的内容拷贝到新分配的cred中。 retval = copy_creds(p, clone_flags); if (retval < 0) goto bad_fork_free; retval = -EAGAIN; 检查系统中的进程数量是否超过max_threads。这个变量的缺省值取决于系统内存容量。 if (nr_threads >= max_threads) goto bad_fork_cleanup_count; 如果实现新的进程的执行脚本执行域和可执行格式的内核函数都包含在内核模块中,则递增它们的使用计数器。 if (!try_module_get(task_thread_info(p)->exec_domain->module)) goto bad_fork_cleanup_count; did_exec记录了进程发出execve调用的次数 p->did_exec = 0; ...... 函数sched_fork中初始化了调度实体结构struct sched_entity se;,设置了进程优先级,设置了进程调度类,该函数后面详细讲解。 sched_fork(p); retval = perf_event_init_task(p); if (retval) goto bad_fork_cleanup_policy; retval = audit_alloc(p); if (retval) goto bad_fork_cleanup_policy; 如果设置了COPY_SYSVSEM,则使用父进程的System V信号量。 retval = copy_semundo(clone_flags, p); if (retval) goto bad_fork_cleanup_audit; 如果CLONE_FILES置位,则使用父进程的文件描述符列表,否则创建新的文件描述符列表,并将父进程的文件描述符列表拷到新的列表中。 retval = copy_files(clone_flags, p); if (retval) goto bad_fork_cleanup_semundo; 如果CLONE_FS置位,则使用父进程的文件系统上下文,否者创建自己的 struct fs_struct结构并用父进程的 struct fs_struct初始化新的 struct fs_struct。该结构中包含根目录、当前工作目录等。 retval = copy_fs(clone_flags, p); if (retval) goto bad_fork_cleanup_files; retval = copy_sighand(clone_flags, p); if (retval) goto bad_fork_cleanup_fs; 如果CLONE_SIGHAND置位,则使用父进程的信号处理程序,否则创建新的sighand_struct,并将父进程的信号处理结构内容拷到新的列信号处理结构中。 retval = copy_signal(clone_flags, p); if (retval) goto bad_fork_cleanup_sighand; 如果CLONE_VM置位,则共享父进程的地址空间,两个进程共用同一mm_struct结构,否者创建一个父进程的页表副本但不复制页的实际内容,而是使用写时复制机制。 retval = copy_mm(clone_flags, p); if (retval) goto bad_fork_cleanup_signal; 共享父进程的命名空间或是建立新的命名空间。下面对命名空间的做一点说明: 将子系统的全局属性封装到命名空间中,每个进程关联到一个选定的命名空间。每个可以感知命名空间的子系统都必须提供一个数据结构,将所有通过命名空间形式提供的对象集中起来。struct nsproxy 用于汇集指向特定于子系统的命名空间包装的指针: struct nsproxy { atomic_t count; struct uts_namespace *uts_ns; 包含内核的名称、版本、底层体系结构类型等信息 struct ipc_namespace *ipc_ns;进程间通信的IPC有关信息。 struct mnt_namespace *mnt_ns;已经装载的文件系统的视图。 struct pid_namespace *pid_ns;有关进程ID的信息 struct net *net_ns;半酣所有网络相关的命名空间参数 }; 与上面命名空间相对应的几个标志: CLONE_NEWUTS:创建新的utsname组 CLONE_NEWIPC :创建新的IPC命名空间 CLONE_NEWUSER:创建新的用户命名空间 CLONE_NEWPID:创建新的PID命名空间 CLONE_NEWNET:创建新的网络命名空间 retval = copy_namespaces(clone_flags, p); if (retval) goto bad_fork_cleanup_mm; retval = copy_io(clone_flags, p); if (retval) goto bad_fork_cleanup_namespaces; 复制进程中特定于线程的数据并且让CPU上下文中的pc字段指向函数ret_from_fork,该函数用汇编实现,fork调用就是通过它返回到子进程中的。 retval = copy_thread(clone_flags, stack_start, stack_size, p, regs); if (retval) goto bad_fork_cleanup_io; 为进城分配pid,下面函数做了很多处理,将在后面分析。 if (pid != &init_struct_pid) { retval = -ENOMEM; pid = alloc_pid(p->nsproxy->pid_ns); if (!pid) goto bad_fork_cleanup_io; } 字段p->pid中保存的是进程全局pid,所以它的pid应当从最顶层命名空间中获取,nr = pid->numbers[0].nr;(每一个进程在它之上的每一级命名空间中都有一个id)。 p->pid = pid_nr(pid); p->tgid = p->pid; 如果创建的是线程,则它的线程组id于当前进程的线程组id相同 if (clone_flags & CLONE_THREAD) p->tgid = current->tgid; p->set_child_tid = (clone_flags & CLONE_CHILD_SETTID) ? child_tidptr : NULL; p->clear_child_tid = (clone_flags & CLONE_CHILD_CLEARTID) ? child_tidptr : NULL; ...... if ((clone_flags & (CLONE_VM|CLONE_VFORK)) == CLONE_VM) ...... 如果创建线程就将 p->exit_signal设为-1,因为只有当线程组的最后一个成员死亡才会产生一个信号已通知线程组首领进程的父进程。 if (clone_flags & CLONE_THREAD) p->exit_signal = -1; else if (clone_flags & CLONE_PARENT) p->exit_signal = current->group_leader->exit_signal; else p->exit_signal = (clone_flags & CSIGNAL); ...... p->group_leader = p; INIT_LIST_HEAD(&p->thread_group); INIT_HLIST_HEAD(&p->task_works); cgroup_fork_callbacks(p); cgroup_callbacks_done = 1; write_lock_irq(&tasklist_lock); 如果创建线程,新进程与当前进程有相同的父进程,否则当前进程就是新进程的父进程。 if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) { p->real_parent = current->real_parent; p->parent_exec_id = current->parent_exec_id; } else { p->real_parent = current; p->parent_exec_id = current->self_exec_id; } spin_lock(¤t->sighand->siglock); recalc_sigpending(); 如果当前进程还有未决信号就错误返回。 if (signal_pending(current)) { spin_unlock(¤t->sighand->siglock); write_unlock_irq(&tasklist_lock); retval = -ERESTARTNOINTR; goto bad_fork_free_pid; } 如果创建进程就将线程数递增1,并且让新进程与当前进程有相同的组长进程 if (clone_flags & CLONE_THREAD) { current->signal->nr_threads++; atomic_inc(¤t->signal->live); atomic_inc(¤t->signal->sigcnt); p->group_leader = current->group_leader; list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group); } if (likely(p->pid)) { ptrace_init_task(p, (clone_flags & CLONE_PTRACE) || trace); 如果新进程是线程组组长进程,就进入下面分支 if (thread_group_leader(p)) { if (is_child_reaper(pid)) p->nsproxy->pid_ns->child_reaper = p; 将当前进程组进程的组id和会话id挂到新进程哈希数组task->pids[]中对应位置。 p->signal->leader_pid = pid; p->signal->tty = tty_kref_get(current->signal->tty); attach_pid(p, PIDTYPE_PGID, task_pgrp(current)); attach_pid(p, PIDTYPE_SID, task_session(current)); list_add_tail(&p->sibling, &p->real_parent->children); list_add_tail_rcu(&p->tasks, &init_task.tasks); __this_cpu_inc(process_counts); } 将新的pid结构挂到新进程哈希数组task->pids[]中对应位置。 attach_pid(p, PIDTYPE_PID, pid); 新进程加入进程集合,递增nr_threads。 nr_threads++; } 变量total_forks记录被创建进程的数量 total_forks++; return p; ...... } 【do_fork--->copy_process--->sched_fork】 void sched_fork(struct task_struct *p) { unsigned long flags; int cpu = get_cpu(); 初始化进程调度实体se的相关成员 __sched_fork(p); 将进程状态置于运行状态 p->state = TASK_RUNNING; 新进程的优先级继承于父进程的普通优先级。对于优先级做一下说明,进程有三个优先级:static_prio:静态优先级是进程启动时分配的优先级。它可以用nice系统调用修改。 normal_prio:基于进程的静态优先级和调度策略计算出来的优先级,调度策略也可以通过系 统调用改变,所以即使静态优先级相同,普通优先级也可能不同。 prio:在某些情况下内核需要暂时提高进程的优先级,所以就需要第三个成员来表示。调度 器算法中要考虑的优先级就保存在prio。 p->prio = current->normal_prio; sched_reset_on_fork用于判断是否恢复默认的优先级或调度策略。 if (unlikely(p->sched_reset_on_fork)) { if (task_has_rt_policy(p)) { p->policy = SCHED_NORMAL; p->static_prio = NICE_TO_PRIO(0); p->rt_priority = 0; } else if (PRIO_TO_NICE(p->static_prio) < 0) p->static_prio = NICE_TO_PRIO(0); p->prio = p->normal_prio = __normal_prio(p); 设置进程权重,关于权重的问题后面会讲解。 set_load_weight(p); p->sched_reset_on_fork = 0; } 如果不是事实进程,就设置调度类为完全公平调度类 if (!rt_prio(p->prio)) p->sched_class = &fair_sched_class; if (p->sched_class->task_fork) p->sched_class->task_fork(p); raw_spin_lock_irqsave(&p->pi_lock, flags); 设置进程在那个cpu上运行; set_task_cpu(p, cpu); raw_spin_unlock_irqrestore(&p->pi_lock, flags); ...... } 【do_fork--->copy_process--->alloc_pid】 struct pid *alloc_pid(struct pid_namespace *ns) { struct pid *pid; enum pid_type type; int i, nr; struct pid_namespace *tmp; struct upid *upid; 分配一个pid结构,一个进程对应着一个这样的结构,该结构用于内核空间对进程的管理 pid = kmem_cache_alloc(ns->pid_cachep, GFP_KERNEL); PID命名空间结构中有一个位图pid_ns->pidmap[],每一个位就表示在该命名空间中的一个 进程号,如果该位位1表示该位对应的进程号已经被分配。所以在该命名空间分配进程号就是在位图中找第一个为0的位。ns->level表示该命名空间所在的层次,其值越小表示层次越高。上面提到pid结构是内核对进程管理的结构,upid就对应着用户空间的进程号。进程PID命名空间可能有很多层次,每一个层次中就对应着一个这样的结构,所以新进程建立是要为它在它本命名空间和上层所有命名空间中分配一个id。 tmp = ns; for (i = ns->level; i >= 0; i--) { nr = alloc_pidmap(tmp); if (nr < 0) goto out_free; pid->numbers[i].nr = nr; pid->numbers[i].ns = tmp; tmp = tmp->parent; } 获取新进程的命名空间。 get_pid_ns(ns); pid->level = ns->level; atomic_set(&pid->count, 1); for (type = 0; type < PIDTYPE_MAX; ++type) INIT_HLIST_HEAD(&pid->tasks[type]); upid = pid->numbers + ns->level; spin_lock_irq(&pidmap_lock); 将进程的所有upid结构都置于pid哈希表中。 for ( ; upid >= pid->numbers; --upid) hlist_add_head_rcu(&upid->pid_chain, &pid_hash[pid_hashfn(upid->nr, upid->ns)]); spin_unlock_irq(&pidmap_lock); ...... } 【do_fork--->wake_up_new_task】 void wake_up_new_task(struct task_struct *p) { #ifdef CONFIG_SMP 设置进程的调度队列完全公平调度队列p->se.cfs_rq或者实时调度队列p->rt.rt_rq。 set_task_cpu(p, select_task_rq(p, SD_BALANCE_FORK, 0)); #endif rq指向就绪队列,每个CPU只有一个这样的队列,所有将要在该CPU上运行等待调度的进程都在该队列中。 rq = __task_rq_lock(p); 函数activate_task将新进程插入队列中p->sched_class->enqueue_task(rq, p, flags);等待调度,该函数特定于调度类,后面讲解调度算法时讲解。 activate_task(rq, p, 0); 现在进程已经在队列中了。 p->on_rq = 1; trace_sched_wakeup_new(p, true); 用一个新换新的进程来抢占当前进程,当然,能否抢占得了,还得看其优先级等相关参数。 check_preempt_curr(rq, p, WF_FORK); #ifdef CONFIG_SMP if (p->sched_class->task_woken) p->sched_class->task_woken(rq, p); #endif task_rq_unlock(rq, p, &flags); |
|