昨天在do_fork实现–上中学习了do_fork创建的前半段,今天我们接着继续分析copy_Process函数
分析了copy_fs, copy_files, copy_signal, copy_sighand, copy_mm,今天接着分析copy_thread, copy_thread是和架构相关的,需要到具体的ARCH目录下去看
在分析copy_thread之前,我们先看几个知识点:
重点结构体学习
代码语言:javascript复制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体系相关的。
代码语言:javascript复制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中。这个会在进程调度文章中有详细描述。
再看一个结构体:
代码语言:javascript复制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继续分析
代码语言:javascript复制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函数
代码语言:javascript复制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函数
代码语言:javascript复制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函数
代码语言:javascript复制/* 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分析
代码语言:javascript复制/*
* 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的代码有点长,分段来简单看下。
代码语言:javascript复制.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的解释