当前位置:首页 >> >>

计算机操作系统实验_运行用户态程序

西北工业大学 操作系统实验 实验报告
一、实验目的
掌握在 GeekOS 系统用户态模式下加载并运行可执行程序的方法。

二、实验要求
1. 按照实验讲义 P127 页中的设计要求,实现在用户态模式下加载并运行可 执行程序的代码,给出关键函数的代码以及实验结果。

三、实验过程及结果
答:核心函数代码如下:
================== user.c =============== //产生一个进程(用户态) int Spawn(const char *program, const char *command, struct Kernel_Thread **pThread) { //TODO("Spawn a process by reading an executable from a filesystem"); int rc; char *exeFileData = 0; ulong_t exeFileLength; struct User_Context *userContext = 0; struct Kernel_Thread *process = 0; struct Exe_Format exeFormat; if ((rc = Read_Fully(program, (void**) &exeFileData, &exeFileLength)) != 0 ) { Print("Failed to Read File %s!\n", program); goto fail; } if((rc = Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat)) != 0 ) { Print("Failed to Parse ELF File!\n"); goto fail; } if((rc = Load_User_Program(exeFileData, exeFileLength, &exeFormat, command, &userContext)) != 0) { Print("Failed to Load User Program!\n"); goto fail; } //在堆分配方式下释放内存并再次初始化 exeFileData Free(exeFileData); exeFileData = 0;
-1-

/* 开始用户进程,调用 Start_User_Thread 函数创建一个进程并使其进入准备运行队列*/ process = Start_User_Thread(userContext, false); if (process != 0) { KASSERT(process->refCount == 2); /* 返回核心进程的指针 */ *pThread = process; rc = process->pid;//记录当前进程的 ID } else rc = ENOMEM; return rc; fail: //如果新进程创建失败则注销 User_Context 对象 if (exeFileData != 0) Free(exeFileData);//释放内存 if (userContext != 0) Destroy_User_Context(userContext);//销毁进程对象 return rc; } ------------------------------------//切换至用户上下文 void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state) { static struct User_Context* s_currentUserContext; /* last user context used */ //extern int userDebug; struct User_Context* userContext = kthread->userContext; KASSERT(!Interrupts_Enabled()); if (userContext == 0) { //userContext 为 0 表示此进程为核心态进程就不用切换地址空间 return; } if (userContext != s_currentUserContext) { ulong_t esp0; //if (userDebug) Print("A[%p]\n", kthread); Switch_To_Address_Space(userContext);//为用户态进程时则切换地址空间 esp0 = ((ulong_t) kthread->stackPage) + PAGE_SIZE; //if (userDebug) // Print("S[%lx]\n", esp0); /* 新进程的核心栈. */ Set_Kernel_Stack_Pointer(esp0);//设置内核堆栈指针 /* New user context is active */ s_currentUserContext = userContext; } } ================== elf.c ====================

-2-

int Parse_ELF_Executable(char *exeFileData, ulong_t exeFileLength, struct Exe_Format *exeFormat) { int i; elfHeader *head=(elfHeader*)exeFileData; programHeader *proHeader=(programHeader *)(exeFileData+head->phoff); KASSERT(exeFileData!=NULL); KASSERT(exeFileLength>head->ehsize+head->phentsize*head->phnum); KASSERT(head->entry%4==0); exeFormat->numSegments=head->phnum; exeFormat->entryAddr=head->entry; for(i=0;i<head->phnum;i++) { exeFormat->segmentList[i].offsetInFile=proHeader->offset; exeFormat->segmentList[i].lengthInFile=proHeader->fileSize; exeFormat->segmentList[i].startAddress=proHeader->vaddr; exeFormat->segmentList[i].sizeInMemory=proHeader->memSize; exeFormat->segmentList[i].protFlags=proHeader->flags; proHeader++; } return 0; } =================== userseg.c =================== //需在此文件各函数前增加一个函数,此函数的功能是按给定的大小创建一个用户级进程上 下文,具体实现如下: //函数功能:按给定的大小创建一个用户级进程上下文 static struct User_Context* Create_User_Context(ulong_t size) { struct User_Context * UserContext; size = Round_Up_To_Page(size); UserContext = (struct User_Context *)Malloc(sizeof(struct User_Context)); if (UserContext != 0) UserContext->memory = Malloc(size); //为核心态进程 else goto fail; //内存为空 if (0 == UserContext->memory) goto fail; memset(UserContext->memory, '\0', size); UserContext->size = size; //以下为用户态进程创建 LDT(段描述符表) //新建一个 LDT 描述符 UserContext->ldtDescriptor = Allocate_Segment_Descriptor();

-3-

if (0 == UserContext->ldtDescriptor) goto fail; //初始化段描述符 Init_LDT_Descriptor(UserContext->ldtDescriptor, UserContext->ldt, NUM_USER_LDT_ENTRIES); //新建一个 LDT 选择子 UserContext->ldtSelector = Selector(KERNEL_PRIVILEGE, true, Get_Descriptor_Index(UserContext->ldtDescriptor)); //新建一个文本段描述符 Init_Code_Segment_Descriptor( &UserContext->ldt[0], (ulong_t) UserContext->memory, size / PAGE_SIZE, USER_PRIVILEGE ); //新建一个数据段 Init_Data_Segment_Descriptor( &UserContext->ldt[1], (ulong_t) UserContext->memory, size / PAGE_SIZE, USER_PRIVILEGE ); //新建数据段和文本段选择子 UserContext->csSelector = Selector(USER_PRIVILEGE, false, 0); UserContext->dsSelector = Selector(USER_PRIVILEGE, false, 1); //将引用数清 0 UserContext->refCount = 0; return UserContext; fail: if (UserContext != 0){ if (UserContext->memory != 0){ Free(UserContext->memory); } Free(UserContext); } return 0; } -------------------------------------------//摧毁用户上下文 void Destroy_User_Context(struct User_Context* userContext) { //TODO("Destroy a User_Context"); //释放占用的 LDT Free_Segment_Descriptor(userContext->ldtDescriptor);

-4-

userContext->ldtDescriptor=0; //释放内存空间 Free(userContext->memory); userContext->memory=0; //释放 userContext 本身占用的内存 Free(userContext); userContext=0; } ---------------------------------------------int Load_User_Program(char *exeFileData,ulong_t exeFileLength,struct Exe_Format *exeFormat,const char *command,struct User_Context **pUserContext) { //TODO("Load a user executable into a user memory space using segmentation"); int i; ulong_t maxva = 0;//要分配的最大内存空间 unsigned numArgs;//进程数目 ulong_t argBlockSize;//参数块的大小 ulong_t size, argBlockAddr;//参数块地址 struct User_Context *userContext = 0; //计算用户态进程所需的最大内存空间 for (i = 0; i < exeFormat->numSegments; ++i) { //elf.h struct Exe_Segment *segment = &exeFormat->segmentList[i]; ulong_t topva = segment->startAddress + segment->sizeInMemory; /* FIXME: range check */ if (topva > maxva) maxva = topva; } Get_Argument_Block_Size(command, &numArgs, &argBlockSize);//获取参数块信息 size = Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE; argBlockAddr = size; size += argBlockSize; userContext = Create_User_Context(size);//按相应大小创建一个进程 if (userContext == 0)//如果为核心态进程 return -1; for (i = 0; i < exeFormat->numSegments; ++i) { struct Exe_Segment *segment = &exeFormat->segmentList[i]; //根据段信息将用户程序中的各段内容复制到分配的用户内存空间 memcpy(userContext->memory + segment->startAddress, exeFileData + segment->offsetInFile,segment->lengthInFile); } //格式化参数块 Format_Argument_Block(userContext->memory + argBlockAddr, numArgs, argBlockAddr, command);

-5-

//初始化数据段,堆栈段及代码段信息 userContext->entryAddr = exeFormat->entryAddr; userContext->argBlockAddr = argBlockAddr; userContext->stackPointerAddr = argBlockAddr; //将初始化完毕的 User_Context 赋给*pUserContext *pUserContext = userContext; return 0;//成功 } ---------------------------------------------//将用户态的进程复制到内核缓冲区 bool Copy_From_User(void* destInKernel, ulong_t srcInUser, ulong_t bufSize) { struct User_Context * UserContext = g_currentThread->userContext; //--: check if memory if validated if (!Validate_User_Memory(UserContext,srcInUser, bufSize)) return false; memcpy(destInKernel, UserContext->memory + srcInUser, bufSize); return true; } ----------------------------------------//将内核态的进程复制到用户态 bool Copy_To_User(ulong_t destInUser, void* srcInKernel, ulong_t bufSize) { struct User_Context * UserContext = g_currentThread->userContext; if (!Validate_User_Memory(UserContext, destInUser, bufSize)) return false; memcpy(UserContext->memory + destInUser, srcInKernel, bufSize); return true; } ---------------------------------------//切换到用户地址空间 void Switch_To_Address_Space(struct User_Context *userContext) { ushort_t ldtSelector= userContext->ldtSelector;/* Switch to the LDT of the new user context */ __asm__ __volatile__ ("lldt %0"::"a"(ldtSelector)); } ================= kthread.c =============== 添加头文件 #include <geekos/user.h> ---------------------------------//创建一个用户进程 /*static*/ void Setup_User_Thread(struct Kernel_Thread* kthread, struct User_Context* userContext) {

-6-

ulong_t eflags = EFLAGS_IF; unsigned csSelector=userContext->csSelector;//CS 选择子 unsigned dsSelector=userContext->dsSelector;//DS 选择子 Attach_User_Context(kthread, userContext); //初始化用户态进程堆栈,使之看上去像刚被中断运行一样 //分别调用 Push 函数将以下数据压入堆栈 Push(kthread, dsSelector); //数据选择子 Push(kthread, userContext->stackPointerAddr); //堆栈指针 Push(kthread, eflags); //Eflags Push(kthread, csSelector); //文本选择子 Push(kthread, userContext->entryAddr); //程序计数器 Push(kthread, 0); //错误代码(0) Push(kthread, 0); //中断号(0) //初始化通用寄存单元,将 ESI 用户传递参数块地址 Push(kthread, 0); /* eax */ Push(kthread, 0); /* ebx */ Push(kthread, 0); /* edx */ Push(kthread, 0); /* edx */ Push(kthread, userContext->argBlockAddr); /* esi */ Push(kthread, 0); /* edi */ Push(kthread, 0); /* ebp */ //初始化数据段寄存单元 Push(kthread, dsSelector); /* ds */ Push(kthread, dsSelector); /* es */ Push(kthread, dsSelector); /* fs */ Push(kthread, dsSelector); /* gs */ } //开始用户进程 struct Kernel_Thread* Start_User_Thread(struct User_Context* userContext, bool detached) { struct Kernel_Thread* kthread = Create_Thread(PRIORITY_USER, detached); if (kthread != 0){ Setup_User_Thread(kthread, userContext); Make_Runnable_Atomic(kthread); } return kthread; } ================ syscall.c ================= // 需 在 此 文 件 别 的 函 数 前 增 加 一 个 函 数 , 函 数 名 为 Copy_User_String , 它 被 函 数 Sys_PrintString 调用,具体实现如下:

-7-

static int Copy_User_String(ulong_t uaddr, ulong_t len, ulong_t maxLen, char **pStr) { int rc = 0; char *str; if (len > maxLen){ //超过最大长度 return EINVALID; } str = (char*) Malloc(len+1); //为字符串分配空间 if (0 == str){ rc = ENOMEM; goto fail; } if (!Copy_From_User(str, uaddr, len)){ //从用户空间中复制数据 rc = EINVALID; Free(str); goto fail; } str[len] = '\0'; //成功 *pStr = str; fail: return rc; } ----------------------------------------static int Sys_Exit(struct Interrupt_State* state) { Exit(state->ebx); } ----------------------------------------static int Sys_PrintString(struct Interrupt_State* state) { int rc = 0;//返回值 uint_t length = state->ecx;//字符串长度 uchar_t* buf = 0; if (length > 0) { if ((rc = Copy_User_String(state->ebx, length, 1023, (char**) &buf)) != 0) goto done; Put_Buf(buf, length); } done: if (buf != 0) Free(buf); return rc; } ----------------------------------------------

-8-

static int Sys_GetKey(struct Interrupt_State* state) { return Wait_For_Key(); //返回按键码 keyboard.c/Wait_For_Key() } --------------------------------------------static int Sys_SetAttr(struct Interrupt_State* state) { Set_Current_Attr((uchar_t) state->ebx); return 0; } --------------------------------------------static int Sys_GetCursor(struct Interrupt_State* state) { int row, col; Get_Cursor(&row, &col); if (!Copy_To_User(state->ebx, &row, sizeof(int)) ||!Copy_To_User(state->ecx, &col, sizeof(int))) return -1; return 0; } ----------------------------------------------static int Sys_PutCursor(struct Interrupt_State* state) { return Put_Cursor(state->ebx, state->ecx) ? 0 : -1; } ----------------------------------------------static int Sys_Spawn(struct Interrupt_State* state) { int rc; //函数返回值 char *program = 0; //进程名称 char *command = 0; //用户命令 struct Kernel_Thread *process; if ((rc = Copy_User_String(state->ebx, state->ecx, VFS_MAX_PATH_LEN, &program)) != 0) { goto fail; } if(rc = Copy_User_String(state->edx, state->esi, 1023, &command)) != 0) {//从用户空间复制用户命令 goto fail; } Enable_Interrupts(); //开中断 rc = Spawn(program, command, &process);//得到进程名称和用户命令后便可生成一个新 进程

-9-

if (rc == 0) {//若成功则返回新进程 ID 号 KASSERT(process != 0); rc = process->pid; } Disable_Interrupts();//关中断 fail://返回小于 0 的错误代码 if (program != 0) Free(program); if (command != 0) Free(command); return rc; } ----------------------------------------static int Sys_Wait(struct Interrupt_State* state) { //TODO("Wait system call"); int exitCode; struct Kernel_Thread *kthread = Lookup_Thread(state->ebx); if (kthread == 0) return -12; Enable_Interrupts(); exitCode = Join(kthread); Disable_Interrupts(); return exitCode; } --------------------------------------static int Sys_GetPID(struct Interrupt_State* state) { //TODO("GetPID system call"); return g_currentThread->pid; } ================= main.c ================== static void Spawn_Init_Process(void) { //TODO("Spawn the init process"); struct Kernel_Thread *pThread; Spawn("/c/shell.exe","/c/shell.exe",&pThread); }

实验结果如图所示:

-10-

原理: Geekos 提供了一个简单的 shell, 保存在 PFAT 文件系统内, 所以 geekos 系统启动后,启动 shell 程序/c/shell.exe 运行,将/c/shell.exe 作为可执行文件传递 给 spawn 函数的 program 参数,创建第一个用户态进程,然后由它来创建其他进 程。运行后,geekos 就可以挂在这 shell,并能运行测试文件 c.exe 和 b.exe。

四、实验分析
GeekOS 系统最早创建的内核进程有 Idle、Reaper 和 Main 三个进程,它们 由 Init_Scheduler 函数创建:最先初始化一个核态进程 mainThread,并将该进程 作为当前运行进程,函数最后还调用 Start_Kernel_Thread 函数创建了两个系统 进程 Idle 和 Reaper。 所以,Idle、Reaper 和 Main 三个进程是系统中最早存在的 进程。 在 GeekOS 中为了区分用户态进程和内核进程,在 Kernel_Thread 结构体中 设置了一个字段 userContext,指向用户态进程上下文。对于内核进程来说,这 个指针为空,而用户态进程都拥有自己的用户上下文(User_Context) 。因此,在 GeekOS 中要判断一个进程是内核进程还是用户态进程,只要通过 userContext 字段是否为空来判断就可以了。

-11-

每个用户态进程都拥有属于自己的内存段空间,如:代码段、数据段、堆栈 段等,每个段有一个段描述符(segment descriptor) ,并且每个进程有一个段描述 符表(Local Descriptor Table) ,用于保存该进程的所有段描述符。操作系统中还 设置一个全局描述符表(GDT,Global Descriptor Table) ,用于记录了系统中所 有进程的 ldt 描述符。

用户态进程创建 LDT 的步骤: (1)调用函数 Allocate_Segment_Descriptor()新建一个 LDT 描述符; (2)调用函数 Selector()新建一个 LDT 选择子; (3)调用函数 Init_Code_Segment_Descriptor()新建一个文本段描述符;

-12-

(4)调用函数 Init_Data_Segment_Descriptor()新建一个数据段; (5)调用函数 Selector()新建一个数据段选择子; (6)调用函数 Selector()新建一个文本(可执行代码)段选择子。 用户态进程创建流程:

五、所遇问题及解决方法
答:对于 project 2,更要求读懂代码。虽说网上有很多现成的答案,但不免 有出入,不能直接拿来用。还得要求自己去读懂代码,并根据实际情况作出适当 的修改。 而在读懂代码过程中, 仍免不了有各种各样的疑惑。 比如 Spawn_Program 函数,该函数在 main.c 文件里被调用,作用是导入用户程序并初始化。但当时 就有疑惑:main 函数只是直接调用了 shell.exe, 那其他执行文件又该在哪里调 用?后来经过查询,知道 Syscall.c 文件里有个 int_Spawn 函数,该函数从空间复 制进程名称及用户命令,生成新进程,并调用 Spawn_Program 函数,其余过程 与 shell.exe 一样。 通过这个实验,我对操作系统也有了更深的了解,对操作系统运行用户态程 序也有了更深的认识,受益匪浅。

-13-


更多相关标签: