单处理机系统的进程调度实验_Chcore -- 上交IPADS操作系统银杏书配套Lab实验笔记 - Lab3进程与异常(一)...

实验为IPADS《现代操作系统原理与实现》配套实验,慕课上有完整的课程教学视频,以下为个人实验过程和踩坑记录。

Capability

为什么需要有capability?用户进程是不能直接访问内核资源的,因此我们需要借助一种机制,既能让用户进程能告诉操作系统需要操作的资源,同时不能让用户进程直接访问到。而capability就相当于一个key,在系统调用中,用户把这个key作为参数传入(这个key可以是用户进程创建该资源时返回给用户的),操作系统根据key取出对应的资源进行操作。

process.h文件中定义了一个进程的结构,包括了线程的链表和slot_table。这个table就是记录了该进程拥有的资源,包括了线程,进程本身等。

struct process {
	struct slot_table slot_table;
	struct list_head thread_list;
};

所有的资源都被object类型所抽象,其中的opaque字段就是指向真正的资源(注意这里是个数组类型,具体原因后面会说)。即若是有一个object对象,那么可以通过其opaque字段取出其指向的真实对象(可以是一个线程,一个pmo等);若是有一个pmo对象或是线程对象,可以通过container_of(obj, struct object, opaque);可以取出其对应的object

而之前提到的cap,它是processslot_table的下标,通过这个下标,我们可以取出一个object_slot,这个结构体中的信息比object稍微丰富一些,包括了权限等,其中一个字段就是指向object,然后通过object,我们就可以用opaque字段拿到其真正管理的资源(线程、pmo等)

struct object {
	u64 type;
	u64 size;
	/* Link all slots point to this object */
	struct list_head copies_head;
	/*
	 * refcount is added when a slot points to it and when get_object is
	 * called. Object is freed when it reaches 0.
	 */
	u64 refcount;
	u64 opaque[];
};

struct object_slot {
	u64 slot_id;
	struct process *process;
	int isvalid;
	u64 rights;
	struct object *object;
	/* link copied slots pointing to the same object */
	struct list_head copies;
};

struct slot_table {
	unsigned int slots_size;
	struct object_slot **slots;
	/*
	 * if a bit in full_slots_bmp is 1, corresponding
	 * sizeof(unsigned long) bits in slots_bmp are all set
	 */
	unsigned long *full_slots_bmp;
	unsigned long *slots_bmp;
};

下面可以看几个例子感受一下cap是如何被使用的。

创建object并分配cap

thread_create函数中,我们需要创建线程,然后把线程加入到进程的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;

	// opaque is u64 so sizeof(*object) is 8-byte aligned.
	// Thus the address of object-defined data is always 8-byte aligned.
	total_size = sizeof(*object) + size;
	object = kmalloc(total_size);
	if (!object)
		return NULL;

	object->type = type;
	object->size = size;
	object->refcount = 0;
	init_list_head(&object->copies_head);

	return object->opaque;
}

在分配object的时候,kmalloc的大小为sizeof(*object)+size,最后返回的是opaque字段。此时object的内存布局如下:​

436fd39ff609eeeb5d2e8494b4cab15e.png
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,最终返回给用户,这个函数定义如下:

int cap_alloc(struct process *process, void *obj, u64 rights)
{
	struct object *object;
	struct object_slot *slot;
	int r, slot_id;

	object = container_of(obj, struct object, opaque);

	slot_id = alloc_slot_id(process);
	if (slot_id < 0) {
		r = -ENOMEM;
		goto out_unlock_table;
	}

	slot = kmalloc(sizeof(*slot));
	if (!slot) {
		r = -ENOMEM;
		goto out_free_slot_id;
	}
	slot->slot_id = slot_id;
	slot->process = process;
	slot->isvalid = true;
	slot->rights = rights;
	slot->object = object;
	list_add(&slot->copies, &object->copies_head);

	BUG_ON(object->refcount != 0);
	object->refcount = 1;

	install_slot(process, slot_id, slot);

	return slot_id;
 out_free_slot_id:
	free_slot_id(process, slot_id);
 out_unlock_table:
	return r;
}

该函数的第二个参数我们传入的是thread的指针,这个是真实的对象,因此我们需要获取它对应的object,这里就用到了前面提到的宏定义。获取到object之后,后面就是加入到slot_table中的一些操作,包括了权限的设置等。

根据cap获取对应的被管理的对象(线程,pmo等)

如何根据线程的cap来获取线程本身?在process_create_root函数中有如下语句:root_thread = obj_get(root_process, thread_cap, TYPE_THREAD);

其中obj_get定义如下:

/* object refenrence */
void *obj_get(struct process *process, int slot_id, int type)
{
	return get_opaque(process, slot_id, true, type);
}

/* local object operation methods */
static void *get_opaque(struct process *process, int slot_id,
			bool type_valid, int type)
{
	struct slot_table *slot_table = &process->slot_table;
	struct object_slot *slot;
	void *obj;

	if (!is_valid_slot_id(slot_table, slot_id)) {
		obj = NULL;
		goto out_unlock_table;
	}

	slot = get_slot(process, slot_id);
	BUG_ON(slot->isvalid == false);
	BUG_ON(slot->object == NULL);

	if (!type_valid || slot->object->type == type) {
		obj = slot->object->opaque;
	} else {
		obj = NULL;
		goto out_unlock_slot;
	}

	atomic_fetch_add_64(&slot->object->refcount, 1);

 out_unlock_slot:
 out_unlock_table:
	return obj;
}

get_opaque方法中,我们根据cap(即slot_id)来查找object_slot,之后取出objectopaque字段返回,即返回了cap对应的真实对象本身(线程)。

get和put一一对应

要注意对于obj_getobj_put的操作要一一对应,因为在get和put的时候会改变refcount的值,因此如果不是一一对应的话可能会造成无法正常释放内存空间或是提前释放。

创建用户进程和线程

加载ELF

ELF format参考wiki:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format

加载ELF的意思就是读取ELF段,然后映射到其指定的虚拟内存地址上。

因此需要:

  1. 正确读取ELF文件的段,包括在ELF文件中的起始位置长度
  2. 正确映射到虚拟地址,包括正确的虚拟地址起始位置长度

需要仔细阅读下面几个字段的含义:

a0dcdf43810b089060d224fdf09d9a4d.png
ELF

其中p_offset指的是在img文件中,该段的偏移量,即若是要读取img文件的某一段,那么(void*)bin + elf->p_headers[i].p_offset;就是该段的开头。对应的是1中的起始位置。

p_filesz指的是该段在img文件中的长度,对应的是1中的长度。

p_vaddr指定了该段需要被映射到的虚拟地址开头。要特别注意这个虚拟地址不是页对齐的,也就是说,当我们映射ELF的某一段到虚拟内存的时候,需要注意一下页对齐。对应的是2中虚拟地址的起始位置。

p_memsz是占用的虚拟内存大小,同样需要考虑对齐。对应的是2中的长度。

通过上面四个字段我们就可以完成这个加载函数。要特别注意起始的虚拟地址和虚拟内存长度的对齐,这将会影响到我们如何将ELF文件的段copy到物理内存中,以及如何将物理内存映射到虚拟内存。考虑如下情况:

e39da12ac4a657615aaeb29c49bbc741.png
虚拟地址对齐

上图中竖着的细线都是页对齐的,p_vaddr可能出现在一个页的中间,同样,因为p_memsz也不是页对齐的,因此结束位置也可能会出现在页的中间。这样我们在申请一块物理内存的时候,实际上需要4个页(将第一个和最后一个页补齐)。在映射的时候,我们把申请的4个物理页映射到虚拟地址时,起始页的地址应该是ROUND_DOWN(p_vaddr, PAGE_SIZE)(对应的是最左边的细线),这就导致我们在将ELF文件copy到内存中的时候,也需要考虑这段偏移,即p_vaddr - ROUND_DOWN(p_vaddr, PAGE_SIZE);(对应的是左边粗线到页开头的那一段)。这样才能在访问虚拟地址的时候,正确读取到ELF文件中段的内容。

核心代码如下:

seg_sz = elf->p_headers[i].p_memsz;
p_vaddr = elf->p_headers[i].p_vaddr;

seg_map_sz = ROUND_UP(seg_sz + p_vaddr, PAGE_SIZE) - ROUND_DOWN(p_vaddr, PAGE_SIZE);

拷贝时候考虑偏移:

vaddr_t pmo_start_vaddr = (vaddr_t) phys_to_virt(pmo->start);
pmo_start_vaddr += p_vaddr - ROUND_DOWN(p_vaddr, PAGE_SIZE);
char *pos = (void*)bin + elf->p_headers[i].p_offset;
size_t copy_size = elf->p_headers[i].p_filesz;
for(int i = 0; i < copy_size; i++){
  *((char *)pmo_start_vaddr + i) = *(pos + i);
}

初始化线程上下文

创建完线程之后,通过eret_to_thread(switch_context());来返回,其中eret_to_threadexception_table.S中定义:

/* void eret_to_thread(u64 sp) */
BEGIN_FUNC(eret_to_thread)
	mov	sp, x0
	exception_exit
END_FUNC(eret_to_thread)

第一个参数会赋值给sp,之后exception_exit的时候,会把栈中的值复制给寄存器,即恢复了线程执行的上下文,之后通过eret指令切换到用户态就能正确执行了。

因此在初始化线程上下文的时候,只需要在正确的结构体内填入信息即可:

arch_set_thread_stack(thread, stack);
arch_set_thread_next_ip(thread, func);
thread->thread_ctx->ec.reg[SPSR_EL1] = SPSR_EL1_EL0t;

切换线程上下文

switch_context能正确返回刚刚保存的结构体的开头,这样弹栈的时候就能正确赋值给对应寄存器:

return (u64)target_ctx->ec.reg;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值