如何创建
分为两个步骤:
fork()/clone()
创建新内核线程execve()
执行新用户态程序
新 forked 出来的线程如何开始工作
在 fork()/clone()
的过程中:
- 由
copy_process()
调用体系结构相关的copy_thread()
来构造新线程的上下文; - 由
wake_up_new_task()
将新线程的task_struct
挂到某个 CPU 的 run queue 上,待调度器调度执行。
x86_64
1copy_thread(p, args) {
2 struct pt_regs *childregs = task_pt_regs(p);
3 struct fork_frame *fork_frame = container_of(childregs, struct fork_frame, regs);
4 struct inactive_task_frame *frame = &fork_frame->frame;
5
6 frame->bp = encode_frame_pointer(childregs);
7 frame->ret_addr = (unsigned long) ret_from_fork; // assembly func
8 p->thread.sp = (unsigned long) fork_frame; // used by __switch_to_asm to
9 // locate the top stack frame
10
11 if (/* spawned from kernel thread */) {
12 kthread_frame_init(frame, args->fn, args->fn_arg) {
13 frame->bx = (unsigned long)fun;
14 frame->r12 = (unsigned long)arg;
15 }
16 }
17}
刚 forked 出来的线程(代码注释中称之为inactive task)的上下文构造并记录于 struct fork_frame
中。该结构包括两部分:
struct inactive_task_frame
:一层内核态栈帧,其中保存的寄存器的排布需与__switch_to_asm()
对齐;struct pt_regs
:保存于内核栈底的用户态寄存器。
如下图所示,内核在创建新线程的 task_struct
时会为其分配一段长度为 THREAD_SIZE
的栈空间并记录于 task.stack
成员。通过这些信息可以找到位于内栈底的 struct pt_regs
,然后即可获得 struct fork_frame
的基地址。
结合 __switch_to_asm()
可以看出,struct inactive_stack_frame
中的寄存器排布与上下文切换的过程严丝合缝,且最后的 return
会将内核栈帧中的 ret_addr
弹出并将执行流导向这个地址,即 ret_from_fork()
。
1switch_to(prev, next, last) {
2 last = __switch_to_asm(prev, next) {
3
4 /* switch stack */
5 movq %rsp, TASK_threadsp(%rdi)
6 movq TASK_threadsp(%rsi), %rsp
7
8 /* load registers from inactive_stack_frame */
9 popq %r15
10 popq %r14
11 popq %r13
12 popq %r12
13 popq %rbx
14 popq %rbp
15 jmp __switch_to /* (prev_p, next_p) */ {
16 return prev_p; // pop out and jump to ret_addr
17 }
18 }
19}
这里 ret_addr
在内核栈帧中的位置与 x86_64 的调用约定(Calling Convention)也有关,详情请参考本站的另一篇博文《Things That Happen Around Function Calls》
对 x86_64 栈帧结构的讲解。
ARM64
1copy_thread(p, args) {
2 childregs = task_pt_regs(p);
3 if (likely(!args->fn)) {
4 *childregs = *current_pt_regs();
5 childregs->regs[0] = 0;
6 } else {
7 memset(childregs, 0, sizeof(struct pt_regs));
8 p->thread.cpu_context.x19 = (unsigned long)args->fn;
9 p->thread.cpu_context.x20 = (unsigned long)args->fn_arg;
10 }
11 p->thread.cpu_context.pc = (unsigned long)ret_from_fork; // assembly func
12 p->thread.cpu_context.sp = (unsigned long)childregs;
13}
与 x86 在栈上构造上下文不同,ARM64 直接将上下文保存在 task_struct
里面,用一个专门的成员 thread.cpu_context
来保存,因此这部分的代码逻辑也简单得多。结合 cpu_switch_to()
来看,在上下文切换的时候直接通过事先记录好的偏移量(即 THREAD_CPU_CONTEXT
)从前后两个线程的 task_struct
中获取到 thread.cpu_context
,然后保存和加载寄存器的值。
1switch_to(prev, next, last) {
2 last = __switch_to(prev, next) {
3 last = cpu_switch_to(prev, next) {
4 /* DEFINE(THREAD_CPU_CONTEXT, offsetof(struct task_struct, thread.cpu_context)) */
5 mov x10, #THREAD_CPU_CONTEXT
6 /* registers -> prev.thread.cpu_context */
7 add x8, x0, x10
8 stp x19, x20, [x8], #16
9 ...
10 /* next.thread.cpu_context -> registers */
11 add x8, x1, x10
12 ldp x19, x20, [x8], #16
13 ...
14 }
15 return last;
16 }
17}
execve()
如何执行新用户态程序
- 修改保存于内核栈底的用户态寄存器值
- 通过
return
返回到用户态
1do_execveat_common/kernel_execve()
2 bprm = alloc_bprm(fd, filename);
3 retval = bprm_execve(bprm, fd, filename, 0)
4 retval = exec_binprm(bprm) {
5 for (depth = 0;; depth++) {
6 ret = search_binary_handler(bprm) {
7 list_for_each_entry(fmt, &formats, lh) {
8 retval = fmt->load_binary(bprm);
9 }
10 }
11 }
12 }
由于运行的是 ELF 文件,因此 fmt->load_binary()
这个钩子函数实际执行的是 fs/binfmt_elf.c
中的 load_elf_binary()
:
1load_elf_binary(bprm)
2 struct pt_regs *regs = current_pt_regs();
3 START_THREAD(elf_ex, regs, elf_entry, start_stack:bprm->p)
4 == start_thread(regs, elf_entry, start_stack) {
5 /* arch related */
6 }
start_thread()
的实现是体系结构相关的,但原理大差不差,都是去修改 pt_regs
里的值。
-
x86_64
1start_thread(regs, new_ip:elf_entry, new_sp:start_stack) { 2 start_thread_common(regs, new_ip, new_sp, _cs:__USER_CS, _ss:__USER_DS, 0) { 3 regs->ip = new_ip; 4 regs->sp = new_sp; 5 regs->cs = _cs; 6 regs->ss = _ss; 7 regs->flags = X86_EFLAGS_IF; 8 } 9}
-
ARM64
1start_thread(regs, elf_entry, start_stack) { 2 start_thread_common(regs, pc) { 3 previous_syscall = regs->syscallno; 4 memset(regs, 0, sizeof(*regs)); 5 regs->syscallno = previous_syscall; 6 regs->pc = pc; 7 } 8 regs->sp = sp; 9}
如何销毁
用 strace
随便观测一个用户态程序的运行,比如 strace ls
,可以观察到最后一个 syscall:exit_group(n)
,其中 n
为用户态程序退出时的返回值。
总之就是各种释放,销毁,并且设置进程状态为 TASK_DEAD
。最终调度器会对带有该标志的进程做最后的释放工作,包括释放其 struct task_struct
。
1SYSCALL_DEFINE1(exit, error_code)
2 do_exit(...)
3 do_task_dead()
4 set_special_state(TASK_DEAD)
5 __schedule(SM_NONE)
1__schedule(...)
2 context_switch()
3 finish_task_switch(prev) {
4 prev_state = READ_ONCE(prev->__state);
5 if (unlikely(prev_state == TASK_DEAD)) {
6 put_task_struct_rcu_user(prev) {
7 call_rcu(&task->rcu, delayed_put_task_struct)
8 }
9 }
10 }
11
12delayed_put_task_struct(rhp) {
13 struct task_struct *tsk = container_of(rhp, struct task_struct, rcu);
14 put_task_struct(t:tsk)
15 __put_task_struct(t)
16 free_task(tsk)
17 free_task_struct(tsk)
18 kmem_cache_free(task_struct_cachep, tsk)
19}