ChCore—实验 3:进程与线程、异常处理 部分记录

思考题 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
  1. Chcore 启动后会依次初始化 uart 模块、内存管理模块、中断模块
  2. 然后调用 create_root_thread 创建一个根进程,创建进程的 cap_group 结构体并初始化,包括分配一块虚拟地址空间 vmspaceslot_table 的初始化,__create_root_thread会先从磁盘中载入 ELF 文件,为进程创建一个主线程,最后将线程root_thread放入根进程中切换到线程执行;
  3. eret_to_threadswitch_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 的值实际上就是对象在所属的 processslot_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.hcreate_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的内存布局如下:

img

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;
};

image-20230330174510806

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-------|

异常向量表

  1. mrs用于将程序状态寄存器的内容传送到通用寄存器中

主要关注同步异常处理:

image-20230330204615409

异常症状寄存器(Exception Syndrome Register)ESR_ELx:存储有 关异常原因的信息。有关此寄存器中各个位的含义,请参阅《ARMv8 程 序员指南》

错误地址寄存器(Fault Address Register)FAR_ELx:存储所有同步 指令中止、同步数据中止和对齐异常所对应的虚拟地址。如发生缺页异 常时,触发该异常的页的虚拟地址即存储在该寄存器中。

异常链接寄存器(Exception Link Register)ELR_ELx:该异常的首选 返回地址。对于某些同步异常(例如 SVC),它指向异常生成指令的下一 条指令的地址。对于其他的同步异常,它指向发生异常的指令,以便于 重新执行。对于由中断等导致的异步异常,ELR_ELx 指向尚未执行或未 完全执行的第一条指令的地址。

处理过程示例:

假设 AArch64 处理器正在用户线程中执行代码,并且遇到了一条指令集中未定义的指令。此 时,处理器会发生未定义指令异常,并执行如下主要操作:

  1. 处理器将异常原因放入 ESR_EL1 中,并将返回地址(即未定义指令的 地址)放入 ELR_EL1 中。
  2. 处理器检查 VBAR_EL1 以获得 EL1 中使用的异常向量表的地址。由于 当前异常是来自 AArch64 模式中 EL0 特权级的同步异常,因此处理器将 选择条目 VBAR_EL1+0x400。
  3. 处理器将特权级切换到 EL1。这一过程中,包含了如保存 PSTATE、使 用 SP_EL1 作为栈指针等内容,从而完成了从用户栈到内核栈的切换。
  4. 处理器执行 VBAR_EL1+0x400 处的代码,在 ChCore 中,这是一条跳转 到异常处理程序的b指令。
posted @ 2023-03-30 20:51  O_fly_O  阅读(217)  评论(0编辑  收藏  举报