实验为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,它是process
中slot_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](https://img-blog.csdnimg.cn/img_convert/436fd39ff609eeeb5d2e8494b4cab15e.png)
语句: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
,之后取出object
的opaque
字段返回,即返回了cap对应的真实对象本身(线程)。
get和put一一对应
要注意对于obj_get
和obj_put
的操作要一一对应,因为在get和put的时候会改变refcount
的值,因此如果不是一一对应的话可能会造成无法正常释放内存空间或是提前释放。
创建用户进程和线程
加载ELF
ELF format参考wiki:https://en.wikipedia.org/wiki/Executable_and_Linkable_Format
加载ELF的意思就是读取ELF段,然后映射到其指定的虚拟内存地址上。
因此需要:
- 正确读取ELF文件的段,包括在ELF文件中的起始位置和长度
- 正确映射到虚拟地址,包括正确的虚拟地址起始位置和长度
需要仔细阅读下面几个字段的含义:
![a0dcdf43810b089060d224fdf09d9a4d.png](https://img-blog.csdnimg.cn/img_convert/a0dcdf43810b089060d224fdf09d9a4d.png)
其中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](https://img-blog.csdnimg.cn/img_convert/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_thread
在exception_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;