本项目需要对以下几个文件进行修改:
⑴ “src/GeekOS/user.c”文件中的函数Spawn(),其功能是生成一个新的用户级进程;
⑵ “src/GeekOS/user.c”文件中的函数Switch_To_User_Context(),调度程序在执行一个新的进程前调用该函数以切换用户地址空间;
⑶ “src/GeekOS/elf.c”文件中的函数Parse_ELF_Executable()。该函数的实现要求和项目1相同。
⑷ “src/GeekOS/userseg.c”文件中主要是实现一些为实现对“src/GeekOS/user.c”中高层操作支持的函数。
Destroy_User_Context()函数的功能是释放用户态进程占用的内存资源。
Load_User_Program()函数的功能通过加载可执行文件镜像创建新进程的User_Context结构。
Copy_From_User()和Copy_To_User()函数的功能是在用户地址空间和内核地址空间之间复制数据,在分段存储器管理模式下,只要段有效,调用memcpy函数就可以实现这两个函数的功能。
Switch_To_Address_Space()函数的功能是通过将进程的LDT装入到LDT寄存器来激活用户的地址空间;
⑸ “src/GeekOS/kthread.c”文件中的Start_User_Thread函数和Setup_User_Thread函数。
Setup_User_Thread()函数的功能是为进程初始化内核堆栈,堆栈中是为进程首次进入用户态运行时设置处理器状态要使用的数据。
Start_User_Thread()是一个高层操作,该函数使用User_Context对象开始一个新进程。
⑹ “src/GeekOS/kthread.c”文件中主要是实现用户程序要求内核进行服务的一些系统调用函数定义。要求用户实现的有Sys_Exit()函数、Sys_PrintString()函数、Sys_GetKey()、Sys_SetAttr()、Sys_GetCursor()、Sys_PutCursor()、Sys_Spawn()函数、Sys_Wait()函数和Sys_GetPID( )函数。
⑺在main.c文件中改写生成第一个用户态进程的函数调用:Spawn_Init_Process(void)。创建第一个用户态进程,然后由它来创建其它进程。
⑴在GeekOS中为了区分用户态进程和内核进程,在Kernel_Thread结构体中设置了一个字段 userContext,指向用户态进程上下文。对于内核进程来说,这个指针为空,而用户态进程都拥有自己的用户上下文(User_Context)。因此,在GeekOS中要判断一个进程是内核进程还是用户态进程,只要通过userContext字段是否为空来判断就可以了。
图1 用户态进程结构⑵项目2需要实现用户进程,其实用户进程就是基于内核进程的一个改进。内核进程控制块结构Kernel_Thread中有一个字段User_Context,而在初始化内核进程的函数时系统将其初始化为零;User_Context字段其实就是上下文数据结构,定义如下:
StructUser_Context{
#defineNUM_USER_LDT_ENTRIES 3
StructSegment_Descriptor ldt[NUM_USRE_LDT_ENTRIES];
Structsegment_Descriptor* ldtDsecriptor;
Char* memory;
Ulong_t size;
Ushort_tldtSelector;
Ushort_t csSelector;
Ushort_tdsSelector;
Pde_t*pageDir;
Ulong_tentryAddr;
Ulong_targBlockAddr;
Ulong_tstackPointerAddr;
Int refCount;
#if 0
Int *semaphores
#endif
};
Struct Segment Descriptor ldt[NUM_USER_LDT_ENTRIES]: 是Segment Descriptor数组,这里只有两个元素,一个Segment用于用户进程的数据,一个Segment用于用户进程的代码。ldtDescriptor 是LDT的描述Segment Descriptor,memory是用户内存空间的其实地址。Size是用户空间的大小。entryAddr是用户代码的其实地址,进程就是从这个地址开始运行的。
⑶ 每个用户态进程都拥有属于自己的内存段空间,如:代码段、数据段、堆栈段等,每个段有一个段描述符(segment descriptor),并且每个进程有一个段描述符表(LocalDescriptor Table),用于保存该进程的所有段描述符。操作系统中还设置一个全局描述符表(GDT,Global Descriptor Table),用于记录了系统中所有进程的ldt描述符。
图2 GDT、LDT和User_Context的关系Geekos系统初始的内核是不支持用户态进程,但在内核进程控制块Kernel_Thread中有一个字段User_Context用于标志进程是内核进程还是用户态进程。若为核心进程,则字段赋值为0。
从user.h中我们知道User_Context结构,用户的LDT及各段描述符选择子、代码入口都保存在这个结构中。所以,在装入用户程式时,首先需要对User_Context结构进程初始化,然后进行用户进程初始化。下面是用户太进程创建过程的描述:
(1)Spawn函数导入用户程序并初始化:调用Load_User_Program进行User_Context的初始化及用户态进程空间的分配,用户程序各段的装入;
(2)Spawn函数调用Start_User_Thread函数,初始化一个用户太进程;
(3)Spawn退出,用户进程已经被调用,可以调度了;
Spawn函数原型如下:
int Spawn(const char*program, const char *command , struct Kernel_Thread **pThread)
参数说明:Program对应的是要读入的可执行文件,Command是用户执行程序执行时的命令行字符串,PTread是存放指向进程的指针。
Spawn完成的主要功能有:
(1)调用Read_Fully函数就可执行文件读入内存缓冲区。
(2)调用Parse_ELF_Executable函数分析ELF格式文件。
(3)调用Load_User_Program函数就可执行文件的程序段和数据段等装入内存,初始化User_context数据结构。
(4)调用Start_User_Thread函数创建一个进程并使该进程进入准备队列。
(1)Parse_ELF_Executable函数见项目一
(2)Spawn函数具体实现如下:
int Spawn(const char *program, const char *command, structKernel_Thread **pThread)
{
int result;
char *exeFileData = 0;
ulong_t exeFileLength;
structUser_Context *UserContext = 0;
structExe_Format exeFormat;
structKernel_Thread * thread;
if ((result =Read_Fully(program,(void **) &exeFileData, &exeFileLength)) != 0)
{
Print("Oh,mygod!Failed to read file %s\n", program);
goto fail;
}
if ((result =Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat)) != 0)
{
Print("Oh,mygod!Failed to parse ELF file \n");
goto fail;
}
if ((result =Load_User_Program(exeFileData, exeFileLength, &exeFormat, command,&UserContext)) != 0)
{
Print("Oh,mygod!Failed to Load User Program\n");
goto fail;
}
Free(exeFileData);
exeFileData = 0;
thread =Start_User_Thread(UserContext, false);
if (thread != 0)
{
*pThread = thread;
result =thread->pid;
}
else
{
result = ENOMEM;
}
return result;
fail:
if (exeFileData != 0)
Free(exeFileData);
if (UserContext != 0)
Destroy_User_Context(UserContext);
return result;
}
(3)Switch_To_User_Context()函数实现如下:
void Switch_To_User_Context(struct Kernel_Thread* kthread,struct Interrupt_State* state)
{
struct User_Context *UserContext = kthread->userContext;
if (UserContext == 0)
{
return ;
}
(4)Load_User_Program函数实现如下:
int Load_User_Program(char *exeFileData, ulong_t exeFileLength,struct Exe_Format *exeFormat, const char *command, struct User_Context**pUserContext)
{
ulong_t maxva = 0;
unsigned numArgs;
ulong_t argBlockSize;
ulong_t virtSize,argBlockAddr;
struct User_Context *UserContext = 0;
int i;
for (i = 0; i <exeFormat->numSegments; i++)
{
struct Exe_Segment* segment = &exeFormat->segmentList[i];
ulong_t topva =segment->startAddress + segment->sizeInMemory;
if (topva > maxva)
maxva = topva;
}
Get_Argument_Block_Size(command,&numArgs, &argBlockSize);
virtSize =Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE;
argBlockAddr =virtSize;
virtSize +=argBlockSize;
UserContext =Create_User_Context(virtSize);
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);
UserContext->argBlockAddr= argBlockAddr;
UserContext->stackPointerAddr= argBlockAddr;
UserContext->entryAddr= exeFormat->entryAddr;
*pUserContext= UserContext;
return 0;
}
修改Spawn_With_Path函数如下:
int Spawn_With_Path(const char *program, const char*command,
const char*path)
{
int pid;
char exeName[(CMDLEN*2)+5];
/* Try executing program asspecified */
pid = Spawn_Program(program,command
);
if (pid == ENOTFOUND &&strchr(program, '/') == 0) {
Print("Try to search file in/c or /a......\n");
/* Search for program on path. */
for (;;) {
char *p;
while (*path == ':')
++path;
if (strcmp(path, "") == 0)
break;
p = strchr(path, ':');
if (p != 0) {
memcpy(exeName, path, p -path);
exeName[p - path] = '\0';
path = p + 1;
} else {
strcpy(exeName, path);
path = "";
}
strcat(exeName, "/");
strcat(exeName, program);
if (!Ends_With(exeName, ".exe"))
strcat(exeName,".exe");
/*Print("exeName=%s\n", exeName);*/
pid = Spawn_Program(exeName, command
);
if (pid != ENOTFOUND)
break;
}
}
return pid;
}
运行结果如下图:
图3 项目2测试界面
本项目中真正建立的第一个用户级进程其实是shell,通过它创建其他进程。Shell是一个简单的命令解析器,它能分析我们的输入,shell中预制了很多命令,比如pid命令可以实现查询当前进程的pid。
在这里有两个问题。一个问题是,GeekOS启动后输入pid是显示的是为6,这是因为系统那个初始化时建立了包括shell在内的6个进程(Main,Idle,Repair,Floppy_Request_Thread,IDE_Request_Thread)。还有一点就是,pid命令是调用Get_PID函数来返回当前进程的pid,所以会出现一个“假象”:你通过shell建立了多个进程(比如b.exe,c.exe等)后,输入pid命令返回的还是6。这其实是shell的pid命令是返回当前进程的pid,而shell就是当前进程。当在shell中执行shell是,再输入pid命令所返回的就不再是6了。
另一个问题是,shell在动态创建用户进程时,默认用户输入的exe文件的路径。所里当你在shell中输入c(/c目录下有c.exe文件)时,会先提示读取文件失败(如上图所示),然后再在/c和/a目录下搜索文件(其实应该是加上路径再读取一次而已,从输入mm就可以看出来),如有这个文件就加载它。当输入/c/c.exe是就不会出现读取文件失败的错误提示。