文章目录
- 2.进程创建和终止
- 2.1 进程创建的4种方法
- 2.2 进程创建过程分析
- 2.2.1 copy_process函数分析
- 2.2.1.1 dup_task_struct函数分析
- 2.2.1.2 sched_fork函数分析
- 2.2.1.3 copy_mm函数分析
- 2.2.1.4 copy_thread函数分析
- 2.2.2 wake_up_new_task函数分析
2.进程创建和终止
在 Linux 系统中,每个进程都有独立的内存空间和上下文环境,并且可以独立地执行、分配资源和与其他进程进行通信。线程是进程中的一种执行单元,它也有自己的栈、程序计数器和寄存器等,但是它们共享进程的内存和大部分上下文环境,可以方便地进行数据共享和协作。线程实际上也是一种特殊的进程,轻量级进程,它们与普通进程的区别只是在于它们共享内存和上下文环境的方式。因此,每个线程都会有自己的进程标识符和堆栈,但它们使用相同的地址空间并共享同一组文件描述符、信号处理器和进程调度器等,所以这里的进程创建包括了线程。
2.1 进程创建的4种方法
我们一般使用fork系统调用创建进程,其实Linux操作系统还提供了vfork、clone这两个系统调用让我们创建进程。
vfork系统调用和fork系统调用类似,但是vfork的父进程会一直阻塞,直到子进程调用exit()或者execve()为止。clone系统调用通常用于创建用户线程。在Linux内核中没有专门的线程,而是把线程当成普通进程来看待,在内核中还以task_struct数据结构来描述线程,并没有使用特殊的数据结构或者调度算法来描述线程。Clone允许创建一个与父进程共享某些资源的子进程,这些资源可以是内存、文件或其他系统资源。所以,clone与fork相比具有更高的灵活性,可以更细粒度地控制子进程与父进程之间的资源共享,支持创建轻量级进程,即占用比较少的内存和资源的进程。我们看看这几个系统调用的代码:
1.SYSCALL_DEFINE0(fork)
2.{
3. struct kernel_clone_args args = {
4. .exit_signal = SIGCHLD,
5. };
6.
7. return kernel_clone(&args);
8.}
9.
10.SYSCALL_DEFINE0(vfork)
11.{
12. struct kernel_clone_args args = {
13. .flags = CLONE_VFORK | CLONE_VM,
14. .exit_signal = SIGCHLD,
15. };
16.
17. return kernel_clone(&args);
18.}
19.
20.SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
21. int __user *, parent_tidptr,
22. unsigned long, tls,
23. int __user *, child_tidptr)
24.{
25. struct kernel_clone_args args = {
26. .flags = (lower_32_bits(clone_flags) & ~CSIGNAL),
27. .pidfd = parent_tidptr,
28. .child_tid = child_tidptr,
29. .parent_tid = parent_tidptr,
30. .exit_signal = (lower_32_bits(clone_flags) & CSIGNAL),
31. .stack = newsp,
32. .tls = tls,
33. };
34.
35. return kernel_clone(&args);
36.}
我们可以直观看到kthread_run → kthread_create → kthread_create_on_node → __kthread_create_on_node的调用流程。我们继续看__kthread_create_on_node函数:
1.static __printf(4, 0)
2.struct task_struct *__kthread_create_on_node(int (*threadfn)(void *data),
3. void *data, int node,
4. const char namefmt[],
5. va_list args)
6.{
7. DECLARE_COMPLETION_ONSTACK(done);
8. struct task_struct *task;
9. struct kthread_create_info *create = kmalloc(sizeof(*create),
10. GFP_KERNEL);
11.
12. if (!create)
13. return ERR_PTR(-ENOMEM);
14. create->threadfn = threadfn;
15. create->data = data;
16. create->node = node;
17. create->done = &done;
18.
19. spin_lock(&kthread_create_lock);
20. list_add_tail(&create->list, &kthread_create_list);
21. spin_unlock(&kthread_create_lock);
22.
23. wake_up_process(kthreadd_task);
24.
25. if (unlikely(wait_for_completion_killable(&done))) {
26. wait_for_completion(&done);
27. }
28. task = create->result;
29. return task;
30.}
我们可以看到__kthread_create_on_node函数主要做了一下几件事:
- 创建了一个struct kthread_create_info数据类型的变量create,同时把要运行的函数和参数写入create中;
- 把create加入了kthread_create_list队列;
- 唤醒kthreadd_task进程后等待kthreadd_task执行完成;
- 从create中获取到task_struct结构体,并且返回这个结构体。
返回的这个结构体就是我们已经创建成功的进程了,那么kthreadd_task进程是怎么创建进程的呢?继续看下去,kthreadd_task是一个全局变量,他的初始化在init/main.c文件的rest_init函数中:
1.noinline void __ref rest_init(void)
2.{
3....
4. pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
5. rcu_read_lock();
6. kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
7....
8.}
我们可以看到系统通过kernel_thread函数创建了kthreadd进程得到进程的pid,并且通过这个pid找到kthreadd进程的进程描述符赋值给全局变量kthreadd_task。我们前面唤醒kthreadd_task其实就是唤醒kthreadd进程,我们在看看kthreadd吧:
1.int kthreadd(void *unused)
2.{
3. struct task_struct *tsk = current;
4.
5. /* Setup a clean context for our children to inherit. */
6. set_task_comm(tsk, "kthreadd");//把本进程的名字改为kthreadd
7. ignore_signals(tsk);//屏蔽进程的所有信号
8. //设置进程的 CPU 亲和性,去除了ISOLATION的cpu
9. set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_FLAG_KTHREAD));
10. //设置进程的可用NUMA节点
11. set_mems_allowed(node_states[N_MEMORY]);
12.
13. current->flags |= PF_NOFREEZE;//设置标志位表示这个进程不可以冻结
14. cgroup_init_kthreadd();//禁止用户发起cgroup迁移
15.
16. for (;;) {//死循环
17. set_current_state(TASK_INTERRUPTIBLE);//设置当前进程为轻度睡眠
18. if (list_empty(&kthread_create_list))//如果链表为空
19. schedule();//主动发起调度
20. __set_current_state(TASK_RUNNING);//设置当前进程为可运行状态
21.
22. spin_lock(&kthread_create_lock);
23. //当kthread_create_list不为空的时候进入while循环
24. while (!list_empty(&kthread_create_list)) {
25. struct kthread_create_info *create;
26. //从kthread_create_list链表中取出一个create
27. create = list_entry(kthread_create_list.next,
28. struct kthread_create_info, list);
29. list_del_init(&create->list);//把create踢出链表
30. spin_unlock(&kthread_create_lock);
31.
32. create_kthread(create);//根据create创建进程
33.
34. spin_lock(&kthread_create_lock);
35. }
36. spin_unlock(&kthread_create_lock);
37. }
38.
39. return 0;
40.}
我么可以看到kthreadd一开始是初始化自己,然后在死循环中取出kthread_create_list链表数据create,最后调用create_kthread(create)创建进程了。create_kthread函数仅仅是调用kernel_thread函数而已,我们看看kernel_thread:
1.pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
2.{
3. struct kernel_clone_args args = {
4. .flags = ((lower_32_bits(flags) | CLONE_VM |
5. CLONE_UNTRACED) & ~CSIGNAL),
6. .exit_signal = (lower_32_bits(flags) & CSIGNAL),
7. .stack = (unsigned long)fn,
8. .stack_size = (unsigned long)arg,
9. };
10.
11. return kernel_clone(&args);
12.}
到这里我们又看到很熟悉的画面了,就是kernel_clone_args和kernel_clone函数。原来内核创建线程也是通过kernel_clone函数的。
到目前为止,我们无论是用户态的系统调用还是内核提供的函数,他们都是通过设置struct kernel_clone_args结构体为传入参数,然后调用kernel_clone函数来创建进程的。
2.2 进程创建过程分析
我们已经知道系统是通过kernel_clone函数来创建进程的,这里仅仅是创建进程,并且加入到就绪队列中,至于运行是由调度器决定是否运行的,我们来看看kernel_clone函数吧:
1.pid_t kernel_clone(struct kernel_clone_args *args)
2.{
3....
4. //函数创建一个新的子进程
5. p = copy_process(NULL, trace, NUMA_NO_NODE, args);
6. add_latent_entropy();
7.
8. //根据子进程的task_struct数据结构获取 pid结构体
9. pid = get_task_pid(p, PIDTYPE_PID);
10. nr = pid_vnr(pid);//由子pid结构体来计算PID值
11.
12. if (clone_flags & CLONE_VFORK) {
13. p->vfork_done = &vfork;
14. init_completion(&vfork);//初始化完成量
15. get_task_struct(p);
16. }
17.
18. //唤醒新创建的进程,把进程加入就绪队列里并接受调度、运行
19. wake_up_new_task(p);
20.
21. if (clone_flags & CLONE_VFORK) {
22. //等待子进程调用exec()或者exit()
23. if (!wait_for_vfork_done(p, &vfork))
24. ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
25. }
26.
27. put_pid(pid);//减少pid数据得应用计数
28. return nr;//返回值为子进程的ID
29.}
从代码可以看到kernel_clone函数主要做了一下几件事:
- 调用函数copy_process创建一个新的子进程,返回进程描述符;
- 调用函数get_task_pid根据进程描述符得到pid结构体,然后计算出pid值;
- 调用函数wake_up_new_task唤醒进程,其实也就是把进程加入到就绪队列中,并且设置进程的状态,等待调度器调度;
- 如果有CLONE_VFORK标志位表示是vfork创建的进程,需要设置父进程的完成量,要等待子进程运行完毕;
- 最后返回进程的PID值。
这里比较重要的函数有创建进程描述符的copy_process函数和把进程加入就绪队列的wake_up_new_task。
2.2.1 copy_process函数分析
1.static __latent_entropy struct task_struct *copy_process(
2. struct pid *pid,
3. int trace,
4. int node,
5. struct kernel_clone_args *args)
6.{
7. int pidfd = -1, retval;
8. struct task_struct *p;
9. struct multiprocess_signals delayed;
10. struct file *pidfile = NULL;
11. u64 clone_flags = args->flags;
12. struct nsproxy *nsp = current->nsproxy;
13. ...
14. //为新进程分配一个task_struct数据结构
15. p = dup_task_struct(current, node);
16. if (!p)
17. goto fork_out;
18.
19. rt_mutex_init_task(p);//初始化新task_struct中的几个实时互斥量
20.
21. retval = copy_creds(p, clone_flags);//初始化进程的凭据,实现权限和安全相关
22. if (retval < 0)
23. goto bad_fork_free;
24.
25. delayacct_tsk_init(p);//初始化延迟任务,用于计算延迟信息
26. INIT_LIST_HEAD(&p->children);//初始化子进程链表
27. INIT_LIST_HEAD(&p->sibling);//初始化兄弟进程链表
28. rcu_copy_process(p);//初始化task_struct中rcu相关数据和链表
29. p->utime = p->stime = p->gtime = 0;//初始化task的用户态、系统态和子进程消耗时间
30. prev_cputime_init(&p->prev_cputime);//初始化prev_cputime结构体,用于计算cpu时间
31.
32. //初始化ioac成员,用于存储进程的 I/O 操作统计信息
33. task_io_accounting_init(&p->ioac);
34. acct_clear_integrals(p);//清除task中的acct*信息
35.
36. posix_cputimers_init(&p->posix_cputimers);
37.
38. p->io_context = NULL;//初始化io_context
39. audit_set_context(p, NULL);//初始化审计上下文
40. cgroup_fork(p);//初始化cgroup相关字段
41.
42. /* Perform scheduler related setup. Assign this task to a CPU. */
43. //初始化与进程调度相关的数据结构。将此任务分配给CPU。
44. retval = sched_fork(clone_flags, p);
45. if (retval)
46. goto bad_fork_cleanup_policy;
47.
48. //初始化task_struct中的perf_event上下文
49. retval = perf_event_init_task(p);
50. if (retval)
51. goto bad_fork_cleanup_policy;
52. retval = audit_alloc(p);//为任务分配审计上下文块
53. if (retval)
54. goto bad_fork_cleanup_perf;
55. /* copy all the process information */
56. shm_init_task(p);//初始化sysvshm成员
57. //申请task的security资源
58. retval = security_task_alloc(p, clone_flags);
59. if (retval)
60. goto bad_fork_cleanup_audit;
61. //初始化task的sysvsem.undo_list
62. retval = copy_semundo(clone_flags, p);
63. if (retval)
64. goto bad_fork_cleanup_security;
65. //复制父进程打开的文件等信息
66. retval = copy_files(clone_flags, p);
67. if (retval)
68. goto bad_fork_cleanup_semundo;
69. //复制父进程的fs_struct数据结构
70. retval = copy_fs(clone_flags, p);
71. if (retval)
72. goto bad_fork_cleanup_files;
73. //复制父进程的信号处理函数,主要是sighand成员的初始化
74. retval = copy_sighand(clone_flags, p);
75. if (retval)
76. goto bad_fork_cleanup_fs;
77. //复制父进程的信号系统,主要是signal成员的初始化
78. retval = copy_signal(clone_flags, p);
79. if (retval)
80. goto bad_fork_cleanup_sighand;
81. //复制父进程的进程地址空间的页表
82. retval = copy_mm(clone_flags, p);
83. if (retval)
84. goto bad_fork_cleanup_signal;
85. //复制父进程的命名空间
86. retval = copy_namespaces(clone_flags, p);
87. if (retval)
88. goto bad_fork_cleanup_mm;
89. //复制父进程中与I/O相关的内容,主要是io_context成员
90. retval = copy_io(clone_flags, p);
91. if (retval)
92. goto bad_fork_cleanup_namespaces;
93. //复制父进程的内核堆信息,主要是thread成员
94. retval = copy_thread(clone_flags, args->stack, args->stack_size, p, args->tls);
95. if (retval)
96. goto bad_fork_cleanup_io;
97.
98. stackleak_task_init(p);//设置最低栈地址lowest_stack,用于栈溢出检查
99.
100. if (pid != &init_struct_pid) {
101. //为新进程分配一个pid数据结构和PID
102. pid = alloc_pid(p->nsproxy->pid_ns_for_children, args->set_tid,
103. args->set_tid_size);
104. if (IS_ERR(pid)) {
105. retval = PTR_ERR(pid);
106. goto bad_fork_cleanup_thread;
107. }
108. }
109.
110. futex_init_task(p);//初始化进程的Futex相关数据,包括robust_list、futex_state、futex_exit_mutex、pi_state_list、pi_state_cache
111.
112.
113. /* ok, now we should be set up.. */
114. p->pid = pid_nr(pid);//分配一个全局PID号
115. if (clone_flags & CLONE_THREAD) {//创建一个线程
116. p->group_leader = current->group_leader;
117. p->tgid = current->tgid;
118. } else {//创建一个进程
119. p->group_leader = p;
120. p->tgid = p->pid;
121. }
122.
123. //初始化脏页相关
124. p->nr_dirtied = 0;
125. p->nr_dirtied_pause = 128 >> (PAGE_SHIFT - 10);
126. p->dirty_paused_when = 0;
127.
128. p->pdeath_signal = 0;
129. INIT_LIST_HEAD(&p->thread_group);//初始化线程组链表
130. p->task_works = NULL;//回调任务链表初始化
131. clear_posix_cputimers_work(p);//空
132.
133. //初始化进程的开始时间
134. p->start_time = ktime_get_ns();
135. p->start_boottime = ktime_get_boottime_ns();
136.
137. spin_lock(¤t->sighand->siglock);
138. copy_seccomp(p);//初始化seccomp数据,用于安全计算
139.
140. rseq_fork(p, clone_flags);//初始化task的rseq_fork、rseq_sig和rseq_event_mask,作用不了解
141.
142. init_task_pid_links(p);//初始化task的pid_links数组
143.
144. if (pidfile)
145. fd_install(pidfd, pidfile);//将文件描述符和文件/管道的指针关联起来
146.
147. sched_post_fork(p, args);//为新进程设置调度属性
148. cgroup_post_fork(p, args);//为新进程进行组设置
149. perf_event_fork(p);//设置新进程性能事件
150.
151. trace_task_newtask(p, clone_flags);//trace相关
152. uprobe_copy_process(p, clone_flags);//复制父进程的用户层断点
153.
154. copy_oom_score_adj(clone_flags, p);//复制父进程的signal->oom_score_adj
155.
156. return p;//返回子进程描述符
157.
158.
159.}
copy_process函数主要做了一下几件事:
- 根据标志位kernel_clone_args.flags,判断是否存在不合理的地方,如果是则返回错误码;比如CLONE_NEWNS和CLONE_FS不可以同时出现,因为不允许不同命名空间共享根目录;又比如CLONE_THREAD和CLONE_SIGHAND需要同时出现,因为线程组必须共享信号相关;
- 调用dup_task_struct函数为新进程分配一个task_struct数据结构;
- 初始化task_struct数据结构,有一些成员是赋初值,有一些成员是从父进程拷贝;从父进程拷贝的下面介绍;
- 调用函数sched_fork根据父进程情况初始化进程调度相关的数据结构;
- 调用函数copy_files复制父进程打开的文件等信息;
- 调用函数copy_fs复制父进程的fs_struct数据结构;
- 调用函数copy_sighand复制父进程的信号处理函数,初始化sighand成员;
- 调用函数copy_signal复制父进程的信号系统,初始化signal成员;
- 调用函数copy_mm复制父进程的进程地址空间的页表;(重点)
- 调用函数copy_thread复制父进程的内核堆信息,初始化thread成员;
- 调用函数alloc_pid为新进程分配一个pid数据结构和PID;
- 初始化其他成员完毕后返回进程描述符。
我们比较关注的是dup_task_struct、sched_fork和copy_mm函数。
2.2.1.1 dup_task_struct函数分析
1.static struct task_struct *dup_task_struct(struct task_struct *orig, int node)
2.{
3. struct task_struct *tsk;
4. unsigned long *stack;
5. struct vm_struct *stack_vm_area __maybe_unused;
6. int err;
7.
8. //为新进程分配一个进程描述符
9. tsk = alloc_task_struct_node(node);
10. if (!tsk)
11. return NULL;
12.
13. //为新进程分配内核栈空间
14. stack = alloc_thread_stack_node(tsk, node);
15. if (!stack)
16. goto free_tsk;
17. //将一个内核线程的内存栈大小计入内存控制组的限额中
18. if (memcg_charge_kernel_stack(tsk))
19. goto free_stack;
20.
21. //复制父进程的内核栈所在的内存区域,也就是stack_vm_area,
22. stack_vm_area = task_stack_vm_area(tsk);
23.
24. //处理进程描述符体系结构相关部分,arm64更新了thread_info.flags
25. err = arch_dup_task_struct(tsk, orig);
26.
27. tsk->stack = stack;//设置栈内存地址
28.#ifdef CONFIG_VMAP_STACK
29. tsk->stack_vm_area = stack_vm_area;//设置内核栈所在的vma
30.#endif
31.#ifdef CONFIG_THREAD_INFO_IN_TASK
32. refcount_set(&tsk->stack_refcount, 1);//设置stack_refcount为1
33.#endif
34.
35. clear_tsk_need_resched(tsk);//清除TIF_NEED_RESCHED标志位
36. set_task_stack_end_magic(tsk);//设置任务栈结束标记
37.
38.#ifdef CONFIG_STACKPROTECTOR
39. tsk->stack_canary = get_random_canary();//初始化stack_canary
40.#endif
41. if (orig->cpus_ptr == &orig->cpus_mask)
42. tsk->cpus_ptr = &tsk->cpus_mask;
43.
44. refcount_set(&tsk->rcu_users, 2);//初始化rcu引用计数
45. /* One for the rcu users */
46. refcount_set(&tsk->usage, 1);//task的引用计数为1
47.#ifdef CONFIG_BLK_DEV_IO_TRACE
48. tsk->btrace_seq = 0;
49.#endif
50. //初始化task_struct的几个成员而已
51. tsk->splice_pipe = NULL;
52. tsk->task_frag.page = NULL;
53. tsk->wake_q.next = NULL;
54.
55. account_kernel_stack(tsk, 1);//更新内核栈这块内存在lrc链表的热度
56.
57. kcov_task_init(tsk);//初始化kcov相关,用于统计内核代码覆盖率,这里是空
58.
59.
60.#ifdef CONFIG_BLK_CGROUP
61. //初始化IO限流相关
62. tsk->throttle_queue = NULL;
63. tsk->use_memdelay = 0;
64.#endif
65.
66.#ifdef CONFIG_MEMCG
67. //初始化mem cgroup
68. tsk->active_memcg = NULL;
69.#endif
70. return tsk;//返回task_struct结构体
71.
72.free_stack:
73. free_thread_stack(tsk);
74.free_tsk:
75. free_task_struct(tsk);
76. return NULL;
77.}
dup_task_struct函数申请进程描述符的内存,同时初始化进程描述符,这几工作主要分为以下几步:
- 调用函数alloc_task_struct_node从kmem中分配进程描述符的内存
- 调用函数alloc_thread_stack_node分配新进程的占空间,优先从cached_stacks缓存栈分配,分配失败再通过vmalloc分配,分配成功会设置到进程描述符中;
- 调用函数memcg_charge_kernel_stack把内核线程的内存栈大小计入内存控制组的限额中,避免内核态栈使用过多内存,导致内存不足或者内存负载不均衡的问题;
- 调用函数task_stack_vm_area复制父进程的内核栈所在的vma,也就是stack_vm_area,后面会设置到新创建的进程描述符中
- 调用函数arch_dup_task_struct处理进程描述符体系结构相关部分,arm64主要是拷贝了thread_info.flags,然后清除了TIF_SVE和TIF_MTE_ASYNC_FAULT,这部分主要是跟架构相关,x86会有很大的差别;
- 调用函数clear_tsk_need_resched清除clear_tsk_need_resched标志位,这样子系统回到用户态会让父进程继续运行,直到式时间片的到来;
- 调用函数set_task_stack_end_magic设置任务栈结束标记,每次占空间被使用都会检查这个标记,保证发生栈溢出的时候会报警;
- 后面还会初始化一些成员,这些初始化都很直观,就不细说,最后会返回进程描述符。
2.2.1.2 sched_fork函数分析
1.int sched_fork(unsigned long clone_flags, struct task_struct *p)
2.{
3. __sched_fork(clone_flags, p);//设置cfs、rt和dl调度实体,主要是se、rt、dl这三个成员
4.
5. p->state = TASK_NEW;//设置进程的状态:TASK_NEW,它还没被添加到调度器里
6.
7. p->prio = current->normal_prio;//继承父进程优先级
8.
9. //如果调度信息需要重置
10. if (unlikely(p->sched_reset_on_fork)) {
11. if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
12. p->policy = SCHED_NORMAL;
13. p->static_prio = NICE_TO_PRIO(0);
14. p->rt_priority = 0;
15. } else if (PRIO_TO_NICE(p->static_prio) < 0)
16. p->static_prio = NICE_TO_PRIO(0);
17.
18. p->prio = p->normal_prio = p->static_prio;
19. set_load_weight(p);
20.
21. p->sched_reset_on_fork = 0;
22. }
23.
24. //根据优先级设置调度类
25. if (dl_prio(p->prio))//如果是dl进程
26. return -EAGAIN;
27. else if (rt_prio(p->prio))//如果是rt进程
28. //选用RT的调度类rt_sched_class
29. p->sched_class = &rt_sched_class;
30. else//那就是普通进程
31. //选用CFS的调度类fair_sched_class
32. p->sched_class = &fair_sched_class;
33.
34. init_entity_runnable_average(&p->se);//初始化与子进程的调度实体,主要是se->avg
35.
36.#ifdef CONFIG_SCHED_INFO
36. //初始化sched_info数据
37. if (likely(sched_info_on()))
38. memset(&p->sched_info, 0, sizeof(p->sched_info));
40.#endif
41.#if defined(CONFIG_SMP)
39. p->on_cpu = 0;//还没有进入就绪队列
43.#endif
40. init_task_preempt_count(p);//初始化task的thread_info的preempt_count
45.#ifdef CONFIG_SMP
41. //初始化pushable_tasks和pushable_dl_tasks
42. plist_node_init(&p->pushable_tasks, MAX_PRIO);
43. RB_CLEAR_NODE(&p->pushable_dl_tasks);
49.#endif
44. return 0;
51.}
sched_fork函数主要是初始化进程调度相关的数据结构,包括了:
- 调用函数__sched_fork初始化cfs、rt和dl调度实体,包括是否在就绪队列、虚拟运行时间、迁移此时还有统计调度的其他信息等等;
- 设置进程的状态为TASK_NEW,这是一个临时的状态;
- 继承父进程的优先级;
- 判断sched_reset_on_fork参数决定是否重置调度优先级,一般不会重置的;
- 根据进程的优先级设置进程的调度类,也就是sched_class;
- 调用函数init_entity_runnable_average初始化新进程的调度实体se,也就是把整个se设置为0,然后把进程的平均负载设置为最低负载;
- 初始化sched_info和on_cpu成员等等,最后返回0。
2.2.1.3 copy_mm函数分析
1.static int copy_mm(unsigned long clone_flags, struct task_struct *tsk)
2.{
3. struct mm_struct *mm, *oldmm;
4. int retval;
5.
6. //初始化进程的缺页情况
7. tsk->min_flt = tsk->maj_flt = 0;
8. tsk->nvcsw = tsk->nivcsw = 0;
9.#ifdef CONFIG_DETECT_HUNG_TASK
9. //更新进程调度次数和调度时间
10. tsk->last_switch_count = tsk->nvcsw + tsk->nivcsw;
11. tsk->last_switch_time = 0;
13.#endif
12.
13. tsk->mm = NULL;
14. tsk->active_mm = NULL;
15.
16. //借用父进程的mm
17. oldmm = current->mm;
18. if (!oldmm)
19. return 0;
20.
21. /* initialize the new vmacache entries */
22. vmacache_flush(tsk);//初始化vmacache.vmas
23.
24. //如果创建的是线程
25. if (clone_flags & CLONE_VM) {
26. mmget(oldmm);//父进程的user数量+1
27. mm = oldmm;
28. goto good_mm;
29. }
30.
31. retval = -ENOMEM;
32. //复制父进程的进程地址空间
33. mm = dup_mm(tsk, current->mm);
34. if (!mm)
35. goto fail_nomem;
36.
39.good_mm:
37. //设置mm和active_mm
38. tsk->mm = mm;
39. tsk->active_mm = mm;
40. return 0;
41.
45.fail_nomem:
42. return retval;
47.}
copy_mm主要是初始化进程的内存相关数据,主要是mm_struct,具体作用一下几件事:
- 初始化进程的缺页情况和更新进程的调用次数;
- 找到父进程的mm_struct。如果父进程的mm_struct为空,说明父进程是内核进程,那子进程也是内核进程,不需要mm_struct,直接返回0即可;
- 调用函数vmacache_flush初始化vmacache的vmas指针数组,它是进程查找vma的快速路径;
- 如果clone_flags的CLONE_VM被置位了,说明创建的是线程,父子进程共用一个mm_struct,所以要把mm_struct的user数量加一,设置好进程的mm和active_mm成员就可以返回0了;
- 到这里说明创建的是进程,需要调用函数dup_mm复制父进程的地址空间,然后设置好进程的mm和active_mm成员再返回;
我们继续看看dup_mm函数是怎么复制父进程的地址空间的:
1.static struct mm_struct *dup_mm(struct task_struct *tsk,
48. struct mm_struct *oldmm)
3.{
49. struct mm_struct *mm;
50. int err;
51.
52. mm = allocate_mm();//子进程分配一个内存描述符mm
53. if (!mm)
54. goto fail_nomem;
55.
56. //把父进程的内存描述符的内容全部复制到子进程
57. memcpy(mm, oldmm, sizeof(*mm));
58.
59. //初始化子进程的内存描述符的一些成员
60. if (!mm_init(mm, tsk, mm->user_ns))
61. goto fail_nomem;
62.
63. //复制父进程的进程地址空间的页表到子进程
64. err = dup_mmap(mm, oldmm);
65. if (err)
66. goto free_pt;
67.
68. //初始化内存水位
69. mm->hiwater_rss = get_mm_rss(mm);
70. mm->hiwater_vm = mm->total_vm;
71.
72. if (mm->binfmt && !try_module_get(mm->binfmt->module))
73. goto free_pt;
74.
75. return mm;//返回mm_struct
76.
32.free_pt:
77. /* don't put binfmt in mmput, we haven't got module yet */
78. mm->binfmt = NULL;
79. mm_init_owner(mm, NULL);
80. mmput(mm);
81.
38.fail_nomem:
82. return NULL;
40.}
dup_mm函数主要做了以下几件事:
- 调用函数allocate_mm从kmem中申请mm_struct数据结构的内存;
- 调用函数memcpy把父进程的内存描述符的内容全部复制到子进程;
- 调用函数mm_init初始化mm_struct,毕竟是新进程,有一些数据还是要初始化的,函数把mm_users、owner、flags和context等成员,然后申请一级页表写入pgd成员;
- 调用函数dup_mmap首先初始化vm和红黑树相关的数据,如果有大页,把大页的数据设置为共享,然后遍历mm的所有vma,把共享的vma一个个拷贝后插入到子进程的mm中;
- 初始化内存水位后返回mm_struct数据结构体。
2.2.1.4 copy_thread函数分析
1.int copy_thread(unsigned long clone_flags, unsigned long stack_start,
2. unsigned long stk_sz, struct task_struct *p, unsigned long tls)
3.{
4. //找到进程的栈底,这里保存了发生异常时寄存器的信息
5. struct pt_regs *childregs = task_pt_regs(p);
6.
7. //清空cpu_context,调度进程时使用这个成员保存通用寄存器的值的
8. memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));
9.
10. fpsimd_flush_task_state(p);
11.
12. ptrauth_thread_init_kernel(p);
13.
14. //处理子进程是用户进程的情况
15. if (likely(!(p->flags & PF_KTHREAD))) {
16. //把当前进程内核栈底部的 pt_regs 结构体复制一份
17. *childregs = *current_pt_regs();
18. //把子进程的X0寄存器设置为0,也就是fork返回的0
19. childregs->regs[0] = 0;
20.
21. //设置子进程的TPIDR_EL0寄存器跟为父进程的一样
22. *task_user_tls(p) = read_sysreg(tpidr_el0);
23.
24. if (stack_start) {//如果指定了用户栈起始地址,
25. //需要设置子进程的sp
26. if (is_compat_thread(task_thread_info(p)))
27. childregs->compat_sp = stack_start;
28. else
29. childregs->sp = stack_start;
30. }
31.
32. //如果设置了CLONE_SETTLS标志
33. if (clone_flags & CLONE_SETTLS)
34. //把传入的参数tls设置到子进程中
35. p->thread.uw.tp_value = tls;
36. } else {//处理子进程是内核线程的情况
37. //把子进程内核栈底部的 pt_regs 结构体清零
38. memset(childregs, 0, sizeof(struct pt_regs));
39. //设置子进程的处理器状态为异常级别1,也就是内核态
40. childregs->pstate = PSR_MODE_EL1h;
41. if (IS_ENABLED(CONFIG_ARM64_UAO) &&
42. cpus_have_const_cap(ARM64_HAS_UAO))
43. childregs->pstate |= PSR_UAO_BIT;
44.
45. spectre_v4_enable_task_mitigation(p);
46.
47. if (system_uses_irq_prio_masking())
48. childregs->pmr_save = GIC_PRIO_IRQON;
49. //把子进程的x19寄存器设置为线程函数的地址
50. p->thread.cpu_context.x19 = stack_start;
51. //把子进程的x20寄存器设置为传给线程函数的参
52. p->thread.cpu_context.x20 = stk_sz;
53. }
54. //设置子进程的进程硬件上下文(struct cpu_context)中pc和sp成员的值
55. p->thread.cpu_context.pc = (unsigned long)ret_from_fork;//新进程内核开始运行的地方
56. p->thread.cpu_context.sp = (unsigned long)childregs;//新进程的内核栈
57.
58. ptrace_hw_copy_thread(p);
59.
60. return 0;
61.}
copy_thread函数主要是初始化子进程的堆栈信息,也是调度器调度子进程的状态信息,主要是以下几件事:
- 调用函数task_pt_regs找到子进程的栈底,这里应该保存发生异常时寄存器的信息,调度器调度的时候可以恢复的上下文就在这里;
- 清空cpu_context,调度进程时使用这个成员保存通用寄存器的值的;
- 如果创建的子进程是用户进程,拷贝父进程的栈底,也就是pt_regs结构体信息,然后把子进程的X0寄存器设置为0,这是fork系统调用的返回值,子进程返回的0就是X0寄存器设置的0;
- 设置子进程的TPIDR_EL0寄存器跟为父进程的一样,TPIDR_EL0 是用户读写线程标识符寄存器,用来存放每线程数据的基准地址,存放每线程数据的区域通常被称为线程本地存储(Thread Local Storage,TLS);
- 如果指定了用户栈起始地址,需要设置子进程的sp,正常操作我们都不会设置这个参数;
- 如果创建的是内核进程,需要把子进程内核栈底部的 pt_regs 结构体清零,因为内核进程跟父进程基本是独立的,不需要继承其堆栈;
- 设置子进程的处理器状态为异常级别1,也就是内核态;把子进程的x19寄存器设置为线程函数的地址;把子进程的x20寄存器设置为传给线程函数的参数;
- 最后设置子进程的进程硬件上下文(struct cpu_context)中pc和sp成员的值,其中设置的cp值为ret_from_fork函数;
我们的子进程的PC是ret_from_fork函数,也就是新进程是从ret_from_fork函数开始执行的,这是汇编代码:
1.SYM_CODE_START(ret_from_fork)
2. bl schedule_tail //对prev进程做收尾工作
3. cbz x19, 1f //用户进程跳到1 // not a kernel thread
4. mov x0, x20
5. blr x19 //异常返回,跳到内核线程回调函数
6.1: get_current_task tsk
7. b ret_to_user //调用函数返回用户态
8.SYM_CODE_END(ret_from_fork)
ret_from_fork函数主要做了以下几件事情:
- 首先调用函数schedule_tail对父进程做一些收尾工作,主要是保存现场;
- 判断x19寄存器是否为0,如果x19不为0,表示子进程是内核进程,把x20寄存器的值移动到x0寄存器后跳转到x19寄存器的地址处,我们还记得前面的copy_thread函数中,对x19和x20寄存器的初始化吗?那时候的x19寄存器保存的是线程函数的地址,x20寄存器保存的是线程函数的参数,这就正好跳转的内核线程的地址了;
- 如果x19为0,说明子进程是用户进程,跳转到1处,调用函数get_current_task,把读取sp_el0写入tsk参数中,然后跳转到ret_to_user函数通过kernel_exit回到用户态运行;
2.2.2 wake_up_new_task函数分析
1.void wake_up_new_task(struct task_struct *p)
2.{
3. struct rq_flags rf;
4. struct rq *rq;
5.
6. raw_spin_lock_irqsave(&p->pi_lock, rf.flags);
7. p->state = TASK_RUNNING;//进程的状态变为TASK_RUNNING
8.#ifdef CONFIG_SMP
8. p->recent_used_cpu = task_cpu(p);//设置recent_used_cpu
9. rseq_migrate(p);
10. //设置子进程将来要在哪个CPU上运行
11. __set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
13.#endif
12. rq = __task_rq_lock(p, &rf);
13. update_rq_clock(rq);//更新运行队列中的计时器
14. post_init_entity_util_avg(p);
15.
16. //调用enqueue_task把子进程放入就绪队列并且设置状态(on_rq)
17. activate_task(rq, p, ENQUEUE_NOCLOCK);
18. trace_sched_wakeup_new(p);
19. //检查是否需要抢占父进程
20. check_preempt_curr(rq, p, WF_FORK);
23.#ifdef CONFIG_SMP
21. //调用cfs的ops的task_woken处理进程被唤醒的情况,毕竟是第一次唤醒
22. if (p->sched_class->task_woken) {
23. /*
24. * Nothing relies on rq->lock after this, so its fine to
25. * drop it.
26. */
27. rq_unpin_lock(rq, &rf);
28. p->sched_class->task_woken(rq, p);
29. rq_repin_lock(rq, &rf);
30. }
34.#endif
31. task_rq_unlock(rq, p, &rf);
36.}
wake_up_new_task函数主要作用是用于唤醒一个新创建的进程,把进程加入就绪队列里并接受调度器的调度,它主要做了以下几件事:
- 把保护进程描述符的自旋锁上锁
- 把进程的状态设置为TASK_RUNNING,表示进程已经就绪;
- 设置进程的recent_used_cpu成员为进程当前运行的cpu,也是父进程运行的cpu,便于后续找到最适合的cpu运行;
- 调用函数select_task_rq进行选择适合运行的cpu,这个函数是根据根据调度类来选择的,如果可运行的cpu数量大于1,则调用调度类的select_task_rq函数来找到合适的cpu;否则就返回那个唯一能运行的cpu;
- 调用函数__set_task_cpu设置子进程将来要在哪个CPU上运行,主要是设置se.cfs_rq和rt.rt_rq的调度组信息,还要设置cpu和wake_cpu;
- 调用函数__task_rq_lock锁住进程所在的运行队列;
- 调用函数post_init_entity_util_avg根据调度队列的util_avg初始化调度实体的util_avg;
- 调用函数check_preempt_curr检查是否需要抢占,这个函数判断父子进程的调度类是否一致,一致则使用调度类的check_preempt_curr函数判断是否需要抢占;否则再判断如果子进程的调度类比父进程的打打, 则调用函数resched_curr设置抢占标志;
- 调用调度类的task_woken方法对进程被唤醒的情况进行处理;
上面的说明已经比较详细了,不过继续分析check_preempt_curr函数师怎么检测是否需要抢占的:
1.void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
2.{
41. //如果父子进程的调度类相同
42. if (p->sched_class == rq->curr->sched_class)
43. //调用调度类的check_preempt_curr函数
44. rq->curr->sched_class->check_preempt_curr(rq, p, flags);
45. //如果子进程的调度类比父进程的调度类大
46. else if (p->sched_class > rq->curr->sched_class)
47. resched_curr(rq);//说明需要重新调度
48.
49. //如果父进程在队列中并且父进程需要调度
50. if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
51. rq_clock_skip_update(rq);//需要更新CPU运行队列的时钟
14.}
check_preempt_curr函数的作用是检测当前进程是否需要调度,并且设置是否调度的标志位,工作顺序如下:
- 如果子进程的调度类和父进程的相同,则调用调度类的check_preempt_curr函数判断是否需要调度,在check_preempt_curr函数中会根据情况来设置抢占标志位的;
- 如果子进程的调度类比父进程的大,也就是子进程的优先级肯定更高,说明肯定需要重新调度,那么调用函数resched_curr函数设置调度标志位;
- 如果父进程在队列中并且父进程需要调度,那么调用函数rq_clock_skip_update来更新CPU运行队列的时钟;因为当CPU上没有正在运行的进程时,内核会将该CPU的运行队列的时钟暂停,以节省系统资源,为了避免这种情况,我们需要修正cpu的时钟。
到了这里我们只需要看看resched_curr函数函数就知道系统是怎么设置重新调度的标志位的了:
1.void resched_curr(struct rq *rq)
2.{
55. struct task_struct *curr = rq->curr;
56. int cpu;
57.
58. //防止死锁的WARN_ON,
59. lockdep_assert_held(&rq->lock);
60.
61. //通过判断TIF_NEED_RESCHED标志位查看进程是否需要调度
62. if (test_tsk_need_resched(curr))
63. return;
64.
65. cpu = cpu_of(rq);//获取rq执行的cpu号
66.
67. //如果进程在当前cpu上执行
68. if (cpu == smp_processor_id()) {
69. set_tsk_need_resched(curr);//设置thread_info.flags置位TIF_NEED_RESCHED
70. set_preempt_need_resched();//设置thread_info.preempt.need_resched置位
71. return;
72. }
73. //如果进程在其他cpu上执行
74.
75. if (set_nr_and_not_polling(curr))//设置TIF_NEED_RESCHED标志
76. smp_send_reschedule(cpu);//使用IPI_RESCHEDULE通知其他cpu
77. else
78. trace_sched_wake_idle_without_ipi(cpu);
27.}
resched_curr函数主要是做了三件事:
- 首先调用函数test_tsk_need_resched查看当前进程的调度标志位已经置位,如果已经置位则可以直接返回了;
- 如果rq队列在当前进程执行,调用函数set_tsk_need_resched设置TIF_NEED_RESCHED标志和调用函数给thread_info.preempt.need_resched置位后返回;
- 否则就是rq队列在其他cpu上执行了,这时候需要调用函数set_nr_and_not_polling给thread_info.flags置位TIF_NEED_RESCHED,然后调用函数smp_send_reschedule使用IPI_RESCHEDULE中断通知其他cpu。