zoukankan      html  css  js  c++  java
  • 现代操作系统:原理与实现配套实验ChCore03(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之中即完成第一个线程创建的全部工作~

  • 相关阅读:
    HDU 4472 Count DP题
    HDU 1878 欧拉回路 图论
    CSUST 1503 ZZ买衣服
    HDU 2085 核反应堆
    HDU 1029 Ignatius and the Princess IV
    UVa 11462 Age Sort
    UVa 11384
    UVa 11210
    LA 3401
    解决学一会儿累了的问题
  • 原文地址:https://www.cnblogs.com/bluettt/p/15474487.html
Copyright © 2011-2022 走看看