如何创建
分为两个步骤:
- 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            msr     sp_el0, x1  // x1: next
15            ...
16        }
17        return last;
18    }
19}
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}
 
          