Skip to main content

第一个线程

前置知识

  1. elf 文件,不知道的同学需要复习一下ics了

正如文档所说

然而,完成 cap_group 的分配之后,用户程序并没有办法直接运行,因为cap_group只是一个资源集合的概念。线程才是内核中的调度执行单位,因此还需要进行线程的创建,将用户程序 ELF 的各程序段加载到内存中。(此为内核中 ELF 程序加载过程,用户态进行 ELF 程序解析可参考user/system-services/system-servers/procmgr/libs/libchcoreelf/libchcoreelf.c,如何加载程序可以对user/system-services/system-servers/procmgr/srvmgr.c中的procmgr_launch_process函数进行详细分析)

详细看一下几个函数就能分析出来,elf的文件解析,加载elf指定的env和segment,其RWX的权限是由解析的flags得到的,而起始地址也是elf之中指定的vaddr转换而成的

然后可能有问题的是binary_procmgr_bin_start,它指向程序中嵌入的二进制代码的起始地址。这个变量是通过链接器脚本或者编译器的特殊指令(如GCC的incbin)嵌入到二进制文件中的

需要注意的是,segment在加载的时候是一个DATA类型的pmo

anyway, 我觉得没有自己实现的必要(

chcore是对齐glibc 的thread abi的,所以有thread env之类的

一个有趣的讨论兼容性的帖子: https://news.ycombinator.com/item?id=35271498

create_root_thread这个函数是真的长……

最后的大致的流程

  1. 读取初始化进程的元数据
    • 从内核二进制文件中读取初始化进程(procmgr)的入口点、标志、程序头表项大小、程序头表项数量和程序头表地址。
  2. 创建根能力组
    • 创建一个根能力组(root_cap_group),这是管理线程和进程的能力组。
  3. 获取初始化虚拟地址空间
    • 从根能力组中获取初始化虚拟地址空间(init_vmspace)。
  4. 为根线程分配用户栈
    • 分配一个物理内存对象(PMO)作为根线程的用户栈,并将其映射到初始化虚拟地址空间。
  5. 分配根线程
    • 分配一个线程对象(thread)。
  6. 映射程序头表项
    • 遍历程序头表项,为每个段分配PMO,并将其映射到初始化虚拟地址空间。
  7. 释放初始化虚拟地址空间的引用
    • 释放对初始化虚拟地址空间的引用。
  8. 准备环境
    • 为根线程准备环境,包括栈和程序入口点。
  9. 初始化根线程
    • 使用根能力组、栈地址、入口点和优先级初始化根线程。
  10. 将根线程添加到能力组的线程列表
    • 将根线程添加到根能力组的线程列表中,并增加线程计数。
  11. 为根线程分配能力
    • 为根线程分配一个能力(thread_cap)。
  12. 刷新缓存
    • 刷新指令缓存和数据缓存,以确保新线程的指令和数据是最新的。
  13. 将根线程放入就绪队列
    • 将根线程放入调度器的就绪队列,准备执行。

不过对于要求填写lab的部分,实际上只是ELF的一些信息处理,比较复杂的缓存、线程栈分配等都已经在lab之中给出了,总之这几个文件都其实是在做一些ABI的对齐工作

然后是 init_thread_ctx 函数

要填写的也很少,只有thread结构体之中 *arch_exec_ctx_t* ec

就是体系结构特定的寄存器

至此,我们完成了第一个用户进程与第一个用户线程的创建。接下来就可以从内核态向用户态进行跳转了。 回到kernel/arch/aarch64/main.c,在create_root_thread()完成后,分别调用了sched()eret_to_thread(switch_context())。 sched()的作用是进行一次调度,在此场景下我们创建的第一个线程将被选择。

switch_context()函数的作用则是进行线程上下文的切换,包括vmspace、fpu、tls等。并且将cpu_info中记录的当前CPU线程上下文记录为被选择线程的上下文(完成后续实验后对此可以有更深的理解)。switch_context() 最终返回被选择线程的thread_ctx地址,即target_thread->thread_ctx

eret_to_thread最终调用了kernel/arch/aarch64/irq/irq_entry.S中的 __eret_to_thread 函数。其接收参数为target_thread->thread_ctx,将 target_thread->thread_ctx 写入sp寄存器后调用了 exception_exit 函数,exception_exit 最终调用 eret 返回用户态,从而完成了从内核态向用户态的第一次切换。

注意此处因为尚未完成exception_exit函数,因此无法正确切换到用户态程序,在后续完成exception_exit后,可以通过 gdb 追踪 pc 寄存器的方式查看是否正确完成内核态向用户态的切换。