文章目錄
- 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。