微信公众号:奔跑吧linux社区

本文节选自《奔跑吧linux内核》第二版卷1第7.3.6章

在用户空间可以使用fork()接口函数来创建一个用户进程,或者使用clone()接口函数来创建一个用户线程,它们在内核空间都是调用_do_fork()函数来实现的。读者常常会对_do_fork()函数的返回感到疑惑,例如以下两个问题。

  1. 以fork()接口函数为例,为什么会有两次返回?其中父进程的返回值是子线程的PID,而子进程返回0。子线程是如何返回0的?
  2. 子进程第一次返回用户空间时,它是返回到哪里?

对于第一个问题,在调用_do_fork()函数创建子进程后,子进程也会加入内核的调度器里,在调度器中参与调度。子进程会在稍后时刻得到调度和执行,因此fork()函数会有两次返回,一次是父进程的返回,另一次是子进程被调度执行后的返回。

另外,在copy_thread()函数里会复制父进程struct pt_regs栈框的全部内容到子进程的栈框里,这个栈框描述内核栈上保存寄存器的全部信息,以ARM64架构为例,包括X0~X30寄存器、栈指针寄存器、PC寄存器以及PSTATE寄存器等信息。另外,copy_thread()函数还会修改子进程的栈框中X0寄存器的值为0,因此在返回用户空间时子进程的返回值就是0,通过X0寄存器来传递返回值。
copy_thread()函数的代码片段如下。

<arch/arm64/kernel/process.c>

1 int copy_thread()
2 {
3 ...
4 if (likely(!(p->flags & PF_KTHREAD))) {
5 childregs = current_pt_regs();
6 childregs->regs[0] = 0;
7 } else {
8 childregs->pstate = PSR_MODE_EL1h;
9 p->thread.cpu_context.x19 = stack_start;
10 p->thread.cpu_context.x20 = stk_sz;
11 }
12
13 p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
14 p->thread.cpu_context.sp = (unsigned long)childregs;
15
16 return 0;
17 }
在第4~6行中处理子进程是用户进程的情况。
在第8~10行中,处理子进程是内核线程的情况。
在第13~14行中,设置子进程的进程硬件上下文(struct cpu_context数据结构)中pc和sp成员的值。

对于第二个问题,这里面涉及新创建出来的子进程第一次是从哪里开始执行的。在copy_thread()函数里会使子进程的入口地址指向一个汇编函数ret_from_fork。它通过设置子进程的进程硬件上下文中的pc成员来实现,这涉及进程切换相关的知识,可以参考本书8.1.6节。因此,子进程第一次执行时会跳转到ret_from_fork汇编程序里。由于子进程是内核线程,因此X19寄存器会指向内核线程的回调函数。对于用户进程,子进程会调用ret_to_user汇编函数来返回用户空间。
ret_from_fork汇编函数实现在arch/arm64/kernel/entry.S文件中。

<arch/arm64/kernel/entry.S>

1 ENTRY(ret_from_fork)
2 cbz x19, 1f // 不是一个内核线程?
3 mov x0, x20
4 blr x19
5 1:
6 b ret_to_user
ret_from_fork汇编函数的代码逻辑比较简单。在第2行中,判断X19寄存器的值是否为空,如果为空,说明这是一个用户进程,则跳转到第5行代码中,调用ret_to_user汇编函数,直接返回用户空间。如果X19寄存器的值不为空,说明这是一个内核线程,直接执行X19寄存器中保存的内核线程回调函数。
对于新创建的用户进程,它会返回父进程,调用fork或者clone系统调用的下一条指令。父进程通常使用svc指令来自动陷入内核空间。以glibc-2.20版本为例,clone函数实现在sysdeps/unix/sysv/linux/aarch64/clone.S文件中,下面是__clone汇编函数的代码片段。

<glibc-2.20/sysdeps/unix/sysv/linux/aarch64/clone.S>
1 /*
2 clone()函数原型:
3 int clone(int (fn)(void arg),
4 void *child_stack,
5 int flags,
6 void *arg)
7 */
8 .global __clone
9 __clone:
10 mov x10, x0
11 mov x11, x2
12 mov x12, x3
13
14 / 调用svc指令来进入内核空间/
15 mov x0, x2
16 mov x8, #__NR_clone
17 svc 0x0 //陷入内核空间
18
19 /*

        从内核空间返回

20 判断是否为子用户进程
21 */
22 cmp x0, #0
23 beq thread_start
24 ret
25
26 .align 4
27 thread_start:
28 /执行子进程的回调函数./
29 mov x0, x12
30 blr x10
31
32 / 退出 /
33 ret
在第10~12行中,暂时先保存相关参数,例如,把子进程回调函数的入口地址保存到X10寄存器中,后面在thread_start函数里会用到。
在第15~17行中,把clone()函数的参数通过寄存器的方式来传递,然后调用svc指令来陷入内核空间,其中X8寄存器记录了系统调用号。
在第22行中,父进程和子进程都会从内核空间返回这里。由于子进程在_do_fork期间直接复制了父进程的pt_regs栈框,因此pt_regs->pc和pt_regs->pstate也复用了父进程的值。当从内核态返回用户空间时,子进程也返回此处,因为pt_regs->pc记录了返回用户空间的地址。如果判断返回的是子进程,那么会跳转到thread_start函数里。在thread_start函数里会通过blr指令来跳转到子进程的回调函数,X10寄存器保存了子进程回调函数的入口地址。

新书预告

《奔跑吧linux内核》第二版卷1已经上架了。

20220316_170342_003.jpg

《奔跑吧linux内核》第二版卷2预计春节后上架!

640.jpg

金色年华,流金岁月,奔二入门篇预计春节后上架!

20220316_170342_004.jpg
20220316_170342_005.jpg


奔跑吧Linux社区
4 声望4 粉丝

奔跑吧Linux社区,为广大小伙伴布道Linux开源!


引用和评论

0 条评论