现代操作系统:原理与实现配套实验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文件的目录,完成以下的工作:

  1. 创建root进程
  2. 创建root进程的一个线程,加载bin文件。

1 创建进程

ChCore中的进程-线程的组织方式是:一个进程可以创建多个线程。

那么首先我们需要创建进程。

创建进程的实现在process_create()中实现,由于ChCore基于Capability-object的组织方式,所以创建进程的过程大概可以进行划分:

  1. 创建type为process的实体(object),并做初始化。
  2. 初始化进程的capability。
  3. 创建type为vmspace的实体,并做初始化。
  4. 进程的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。这个函数相对比较复杂,所以我们有必要多说一点。

首先根据代码来归纳一下它的任务:

  1. 创建type为PMO的实体用作线程栈(也可能是进程栈?TODO),并初始化。
  2. 进程的capability增加上面创建的堆栈
  3. 配置堆栈属于用户进程内存空间
  4. 加载hello.bin
  5. 创建thread实体并初始化
  6. 进程的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之中即完成第一个线程创建的全部工作~

posted @ 2021-10-28 10:47  cnwanglu  阅读(283)  评论(0编辑  收藏  举报