do_fork实现--下

Posted Loopers

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了do_fork实现--下相关的知识,希望对你有一定的参考价值。

昨天在do_fork实现–上中学习了do_fork创建的前半段,今天我们接着继续分析copy_Process函数

分析了copy_fs, copy_files, copy_signal, copy_sighand, copy_mm,今天接着分析copy_thread, copy_thread是和架构相关的,需要到具体的ARCH目录下去看

 

在分析copy_thread之前,我们先看几个知识点:

重点结构体学习

struct task_struct 
    struct thread_info thread_info;
 
    void* stack;
 
   /* CPU-specific state of this task: */
    struct thread_struct        thread;

上次在学threadinfo和内核栈的时候介绍过thread_info和stack的关系,今天再需要介绍一个结构体struct thread_struct结构。从注释上看这个结构体是个CPU体系相关的。

struct cpu_context 
    unsigned long x19;
    unsigned long x20;
    unsigned long x21;
    unsigned long x22;
    unsigned long x23;
    unsigned long x24;
    unsigned long x25;
    unsigned long x26;
    unsigned long x27;
    unsigned long x28;
    unsigned long fp;
    unsigned long sp;
    unsigned long pc;
;
 
 
struct thread_struct 
    struct cpu_context    cpu_context;    /* cpu context */
 
    unsigned int        fpsimd_cpu;
    void            *sve_state;    /* SVE registers, if any */
    unsigned int        sve_vl;        /* SVE vector length */
    unsigned int        sve_vl_onexec;    /* SVE vl after next exec */
    unsigned long        fault_address;    /* fault info */
    unsigned long        fault_code;    /* ESR_EL1 value */
    struct debug_info    debug;        /* debugging */
;

当然了我们目前只关注struct cpu_context结构,此结构会在进程切换时用来保存上一个进程的寄存器的值。一般会需要切换出去的进程的x19-x28以及fp, sp, lr寄存器保存到cpu_context中。这个会在进程调度文章中有详细描述。

 

再看一个结构体:

struct user_pt_regs 
    __u64        regs[31];
    __u64        sp;
    __u64        pc;
    __u64        pstate;
;
 
/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs 
    union 
        struct user_pt_regs user_regs;
        struct 
            u64 regs[31];
            u64 sp;
            u64 pc;
            u64 pstate;
        ;
    ;
    u64 orig_x0;
#ifdef __AARCH64EB__
    u32 unused2;
    s32 syscallno;
#else
    s32 syscallno;
    u32 unused2;
#endif
 
    u64 orig_addr_limit;
    u64 unused;    // maintain 16 byte alignment
    u64 stackframe[2];
;

从注释上看struct pt_regs主要的作用是用来保存,当用户空间的进程发生异常(系统调用,中断等)进入内核模式,则需要将用户进程当前的寄存器状态保存到pt_regs中。

struct thread_struct & struct pt_regs的区别

  • thread_struct结构体主要是在内核态两个进程发生切换时,thread_struct用来保存上一个进程的相关寄存器。
  • pt_regs结构体主要是当用户态的进程陷入到内核态时,需要使用pt_regs来保存用户态进程的寄存器状态。

copy_process继续分析

static inline int copy_thread_tls(
        unsigned long clone_flags, unsigned long sp, unsigned long arg,
        struct task_struct *p, unsigned long tls)

    return copy_thread(clone_flags, sp, arg, p);

 
//代码路径:arch/arm64/kernel/process.c
int copy_thread(unsigned long clone_flags, unsigned long stack_start,
        unsigned long stk_sz, struct task_struct *p)

    struct pt_regs *childregs = task_pt_regs(p);
 
    memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));

在copy_thread就会涉及到我们刚才上面学习的两个结构体,我们来做简单的分析下。

  • struct pt_regs *childregs = task_pt_regs(p);   获取到新创建进程的pt_regs结构,看下是如何获取的。
#define task_stack_page(task)    ((void *)(task)->stack)
 
#define task_pt_regs(p) \\
    ((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)
  • 很清楚,通过进程的task_struct结构获取到内核栈stack成员,然后加上THREAD_SIZE就是内核栈的大小,所以pt_regs是存储在内核栈的栈底的。
  • memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context));  将新创建进程的thread_struct结构清空

 

我们继续分析copy_thread函数,为了更清楚的表述,我们分段描述copy_thread函数

if (likely(!(p->flags & PF_KTHREAD)))        //用户进程
    *childregs = *current_pt_regs();
    childregs->regs[0] = 0;
 
    /*
     * Read the current TLS pointer from tpidr_el0 as it may be
     * out-of-sync with the saved value.
     */
    *task_user_tls(p) = read_sysreg(tpidr_el0);
 
    if (stack_start) 
        if (is_compat_thread(task_thread_info(p)))
            childregs->compat_sp = stack_start;
        else
            childregs->sp = stack_start;
    
 
    /*
     * If a TLS pointer was passed to clone (4th argument), use it
     * for the new thread.
     */
    if (clone_flags & CLONE_SETTLS)
        p->thread.uw.tp_value = childregs->regs[3];
  • 接着就会去判断当前进程是不是内核线程,很明显没有设置PF_KTHREAD标志
  • 通过current_pt_regs获取当前进程的pt_regs, 然后将当前进程的pt_regs结构的值赋值给新创建进程的pt_regs
  • childregs->regs[0] = 0; 这里操作的原因是,一般用户态通过系统调度陷入到内核态后处理完毕后会通过x0寄存器设置返回值的,这里首先将返回值设置为0
  • 如果stack_start设置了,这个是在clone时候传递的参数。当创建内核线程或者通过pthread_create会设置此值,此值就对应的是线程的回调处理函数
  • 如果stack_start设置了,则这里是pthread_create创建的用户线程,则设置用户态的SP_EL0指针,childregs->sp = stack_start;
 else 
    memset(childregs, 0, sizeof(struct pt_regs));
    childregs->pstate = PSR_MODE_EL1h;
    if (IS_ENABLED(CONFIG_ARM64_UAO) &&
        cpus_have_const_cap(ARM64_HAS_UAO))
        childregs->pstate |= PSR_UAO_BIT;
 
    if (arm64_get_ssbd_state() == ARM64_SSBD_FORCE_DISABLE)
        childregs->pstate |= PSR_SSBS_BIT;
 
    p->thread.cpu_context.x19 = stack_start;
    p->thread.cpu_context.x20 = stk_sz;
  • 走到这里,则当前创建的是一个内核线程。如果是内核线程的话则不需要pt_regs结构,则需要清空memset(childregs, 0, sizeof(struct pt_regs));
  • childregs->pstate = PSR_MODE_EL1h;  设置当前进程是pstate是在EL1模式下,ARM64架构中使用pstate来描述当前处理器模式.
  • p->thread.cpu_context.x19 = stack_start;  创建内核线程的时候会传递内核线程的回调函数到stack_start的参数,将其设置到x19寄存器。
  • p->thread.cpu_context.x20 = stk_sz; 通用创建内核线程的时候也会传递回调函数的参数,设置到x20寄存器
  p->thread.cpu_context.pc = (unsigned long)ret_from_fork;
    p->thread.cpu_context.sp = (unsigned long)childregs;
 
asmlinkage void ret_from_fork(void) asm("ret_from_fork")
  • 设置新创建进程的pc指针为ret_from_fork,当新创建的进程运行时会从ret_from_fork运行,ret_from_fork是个汇编语言编写的
  • 设置新创建进程的SP_EL1的值为childregs, SP_EL1则是指向内核栈的栈底处。

我们用一张图简单的总结下:

至此分析完毕了copy_thread函数。

 

继续分析copy_process函数

if (pid != &init_struct_pid) 
    pid = alloc_pid(p->nsproxy->pid_ns_for_children);
    if (IS_ERR(pid)) 
        retval = PTR_ERR(pid);
        goto bad_fork_cleanup_thread;
    

 
/* ok, now we should be set up.. */
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;

 
total_forks++;
return  p;
  • init_struct_pid是0号进程静态分配的pid,如果不相等的话则分配struct pid的结构体
  • pid_nr(pid) 真正的返回pid的number
  • 如果设置了CLONE_THREAD,则创建的新进程是一个线程,则退出信号设置为-1, 因为线程退出不需要发送信号给父进程。
  • 因为创建的是一个线程,则group_leader和tgid和当前进程保持一致
  • 如果设置了CLONE_PARENT,则新创建的进程和当前进程是兄弟关系,则退出信号跟踪当前进程的。
  • 设置新创建进程的线程组的组长和tgid都是自己
  • 增加一次fork的次数,返回新创建进程的task_struct结构。

至此do_fork的源代码就分析完毕了。do_fork的源代码比较长,在这个过程中只讲解了大概的主干分支,细节有可能没分析到,感兴趣的小伙伴去分析。

 

新创建的进程第一次运行

当copy_process返回新创建进程的task_struct结构后,则wake_up_new_task来唤醒进程,此函数中设置进程的状态为TASK_RUNNING, 选择需要在那个cpu上运行,然后将此进程加入到该cpu的对应的就绪队列中,等待CPU的调度。

当调度器选择此进程运行时,则就会运行之前在copy_thread中设置的ret_from_fork函数

/* GPRs used by entry code */
tsk    .req    x28        // current thread_info
 
/*
 * Return the current thread_info.
 */
    .macro    get_thread_info, rd
    mrs    \\rd, sp_el0
    .endm
 
 
/*
 * This is how we return from a fork.
 */
ENTRY(ret_from_fork)
    bl    schedule_tail
    cbz    x19, 1f                // not a kernel thread
    mov    x0, x20
    blr    x19
1:    get_thread_info tsk
    b    ret_to_user
ENDPROC(ret_from_fork)
  • schedule_tail 此函数主要是为上一个切换出去的进程做一个扫尾的工作,在进程切换小节详解
  • 接着就判断x19的值是不是为0,在copy_thread中如果是一个内核线程会设置x19的。
  • 如果x19的值不为0,则会通过blr x19,去处理内核线程的回调函数的。其中x20要赋值给x0, x0一般当做参数传递
  • 如果x19的值是为0的话,则会跳到标号1处。
  • get_thread_info会去读SP_EL0的值,SP_EL0的值存储的是当前进程的thread_info的值。
  • tsk代表的是x28,则使用x28存储当前进程thread_info的值,然后跳转到ret_to_user处返回用户空间

ret_to_user分析

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
    mov    x0, sp                // 'regs'
    bl    do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
    bl    trace_hardirqs_on        // enabled while in userspace
#endif
    ldr    x1, [tsk, #TSK_TI_FLAGS]    // re-check for single-step
    b    finish_ret_to_user
/*
 * "slow" syscall return path.
 */
ret_to_user:
    disable_daif
    ldr    x1, [tsk, #TSK_TI_FLAGS]
    and    x2, x1, #_TIF_WORK_MASK
    cbnz    x2, work_pending
finish_ret_to_user:
    enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
    bl    stackleak_erase
#endif
    kernel_exit 0
ENDPROC(ret_to_user)
  • ret_to_user的第一句就是disable_daif, daif是ARM64 PSTATE代表当前处理器状态的,disable_daif则就会关闭DAIF各个位,D(debug)A(Serror)I(IRQ)F(FIQ)
  • ldr    x1, [tsk, #TSK_TI_FLAGS], 将thread_info.flags的值赋值给X1
  • and    x2, x1, #_TIF_WORK_MASK, 将X1的值和_TIF_WORK_MASK的值或,_TIF_WORK_MASK是一个宏,里面包含了很多字段,比如是否需要调度字段_TIF_NEED_RESCHED等
  • cbnz    x2, work_pending 当X2的值不等于0时,则跳转到work_pending做一个慢速的ret过程,在do_notify_resume中检查是否要对pending的任务做进一步的操作
  • 然后调用kernel_exit 0返回到用户空间

 

kernel_exit分析

kernel_exit的代码有点长,分段来简单看下。

.macro    kernel_exit, el
.if    \\el != 0
disable_daif
 
/* Restore the task's original addr_limit. */
ldr    x20, [sp, #S_ORIG_ADDR_LIMIT]
str    x20, [tsk, #TSK_TI_ADDR_LIMIT]
 
/* No need to restore UAO, it will be restored from SPSR_EL1 */
.endif
 
ldp    x21, x22, [sp, #S_PC]        // load ELR, SPSR
.if    \\el == 0
ct_user_enter
.endif
  • 当el不等于0时,此时还是调用disable_daif来关闭中断,debug等功能
  • 恢复task原始的add_limit,没研究这东西是做啥的,不关系。
  • ldp    x21, x22, [sp, #S_PC] ,其中SP是在copy_thread的时候设置了,sp是指向了struct pt_regs结构的。而此条指令是load pt_regs结构中的PC=X21, PSTATE=X22寄存器
.if    \\el == 0
ldr    x23, [sp, #S_SP]        // load return stack pointer
msr    sp_el0, x23
tst    x22, #PSR_MODE32_BIT        // native task?
b.eq    3f
  • 如果el=0的话,ldr    x23, [sp, #S_SP] 这条指令是返回struct pt_regs结构中的SP=X23
  • msr    sp_el0, x23  #将x23的值设置到SP_EL0寄存器中,SP_EL0就是用户态EL0的堆栈寄存器
3:
    apply_ssbd 0, x0, x1
    .endif
 
    msr    elr_el1, x21            // set up the return data
    msr    spsr_el1, x22
    ldp    x0, x1, [sp, #16 * 0]
    ldp    x2, x3, [sp, #16 * 1]
    ldp    x4, x5, [sp, #16 * 2]
    ldp    x6, x7, [sp, #16 * 3]
    ldp    x8, x9, [sp, #16 * 4]
    ldp    x10, x11, [sp, #16 * 5]
    ldp    x12, x13, [sp, #16 * 6]
    ldp    x14, x15, [sp, #16 * 7]
    ldp    x16, x17, [sp, #16 * 8]
    ldp    x18, x19, [sp, #16 * 9]
    ldp    x20, x21, [sp, #16 * 10]
    ldp    x22, x23, [sp, #16 * 11]
    ldp    x24, x25, [sp, #16 * 12]
    ldp    x26, x27, [sp, #16 * 13]
    ldp    x28, x29, [sp, #16 * 14]
    ldr    lr, [sp, #S_LR]
    add    sp, sp, #S_FRAME_SIZE        // restore sp
 
DEFINE(S_LR,            offsetof(struct pt_regs, regs[30]));
DEFINE(S_FRAME_SIZE,        sizeof(struct pt_regs))
  • 刚才已经从x21,x22获取了pc和pstate的值,则通过msr指令将x21和x22的设置到elr_el1,spsr_el1寄存器中。
  • 接着就是从pt_regs结构体宏恢复x0-x29寄存器的值。这些寄存器都是从用户态陷入到内核态时保存的。现在给恢复回去
  • ldr    lr, [sp, #S_LR] 获取LR寄存器的值,LR就是连接返回地址。
  • add    sp, sp, #S_FRAME_SIZE, 给sp加上pt_regs结构体的大小,则恢复SP堆栈指针的值
  .if    \\el == 0
alternative_insn eret, nop, ARM64_UNMAP_KERNEL_AT_EL0
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
    bne    4f
    msr    far_el1, x30
    tramp_alias    x30, tramp_exit_native
    br    x30
4:
    tramp_alias    x30, tramp_exit_compat
    br    x30
#endif
    .else
    eret
    .endif
    sb
    .endm
  • 等el=0时,则br跳转到x30返回,x30就是lr寄存器
  • 否则通过eret返回。

这个是kernel_exit的实现,大家有兴趣的话可以看看kernel_entry的实现,里面会有保存寄存器的过程。这里就不分析了。

至此我们关系do_fork的实现分析完毕,总结下我们都涉及的内容

  • copy_process的实现,有几个重点
    • sched_fork
    • copy_mm
    • copy_thread
    • 这三个函数是重点,调度会在后面学习调度的时候分析。mm会在内存管理的时候分析
  • 新创建进程的第一次运行
  • ret_to_user的解释
  • kernel_exit的解释

 

以上是关于do_fork实现--下的主要内容,如果未能解决你的问题,请参考以下文章

do_fork实现--上

do_fork函数

do_fork() 源码剖析

Linux 内核进程管理 ( 进程相关系统调用源码分析 | fork() 源码 | vfork() 源码 | clone() 源码 | _do_fork() 源码 | do_fork() 源码 )

fork()相关的源码解析

do_fork源码阅读