思考题 1: 内核从完成必要的初始化到用户态程序的过程是怎么样的?尝试描述一下调用关系。
内核启动到用户程序启动的流程:
main
├── uart_init
├── mm_init
├── arch_interrupt_init
├── create_root_thread
│ ├── create_root_cap_group
│ ├── __create_root_thread
│ └── switch_to_thread
└── eret_to_thread
└── switch_context
- Chcore 启动后会依次初始化
uart
模块、内存管理模块、中断模块 - 然后调用
create_root_thread
创建一个根进程,创建进程的cap_group
结构体并初始化,包括分配一块虚拟地址空间vmspace
和slot_table
的初始化,__create_root_thread
会先从磁盘中载入 ELF 文件,为进程创建一个主线程,最后将线程root_thread放入根进程中切换到线程执行; eret_to_thread
则switch_context
完成从内核模式到用户模式的切换,并在用户模式下开始运行用户代码。
sys_create_cap_group()
这段代码是一个系统调用函数,名为 sys_create_cap_group,用于创建一个新的能力组(cap_group),并将其分配给指定的进程(pid)。
在这个函数中,首先检查当前的能力组是否为 ROOT_PID(即根能力组),如果不是,则返回错误码 -EPERM。
接下来,通过 obj_alloc 函数分配一个新的能力组结构体(new_cap_group),如果分配失败,则返回错误码 -ENOMEM。
然后,通过 cap_group_init 函数初始化新的能力组,将其与基本对象编号(BASE_OBJECT_NUM)和指定的进程 ID(pid)关联起来。
接着,通过 cap_alloc 函数在当前的能力组中分配一个新的能力(cap),并将其与新的能力组(new_cap_group)关联起来。如果分配失败,则返回错误码 -1。
然后,通过 cap_copy 函数将新的能力组(new_cap_group)复制到当前线程的能力组中,并将新的能力组设为第一个能力(cap[0])。
接下来,通过 obj_alloc 函数分配一个新的虚拟内存空间结构体(vmspace),如果分配失败,则返回错误码 -ENOMEM。
然后,通过 vmspace_init 函数初始化新的虚拟内存空间,并将其与指定的进程 ID(pid)关联起来。
接着,通过 cap_alloc 函数在新的能力组中分配一个新的能力(cap),并将其与新的虚拟内存空间(vmspace)关联起来。如果分配失败,则返回错误码 -1。
最后,通过 copy_from_user 函数将指定的能力组名称(cap_group_name)复制到新的能力组结构体(new_cap_group)中,并返回新的能力(cap)。
如果出现任何错误,则会释放之前分配的对象并返回相应的错误码。
Capability
Capability 可以理解为 Linux 下的文件描述符。它把一个资源对象和访问权限封装到了一起,并对外提供一个整形 cap 做访问的句柄(句柄就是对资源对象的指针或者引用的一种抽象)
ChCore 中每个 capability 都属于一个进程。cap 的值实际上就是对象在所属的 process
的 slot_table
中的下标。
示例代码:
// 仅为演示,删掉了部分异常处理的代码
// 分配 cap
int sys_create_pmo(u64 size, u64 type)
{
int cap;
struct pmobject *pmo;
pmo = obj_alloc(TYPE_PMO, sizeof(*pmo)); // 分配对象
pmo_init(pmo, type, size, 0); // 初始化
cap = cap_alloc(current_process, pmo, 0); // 挂载到进程上,分配 cap 编号
return cap;
}
// 使用 cap
int sys_map_pmo(u64 target_process_cap, u64 pmo_cap, u64 addr, u64 perm)
{
struct pmobject *pmo;
// 根据 cap 获取对象的指针
pmo = obj_get(current_process, pmo_cap, TYPE_PMO);
// 操作对象,省略之
// ......
// 声明自己操作结束,为了并发安全准备的。
obj_put(pmo);
}
下面可以看几个例子感受一下cap是如何被使用的。
创建object并分配cap
在thread.h
的 create_thread
函数中,我们需要创建线程,然后把线程加入到进程的slot_table
中管理起来,同时要返回cap作为索引。其中核心的一句如下:
thread = obj_alloc(TYPE_THREAD, sizeof(*thread));
任何需要通过cap来管理的资源都是通过object
来抽象的,所以需要先创建一个object
对象。该函数的第二个参数是线程的大小,这是因为我们需要用这个大小来初始化object
,使其能容纳我们需要的资源。
仔细看一下这个函数的定义:
void *obj_alloc(u64 type, u64 size)
{
u64 total_size;
struct object *object;
total_size = sizeof(*object) + size;
object = kmalloc(total_size);
if (!object)
return NULL;
object->type = type;
object->size = size;
object->refcount = 0;
/*
* If the cap of the object is copied, then the copied cap (slot) is
* stored in such a list.
*/
init_list_head(&object->copies_head);
return object->opaque;
}
在分配object
的时候,kmalloc
的大小为sizeof(*object)+size
,最后返回的是opaque
字段。此时object
的内存布局如下:
object
语句:thread = obj_alloc(TYPE_THREAD, sizeof(*thread));
,相当于是thread = malloc(sizeof(struct thread))
,并且额外地,在头部加上了点别的信息,这样组成了一个object
。这样一个函数调用完成了thread空间的分配和object的初始化。
这里最精妙的地方就是这个opaque
的类型,是个数组,因此最后返回这个数组名的时候,实际上返回的是指向第一个元素的指针,即第二个参数size
分配的额外的内存空间的起始地址。如果换成u64*
指针类型的话就没有这种效果了。
然后我们通过cap = cap_alloc(process, thread, 0);
,将线程加入到进程的slot_table
中,并且返回cap,最终返回给用户,
根据cap获取对应的被管理的对象(线程,pmo等)
如何根据线程的cap来获取线程本身?在cap_group
函数中有如下语句:root_thread = obj_get(root_process, thread_cap, TYPE_THREAD);
PMO(Physical Memory Object)
物理内存对象
memory.c
中包含对PMO操作的各种方法
ELF
https://paper.seebug.org/papers/Archive/refs/elf/Understanding_ELF.pdf
解析 ELF 文件,并将其内容加载到新线程的用户内存空间中。
struct elf_file {
struct elf_header header;
struct elf_program_header *p_headers;
struct elf_section_header *s_headers;
};
Field | Purpose |
---|---|
p_type | 此数据成员说明了本程序头所描述的段的类型,或者如何解析本程序头的信 息。 |
p_flags | Segment-dependent flags (position for 64-bit structure). |
p_offset | Offset of the segment in the file image.文件镜像中该段的偏移量。 |
p_vaddr | Virtual address of the segment in memory.该段在内存中的虚拟地址。 |
p_paddr | On systems where physical address is relevant, reserved for segment's physical address.在物理地址相关的系统中,为段的物理地址保留。 |
p_filesz | Size in bytes of the segment in the file image. May be 0.文件镜像中段的大小,以字节为单位。可以是0。 |
p_memsz | Size in bytes of the segment in memory. May be 0.内存中段的大小,以字节为单位。可以是0。 |
p_flags | Segment-dependent flags (position for 32-bit structure). |
p_align | 0 and 1 specify no alignment. Otherwise should be a positive, integral power of 2, with p_vaddr equating p_offset modulus p_align.对于可装载的段来说,其 p_vaddr 和 p_offset 的值至少要向内存页面大小对 齐。此数据成员指明本段内容如何在内存和文件中对齐。如果该值为 0 或 1,表明 没有对齐要求;否则,p_align 应该是一个正整数,并且是 2 的幂次数。p_vaddr 和 p_offset 在对 p_align 取模后应该相等。 |
PT_LOAD 1
此类型表明本程序头指向一个可装载的段。段的内容会被从文件中拷贝到 内存中。如前所述,段在文件中的大小是 p_filesz,在内存中的大小是 p_memsz。如果 p_memsz 大于 p_filesz,在内存中多出的存储空间应填 0 补 充,也就是说,段在内存中可以比在文件中占用空间更大;而相反,p_filesz 永远不应该比 p_memsz 大,因为这样的话,内存中就将无法完整地映射段的 内容。在程序头表中,所有 PT_LOAD 类型的程序头按照 p_vaddr 的值做升序排列。
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ PAGE │ PAGE │ PAGE │ PAGE │ PAGE │ PAGE │
└──────┴──────┴──────┴──────┴──────┴──────┘
| |-------p_memsz------| |
| p_vddr |
|----------seg_map_sz-------|
异常向量表
- mrs用于将程序状态寄存器的内容传送到通用寄存器中
主要关注同步异常处理:
异常症状寄存器(Exception Syndrome Register)ESR_ELx:存储有 关异常原因的信息。有关此寄存器中各个位的含义,请参阅《ARMv8 程 序员指南》
错误地址寄存器(Fault Address Register)FAR_ELx:存储所有同步 指令中止、同步数据中止和对齐异常所对应的虚拟地址。如发生缺页异 常时,触发该异常的页的虚拟地址即存储在该寄存器中。
异常链接寄存器(Exception Link Register)ELR_ELx:该异常的首选 返回地址。对于某些同步异常(例如 SVC),它指向异常生成指令的下一 条指令的地址。对于其他的同步异常,它指向发生异常的指令,以便于 重新执行。对于由中断等导致的异步异常,ELR_ELx 指向尚未执行或未 完全执行的第一条指令的地址。
处理过程示例:
假设 AArch64 处理器正在用户线程中执行代码,并且遇到了一条指令集中未定义的指令。此 时,处理器会发生未定义指令异常,并执行如下主要操作:
- 处理器将异常原因放入 ESR_EL1 中,并将返回地址(即未定义指令的 地址)放入 ELR_EL1 中。
- 处理器检查 VBAR_EL1 以获得 EL1 中使用的异常向量表的地址。由于 当前异常是来自 AArch64 模式中 EL0 特权级的同步异常,因此处理器将 选择条目 VBAR_EL1+0x400。
- 处理器将特权级切换到 EL1。这一过程中,包含了如保存 PSTATE、使 用 SP_EL1 作为栈指针等内容,从而完成了从用户栈到内核栈的切换。
- 处理器执行 VBAR_EL1+0x400 处的代码,在 ChCore 中,这是一条跳转 到异常处理程序的b指令。