现代操作系统:原理与实现配套实验ChCore-03(1)
-----------------------------------------------------------------
邮箱:wanglu082@yeah.net
QQ : 1052658906
欢迎交流~
-----------------------------------------------------------------
实验3加入用户进程的概念,使用Capability-object模式来管理系统资源和分配权限,开始慢慢地体现微内核地设计原则 。
本次实验内容比较多,所以暂时分三个部分吧,后面如果有变动再更改。
练习 2
请简要描述process_create_root这一函数所的逻辑。 注意: 描述中需包含thread_create_main函数的详细说明,建议绘制函数调用图以描述相关逻辑。
提示:把练习2在练习1之前来写是有原因的,因为练习1要实现的所有函数的根都是process_create_root
这一函数,所以干脆就从头开始按照函数调用的关系进行分析,建立比较好的逻辑性。
可以找到调用process_create_root
的位置是kernel/main.c中,这个函数我们很熟悉,因为在这之前我们完成了一些main函数中的工作,包括串口的初始化、虚拟内存的初始化和配置等。
(以下的所有代码都省略了不必要的语句和注释等)
void main(void *addr)
{
uart_init();
mm_init();
/* Init exception vector */
exception_init(); //--------------------------------------------(1)
kinfo("[ChCore] interrupt init finished\n");
#ifdef TEST
/* Create initial thread here */
process_create_root(TEST);
kinfo("[ChCore] root thread init finished\n");
#else
/* We will run the kernel test if you do not type make bin=xxx */
break_point();
BUG("No given TEST!");
#endif
eret_to_thread(switch_context());
}
(1)关于异常的配置会在以后的练习中涉及,这里先不介绍。
预备知识1:宏“TEST”的定义
可以看到process_create_root被一个#ifdef给包裹,这个TEST
变量同时作为参数传递给process_create_root
,那么它是哪里定义的呢?
找到根目录下的Makefile,发现以下规则:
...
prep-%:
@echo "*** Now building application $*"
./scripts/docker_build.sh $*
./scripts/create_gdbinit.sh $*
run-%: prep-%
@echo "*** Now starting qemu"
$(QEMU) $(QEMUOPTS)
run-%-gdb: prep-%
@echo "*** Now starting qemu-gdb"
$(QEMU) $(QEMUOPTS) -S
...
举个例子,当我们执行make run-hello
指令时,run-hello目标会依赖prep-hello目标,最终来到./scripts/docker_build.sh $*
这条命令。
参数hello通过$*变量进行传递。
通常,$*变量表示目标模式中“%”及之前的部分,此规则中应当是prep-hello。但是,GNU make规定在静态规则中,它代表的是“%”的内容,也就是hello字段。同时,如果目标名称以Make可识别的后缀结尾,那么也将识别为“%”(例如,%.c, %.o等 )。
详见:GNU make
找到执行的脚本文件docker_build.sh,当中实现了根据传入参数的个数执行不同的docker命令:
if [ $# == 0 ]; then
docker run -it --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $(pwd):/chos -w /chos ipads/chcore_builder:v1.0 ./scripts/build.sh
else
docker run -it --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $(pwd):/chos -w /chos ipads/chcore_builder:v1.0 ./scripts/build.sh -DTEST=\"/$1.bin\"
fi
容易看出,无论参数的个数是否为0,实际的工作都是创建一个docker并执行脚本./scripts/build.sh
。
唯一不同的是,参数的个数非0时(即执行make run-hello
指令而非make
)时,会将参数<-DTEST="/hello.bin">传递给build.sh脚本。
build.sh 中会对这个参数进一步处理,与之对应的是命令:
...
cmake -DCMAKE_LINKER=aarch64-linux-gnu-ld -DCMAKE_C_LINK_EXECUTABLE="<CMAKE_LINKER> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>" .. -G Ninja "$@"
...
将指令末尾的”$@"展开即得到-DTEST="/hello.bin",效果就是在根目录下CMakeLists.txt中追加定义TEST=“/hello.bin”。
这就由使CMakeLists.txt中的以下语句生效:
...
if(TEST)
add_definitions("-DTEST=${TEST}")
endif()
...
将TEST="/hello.bin"转为源文件预处理过程中的宏定义,最终使main.c中的#ifdef TEST
生效,process_create_root(TEST)
得以执行。
add_definitions函数添加源文件预编译参数:CMAKE手册 -
总结
由于前面的绕来绕去,我觉得这个部分需要一个总结。
我们的目的是弄懂从执行make run-hello
,到#ifdef TEST
判断通过,中间经过了怎样一个过程。
这里给出一个总结:
Makefile
+
| 执行make run-hello
| 通过$*将"hello"字段向下传递
v
docker_build.sh
+
| 创建docker并执行build.sh
| 传递参数:-DTEST="/hello.bin"
v
build.sh
+
| -DTEST="/hello.bin"可以对
| CMakeLists.txt追加变量TEST
v
CMakeLists.txt
+
| 使用add_definitions函数
| 对源文件增加宏定义TEST="/hello.bin"
v
"#ifdef TEST"通过
预备知识2:xxx.bin的生成
了解“TEST”的传递过程之后,还有一件事很重要:"TEST"传过来的hello.bin文件是什么时候生成的?
从最初的根目录Makefile开始,我们执行make
命令编译工程的时候,就会生成下面的目标:
all: user build
gdb:
gdb-multiarch -n -x .gdbinit
build: FORCE
./scripts/docker_build.sh $(bin)
user: FORCE
./scripts/docker_build_user.sh
其中user这个目标使用FORCE保证强制执行,调用到scripts下的docker_build_user.sh脚本。
注意:其实认真分析过Makefile可以看出,上面说的make run-hello
仅仅是编译内核(dock_build.sh)和传递hello.bin。所以必须要保证至少执行过一次make
,或者说在每次修改过user下的内容之后都要执行make
,而非单单执行make run-hello
docker_build_user.sh脚本仅包含一句指令:
docker run -it --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $(pwd):/chos -w /chos ipads/chcore_builder:v1.0 ./scripts/compile_user.sh
继续执行compile_user.sh:
cd user
rm -rf build && mkdir build
C_FLAGS="-O3 -ffreestanding -Wall -fPIC -static"
C_FLAGS="$C_FLAGS -DCONFIG_ARCH_AARCH64"
cd build
cmake .. -DCMAKE_C_FLAGS="$C_FLAGS" -G Ninja
ninja
这里就回到了cmake的世界,让我们把视角转会根目录下的CMakeLists.txt.
根目录下的CMakeLists.txt引入了/user/lab3和/user/lib下的CMakeLists.txt
/user/lab3下的CMakeLists.txt就规定了生成目标hello.bin的规则:
set(TEST_LAB3_BINS
"badinsn"
"badinsn2"
"hello"
"testputc"
"testcreatepmo"
"testmappmo"
"testmappmoerr"
"testsbrk"
"faultread"
"faultwrite"
"testpf"
)
foreach(bin ${TEST_LAB3_BINS})
file(GLOB ${bin}_source_files "${bin}.c")
add_executable(${bin}.bin ${${bin}_source_files})
target_link_libraries(${bin}.bin chcore-user-lib)
set_property(
TARGET ${bin}.bin
APPEND_STRING
PROPERTY
LINK_FLAGS
"-e START"
)
message("^^^^^^^^^^^^^[In /user/lab3/CMakeLists.txt]^^^^^^^^^^^^^^")
endforeach(bin)
可以看到,这个.bin就是源文件直接编译后生成的目标文件,并非我们想象中的去除头部信息之后的bin文件,而是正儿八经的elf文件,只是换了个后缀名而已~
至此,我们确定了hello.bin以及其他usr下的源文件生成可执行文件的过程,这对于我们接下来分析process_create_root函数中加载elf文件的部分提供了参考。不然,你可能会觉得hello.bin为什么会有elf头部信息?
正式分析process_create_root
分析一个函数,首先要了解它的任务是什么。
process_create_root的输入是一个bin文件的目录,完成以下的工作:
- 创建root进程
- 创建root进程的一个线程,加载bin文件。
1 创建进程
ChCore中的进程-线程的组织方式是:一个进程可以创建多个线程。
那么首先我们需要创建进程。
创建进程的实现在process_create()
中实现,由于ChCore基于Capability-object的组织方式,所以创建进程的过程大概可以进行划分:
- 创建type为process的实体(object),并做初始化。
- 初始化进程的capability。
- 创建type为vmspace的实体,并做初始化。
- 进程的capability增加vmspace
static struct process *process_create(void)
{
struct process *process;
struct object *object;
struct object_slot *slot;
struct vmspace *vmspace;
int total_size, slot_id;
/* 1 创建type为process的实体 */
total_size = sizeof(*object) + sizeof(*process);
if ((object = kmalloc(total_size)) == NULL)
goto out_fail;
object->type = TYPE_PROCESS;
object->size = sizeof(*process);
object->refcount = 1;
process = (struct process *)object->opaque;
process_init(process, BASE_OBJECT_NUM);
/* 2 此进程首先拥有的capability是它自身,放入进程slots中的第一个 */
slot_id = alloc_slot_id(process);
BUG_ON(slot_id != PROCESS_OBJ_ID);
slot = kzalloc(sizeof(*slot));
if (!slot)
goto out_free_process;
slot->slot_id = slot_id;
slot->process = process;
slot->isvalid = true;
slot->object = object;
init_list_head(&slot->copies);
process->slot_table.slots[slot_id] = slot;
/* 3 创建type为vmspace的实体,并做初始化。
绑定到此进程的capability上 */
vmspace = obj_alloc(TYPE_VMSPACE, sizeof(*vmspace));
BUG_ON(!vmspace);
vmspace_init(vmspace);
slot_id = cap_alloc(process, vmspace, 0);
BUG_ON(slot_id != VMSPACE_OBJ_ID);
return process;
out_free_process:
kfree(process);
out_fail:
return NULL;
}
process_create执行完成后,kernel创建了第一个线程,它的capability包含它本身和vmspace。
注:Capability-object组织方式在这里不过多介绍,可以参考以下文章:
TODO
总结成一句话:”Capability-object系统中,万物皆对象,不是对象的统统为capability。“
如果有需要或许可以写一篇表达一下自己的观点。
2 创建进程的第一个线程
进程创建完成后,紧接着创建它的第一个线程。创建主线程的函数是thread_create_main
。这个函数相对比较复杂,所以我们有必要多说一点。
首先根据代码来归纳一下它的任务:
- 创建type为PMO的实体用作线程栈(也可能是进程栈?TODO),并初始化。
- 进程的capability增加上面创建的堆栈
- 配置堆栈属于用户进程内存空间
- 加载hello.bin
- 创建thread实体并初始化
- 进程的capability增加thread实体
/**
* 创建指定进程的第一个线程
* @param[in] process 创建线程所属的进程
* @param[in] stack_base 线程栈的起始地址
* @param[in] stack_size 线程栈的大小
* @param[in] prio 线程优先级
* @param[in] type 用户线程/内核线程
* @param[in] aff TODO
* @param[in] bin_start 二进制文件流
* @param[in] bin_name 二进制文件名
* @return 成功返回对应cap
* @ref
* @see
* @note
*/
int thread_create_main(struct process *process, u64 stack_base,
u64 stack_size, u32 prio, u32 type, s32 aff,
const char *bin_start, char *bin_name)
{
int ret, thread_cap, stack_pmo_cap;
struct thread *thread;
struct pmobject *stack_pmo;
struct vmspace *init_vmspace;
struct process_metadata meta;
u64 stack;
u64 pc;
/* 拿到进程虚拟地址空间 */
init_vmspace = obj_get(process, VMSPACE_OBJ_ID, TYPE_VMSPACE);
obj_put(init_vmspace);
/* Allocate and setup a user stack for the init thread */
/* 创建PMO用作用户线程栈 */
stack_pmo = obj_alloc(TYPE_PMO, sizeof(*stack_pmo));
pmo_init(stack_pmo, PMO_DATA, stack_size, 0);
/* 进程的capability增加用户线程栈 */
stack_pmo_cap = cap_alloc(process, stack_pmo, 0);
/* 将pmo对象与stack_base(虚拟地址)绑定,并记录到init_vmspace,表明此地址属于此进程 */
ret = vmspace_map_range(init_vmspace, stack_base, stack_size,
VMR_READ | VMR_WRITE, stack_pmo);
/* 分配线程结构体的空间 *///-----------------------------------------------------(1)
thread = obj_alloc(TYPE_THREAD, sizeof(*thread));
if (!thread) {
ret = -ENOMEM;
goto out_free_cap_pmo;
}
/* Fill the parameter of the thread struct */
/* 调整此栈指针 */
stack = stack_base + stack_size;
/* 处理ELF */
pc = load_binary(process, init_vmspace, bin_start, &meta);
prepare_env((char *)phys_to_virt(stack_pmo->start) + stack_size,
stack, &meta, bin_name);
stack -= ENV_SIZE_ON_STACK;
ret = thread_init(thread, process, stack, pc, prio, type, aff);
/* 进程的capability增加用户线程 */
thread_cap = cap_alloc(process, thread, 0);
/* L1 icache & dcache have no coherence */
flush_idcache();
return thread_cap;
}
(1)在分析时,我将线程实体的创建放在加载elf之后,方面归纳,不产生影响!。
首先,PMO是什么?
作为对象(object)的一种,PMO是物理内存对象,代表一块物理内存。(再次强调:”Capability-object系统中,万物皆对象,不是对象的统统为capability“)
申请一块物理内存用来干嘛?——作为此线程的栈空间。
线程如何访问它此栈空间?——通过虚拟地址stack_base
。
自然而言我们就能想到必须创建PA到VA的映射,这个过程由vmspace_map_range()
来实现。同时,它还实现了将stack_base与pmo的对应关系(vmregion结构体)记录在进程虚拟内存空间vmspace中。这个函数的实现比较复杂,这里就不多说了。
上面的内容对应123标号。接下来说4加载hello.bin的问题。过程由load_binary()
实现,这里可以偷个懒先不说,因为这是练习1的一部分,到时我们再详谈。
!!!但是,你必须知道,通过这个函数得到了此elf文件的起始地址,放入变量pc。等到任务切换之时就要转到这个地址来执行指令。
知道了pc的值,下一步得搞清楚它是如何传递的,于是来到thread_init
函数:
static
int thread_init(struct thread *thread, struct process *process,
u64 stack, u64 pc, u32 prio, u32 type, s32 aff)
{
thread->process = obj_get(process, PROCESS_OBJ_ID, TYPE_PROCESS);
thread->vmspace = obj_get(process, VMSPACE_OBJ_ID, TYPE_VMSPACE);
obj_put(thread->process);
obj_put(thread->vmspace);
/* Thread context is used as the kernel stack for that thread */
thread->thread_ctx = create_thread_ctx();
init_thread_ctx(thread, stack, pc, prio, type, aff);
/* add to process */
list_add(&thread->node, &process->thread_list);
return 0;
}
thread_init
完成线程上下文的创建的初始化,这一步也是在练习1中需要我们实现的,到时再细说。
最后一步将此线程放入进程的capability之中即完成第一个线程创建的全部工作~