fork,意味“分支”,可以创建一个进程。那创建进程这个动作在内核里做了什么事情呢?
fork是一个系统调用,根据系统调用的流程,流程的最后会在sys_call_table中找到相应的系统调用sys_fork。
sys_fork是如何定义的了,如下,根据SYSCALL_DEFINE0这个宏的定义,就定义出了sys_fork:
SYSCALL_DEFINE0(fork)
{
......
return _do_fork(SIGCHLD, 0, 0, NULL, NULL, 0);
}
sys_fork会调用_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)
{
struct task_struct *p;
int trace = 0;
long nr;
......
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
......
if (!IS_ERR(p)) {
struct pid *pid;
pid = get_task_pid(p, PIDTYPE_PID);
nr = pid_vnr(pid);
if (clone_flags & CLONE_PARENT_SETTID)
put_user(nr, parent_tidptr);
......
wake_up_new_task(p);
......
put_pid(pid);
}
......
fork的第一件大事:复制结构
_dor_fork里面做的第一件大事就是copy_process。如果所有的数据结构都从头创建一份太麻烦了,所以Ctrl+ C + Ctrl+ V
其复制的是task_struct这个进程数据结构
static __latent_entropy struct task_struct *copy_process(
unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *child_tidptr,
struct pid *pid,
int trace,
unsigned long tls,
int node)
{
int retval;
struct task_struct *p;
......
p = dup_task_struct(current, node);
dup_task_struct 主要做了下面几件事情:
- 调用alloc_task_struct_node分配一个task_struct结构
- 调用alloc_thread_stack_node 创建内核栈,这里面调用__vmalloc_node_range 分配一个连续的THREAD_SIZE的内存空间,赋值给task_struct的void *stack成员变量
- 调度arch_dup_task_struct(struct task_struct *dst, struct task_struct *src),将task_struct进行复制,其实就是调用memcpy
- 调用setup_thread_stack 设置thread_info
到这里,整个task_struct复制了一份,而且内核栈也创建好了。
接下来就是权限相关了,copy_creds主要做了下面几件事情:
retval = copy_creds(p, clone_flags);
- 调用prepare_creds,准备一个新的struct cred *new。如果准备呢?其实还是从内存中分配一个新的struct cred结果,然后调用memcpy复制一份父进程的cred
- 接着p->cred = p->real_cred = get_cred(new),将新进程的“我能操作谁”和“谁能操作我”两个权限都指向新的cred。
接下来,copy_process重新设置进程运行的统计量:
p->utime = p->stime = p->gtime = 0;
p->start_time = ktime_get_ns();
p->real_start_time = ktime_get_boot_ns();
接下来,copy_process 开始设置调度相关的变量:
retval = sched_fork(clone_flags, p);
sched_fork主要做了下面几件事情:
- 调用__sched_fork,在这里面将on_rq设为0,初始化sched_entity,将里面的exec_start、sum_exec_runtime、prev_exec_runtime、vruntime都设为0。这几个变量涉及进程的实际运行时间和虚拟运行时间。是否到时间应该被调度了,就靠它们几个
- 设置进程的状态: p->state= TASK_NEW;
- 初始化优先级: prio、normal_prio、static_prio
- 设置调度类,如果是普通进程,就设置为p->sched_class = &fair_sched_class;
- 调用调度类的task_fork函数,对于CFS来讲,就是调用task_fork_fair。在这个函数里,
- 先调度员update_curr,对当前的进程进行统计量更新
- 然后把子进程和父进程的vruntime设成一样
- 最后调用place_entity,初始化sched_entity。 这里有一个变量,sysctl_sched_child_runs_first,可以设置父进程和子进程谁先运行。如果设置了子进程先运行,即便两个进程的vruntime一样,也要把子进程的sched_entity放在前面,然后调用resched_curr,标记当前运行的进程TIF_NEED_RESCHED,也就是说,把父进程设置为应该被调度,这样下次调度的时候,父进程会被子进程抢占。
接下来,copy_process开始初始化与文件和文件系统相关的变量:
retval = copy_files(clone_flags, p);
retval = copy_fs(clone_flags, p);
- copy_files主要用于复制一个进程打开的文件信息。
- 这些信息用一个结构files_struct来维护,每个打开的文件都有一个文件描述符。
- 在copy_files函数里面调用dup_fd,在这里会创建一个新的files_struct,然后将所有的文件描述符数组fdtable拷贝一份
- copy_fs主要用于复制一个进程的目录信息。
- 这些信息由一个结构 fs_struct 来维护
- 一个进程有自己的根目录和根文件系统root,也有当前目录pwd和当前目录的文件系统,都在fs_struct里面维护
- copy_fs函数里面调用copy_fs_struct,创建一个新的fs_struct,并赋值原来进行的fs_struct。
接下来,copy_process开始初始化与信号相关的变量:
init_sigpending(&p->pending);
retval = copy_sighand(clone_flags, p);
retval = copy_signal(clone_flags, p);
- copy_sighand会分配一个新的sighand_struct。这里最主要的是维护信号处理函数,在copy_sighand里面会调用memcpy,将信号处理函数sighand->action从父进程赋值到子进程
- init_sigpending 和 copy_signal 用于初始化,并且复制用于维护发给这个进程的信号的数据结构。copy_signal函数会分配一个新的 signal_struct,并进行初始化
接下来,copy_process开始复制进程内存空间
retval = copy_mm(clone_flags, p);
- 进程都有自己的内存空间,用mm_struct结构来表示
- copy_mm函数中调用dup_mm,分配一个新的mm_struct,调用memcpy复制这个函数
- dup_mmap用于复制内存空间中内存映射的部分(mmap除了可以分配大块的内存,还可以将一个文件映射到内存中,方便可以像读写内存一样读写文件)
接下来,copy_process开始分配pid、设置tid、group_leader,并且建立进程之间的亲缘关系。
INIT_LIST_HEAD(&p->children);
INIT_LIST_HEAD(&p->sibling);
......
p->pid = pid_nr(pid);
if (clone_flags & CLONE_THREAD) {
p->exit_signal = -1;
p->group_leader = current->group_leader;
p->tgid = current->tgid;
} 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;
p->tgid = p->pid;
}
......
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;
}
好了,copy_process 要结束了,上面图中的组件也初始化的差不多了
fork的第二件大事:唤醒新进程
_do_fork 做的第二件大事是 wake_up_new_task。新任务刚刚建立,有没有机会抢占别人,获得CPU呢?
void wake_up_new_task(struct task_struct *p)
{
struct rq_flags rf;
struct rq *rq;
......
p->state = TASK_RUNNING;
......
activate_task(rq, p, ENQUEUE_NOCLOCK);
p->on_rq = TASK_ON_RQ_QUEUED;
trace_sched_wakeup_new(p);
check_preempt_curr(rq, p, WF_FORK);
......
}
首先,将进程的状态设置为TASK_RUNNING
activate_task函数会调用enqueue_task
static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
.....
p->sched_class->enqueue_task(rq, p, flags);
}
如果是CFS的调度类,则执行相应的enqueue_task_fair
static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
struct cfs_rq *cfs_rq;
struct sched_entity *se = &p->se;
......
cfs_rq = cfs_rq_of(se);
enqueue_entity(cfs_rq, se, flags);
......
cfs_rq->h_nr_running++;
......
}
- 在enqueue_entity中取出的队列就是cfs_rq ,然后调用enqueue_entity
- 在enqueue_entity函数里面,会调用update_curr更新运行的统计量,然后调用__enqueue_entity,将sched_entity 加入到红黑树里面,然后将se->on_rq = 1设置在队列上
- 回到enqueue_task_fair 之后,将这个队列上运行的进程数目加一。然后,wake_up_new_task 会调用check_preempt_curr,看是否能够抢占当前进程。
- 在check_preempt_curr中,会调用相应的调度类q->curr->sched_class->check_preempt_curr(rq, p, flags)。对于CFS调度类来讲,调用的是check_preempt_wakeup
static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
struct task_struct *curr = rq->curr;
struct sched_entity *se = &curr->se, *pse = &p->se;
struct cfs_rq *cfs_rq = task_cfs_rq(curr);
......
if (test_tsk_need_resched(curr))
return;
......
find_matching_se(&se, &pse);
update_curr(cfs_rq_of(se));
if (wakeup_preempt_entity(se, pse) == 1) {
goto preempt;
}
return;
preempt:
resched_curr(rq);
......
}
- 在check_preempt_wakeup函数中,前面调用task_fork_fair 的时候,设置了sysctl_sched_child_runs_first ,已经将当前父进程的TIF_NEED_RESCHED 设置了,则直接返回
- 否则,check_preempt_wakeup 还是会调用update_curr 更新一次统计量,然后wakeup_preempt_entity 将父进程和子进程PK一次,看是不是要抢占,如果要则调度resched_curr 标记父进程为TIF_NEED_RESCHED
- 如果新创建的进程应该抢占父进程,在什么时候抢占了?fork是一个系统调用,从系统调用返回的时候,就是一个抢占的好时机,如果父进程判断自己已经被设置为TIF_NEED_RESCHED,就让子进程先跑,抢占自己