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

  • 相关阅读:
    【学习笔记】一:JavaScript简介
    【学习笔记】Sass入门指南
    【学习笔记】前端开发面试锦集
    庆祝我的博客园正式开张
    python解析AMF协议
    C语言setjmp函数使用
    CONTAINING_RECORD 宏
    samba的安装及其使用
    confluence的安装
    查看mysql字符集及修改表结构--表字符集,字段字符集
  • 原文地址:https://www.cnblogs.com/bluettt/p/15474487.html
Copyright © 2020-2023  润新知