GeekOS之旅-Project2 (Start a new user address Process)

这个Project的编译和bochrc的配置没什么特殊之处,所以编译后然后启动bochs会显示如下界面:

ok, 可以看到 Unimplemented feature.:Switch to a new user address space, if necessary. 这个就是我们的任务了.

这句话比较简洁: 新建新的用户地址空间. 但是我们再来看附带的文档, Project Synopsis .就会发现任务不止这一个 .仔细阅读这段 然后把具体任务整理如下:

1. 在src/geekos/user.c中, 需要实现Spawn()和Switch_To_User_Context()这两个函数,Spawn()这个函数主要用来开启一个新的用户进程,Switch_To_User_Context()用来在被调度之前用来开启一个新的线程去切换用户地址空间.

2.在src/geekos/elf.c中需要实现Parse_ELF_Executable()这个函数, 这个函数就像Project1的处理的一样,包括要读取的可执行ELF文件的 offset, length 和user address 和 data segments. 基于这些信息,填充于Exe_Format这个结构体中, 这个结构体会被 Spawn()函数调用.当然这些取决于怎么去加载这个可执行文件. 这个可以直接使用之前Project1的代码.

3.在src/geekos/userseg.c中,实现的函数比较多,有 Destroy_User_Context() ,Load_User_Program(), Copy_From_User(), Copy_To_User(), Switch_To_Address_Space()

Destroy_User_Context(): 释放用户进程的内存资源

Load_User_Program(): 载入部分可执行程序到内存中

Copy_From_User() 和 Copy_To_User(): 在用户地址空间和内核地址空间之间互相拷贝数据

Switch_To_Address_Space(): 当载入处理器的LDT寄存器时激活用户地址空间

4.在src/geekos/kthread.c中需要实现Setup_User_Thread()和Start_User_Thread()两个函数,Setup_User_Thread 是建立进程的内核初始化堆栈,也即 当进程第一个次进入用户态时,来初始化处理器的寄存器中的内容, Start_User_Thread()主要是使用User_Conect这个对象来启动一个新的进程.

从上面分析下来, 基本目标已经明确 . 下面就需要一步一步填充这些代码了.

先从开启一个新的用户进程来开刀, 看下 Spawn的原型:

int Spawn(const char *program, const char *command, struct Kernel_Thread **pThread);program: 可执行文件的路径

command: 这个参数包括程序的名字 和 参数

pThread: Kernel_Thread 指针

返回值: 新的进程的id Otherwise return erro Code

可以根据Spwan函数中的Hints 来完成整个函数:

88 /* 89 * Hints: 90 * - Call Read_Fully() to load the entire executable into a memory buffer 91 * - Call Parse_ELF_Executable() to verify that the executable is 92 * valid, and to populate an Exe_Format data structure describing 93 * how the executable should be loaded 94 * - Call Load_User_Program() to create a User_Context with the loaded 95 * program 96 * - Call Start_User_Thread() with the new User_Context 97 * 98 * If all goes well, store the pointer to the new thread in 99 * pThread and return 0. Otherwise, return an error code. 100 */Read_Fully() 载入整个可执行文件到内存中,原型在 vfs.h

char* exeFileData; //reference to variable where pointer to allocated buffer ulong_t exeFileLen; //reference to variable where length of file should be stored int ret = Read_Fully(program, &exeFileData, &exeFileLen); if (ret != 0) { Print("Load Program %s failed!\n", program); return -1; }

Parse_ELF_Executable() 检验ELF可执行文件是否有效 ,原型在 elf.h 中 这个函数还需要我们自己来实现的. 不过之前的Project1以前实现过 直接拿来用就可以了

struct Exe_Format exeFmt; int ret = Parse_ELF_Executable(exeFileData, exeFileLen, &exeFmt) ; if (ret != 0) { Print("Parse ELF_Executable File failed!\n); return -1; }

Load_User_Program() 创建一个User_Context来加载程序,原型在 userseg.h中 . 这个函数需要我们来实现的

struct User_Context* userContext; int ret = Load_User_Program(exeFileData, exeFileLen, &exeFmt, command, &userContext); if (ret !=0) { Print("Load User Program failed!\n); return -1; }

Start_User_Thread() 使用创建出来的 User_Context,并使进程进入准备队列中 原型在kthread.h中, 这个函数也是下面我们实现的

struct Kernel_Thread* t; t = Start_User_Thread(userContext, false); if (t ==null) { Print("Start User thread failed\n"); return -1; } 剩下的就是整理Spawn这个函数了,Spawn到此为止.

继续看Switch_To_User_Context()

void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state);kthread: the thread that is about to execute

state: saved processor registers describing the state when the thread wsa interrupted

143 /* 144 * Hint: Before executing in user mode, you will need to call 145 * the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space() 146 * functions. 147 */Set_Kernel_Stack_Pointer()这个函数的原型在tss.h中, 用来设置内核栈的指针,也就是说是新进程的核心栈.这里需要了解到 进程硬件上下文的一部分存在TSS段,而剩余部分存放在内核态堆栈中

Switch_To_Address_Space()这个函数的原型是在userseg.h中,用来切换地址空间的.

这里要了解下,userContext为用户态进程上下文, 对于内核进程来说,这个指针为空, 而用户进程都拥有自己的User_Context. 所以, 要判断一个进程是内核进程还是用户态进程,只需要通过userContext字段是否为空来判断.Switch_To_User_Context的实现如下:

141 void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state) 142 { 143 /* 144 * Hint: Before executing in user mode, you will need to call 145 * the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space() 146 * functions. 147 */ 148 struct User_Context* curUserContext; 149 struct User_Context* userContext = kthread->userContext; 150 if(!userContext) return; //Kernel process 151 if (userContext != curUserContext) { 152 ulong_t stack; 153 Switch_To_Address_Space(userContext); 154 esp0 = kthread->stackPage + PAGE_SIZE; 155 Set_Kernel_Stack_Pointer(esp0); //start new 156 curUserContext = userContext; 157 } 158 } 下面对src/geekos/userseg.c下几个函数进行实现:

//Destroy a User_Context object, including all memory and other resources allocated within it.

68 void Destroy_User_Context(struct User_Context* userContext) 69 { 70 /* 71 * Hints: 72 * - you need to free the memory allocated for the user process 73 * - don't forget to free the segment descriptor allocated 74 * for the process's LDT 75 */ 76 //TODO("Destroy a User_Context"); 77 Free_Segment_Descriptor(userContext->ldtDescriptor); //free LDT 78 Disable_Interrupts(); 79 Free(userContext->memory); 80 Free(userContext); 81 Enable_Interrupts(); 82 }

Load_User_Program()的实现可以阅读下:

Besides loading the executable's text and data segments into memory,you will also need to create two other data structures in theprocess's memory space.

You will need to create a stack for the new process.The stack will reside at the top of the user address space.You can use theDEFAULT_USER_STACK_SIZE macro insrc/geekos/userseg.c as the stack size.

You will also need to create an argument block datastructure. This data structure creates theargc andargv arguments that are passed to themain()function of the user process.Two functions are defined to help you build theargument block (prototypes in<geekos/argblock.h>).Get_Argument_Block_Size() takes thecommand stringpassed to the Spawn() function and determines how manycommand line arguments there will be, and how manybytes are required to store the argument block.TheFormat_Argument_Block()function takes the number of arguments and argument block size fromGet_Argument_Block_Size()and builds the argument block data structure in the memory location you haveallocated for it.

Note that you will need to take the size of the stack and argument blockinto account when you decide how much memory to allocate for theprocess.

然后可以根据Hint 来写:

/* * Hints: * - Determine where in memory each executable segment will be placed * - Determine size of argument block and where it memory it will * be placed * - Copy each executable segment into memory * - Format argument block in memory * - In the created User_Context object, set code entry point * address, argument block address, and initial kernel stack pointer * address */

用户进程块的大小 = Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE; //Round_Up_To_Page为参数块总大小

然后得到size,通过Create_User_Context(size)来创建一个用户进程的上下文,再Copy each executable segment into memory. 这里需要把exeFormat->segment[i]的内容拷贝到userContext下.最后是些初始化data,入口地址,堆栈指针操作.

Copy_From_User和Copy_To_User 用户地址空间和内核地址空间之间互相拷贝数据这两个函数的实现主要根据当前进程的User_Context,g_currentThread->userContext

两者都要用到Validate_User_Memory这个函数,这个函数起了防御性作用,判错功能,下面再进行memcpy,下面是实现:

bool Copy_From_User(void* destInKernel, ulong_t srcInUser, ulong_t bufSize) 198 { 199 /* 200 * Hints: 201 * - the User_Context of the current process can be found 202 * from g_currentThread->userContext 203 * - the user address is an index relative to the chunk 204 * of memory you allocated for it 205 * - make sure the user buffer lies entirely in memory belonging 206 * to the process 207 */ 208 //TODO("Copy memory from user buffer to kernel buffer"); 209 //Validate_User_Memory(NULL,0,0); /* delete this; keeps gcc happy */ 210 struct User_Context* UserContext = g_currentThread->userContext; 211 if (!Validate_User_Memory(UserContext, srcInUser, bufSize)) return false; 212 memcpy(destInKernel, UserContext->memory + srcInUser, bufSize); 213 214 return true; 215 }
229 bool Copy_To_User(ulong_t destInUser, void* srcInKernel, ulong_t bufSize) 230 { 231 /* 232 * Hints: same as for Copy_From_User() 233 */ 234 //TODO("Copy memory from kernel buffer to user buffer"); 235 struct User_Context* UserContext = g_currentThread->userContext; 236 if (!Validate_User_Memory(UserContext, destInUser, bufSize)) return false; 237 memcpy(UserContext->memory + destInUser, srcInKernel, bufSize); 238

switch_To_Address_Space这个函数要使用LDT 也即 userContext->ldtSelector 来切换到局部描述符表

通过下面这段ASM:

__asm__ __volatile__ ( "lldt %0" : :"a"(ldtSelector) );

这个Project要填的内容还真多.还有kthread和syscall的实现. 这里就不细说了,有需要的可以留言询问.

最后实现图如下:




一、项目设计目的\n扩充GeekOS操作系统内核,使得系统能够支持用户级进程的动态创建和执行。\n\n二、项目设计提示\n1、 GeekOS进程状态及转换\n\nGeekOS系统最早创建的内核进程有Idle、Reaper和Main三个进程,它们由Init_Scheduler函数创建:最先初始化一个核态进程mainThread,并将该进程作为当前运行进程,函数最后还调用Start_Kernel_Thread 函数创建了两个系统进程Idle和Reaper。 所以,Idle、Reaper和Main三个进程是系统中最早存在的进程。\n\n2GeekOS的用户态进程\n在GeekOS中为了区分用户态进程和内核进程,在Kernel_Thread结构体中设置了一个字段 userContext,指向用户态进程上下文。对于内核进程来说,这个指针为空,而用户态进程都拥有自己的用户上下文(User_Context)。因此,在GeekOS中要判断一个进程是内核进程还是用户态进程,只要通过userContext字段是否为空来判断就可以了。\n\n\n3、用户态进程创建流程\n\n\nSpawn函数的功能\nint Spawn(const char *program, const char *command, struct Kernel_Thread **pThread)\n参数说明:Program对应的是要读入内存缓冲区的可执行文件,Command是用户执行程序执行时的命令行字符串,pThread是存放指向刚创建进程的指针。\nSpawn函数主要完成的主要功能是:\n(1)调用Read_Fully函数将名为program的可执行文件全部读入内存缓冲区。\n(2)调用Parse_ELF_Executable函数,分析ELF格式文件。Parse_ELF_Executable函数功能在项目1中已经实现。\n(3)调用Load_User_Program将可执行程序的程序段和数据段等装入内存,初始化User_context数据结构。\n(4)调用Start_User_Thread函数创建一个进程并使该进程进入准备运行队列。\n\nLoad_User_Program函数\nLoad_User_Program函数在“/src/geekos/userseg.c”文件中实现,代码也需要开发人员自己完成,函数原型如下:\nint Load_User_Program(char *exeFileData, ulong_t exeFileLength,\nstruct Exe_Format *exeFormat, const char *command,\nstruct User_Context **pUserContext)\n/* 参数说明:\nexeFileData——保存在内存缓冲中的用户程序可执行文件;\nexeFileLength——可执行文件的长度;\nexeFormat——调用Parse_ELF_Executable函数得到的可执行文件格式信息;\ncommand——用户输入的命令行,包括可执行文件的名称及其他参数;\npUserContext——指向User_Conetxt的指针,是本函数完成用户上下文初始化的对象\n*/\nLoad_User_Program主要实现功能如下:\n(1)根据Parse_ELF_Executable函数的执行结果Exe_Format中的Exe_Segment结构提供的用户程序段信息,用户命令参数及用户态进程栈大小计算用户态进程所需的最大内存空间,即要分配给用户态进程的内存空间。\n(2)为用户程序分配内存空间,并初始化。\n(3)根据Exe_Segment提供的用户段信息初始化代码段、数据段以及栈段的段描述符和段选择子。\n(4)根据段信息将用户程序中的各段内容复制到分配的用户内存空间。\n(5)根据Exe_Format结构初始化User_Context结构中的用户态进程代码段入口entry字段,并根据command参数初始化用户内存空间中的参数块。\n(6)初始化User_Context结构的用户打开文件列表,并添加标准输入输出文件。\n(7)将初始化完毕的User_Context指针赋予*pUserContext,返回0表示成功。\n\n4、 用户态进程空间\n每个用户态进程都拥有属于自己的内存段空间,如:代码段、数据段、栈段等,每个段有一个段描述符(segment descriptor),并且每个进程有一个段描述符表(Local Descriptor Table),用于保存该进程的所有段描述符。操作系统中还设置一个全局描述符表(GDT,Global Descriptor Table),用于记录了系统中所有进程的ldt描述符。\n\n\n5、用户态进程创建LDT的步骤\n(1)调用函数Allocate_Segment_Descriptor()新建一个LDT描述符;\n(2)调用函数Selector()新建一个LDT选择子;\n(3)调用函数Init_Code_Segment_Descriptor()初始化一个文本段描述符;\n(4)调用函数Init_Data_Segment_Descriptor()初始化一个数据段描述符;\n(5)调用函数Selector()新建一个数据段选择子;\n(6)调用函数Selector()新建一个文本(可执行代码)段选择子。\n\n三、项目2的实现:\n1)“src/GeekOS/user.c”文件中的函数Spawn(),其功能是生成一个新的用户级进程;\n\nint Spawn(const char *program, const char *command, struct Kernel_Thread **pThread) { \n int res; \n\n /* 读取 ELF 文件 */ \n char *exeFileData = NULL; \n ulong_t exeFileLength = 0; \n res = Read_Fully(program, (void**)&exeFileData, &exeFileLength); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n return ENOTFOUND; \n } \n\n /* 分析 ELF 文件 */ \n struct Exe_Format exeFormat; \n res = Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n return res; \n }\n\n /* 加载用户程序 */ \n struct User_Context *userContext = NULL; \n res = Load_User_Program(exeFileData, exeFileLength, &exeFormat, command, &userContext); \n if (res != 0) \n { \n if (exeFileData != NULL) Free(exeFileData); \n if (userContext != NULL) Destroy_User_Context(userContext); \n return res; \n } \n if (exeFileData != NULL) Free(exeFileData); \n exeFileData = NULL; \n\n /* 开始用户进程 */ \n struct Kernel_Thread *thread = NULL; \n thread = Start_User_Thread(userContext, false); \n /* 超出内存 创建新进程失败 */ \n if (thread == NULL) \n { \n if (userContext != NULL) Destroy_User_Context(userContext);\n return ENOMEM; \n }\n\n KASSERT(thread->refCount == 2);\n /* 返回核心进程的指针 */ \n *pThread = thread; \n return 0; \n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n32\n33\n34\n35\n36\n37\n38\n39\n40\n41\n42\n43\n44\n45\n46\n47\n48\n49\n2)“src/GeekOS/user.c”文件中的函数Switch_To_User_Context(),调度程序在执行一个新的进程前调用该函数以切换用户地址空间;\n\nvoid Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state)\n{\n /*\n * Hint: Before executing in user mode, you will need to call\n * the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space()\n * functions.\n */\n\n //之前最近使用过的 userContxt\n static struct User_Context* s_currentUserContext;\n\n //指向User_Conetxt的指针,并初始化为准备切换的进程\n struct User_Context* userContext = kthread->userContext;\n\n KASSERT(!Interrupts_Enabled());\n\n //userContext为0表示此进程为核心态进程就不用切换地址空间\n if (userContext == 0) return;\n\n if (userContext != s_currentUserContext)\n {\n //为用户态进程时则切换地址空间\n Switch_To_Address_Space(userContext);\n //新进程的核心栈指针 \n ulong_t esp0 = ((ulong_t)kthread->stackPage) + PAGE_SIZE;\n //设置内核堆栈指针\n Set_Kernel_Stack_Pointer(esp0);\n //保存新的 userContxt\n s_currentUserContext = userContext;\n }\n}\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n26\n27\n28\n29\n30\n31\n3)“src/GeekOS/elf.c”文件中的函数Parse_ELF_Executable()。该函数的实现要求和项目1相同。\n4)“src/GeekOS/userseg.c”文件中主要是实现一些为实现对“src/GeekOS/user.c”中高层操作支持的函数。\nCreate_User_Context()函数,用于创 建并初始化一个用户上下文结构。
最新发布
11-24
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值