fork 是一个系统调用,流程的最后会在 sys_call_table 中找到相应的系统调用 sys_fork。
_do_fork 里面做的第一件大事就是 copy_process,咱们前面讲过这个思想。如果所有数据结构都从头创建一份太麻烦了,还不如使用惯用“伎俩”,Ctrl C + Ctrl V。
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。
轮到权限相关了,copy_creds 主要做了下面几件事情:
- 调用 prepare_creds,准备一个新的 struct cred *new。如何准备呢?其实还是从内存中分配一个新的 struct cred 结构,然后调用 memcpy 复制一份父进程的 cred;
- 接着 p->cred = p->real_cred = get_cred(new),将新进程的“我能操作谁”和“谁能操作我”两个权限都指向新的 cred。
sched_fork 主要做了下面几件事情:
- 调用 __sched_fork,在这里面将 on_rq 设为 0,初始化 sched_entity,将里面的 exec_start、sum_exec_runtime、prev_sum_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,也就是说,把父进程设置为应该被调度,这样下次调度的时候,父进程会被子进程抢占。
_do_fork 做的第二件大事是 wake_up_new_task。新任务刚刚建立,有没有机会抢占别人,获得 CPU 呢?
在 enqueue_task_fair 中取出的队列就是 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 中,会调用相应的调度类的 rq->curr->sched_class->check_preempt_curr(rq, p, flags)。对于 CFS 调度类来讲,调用的是 check_preempt_wakeup。
这个图的上半部分是复制 task_struct 结构,你可以对照着右面的 task_struct 结构图,看这里面的成员是如何一部分一部分地被复制的。图的下半部分是唤醒新创建的子进程,如果条件满足,就会将当前进程设置应该被调度的标识位,就等着当前进程执行 __schedule 了。
此文章为10月Day29学习笔记,内容来源于极客时间《趣谈Linux操作系统》,推荐该课程。