OS

OS lab8 从内核态到用户态

Posted by RogersLuo's Blog on June 21, 2024

image-20240315232430405

本科生实验报告

实验课程: 操作系统

任课教师: 刘宁

实验题目:从内核态到用户态

专业名称: 信息与计算科学

学生姓名:罗弘杰

学生学号: 22336173

实验地点: 实验中心D503

实验时间: 2024/6/13

Section 1 实验概述

在本章中,我们首先会简单讨论保护模式下的特权级的相关内容。特权级保护是保护模式的特点之一,通过特权级保护,我们区分了内核态和用户态,从而限制用户态的代码对特权指令的使用或对资源的访问等。但是,用户态的代码有时不得不使用一些特权指令,如输入输出等。因此,我们介绍了系统调用的概念和如何通过中断来实现系统调用。通过系统调用,我们可以实现从用户态到内核态转移,然后在内核态下执行特权指令等,执行完成后返回到用户态。在实现了系统调用后,我们通过三步来创建了进程。这里,我们需要重点理解我们是如何通过分页机制来实现进程之间的虚拟地址空间的隔离。最后,我们介绍了fork/wait/exit的一种简洁的实现思路。

Section 2 预备知识与实验环境

Section 3 实验任务

实验任务1:

  1. 请复现“第一个进程”一节,并回答以下问题。
    • 请解释我们为什么需要使用寄存器来传递系统调用的参数,以及我们是如何在执行int 0x80前在栈中找到参数并放入寄存器的。
    • 请使用gdb来分析在我们调用了int 0x80后,系统的栈发生了什么样的变化?esp的值和在setup.cpp中定义的变量tss有什么关系?此外还有哪些段寄存器发生了变化?变化后的内容是什么?
    • 请使用gdb来分析在进入asm_system_call_handler的那一刻,栈顶的地址是什么?栈中存放的内容是什么?为什么会存放的是这些内容?
    • 请结合代码分析asm_system_call_handler是如何找到中断向量号index对应的函数的。
    • 请使用gdb来分析在asm_system_call_handler中执行iret后,哪些段寄存器发生了变化?变化后的内容是什么?这些内容来自于什么地方?

实验任务2:

​ 见下面描述

实验任务3:

​ 见下面的描述

实验任务4:

​ 见下面的描述

Section 4 实验步骤与实验结果

————————- 实验任务1————————-

任务要求:

​ 复现指导书中“第一个进程”一节,并回答以下问题。

  1. 请解释为什么需要使用寄存器来传递系统调用的参数,以及我们是如何在执行 0x80中断前在栈中找到参数并放入寄存器的。
  2. 请使用gdb来分析在我们调用了 int 0x80 int 0x80 后,系统的栈发生了怎样的变化? esp 的值和在 setup.cpp 中jo定义的变量 容是什么?
  3. 请使用gdb来分析在进入 tss 有什么关系?此外还有哪些段寄存器发生了变化?变化后的内 asm_system_call_handler 的那一刻,栈顶的地址是什么? 栈中 存放的内容是什么? 为什么存放的是这些内容?
  4. 请结合代码分析 asm_system_call_handler 是如何找到中断向量号 的。
  5. 请使用gdb来分析在 index 对应的函数 asm_system_call handler 中执行 iret 后,哪些段寄存器发生了变化? 变化后的内容是什么? 这些内容来自于什么地方?

思路分析:

  1. 解释为什么需要使用寄存器来传递系统调用的参数,以及我们是如何在执行 0x80前在栈中找到参数并放入寄存器的。

    答案:如果使用栈来传参,由于CPU在特权级切换的时候会切换特权级对应的栈,那么用户程序(当前进程)的栈就不会被使用,参数就不会被正确传入。我们在中断前,将栈中的参数保存到寄存器中,可以参看下属代码的注释。c语言在32位下传参是通过栈传参的,顺序是从右到左,然我我们需要先保护现场,将当前的寄存器存储到esp中,我们先用ebp获取之前esp的位置,通过简单的计算就可以找到函数参数了(思考,为什么需要保护的是这些寄存器?答案:现场保护分为调用者保存和被调用者保存,对于这些通用寄存器程序员自己可以确定是否要保存)

    push ebp                 ; 保存当前的基址指针寄存器值
    mov ebp, esp             ; 将栈指针保存到基址指针寄存器,建立新的栈帧
       
    push ebx                 ; 保存ebx寄存器的值
    push ecx                 ; 保存ecx寄存器的值
    push edx                 ; 保存edx寄存器的值
    push esi                 ; 保存esi寄存器的值
    push edi                 ; 保存edi寄存器的值
    push ds                  ; 保存ds段寄存器的值
    push es                  ; 保存es段寄存器的值
    push fs                  ; 保存fs段寄存器的值
    push gs                  ; 保存gs段寄存器的值
       
    mov eax, [ebp + 2 * 4]   ; 将函数的第一个参数加载到eax寄存器,因为之前压入了ebp,所有要加1,这其实是中断函数的Index
    mov ebx, [ebp + 3 * 4]   ; 将函数的第二个参数加载到ebx寄存器
    mov ecx, [ebp + 4 * 4]   ; 将函数的第三个参数加载到ecx寄存器
    mov edx, [ebp + 5 * 4]   ; 将函数的第四个参数加载到edx寄存器
    mov esi, [ebp + 6 * 4]   ; 将函数的第五个参数加载到esi寄存器
    mov edi, [ebp + 7 * 4]   ; 将函数的第六个参数加载到edi寄存器
       
    int 0x80                 ; 触发0x80中断,进行系统调用
    ......					 ; 还原之前的压栈
    ret                      ; 返回调用者,恢复调用前的状态
       
    
    1. 使用gdb来分析在我们调用了 int 0x80 后,系统的栈发生了怎样的变化? esp 的值和在 setup.cpp 中定义的变量 tss有什么关系,此外还有那些寄存器发生了哪些变化?变化后的内容是什么?

      以下截图是在函数中断前的寄存器情况

      image-20240616140533897

image-20240616140610719

image-20240616140654071

在使用80中断后,寄存器如图

image-20240616140800064

image-20240616140821017

image-20240616140839773

查看TSS的相关数据

image-20240616141852183

根据上述实验和测试指导,中断之后栈指针发生了跳转,从进程3特权级栈指针切换到了内核的0级指针,查阅资料指导在函数调用中断发生特权切换的时候五个寄存器会被自动保存

0
1
2
特权级发生改变(例如,从用户态到内核态的中断):

处理器将EFLAGS、CS、EIP、SS和ESP压入内核栈中(在这个情况下,当前栈是用户栈,切换到内核态时,会使用内核栈)

再加上调试的时候已经保存的段寄存器ds,就可以说明tss.esp0比当前内核栈高了24位(6个寄存器);

除此之外,变化的寄存器还有EFLAGS、CS、EIP、SS这都是在内核栈中被保存的寄存器,是为了在函数调用结束之后,可以恢复到之前的现场环境。变化后的会设置为中断处理程序的入口地址和处理器的当前状态。包括CS的最后两位是0,说明当前的优先级是0

  1. 请使用gdb来分析在进入asm_system_call_handler的那一刻,栈顶的地址是什么?栈中存放的内容是什么?为什么会存放的是这些内容?

    如2中已经顺便分析过,刚进入中断处理函数的栈顶是保存的用户寄存器组,用于回复现场,下面具体看看其中的内容;

    0
    
    x/6 0xc0025688  #这就是进入中断处理后的栈顶,是低地址,使用该指令查看其上6个四字节单位的数据
    

    image-20240616144149237

​ 可以发现这六个单元分别对应用户态的 ds, eip,cs,eflags, esp, ss,正好可以说明中断跳转的时候会自动保存该后五个寄存器,其他寄存器是程序员主动保护,保护这些特别寄存器是为了中断结束后回复现场。

  1. 请结合代码分析 asm_system_call_handler 是如何找到中断向量号 的。

    asm_system_call_handler:
        push ds ; 保护段寄存器,这是用户态的内容
        push es
        push fs
        push gs
        pushad ;保护所有通用寄存器
       
        push eax ;额外保护eax,便于之后获取函数处理的index
       
        ; 栈段会从tss中自动加载
       
        mov eax, DATA_SELECTOR; 使用内核他的段选择子
        mov ds, eax
        mov es, eax
       
        mov eax, VIDEO_SELECTOR
        mov gs, eax
       
        pop eax ; 获得函数Index
       
        ; 参数压栈
        push edi;通过栈来传参
        push esi
        push edx
        push ecx
        push ebx
       
        sti    ;开启中断,允许更高优先级的中断
        call dword[system_call_table + eax * 4] ;根据Index,在中断表中找到对应函数
        cli
       
        add esp, 5 * 4 ;恢复栈
           
        mov [ASM_TEMP], eax
        popad
        pop gs
        pop fs
        pop es
        pop ds
        mov eax, [ASM_TEMP]
    
  2. 请使用gdb来分析在 index 对应的函数 asm_system_call handler 中执行 iret 后,哪些段寄存器发生了变化? 变化后的内容是什么? 这些内容来自于什么地方?

    这个问题基本是考察现场恢复了image-20240616145720507

image-20240616145739426

iret之后:

image-20240616145817363

image-20240616145915501

改变的段寄存器:CS, SS

变换后的内容是用户态的内容,可以看到CS的后两位是3,对应特权级是3

改变来自于中断结束,Iret自动执行的将之前进入是保存在0特权级栈的5个寄存器恢复。

————————- 实验任务2————————-

任务要求:image-20240616150552042

image-20240616150624243

思路分析:

  1. 进程创建的3个步骤:
  • 创建进程的PCB

    0
    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
    
    int ProgramManager::executeThread(ThreadFunction function, void *parameter, const char *name, int priority)
    {
        // 关中断,防止创建线程的过程被打断
        bool status = interruptManager.getInterruptStatus();
        interruptManager.disableInterrupt();
      
        // 分配一页作为PCB
        PCB *thread = allocatePCB();
      
        if (!thread)
            return -1;
      
        // 初始化分配的页
        memset(thread, 0, PCB_SIZE);
      
        for (int i = 0; i < MAX_PROGRAM_NAME && name[i]; ++i)
        {
            thread->name[i] = name[i];
        }
      
        thread->status = ProgramStatus::READY;
        thread->priority = priority;
        thread->ticks = priority * 10;
        thread->ticksPassedBy = 0;
        thread->pid = ((int)thread - (int)PCB_SET) / PCB_SIZE;
      
        // 线程栈
        thread->stack = (int *)((int)thread + PCB_SIZE - sizeof(ProcessStartStack));
        thread->stack -= 7;
        thread->stack[0] = 0;
        thread->stack[1] = 0;
        thread->stack[2] = 0;
        thread->stack[3] = 0;
        thread->stack[4] = (int)function;
        thread->stack[5] = (int)program_exit;
        thread->stack[6] = (int)parameter;
      
        allPrograms.push_back(&(thread->tagInAllList));
        readyPrograms.push_back(&(thread->tagInGeneralList));
      
        // 恢复中断
        interruptManager.setInterruptStatus(status);
      
        return thread->pid;
    }
    
    0
    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
    
    //
    int ProgramManager::executeProcess(const char *filename, int priority)
    {
        bool status = interruptManager.getInterruptStatus();
        interruptManager.disableInterrupt();
      
        // 在线程创建的基础上初步创建进程的PCB
        int pid = executeThread((ThreadFunction)load_process,
                                (void *)filename, filename, priority);
        if (pid == -1)
        {
            interruptManager.setInterruptStatus(status);
            return -1;
        }
      
        // 找到刚刚创建的PCB
        PCB *process = ListItem2PCB(allPrograms.back() tagInAllList);
      
        // 创建进程的页目录表
        process->pageDirectoryAddress = createProcessPageDirectory();
        if (!process->pageDirectoryAddress)
        {
            process->status = ThreadStatus::DEAD;
            interruptManager.setInterruptStatus(status);
            return -1;
        }
      
        // 创建进程的虚拟地址池
        bool res = createUserVirtualPool(process);
      
        if (!res)
        {
            process->status = ThreadStatus::DEAD;
            interruptManager.setInterruptStatus(status);
            return -1;
        }
      
        interruptManager.setInterruptStatus(status);
      
        return pid;
    }
    
  • 初始化进程的页目录表

  • 0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    int ProgramManager::createProcessPageDirectory()
    {
        // 从内核地址池中分配一页存储用户进程的页目录表
        int vaddr = memoryManager.allocatePages(AddressPoolType::KERNEL, 1);
        if (!vaddr)
        {
            //printf("can not create page from kernel\n");
            return 0;
        }
      
        memset((char *)vaddr, 0, PAGE_SIZE);
      
        // 复制内核目录项到虚拟地址的高1GB
        int *src = (int *)(0xfffff000 + 0x300 * 4);
        int *dst = (int *)(vaddr + 0x300 * 4);
        for (int i = 0; i < 256; ++i)
        {
            dst[i] = src[i];
        }
      
        // 用户进程页目录表的最后一项指向用户进程页目录表本身
        ((int *)vaddr)[1023] = memoryManager.vaddr2paddr(vaddr) | 0x7;
          
        return vaddr;
    }
    
  • 初始化进程的虚拟地址池
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bool ProgramManager::createUserVirtualPool(PCB *process)
{
    int sourcesCount = (0xc0000000 - USER_VADDR_START) / PAGE_SIZE;
    int bitmapLength = ceil(sourcesCount, 8);

    // 计算位图所占的页数
    int pagesCount = ceil(bitmapLength, PAGE_SIZE);

    int start = memoryManager.allocatePages(AddressPoolType::KERNEL, pagesCount);

    if (!start)
    {
        return false;
    }

    memset((char *)start, 0, PAGE_SIZE * pagesCount);
    (process->userVirtual).initialize((char *)start, bitmapLength, USER_VADDR_START);

    return true;
}
  1. 为什么会跳转到load_process?

    首先我们在创建进程的时候使用了函数套用,在创建进程的函数中传入的不是真正的函数,而是(load_process, 和真正函数的名字(相当于地址))

    0
    1
    
        int pid = executeThread((ThreadFunction)load_process,
                                (void *)filename, filename, priority);
    

    image-20240617135122612

​ 在ret完成线程切换之后,就跳转到load_process

image-20240617135220055

  1. 在load_process中怎么利用processstack和iret等实现从特权级0到特权级3的跳转的?

    我们首先在PCS模块中新增加了进程用户态的寄存器存储空间processstack,这个模块会在进程创建的时候被初始化

    • 0
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
      // 获取当前正在运行的进程
      PCB *process = programManager.running;
      // 计算进程启动堆栈的位置
      ProcessStartStack *interruptStack = (ProcessStartStack *)((int)process + PAGE_SIZE - sizeof(ProcessStartStack));
           
      // 初始化通用寄存器,全部设为0
      ...
      // 设置段选择子为用户数据段选择子
      interruptStack->fs = programManager.USER_DATA_SELECTOR;
      interruptStack->es = programManager.USER_DATA_SELECTOR;
      interruptStack->ds = programManager.USER_DATA_SELECTOR;
           
      

    关键是有关于iret的5个寄存器, 可以看到

    1. eip是跳转的地址

    2. cs是跳转的用户进程的代码段

    3. esp要先分配一些内存,因为iret会自动保存5个寄存器在目标栈

    4. ss是用户段选择

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    // 设置EIP为传入的文件名指针(实际上应该是入口地址),这个寄存器是iret的返回地址
    interruptStack->eip = (int)filename;
    // 设置代码段选择子为用户代码段选择子
    interruptStack->cs = programManager.USER_CODE_SELECTOR;
    // 设置EFLAGS寄存器:IOPL=0(特权级为0),IF=1(使能中断),MBS=1(默认必须为1)
    interruptStack->eflags = (0 << 12) | (1 << 9) | (1 << 1);
       
    // 为用户栈分配一页内存
    interruptStack->esp = memoryManager.allocatePages(AddressPoolType::USER, 1);
    if (interruptStack->esp == 0)
    {
        // 如果分配失败,输出错误信息并将进程状态置为死亡,然后停机
        printf("can not build process!\n");
        process->status = ThreadStatus::DEAD;
        asm_halt();
    }
    // 栈顶设置为分配页的末尾
    interruptStack->esp += PAGE_SIZE;
    // 设置栈段选择子为用户栈段选择子
    interruptStack->ss = programManager.USER_STACK_SELECTOR;
    asm_start_process((int)interruptStack);
    

    然后利用asm_start_process跳转到真正的进程,此时esp的地址是返回地址,参数的地址是esp+4,也就是processstack的地址,结合processSTack的结构,弹出通用寄存器,段寄存器之后就获得5个关键的寄存器,此时就可以使用iret正确跳转(查询资料知道,iret弹出次序:eip,cs,eflags,esp,ss。)。

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    struct ProcessStartStack
    {
        int edi;
        int esi;
        int ebp;
        int esp_dummy;
        int ebx;
        int edx;
        int ecx;
        int eax;
           
        int gs;
        int fs;
        int es;
        int ds;
       
        int eip;
        int cs;
        int eflags;
        int esp;
        int ss;
    };
       
    
    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    asm_start_process:
        ;jmp $
        mov eax, dword[esp+4]; 获得参数地址
        mov esp, eax
        popad
        pop gs;
        pop fs;
        pop es;
        pop ds;
       
        iret
    

    gdb调试:

    image-20240617210121579

跳转之后,可以看到对应寄存器是一样的

image-20240617142415805

image-20240617142430812

  1. 在schedule中增加了什么改动?

    增加了一个函数

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    void ProgramManager::activateProgramPage(PCB *program)
    {
        int paddr = PAGE_DIRECTORY;
       
        if (program->pageDirectoryAddress)
        {
            tss.esp0 = (int)program + PAGE_SIZE;  // 设置 TSS 的 esp0 为该进程的内核栈顶
            paddr = memoryManager.vaddr2paddr(program->pageDirectoryAddress);  // 获取该进程页目录的物理地址
        }
       
        asm_update_cr3(paddr);  // 更新 CR3 寄存器以切换页表
    }
       
    

    由于内核栈指针是保存在进程的PCB中的,在进程切换的时候,需要将下一个进程的内核栈指针获取,然后更新页目录表

  2. 可以使用简单的遍历搜索来找到正确的PCB, 考虑到局部性原理从后往前搜索是比较高效的。

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    PCB* ProgramManager::findProgramByPid(int pid)
    {
        int back = allPrograms.size() -1;
        printf("size of the list is %d\n", back);
        for(int i=back; i>=0;--i){
            printf("checking index %d\n", i);
            if (ListItem2PCB(allPrograms.at(i), tagInAllList)->pid == pid){
                printf("index %d is what you need!\n", i);
                return ListItem2PCB(allPrograms.at(i), tagInAllList);
            }
        }
        return nullptr;
    }
    

image-20240617220315168

————————- 实验任务3————————-

任务要求:

复现“fork的实现”一节,并回答如下问题。

  • 请根据代码逻辑概括fork的实现的基本思路,并简要分析我们是如何解决”四个关键问题”的。
  • 请根据gdb来分析子进程第一次被调度执行时,即在asm_switch_thread切换到子进程的栈中时,esp的地址是什么?栈中保存的内容有什么?
  • 从子进程第一次被调度执行时开始,逐步跟踪子进程的执行流程一直到子进程从fork返回,根据gdb来分析子进程的跳转地址、数据寄存器和段寄存器的变化。同时,比较上述过程和父进程执行完ProgramManager::fork后的返回过程的异同。
  • 请根据代码逻辑和gdb来解释子进程的fork返回值为什么是0,而父进程的fork返回值是子进程的pid。
  • 请解释在ProgramManager::schedule中,我们是如何从一个进程的虚拟地址空间切换到另外一个进程的虚拟地址空间的

思路分析:

  • 请根据代码逻辑概括fork的实现的基本思路,并简要分析我们是如何解决”四个关键问题”的:

    四个关键问题:

    1. 如何实现父子进程的代码段共享?
    2. 如何使得父子进程从相同的返回点开始执行?
    3. 除代码段外,进程包含的资源有哪些?
    4. 如何实现进程的资源在进程之间的复制?

    基本思路:

    1. 首先需要创建一个新的进程

    2. 然后需要将资源(包括代码段,页目录表,地址池等,相当于复制PCB)复制到子进程,这可以被进一步拆分为:首先需要复制父进程的0特权级栈,然后顺便修改返回值(eax);然后初始化子进程的0特权级栈;接着复制PCB和管理地址池的bitmap;然后复制页目录表;最后复制页表和页表项;最后切换回父进程的虚拟地址空间,返回pid;

      解决关键问题:

      1. 使子进程和父进程的代码段在虚拟地址转换为物理地址之后指向同一片内存,由于函数的代码本身是放在内核中的,然后进程又划分了3GB-4GB的空间来实现内核共享,所以此时代码段已经实现了共享。

      2. ProgramStartProcess中保存了父进程的eipeip的内容也是asm_system_call_handler的返回地址。我们会通过asm_start_process来启动子进程。此时,asm_start_process的最后的iret会将上面说到的保存在0特权级栈的eip的内容送入到eip中。执行完eip后,子进程便可以从父进程的返回点处开始执行,即asm_system_call_handler的返回地址。然后子进程依次返回到syscall_forkasm_system_call_handler,最终从fork返回。由于我们后面会复制父进程的3特权级栈到子进程的3特权级栈中,而3特权级栈保存了父进程在执行int 0x80后的逐步返回的返回地址。因此,父子进程的逐步返回的地址是相同的,从而实现了在执行fork后,父子进程从相同的点返回。

      3. 进程包含的资源有0特权级栈,PCB、虚拟地址池、页目录表、页表及其指向的物理页。

      4. 对于储在内核空间的(进程包含的资源有0特权级栈,PCB、虚拟地址池、页目录表)直接进行复制就可以;但是对于进程分页机制下面虚拟空间的页表和页表项,则需要利用中转页,首先进入父进程的虚拟空间将该页复制到中转页,然后进入子进程的虚拟空间将中转页的数据对应的复制到子进程的物理页上。


  • 请根据gdb来分析子进程第一次被调度执行时,即在asm_switch_thread切换到子进程的栈中时,esp的地址是什么?栈中保存的内容有什么? alt text) 首先查看esp寄存器存储的地址,然后打印这地址高7个32位的内容,此时栈中的内容是子进程的0特权级栈,此时栈顶是ret指令的跳转地址,通过地址查阅发现是asm_start_process函数

    0
    1
    2
    3
    4
    5
    6
    7
    
    child->stack = (int *)childpss - 7;
      child->stack[0] = 0;
      child->stack[1] = 0;
      child->stack[2] = 0;
      child->stack[3] = 0;
      child->stack[4] = (int)asm_start_process;
      child->stack[5] = 0;             // asm_start_process 返回地址
      child->stack[6] = (int)childpss; // asm_start_process 参数
    

    通过查阅之前的函数已经比对栈的内容,发现另外一个非0参数是子进程的地址。

  • 从子进程第一次被调度执行时开始,逐步跟踪子进程的执行流程一直到子进程从fork返回,根据gdb来分析子进程的跳转地址、数据寄存器和段寄存器的变化。同时,比较上述过程和父进程执行完ProgramManager::fork后的返回过程的异同。

    子进程第一次被调度执行,由于跳转的eip内容和父进程是一致的所以直接从条件判断开始,此时的寄存器环境为:alt textalt text; 而对于父进程,在fork返回之后,同样来到了条件判断语句,此时的寄存器环境为:alt textalt text

    可以看到除了控制fork返回值,也就是Pid的eax寄存器不同之外,所有的寄存器都是一样的。

  • 请根据代码逻辑和gdb来解释子进程的fork返回值为什么是0,而父进程的fork返回值是子进程的pid。 这需要分析fork函数以及其调用的子函数copyprocess,父进程在函数执行上比子进程多了调用系统调用fork的代码段,而fork会返回子进程的pid; 而子进程的返回地址最终是fork的返回,我们又在复制资源的时候将0特权级栈,也就是内核栈的返回内容eax改为了0(参见下面的代码),所以pid会被赋值为0.

    0
    1
    2
    3
    4
    5
    6
    7
    
        // 复制进程0级栈
      ProcessStartStack *childpss =
          (ProcessStartStack *)((int)child + PAGE_SIZE - sizeof(ProcessStartStack));
      ProcessStartStack *parentpss =
          (ProcessStartStack *)((int)parent + PAGE_SIZE - sizeof(ProcessStartStack));
      memcpy(parentpss, childpss, sizeof(ProcessStartStack));
      // 设置子进程的返回值为0
      childpps->eax = 0;
    

    gdb调试可以看到在进入复制进程函数的时候,父进程已经确定了返回值是eax是2 alt text 然后我们将子进程复制为父进程,查看复制后的内容和修改eax后的内容:alt textalt text

    只有$eax变成了0,这说明正是手动修改eax寄存器导致返回值出现0的情况

  • 请解释在ProgramManager::schedule中,我们是如何从一个进程的虚拟地址空间切换到另外一个进程的虚拟地址空间的 虚拟空间切换主要涉及页目录表的切换,在schedule中主要是通过activateProgramPage 和asm_update_cr3来实现的:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    void ProgramManager::activateProgramPage(PCB *program) {
      int paddr = PAGE_DIRECTORY;//获得页目录表地址
      
      if (program->pageDirectoryAddress) {
          tss.esp0 = (int)program + PAGE_SIZE;
          paddr = memoryManager.vaddr2paddr(program->pageDirectoryAddress);//转换为物理地址
      }
      
      asm_update_cr3(paddr); // 写入到硬件中,供cpu查询
    }
      
    asm_update_cr3:
      push eax
      mov eax, dword[esp+8]
      mov cr3, eax
      pop eax
      ret
    

其实主要是asm_update_Cr3中我们把下一个进程的页目录地址传入了Cr3寄存器,而这个寄存器内的地址是cpu分页机制查询的地址。

————————- 实验任务4 ————————-

任务要求:

复现“exit的实现”一节,并回答如下问题。

  1. 请结合代码逻辑和具体的实例来分析exit的执行过程。
  2. 请解释进程退出后自动调用exit的原因。(tips:从栈的角度来分析。)
  3. 请解释进程在exit的最后调用了schedule后,这个进程不会再被调度执行的原因。

思路分析:

  1. 请结合代码逻辑和具体的实例来分析exit的执行过程。 exit的实际过程:
    1. 标记PCB状态为DEAD并放入返回值。
    2. 如果PCB标识的是进程,则释放进程所占用的物理页、页表、页目录表和虚拟地址池bitmap的空间。否则不做处理。
    3. 立即执行线程/进程调度。

实例:使用gdb调试来分析:首先进入exit函数,在其中调用系统调用函数asm_system_call(3, ret),这里 ret= 0; 然后在系统调用中通过查询函数号找到progranmanager:exit函数,参数是返回值ret=0; 获取当前运行的进程的PCB地址(PCB *) 还未修改的PCB: alt text 在宣告进程死亡和标记返回值之后: alt text 然后在虚拟释放地址池之后的PCB alt text

  1. 修改progranmanager:exit,在进入函数的时候打印“start to exit”,运行;alt text

    0
    1
    2
    
    void ProgramManager::exit(int ret)
    {   
        printf("start to exit");
    

    发现第一个进程在退出的时候成功进入到了progranmanager:exit函数,这是因为我们在load_process的时候将3特权级栈也就是用户栈的栈顶修改为exit函数,这样在进程退出的时候,函数会被加载到eip寄存器,跳转到exit函数执行。

  2. 在exit的开头,进程的状态被设置为死亡态,而调度函数只会在就绪队列中寻找下一个进程,而且可能存在的回收函数,会定时回收死亡态的僵尸进程, 所以死亡的进程不会被再次调度上cpu.

Creative Commons License本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.