第一个线程
前置知识
- 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这个函数是真的长……
最后的大致的流程
- 读取初始化进程的元数据:
- 从内核二进制文件中读取初始化进程(procmgr)的入口点、标志、程序头表项大小、程序头表项数量和程序头表地址。
- 创建根能力组:
- 创建一个根能力组(
root_cap_group
),这是管理线程和进程的能力组。
- 创建一个根能力组(
- 获取初始化虚拟地址空间:
- 从根能力组中获取 初始化虚拟地址空间(
init_vmspace
)。
- 从根能力组中获取 初始化虚拟地址空间(
- 为根线程分配用户栈:
- 分配一个物理内存对象(PMO)作为根线程的用户栈,并将其映射到初始化虚拟地址空间。
- 分配根线程:
- 分配一个线程对象(
thread
)。
- 分配一个线程对象(
- 映射程序头表项:
- 遍历程序头表项,为每个段分配PMO,并将其映射到初始化虚拟地址空间。
- 释放初始化虚拟地址空间的引用:
- 释放对初始化虚拟地址空间的引用。
- 准备环境:
- 为根线程准备环境,包括栈和程序入口点。
- 初始化根线程:
- 使用根能力组、栈地址、入口点和优先级初始化根线程。
- 将根线程添加到能力组的线程列表:
- 将根线程添加到根能力组的线程列表中,并增加线程计数。
- 为根线程分配能力:
- 为根线程分配一个能力(
thread_cap
)。
- 为根线程分配一个能力(
- 刷新缓存:
- 刷新指令缓存和数据缓存,以确保新线程的指令和数据是最新的。
- 将根线程放入就绪队列:
- 将根线程放入调度器的就绪队列,准备执行。
不过对于要求填写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 寄存器的方式查看是否正确完成内核态向用户态的切换。