操作系统实验3

Uncategorized
20k words

前言:操作系统系列实验的blog比较抽象,如有需要可以直接去搬我上传的项目直接用
毕竟这几篇都是汇报检查是我打的草稿,内容很乱
链接如下:
可运行项目文件直达

IP签名
参考来源:
1.操作系统课程设计geekos project1-3

2.操作系统课程设计

总结用户态进程创建流程

在GeekOS中,用户态进程的创建涉及多个步骤,从加载可执行文件到最终将新进程加入就绪队列等待运行。下面是用户态进程创建的主要流程总结:

  1. 调用 Spawn 函数

    • Spawn 函数是创建用户态进程的入口点。
    • 该函数接收要加载的可执行文件路径 (program) 和命令行参数 (command) 以及指向 Kernel_Thread 的指针 (pThread)。
  2. 读取可执行文件

    • 使用 Read_Fully 函数将可执行文件的内容读入内存缓冲区。
  3. 解析 ELF 文件

    • 调用 Parse_ELF_Executable 函数分析 ELF 格式的可执行文件,获取段信息、入口地址等关键数据。
  4. 初始化并加载用户程序

    • 调用 Load_User_Program 函数来处理以下任务:
      • 计算用户程序所需的内存空间,包括代码段、数据段和堆栈段。
      • 分配内存,并将可执行文件中的各段内容复制到分配的内存空间中。
      • 初始化 User_Context 结构体,设置段描述符(LDT)、内存布局、入口地址等。
      • 根据命令行参数初始化用户内存空间中的参数块。
      • 初始化用户打开文件列表,并添加标准输入输出文件。
  5. 创建用户上下文

    • Load_User_Program 函数内部,会调用 Create_User_Context 函数来创建用户上下文对象(User_Context)。
    • 这个过程包括为用户进程设置 LDT(本地描述符表)及其描述符选择子,并将其关联到 GDT(全局描述符表)中。
  6. 初始化内核堆栈

    • 调用 Setup_User_Thread 函数为新进程初始化内核堆栈。
    • 设置初始的寄存器状态,模拟中断现场,以便进程首次进入用户态时能够正确恢复运行状态。
  7. 启动用户线程

    • 调用 Start_User_Thread 函数开始一个新进程。
    • 将新进程的状态设置为就绪,并将其加入就绪队列等待调度。
  8. 调度运行

    • 当调度器选择该进程进行运行时,通过 Switch_To_User_Context 函数切换到用户地址空间。
    • Switch_To_User_Context 函数会激活新的用户地址空间,并设置必要的寄存器值,以确保进程可以顺利从用户态开始执行。

用户态进程创建流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
+-----------------+
| Spawn 函数 |
+-----------------+
|
v
+-----------------+
| 读取可执行文件 |
| Read_Fully |
+-----------------+
|
v
+-----------------+
| 解析 ELF 文件 |
| Parse_ELF_Executable |
+-----------------+
|
v
+-----------------+
| 加载用户程序 |
| Load_User_Program|
+-----------------+
|
v
+-----------------+
| 创建用户上下文 |
| Create_User_Context|
+-----------------+
|
v
+-----------------+
| 初始化内核堆栈 |
| Setup_User_Thread|
+-----------------+
|
v
+-----------------+
| 启动用户线程 |
| Start_User_Thread|
+-----------------+
|
v
+-----------------+
| 调度运行 |
| Switch_To_User_Context|
+-----------------+

关键函数说明

  • **Spawn**:创建用户态进程的入口点。
  • **Read_Fully**:读取可执行文件到内存。
  • **Parse_ELF_Executable**:解析 ELF 格式的可执行文件。
  • **Load_User_Program**:计算并分配内存,加载程序段和数据段,初始化 User_Context
  • **Create_User_Context**:创建用户上下文对象。
  • **Setup_User_Thread**:初始化内核堆栈,设置初始寄存器状态。
  • **Start_User_Thread**:开始一个新进程,将其加入就绪队列。
  • **Switch_To_User_Context**:切换到用户地址空间,激活用户上下文。

这个流程确保了用户态进程能够被正确地创建、加载和运行,同时维护了系统的稳定性和安全性。

步骤总览

本项目要求对以下/src/geekos/中的文件进行修改:

**(1) user.c**:完成函数Spawn()Switch_To_User_Context()
创建进程,切换用户上下文

**(2) elf.c**:完成函数Parse_ELF_Executable(),要求与项目1相同。
分析exe文件,用于上下文(context)

**(3) userseg.c**:完成函数Destroy_User_Context()Load_User_Program()Copy_From_User()Copy_To_User()和: Switch_To_Address_Space()
销毁用户进程上下文,加载用户进行,切换用户地址空间,用来进出内核操作

**(4) kthread.c**:完成函数Setup_User_Thread()Start_User_Thread()
设置,启动进程,进入等待队列

**(5) syscall.c**:完成函数Sys_Exit()Sys_PrintString()Sys_GetKey()Sys_SetAttr()Sys_GetCursor()、: Sys_PutCursor()Sys_Spawn()Sys_Wait()Sys_GetPID()
系统调用函数,方便用户进程执行内核操作,以及只有内核才有的权限操作,如创建进程,进行系统调用需要进入内核空间使用(3)中的函数

**(6) main.c**:改写Spawn_Init_Process(void),改写时将“/c/shell.exe”作为可执行文件传递给Spawn函数的program参数,创建第一个用户态进程,然后由它来创建其它进程。

1
开始本项目前需要阅读/src/geekos目录中的entry.c、lowlevel.asm、kthread.c、userseg.c,其中在userseg.c中主要关注Destroy_User_Context()和Load_User_Program()两个函数。

前言
请严格参照实验指导书进行本教程的阅读,否则可能会出现不可预知的错误。

项目2的实现:

1.生成一个新的用户级进程

src/GeekOS/user.c”文件中的函数Spawn(),其功能是生成一个新的用户级进程;

(只展示需要修改的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int Spawn(const char *program, const char *command, struct Kernel_Thread **pThread) {     
int res;

/* 读取 ELF 文件 */
char *exeFileData = NULL;
ulong_t exeFileLength = 0;
res = Read_Fully(program, (void**)&exeFileData, &exeFileLength);
if (res != 0)
{
if (exeFileData != NULL) Free(exeFileData);
return ENOTFOUND;
}

/* 分析 ELF 文件 */
struct Exe_Format exeFormat;
res = Parse_ELF_Executable(exeFileData, exeFileLength, &exeFormat);
if (res != 0)
{
if (exeFileData != NULL) Free(exeFileData);
return res;
}

/* 加载用户程序 */
struct User_Context *userContext = NULL;
res = Load_User_Program(exeFileData, exeFileLength, &exeFormat, command, &userContext);
if (res != 0)
{
if (exeFileData != NULL) Free(exeFileData);
if (userContext != NULL) Destroy_User_Context(userContext);
return res;
}
if (exeFileData != NULL) Free(exeFileData);
exeFileData = NULL;

/* 开始用户进程 */
struct Kernel_Thread *thread = NULL;
thread = Start_User_Thread(userContext, false);
/* 超出内存 创建新进程失败 */
if (thread == NULL)
{
if (userContext != NULL) Destroy_User_Context(userContext);
return ENOMEM;
}

KASSERT(thread->refCount == 2);
/* 返回核心进程的指针 */
*pThread = thread;
return 0;
}

2.切换用户地址空间

src/GeekOS/user.c”文件中的函数Switch_To_User_Context(),调度程序在执行一个新的进程前调用该函数以切换用户地址空间;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void Switch_To_User_Context(struct Kernel_Thread* kthread, struct Interrupt_State* state)
{
/*
* Hint: Before executing in user mode, you will need to call
* the Set_Kernel_Stack_Pointer() and Switch_To_Address_Space()
* functions.
*/

//之前最近使用过的 userContxt
static struct User_Context* s_currentUserContext;

//指向User_Conetxt的指针,并初始化为准备切换的进程
struct User_Context* userContext = kthread->userContext;

KASSERT(!Interrupts_Enabled());

//userContext为0表示此进程为核心态进程就不用切换地址空间
if (userContext == 0) return;

if (userContext != s_currentUserContext)
{
//为用户态进程时则切换地址空间
Switch_To_Address_Space(userContext);
//新进程的核心栈指针
ulong_t esp0 = ((ulong_t)kthread->stackPage) + PAGE_SIZE;
//设置内核堆栈指针
Set_Kernel_Stack_Pointer(esp0);
//保存新的 userContxt
s_currentUserContext = userContext;
}
}

3.解析 ELF 格式的可执行文件

src/GeekOS/elf.c”文件中的函数Parse_ELF_Executable()。该函数的实现要求和项目1相同。

4.计算并分配内存,加载程序段和数据段,初始化 User_Context

src/GeekOS/userseg.c”文件中主要是实现一些为实现对“src/GeekOS/user.c”中高层操作支持的函数。

Create_User_Context()函数,用于创建并初始化一个用户上下文结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**需在此文件各函数前增加一个函数,此函数的功能是按给定的大小创建一个用户级进程上下文,具体实现如下:  
函数功能:按给定的大小创建一个用户级进程上下文**/
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));
/* 内存分配成功则继续为 userContext 下的 memory 分配内存空间 */
if (userContext == NULL)
{
return NULL;
}
userContext->memory = (char *)Malloc(size);
if (userContext->memory == NULL)
{
Free(userContext);
return NULL;
}
memset(userContext->memory, '\0', size);
userContext->size = size;

/* 新建一个 LDT 描述符 */
userContext->ldtDescriptor = Allocate_Segment_Descriptor();
if (userContext->ldtDescriptor == NULL)
{
Free(userContext->memory);
return NULL;
}
/* 初始化段描述符 */
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);
/* 将引用数清零 */
userContext->refCount = 0;

return userContext;
}

Destroy_User_Context()函数的功能是释放用户态进程占用的内存资源。

1
2
3
4
5
6
7
8
9
10
11
void Destroy_User_Context(struct User_Context* userContext) 
{
KASSERT(userContext->refCount == 0);
/* 释放 LDT descriptor */
Free_Segment_Descriptor(userContext->ldtDescriptor);
/* 释放内存空间 */
Disable_Interrupts();
Free(userContext->memory);
Free(userContext);
Enable_Interrupts();
}

Load_User_Program()函数的功能通过加载可执行文件镜像创建新进程的User_Context结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
int Load_User_Program(char *exeFileData, ulong_t exeFileLength,     
struct Exe_Format *exeFormat, const char *command,
struct User_Context **pUserContext)
{
unsigned int i;
struct User_Context *userContext = NULL;

/* 要分配的最大内存空间 */
ulong_t maxva = 0;
/* 计算用户态进程所需的最大内存空间 */
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;
}
/* 程序参数数目 */
unsigned int numArgs;
/* 获取参数块的大小 */
ulong_t argBlockSize;
Get_Argument_Block_Size(command, &numArgs, &argBlockSize);
/* 用户进程大小 = 参数块总大小 + 进程堆栈大小(8192) */
ulong_t size = Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE;
/* 参数块地址 */
ulong_t argBlockAddr = size;
size += argBlockSize;
/* 按相应大小创建一个进程 */
userContext = Create_User_Context(size);
/* 如果进程创建失败则返回错误信息 */
if (userContext == NULL)
{
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->entryAddr = exeFormat->entryAddr;
userContext->argBlockAddr = argBlockAddr;
userContext->stackPointerAddr = argBlockAddr;

/* 将初始化完毕的 User_Context 赋给*pUserContext */
*pUserContext = userContext;

return 0;
}

Copy_From_User()Copy_To_User()函数的功能是在用户地址空间和内核地址空间之间复制数据,在分段存储器管理模式下,只要段有效,调用memcpy函数就可以实现这两个函数的功能。

1
2
3
4
5
6
7
8
9
10
11
bool Copy_From_User(void* destInKernel, ulong_t srcInUser, ulong_t bufSize) 
{
struct User_Context* userContext = g_currentThread->userContext;
/* 如果访问的用户内存空间非法(越界访问),则直接返回失败 */
if (!Validate_User_Memory(userContext, srcInUser, bufSize))
return false;
/* 拷贝当前用户内存空间数据到系统内核空间 */
memcpy(destInKernel, userContext->memory + srcInUser, bufSize);
/* 拷贝成功则返回 1 */
return true;
}
1
2
3
4
5
6
7
8
9
10
11
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);
/* 拷贝成功则返回 1 */
return true;
}

Switch_To_Address_Space()函数的功能是通过将进程的LDT装入到LDT寄存器来激活用户的地址空间。

1
2
3
4
5
6
7
8
9
10
void Switch_To_Address_Space(struct User_Context *userContext) 
{
/* 切换到新的局部描述符表(LDT) */
ushort_t ldtSelector = userContext->ldtSelector;
__asm__ __volatile__ (
"lldt %0"
:
: "a" (ldtSelector)
);
}

5.初始化内核堆栈,设置初始寄存器状态.开始一个新进程,将其加入就绪队列

“src/GeekOS/kthread.c”文件中的Start_User_Thread函数和Setup_User_Thread函数。
修改/project2/src/geekos/kthread.c 文件,添加头文件“#include < geekos/user.h>”。

Setup_User_Thread()函数的功能是为进程初始化内核栈,栈中是为进程首次进入用户态运行时设置处理器状态要使用的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/*
* Set up the a user mode thread.
*/
void Setup_User_Thread(
struct Kernel_Thread* kthread, struct User_Context* userContext)
{
/*
* Push the argument to the thread start function, and the
* return address (the Shutdown_Thread function, so the thread will
* go away cleanly when the start function returns).
*/
ulong_t eflags = EFLAGS_IF;
unsigned int csSelector = userContext->csSelector; /* CS 选择子 */
unsigned int dsSelector = userContext->dsSelector; /* DS 选择子 */

/* 调用 Attach_User_Context 加载用户上下文 */
Attach_User_Context(kthread, userContext);

/* 初始化用户态进程堆栈,使之看上去像刚被中断运行一样 */
/* 分别调用 Push 函数将以下数据压入堆栈 */
Push(kthread, dsSelector); /* DS 选择子 */
Push(kthread, userContext->stackPointerAddr); /* 堆栈指针 */
Push(kthread, eflags); /* Eflags */
Push(kthread, csSelector); /* CS 选择子 */
Push(kthread, userContext->entryAddr); /* 程序计数器 */
Push(kthread, 0); /* 错误代码(0) */
Push(kthread, 0); /* 中断号(0) */

// if (uthreadDebug)
// Print("Entry addr=%lx\n", userContext->entryAddr);

/* 初始化通用寄存单元,向 esi 传递参数块地址 */
Push(kthread, 0); /* eax */
Push(kthread, 0); /* ebx */
Push(kthread, 0); /* ecx */
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 */
}

Start_User_Thread()是一个高层操作,该函数使用User_Context对象开始一个新进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* Start a user-mode thread (i.e., a process), using given user context.
* Returns pointer to the new thread if successful, null otherwise.
*/
struct Kernel_Thread*
Start_User_Thread(struct User_Context* userContext, bool detached)
{
/* 如果传入的用户上下文字段为空(非用户态进程)则返回错误 */
if (userContext == NULL)
{
return NULL;
}

/* 建立用户态进程 */
struct Kernel_Thread *kthread = Create_Thread(PRIORITY_USER, detached);
if (kthread == NULL)
{
return NULL;
}
Setup_User_Thread(kthread, userContext);

/* 将新创建的进程加入就绪进程队列 */
Make_Runnable_Atomic(kthread);

/* 新用户态进程创建成功,返回指向该进程的指针 */
return kthread;
}

6.

“src/GeekOS//syscall.c”文件中主要是实现用户程序要求内核进行服务的一些系统调用函数定义。要求用户实现的有Sys_Exit()函数、Sys_PrintString()函数、Sys_GetKey()、Sys_SetAttr()、Sys_GetCursor()、Sys_PutCursor()、Sys_Spawn()函数、Sys_Wait()函数和Sys_GetPID( )函数。

在/project2/src/geekos/syscall.c 文件中添加函数 Copy_User_String(),用于在 Sys_PrintString 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* 添加函数 Copy_User_String 以便在函数 Sys_PrintString 中使用 */
static int Copy_User_String(ulong_t uaddr, ulong_t len,
ulong_t maxLen, char **pStr)
{
int result = 0;
char *str;
/* 字符串超过最大长度 */
if (len > maxLen) return EINVALID;
/* 为字符串分配内存空间 */
str = (char*)Malloc(len + 1);
if (str == 0){
return ENOMEM;
goto fail;
}
/* 从用户内存空间中复制字符串到系统内核空间 */
if (!Copy_From_User(str, uaddr, len))
{
result = EINVALID;
Free(str);
goto fail;
}
str[len] = '\0';
/* 拷贝成功 */
*pStr = str;
fail:
return result;
}

Sys_Exit()函数

1
2
3
4
static int Sys_Exit(struct Interrupt_State* state)
{
Exit(state->ebx);
}

Sys_PrintString()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int Sys_PrintString(struct Interrupt_State* state)
{
int result = 0;//返回值
uint_t length = state->ecx;//字符串长度
uchar_t* buf = 0;
if (length > 0){
/* 将字符串复制到系统内核空间 */
if (Copy_User_String(state->ebx, length, 1023, (char**)&buf) != 0)
goto done;
/* 输出字符串到控制台 */
Put_Buf(buf, length);
}
done:
if (buf != NULL) Free(buf);
return result;
}

Sys_GetKey()函数

1
2
3
4
5
6
7
static int Sys_GetKey(struct Interrupt_State* state)
{
/* 返回按键码 */
/* /geekos/keyboard.c
Keycode Wait_For_Key(void) */
return Wait_For_Key();
}

Sys_SetAttr()函数

1
2
3
4
5
6
7
8
static int Sys_SetAttr(struct Interrupt_State* state)
{
/* 设置当前文本显示格式 */
/* /geekos/screen.c
void Set_Current_Attr(uchar_t attrib) */
Set_Current_Attr((uchar_t)state->ebx);
return 0;
}

Sys_GetCursor()函数

1
2
3
4
5
6
7
8
9
10
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;
}

Sys_PutCursor()函数

1
2
3
4
5
static int Sys_PutCursor(struct Interrupt_State* state)
{
/* 设置光标的位置(行和列) */
return Put_Cursor(state->ebx, state->ecx) ? 0 : -1;
}

Sys_Spawn()函数函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int Sys_Spawn(struct Interrupt_State* state)
{
int res;//程序返回值
char *program = 0;//进程名称
char *command = 0;//用户命令
struct Kernel_Thread *process;
/* 复制程序名和命令字符串到用户内存空间 */
res = Copy_User_String(state->ebx, state->ecx, VFS_MAX_PATH_LEN, &program);
if (res != 0)
{//从用户空间复制进程名称
goto fail;
}
res = Copy_User_String(state->edx, state->esi, 1023, &command);
if (res != 0)
{//从用户空间复制用户命令
goto fail;
}
/* 生成用户进程 */
Enable_Interrupts();//开中断
res = Spawn(program, command, &process);//得到进程名称和用户命令后便可生成一个新进程
if (res == 0) {//若成功则返回新进程ID号
KASSERT(process != 0);
res = process->pid;
}
Disable_Interrupts();//关中断
fail:
if (program != 0)
Free(program);
if (command != 0)
Free(command);
return res;
}

Sys_Wait()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int Sys_Wait(struct Interrupt_State* state)
{
int exitCode;
/* 查找需要等待的进程 */
struct Kernel_Thread *kthread = Lookup_Thread(state->ebx);
/* 如果没有找到需要等待的进程,则返回错误代码 */
if (kthread == 0) return -1;
/* 等待指定进程结束 */
Enable_Interrupts();
exitCode = Join(kthread);
Disable_Interrupts();
return exitCode;
}

Sys_GetPID( )函数。

1
2
3
4
5
static int Sys_GetPID(struct Interrupt_State* state)
{
/* 返回当前进程的 ID(PID) */
return g_currentThread->pid;
}

7.生成第一个用户态进程

在main.c文件中改写生成第一个用户态进程的函数调用:Spawn_Init_Process(void)

1
2
3
4
5
static void Spawn_Init_Process(void)
{
struct Kernel_Thread *pThread;
Spawn("/c/shell.exe","/c/shell.exe",&pThread);
}

8.

更改bochsrc配置文件,同project1。(可无)

简要说明 LDT 与 GDT 的区别和联系

1. 定义

  • **GDT (Global Descriptor Table)**:全局描述符表,是一个存储在内存中的数据结构,用于定义系统中所有段的描述符。GDT 中的每个条目都是一个段描述符,描述了段的基地址、界限、访问权限等信息。GDT 是全局的,对所有进程都可见。
  • **LDT (Local Descriptor Table)**:局部描述符表,也是一个存储在内存中的数据结构,用于定义特定进程的段描述符。LDT 是局部的,每个进程可以有自己的 LDT,用于管理其私有段。

2. 区别

  • 作用范围

    • GDT:全局作用,对所有进程都可见。GDT 通常包含系统级的段描述符,如代码段、数据段、系统调用门等。
    • LDT:局部作用,仅对特定进程可见。LDT 通常包含用户进程的私有段描述符,如用户代码段、数据段等。
  • 数量

    • GDT:系统中只有一个 GDT。
    • LDT:每个进程可以有一个 LDT,因此系统中可以有多个 LDT。
  • 内容

    • GDT:通常包含系统级的段描述符,如内核代码段、内核数据段、TSS(任务状态段)等。
    • LDT:通常包含用户进程的私有段描述符,如用户代码段、用户数据段等。
  • 选择子

    • GDT:使用全局描述符选择子(GDT selector)来访问 GDT 中的段描述符。
    • LDT:使用局部描述符选择子(LDT selector)来访问 LDT 中的段描述符。

3. 联系

  • 描述符选择子

    • GDT 和 LDT 都使用描述符选择子来访问段描述符。选择子是一个 16 位的值,其中高 13 位是描述符在表中的索引,低 3 位包含特权级别和表指示符(TI 位,0 表示 GDT,1 表示 LDT)。
  • 段描述符

    • GDT 和 LDT 中的段描述符格式是相同的,都包含段的基地址、界限、访问权限等信息。
  • 地址空间

    • GDT 和 LDT 一起工作,共同定义了系统的地址空间。GDT 提供了系统级的段描述符,而 LDT 提供了进程级的段描述符,两者结合可以实现对不同进程的地址空间管理。

示例

在GeekOS中,LDT和GDT的使用如下:

  • GDT

    • 在系统初始化时,设置 GDT 并加载到 GDT 寄存器中。
    • GDT 包含内核代码段、内核数据段、TSS 等系统级段描述符。
  • LDT

    • 在创建用户进程时,为每个进程创建一个 LDT。
    • LDT 包含用户代码段、用户数据段等进程私有段描述符。
    • 使用 lldt 指令将 LDT 加载到 LDT 寄存器中,以便在用户态下访问 LDT 中的段描述符。

总结

GDT 和 LDT 是 x86 架构中用于段管理的两个重要数据结构。GDT 是全局的,对所有进程都可见,主要用于系统级的段管理;LDT 是局部的,每个进程可以有自己的 LDT,主要用于用户进程的私有段管理。两者通过描述符选择子和段描述符共同工作,实现了系统的地址空间管理和进程隔离。

在GeekOS系统中,用户程序的参数传递是一个多步骤的过程,涉及到从命令行参数的解析到将这些参数正确地放置在用户内存空间中,最后通过寄存器传递给用户程序。以下是详细的步骤和解释:

1. 命令行参数解析

当用户通过命令行启动一个程序时,命令行参数会被解析成一个字符串数组。例如,如果用户执行命令 ./myprogram arg1 arg2,那么参数会被解析为:

  • argv[0] = "./myprogram"
  • argv[1] = "arg1"
  • argv[2] = "arg2"

2. 获取参数块大小

Load_User_Program()函数中,首先需要计算参数块的大小。这一步通过调用Get_Argument_Block_Size()函数来实现:

1
Get_Argument_Block_Size(command, &numArgs, &argBlockSize);

这里,command是包含所有命令行参数的字符串,numArgs是参数的数量,argBlockSize是参数块的总大小。

3. 分配用户内存

在计算出用户进程所需的总内存大小后,需要分配足够的内存来存放程序的代码段、数据段、堆栈段以及参数块。内存大小的计算如下:

1
ulong_t size = Round_Up_To_Page(maxva) + DEFAULT_USER_STACK_SIZE + argBlockSize;

其中,maxva是程序段在内存中的最大地址,DEFAULT_USER_STACK_SIZE是默认的用户堆栈大小,argBlockSize是参数块的大小。

4. 格式化参数块

在分配好内存后,需要将参数块格式化并写入用户内存空间。这一步通过调用Format_Argument_Block()函数来实现:

1
Format_Argument_Block(userContext->memory + argBlockAddr, numArgs, argBlockAddr, command);

这里,userContext->memory + argBlockAddr是指向用户内存中参数块起始地址的指针,numArgs是参数的数量,argBlockAddr是参数块的地址,command是包含所有命令行参数的字符串。

5. 初始化User_Context结构

User_Context结构中,需要设置参数块的地址,以便用户程序可以访问到这些参数:

1
userContext->argBlockAddr = argBlockAddr;

6. 设置寄存器状态

Setup_User_Thread()函数中,需要将参数块的地址传递给esi寄存器,以便用户程序启动时可以访问到这些参数:

1
2
3
4
5
6
7
Push(kthread, 0); /* eax */
Push(kthread, 0); /* ebx */
Push(kthread, 0); /* ecx */
Push(kthread, 0); /* edx */
Push(kthread, userContext->argBlockAddr); /* esi */
Push(kthread, 0); /* edi */
Push(kthread, 0); /* ebp */

7. 切换到用户地址空间

Switch_To_User_Context()函数中,确保在执行用户程序前正确切换到用户地址空间:

1
Switch_To_Address_Space(userContext);

8. 开始用户进程

最后,通过调用Start_User_Thread()函数开始用户进程,并将其加入就绪队列:

1
struct Kernel_Thread *thread = Start_User_Thread(userContext, false);

总结

通过上述步骤,用户程序的参数被正确地传递给了程序。具体来说,参数首先被解析成字符串数组,然后格式化并写入用户内存空间,最后通过寄存器传递给用户程序。这些步骤确保了用户程序在启动时能够正确访问到命令行参数。

五、所遇问题及解决方法

在完成本次实验的过程中,遇到了几个主要问题,以下是这些问题及其解决方法的详细描述:

1. 用户程序参数传递问题

问题描述:在尝试将命令行参数传递给用户程序时,发现参数没有正确传递到程序中,导致程序无法正常读取参数。

解决方法

  • Load_User_Program()函数中,通过调用Get_Argument_Block_Size()函数获取参数块的大小和参数个数。
  • 使用Format_Argument_Block()函数将参数块格式化并写入用户内存空间。
  • 初始化User_Context结构中的argBlockAddr成员,使其指向参数块在用户内存空间中的地址。
  • Setup_User_Thread()函数中,将参数块地址传递给esi寄存器,确保程序启动时可以访问到这些参数。

2. 内存分配问题

问题描述:在为用户进程分配内存时,发现内存分配失败,导致进程无法正常创建。

解决方法

  • Create_User_Context()函数中,确保内存分配的大小是页对齐的,使用Round_Up_To_Page()函数来计算所需的内存大小。
  • 在分配内存时,使用Malloc()函数,并检查返回值是否为NULL,如果是,则返回错误码。
  • 在分配完内存后,使用memset()函数将内存初始化为零,避免未初始化的内存导致的不确定行为。

3. 用户地址空间切换问题

问题描述:在切换到用户地址空间时,发现切换不成功,导致程序在内核态下运行,而不是在用户态下运行。

解决方法

  • Switch_To_User_Context()函数中,确保在切换地址空间前禁用中断,防止在切换过程中发生中断。
  • 使用Switch_To_Address_Space()函数切换到用户的地址空间。
  • 设置内核堆栈指针esp0,确保内核栈在用户态下也能正确使用。
  • 保存当前的用户上下文到静态变量s_currentUserContext中,以便后续的切换操作。

4. 系统调用实现问题

问题描述:在实现系统调用函数时,发现某些系统调用(如Sys_PrintString())无法正确处理用户态内存中的数据。

解决方法

  • syscall.c文件中,实现Copy_User_String()函数,用于从用户态内存中安全地复制字符串到内核态内存。
  • Sys_PrintString()函数中,调用Copy_User_String()函数获取用户态内存中的字符串,然后调用Print()函数输出字符串。
  • 在复制数据时,使用Validate_User_Memory()函数验证用户态内存的有效性,确保不会发生越界访问。

六、结论

通过本次实验,我们深入学习了在GeekOS系统中加载和运行用户态程序的方法。具体来说,我们实现了以下几个关键功能:

  1. 解析ELF文件:通过修改Parse_ELF_Executable()函数,成功解析了ELF文件的文件头和程序头,获取了必要的信息。
  2. 加载用户程序:实现了Load_User_Program()函数,计算并分配了用户进程所需的内存,加载了程序段和数据段,初始化了User_Context结构。
  3. 创建用户上下文:实现了Create_User_Context()函数,按给定的大小创建并初始化了一个用户级进程上下文。
  4. 初始化内核堆栈:实现了Setup_User_Thread()函数,初始化了内核堆栈和寄存器状态,确保用户程序在启动时具有正确的初始状态。
  5. 切换用户地址空间:实现了Switch_To_User_Context()函数,确保在执行用户程序前正确切换到用户地址空间。
  6. 实现系统调用:在syscall.c文件中实现了多个系统调用函数,如Sys_PrintString()Sys_Exit()等,确保用户程序可以通过系统调用来请求内核服务。

通过这些实现,我们不仅掌握了GeekOS系统的基本操作,还对操作系统的核心概念有了更深刻的理解。本次实验为我们进一步学习和研究操作系统提供了坚实的基础。

Comments