操作系統實驗ucore lab5

閱讀前注意事項:

1、我的博客從lab2之後,如果沒有特殊說明,所有標註的代碼行數位置,以labcodes_answer(答案包)裏的文件爲準!!!因爲你以後會發現做實驗用meld軟件比較費時費力,對於咱們學校的驗收不如直接對着答案來;

2、感謝網上的各路前輩大佬們,本人在這學期初次完成實驗的過程中,各位前輩們的博客給了我很多有用的指導;本人的博客內容在現有的內容上,做了不少細節的增補內容,有些地方屬個人理解,如果有錯在所難免,還請各位大佬們批評指正;

3、所有實驗的思考題,我把它規整到了文章最後;

4、所有實驗均默認不做challenge,對實驗評分無影響;

5、湖南大學的實驗順序爲1 4 5 6 7 2 3 8,在實驗4-7過程中涉及到實驗二三的頁表虛存問題,當做黑盒處理,沒有過多探索。

 

一、實驗內容

實驗4完成了內核線程,但到目前爲止,所有的運行都在內核態執行。實驗5將創建用戶進程,讓用戶進程在用戶態執行,且在需要ucore支持時,可通過系統調用來讓ucore提供服務。爲此需要構造出第一個用戶進程,並通過系統調用sys_fork/sys_exec/sys_exit/sys_wait來支持運行不同的應用程序,完成對用戶進程的執行過程的基本管理。相關原理介紹可看附錄B。

 

二、目的

瞭解第一個用戶進程創建過程

瞭解系統調用框架的實現機制

瞭解ucore如何實現系統調用sys_fork/sys_exec/sys_exit/sys_wait來進行進程管理

 

三、實驗設計思想和流程

 

練習0:填寫已有實驗

 

本實驗依賴實驗1/2/3/4。請把你做的實驗1/2/3/4的代碼填入本實驗中代碼中有“LAB1”/“LAB2”/“LAB3”/“LAB4”的註釋相應部分。注意:爲了能夠正確執行lab5的測試應用程序,可能需對已完成的實驗1/2/3/4的代碼進行進一步改進。

 

使用meld工具,比對得出需要改動的代碼文件爲:

 

proc.c

default_pmm.c

pmm.c

swap_fifo.c

vmm.c

trap.c

 

需要改進的代碼部分如下:

 

1、實驗4的alloc_proc函數需要增加兩行(kern/process/proc.c,118——119行)

proc->wait_state = 0;
proc->cptr = proc->optr = proc->yptr = NULL;

原因:觀察到實驗四中的TCB部分(kern/process/proc.h)中對於內核線程的結構體聲明多出了兩個部分:

uint32_t wait_state;			                // waiting state
struct proc_struct *cptr, *yptr, *optr;			// relations between processes

新加入的兩個屬性,第一個是進程等待狀態,第二個是進程之間的相關指針初始化,在實驗五中,涉及到了用戶進程,自然需要涉及到調度的問題,所以進程等待狀態和各種指針需要被初始化。

 

2、實驗4的do_fork函數需要增加兩行(kern/process/proc.c,411行、426行)

 

經過更改的函數如下:(實驗4實現部分,對應kern/process/proc.c第406——431行)

    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
    proc->parent = current;
    assert(current->wait_state == 0);//確保當前進程正在等待
    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
    if (copy_mm(clone_flags, proc) != 0) {
        goto bad_fork_cleanup_kstack;
    }
    copy_thread(proc, stack, tf);
    bool intr_flag;
    local_intr_save(intr_flag);
    {
        proc->pid = get_pid();
        hash_proc(proc);
        set_links(proc);
    }
    local_intr_restore(intr_flag);
    wakeup_proc(proc);

其中,更改的部分語句一共有二,如上所示。

 

第一句:assert(current->wait_state == 0);

對應第一步中對於proc->wait_state屬性的初始分配,需要確保當前進程正在等待。

 

第二句:set_links(proc); 這裏調用了一個set link函數來設置進程之間的連接。

 

觀察set links函數代碼(kern/process/proc.c,140——149行)

static void set_links(struct proc_struct *proc) {
    list_add(&proc_list, &(proc->list_link));         //進程加入進程列表
    proc->yptr = NULL;                                //當前進程的younger sibling爲空
    if ((proc->optr = proc->parent->cptr) != NULL) {
        proc->optr->yptr = proc;
    }		////當前進程的older sibling爲當前進程
    proc->parent->cptr = proc;                            //父進程的子進程爲當前進程
    nr_process ++;                                        //當前進程數量加一
}

set_links函數的作用就是設置當前進程的process relations。同樣,它進行一些插入進程、調度、更改“當前進程數量”等對於共享數據的訪問操作,因此它還在實驗4中定義的互斥鎖中。

 

3、實驗1中的idt_init函數(kern/trap/trap.c,59——66行)

    extern uintptr_t __vectors[];
    int i;
    for (i = 0; i < sizeof(idt) / sizeof(struct gatedesc); i ++) {
        SETGATE(idt[i], 0, GD_KTEXT, __vectors[i], DPL_KERNEL);//內核態優先級爲0
    }
SETGATE(idt[T_SYSCALL], 1, GD_KTEXT, __vectors[T_SYSCALL], DPL_USER);
//內核態優先級爲3
lidt(&idt_pd);

需要加上一行,主要是設置相應的中斷門。

 

4、實驗1中的trap_dispatch函數(kern/trap/trap.c,233——238行)

ticks ++;
  	if (ticks % TICK_NUM == 0) {
		assert(current != NULL);
		current->need_resched = 1;
	}
	break;

主要是將時間片設置爲需要調度,說明當前進程的時間片已經用完了。

 

練習1:加載應用程序並運行

 

do_execv函數調用load_icode(位於kern/process/proc.c中)來加載並解析一個處於內存中的ELF執行文件格式的應用程序,建立相應的用戶內存空間來放置應用程序的代碼段、數據段等,且要設置好proc_struct結構中的成員變量trapframe中的內容,確保在執行此進程後,能夠從應用程序設定的起始執行地址開始執行。需設置正確的trapframe內容。

 

load_icode函數的主要工作就是給用戶進程建立一個能夠讓用戶進程正常運行的用戶環境。此函數有一百多行,完成了如下重要工作:

 

1. 調用mm_create函數來申請進程的內存管理數據結構mm所需內存空間,並對mm進行初始化;

2. 調用setup_pgdir來申請一個頁目錄表所需的一個頁大小的內存空間,並把描述ucore內核虛空間映射的內核頁表(boot_pgdir所指)的內容拷貝到此新目錄表中,最後mm->pgdir指向此頁目錄表,這就是進程新的頁目錄表了,且能夠正確映射內核。

3. 根據應用程序執行碼的起始位置來解析此ELF格式的執行程序,並調用mm_map函數根據ELF格式的執行程序說明的各個段(代碼段、數據段、BSS段等)的起始位置和大小建立對應的vma結構,並把vma插入到mm結構中,從而表明了用戶進程的合法用戶態虛擬地址空間。

4. 調用根據執行程序各個段的大小分配物理內存空間,並根據執行程序各個段的起始位置確定虛擬地址,並在頁表中建立好物理地址和虛擬地址的映射關係,然後把執行程序各個段的內容拷貝到相應的內核虛擬地址中,至此應用程序執行碼和數據已經根據編譯時設定地址放置到虛擬內存中了;

5. 需要給用戶進程設置用戶棧,爲此調用mm_mmap函數建立用戶棧的vma結構,明確用戶棧的位置在用戶虛空間的頂端,大小爲256個頁,即1MB,並分配一定數量的物理內存且建立好棧的虛地址<-->物理地址映射關係;

6. 至此,進程內的內存管理vma和mm數據結構已經建立完成,於是把mm->pgdir賦值到cr3寄存器中,即更新了用戶進程的虛擬內存空間,此時的initproc已經被hello的代碼和數據覆蓋,成爲了第一個用戶進程,但此時這個用戶進程的執行現場還沒建立好;

7. 先清空進程的中斷幀,再重新設置進程的中斷幀,使得在執行中斷返回指令“iret”後,能夠讓CPU轉到用戶態特權級,並回到用戶態內存空間,使用用戶態的代碼段、數據段和堆棧,且能夠跳轉到用戶進程的第一條指令執行,並確保在用戶態能夠響應中斷;

 

首先是do_exceve函數,它調用了load_icode去加載ELF二進制格式文件到內存並執行:(kern/process/proc.c,653——685行)

 

do_execve函數主要做的工作就是先回收自身所佔用戶空間,然後調用load_icode,用新的程序覆蓋內存空間,形成一個執行新程序的新進程。

int do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
    struct mm_struct *mm = current->mm;
    if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
        return -E_INVAL;
    }
    if (len > PROC_NAME_LEN) {
        len = PROC_NAME_LEN;
    }

    char local_name[PROC_NAME_LEN + 1];
    memset(local_name, 0, sizeof(local_name));
    memcpy(local_name, name, len);
//第一步:清空空間
    if (mm != NULL) {
        lcr3(boot_cr3);//轉入內核態
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);		//清空內存管理部分和對應頁表
            put_pgdir(mm);//清空頁表
            mm_destroy(mm);//清空內存
        }
        current->mm = NULL;		//最後讓它當前的頁表指向空,方便放入自己的東西
    }
int ret;
//第二步:向清空的內存中填充新的內容,調用load_icode函數
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

 

以下是load_icode函數的實現和註釋:

static int load_icode(unsigned char *binary, size_t size) {
    if (current->mm != NULL) {
        panic("load_icode: current->mm must be empty.\n");
    }

//準備部分:當前進程必須爲空,這樣才能加載到內存。
(在調用它的do_exceve函數中,如果沒有bug,那麼已經清空了)
    int ret = -E_NO_MEM;
struct mm_struct *mm;		//聲明瞭一個頁表

//第1步:爲當前的進程創建一塊內存

    if ((mm = mm_create()) == NULL) {//申請內存
        goto bad_mm;
}

這裏調用了一個函數mm_create(kern/mm/vmm.c,43——60行):

struct mm_struct * mm_create(void) {
    struct mm_struct *mm = kmalloc(sizeof(struct mm_struct));

    if (mm != NULL) {
        list_init(&(mm->mmap_list));
        mm->mmap_cache = NULL;
        mm->pgdir = NULL;
        mm->map_count = 0;

        if (swap_init_ok) swap_init_mm(mm);
        else mm->sm_priv = NULL;
        
        set_mm_count(mm, 0);
        lock_init(&(mm->mm_lock));
    }    
    return mm;
}
可以看到這個函數首先申請了一塊內存空間,如果內存空間申請成功了,那麼就會把這個內存空間返回給外面調用它的mm變量,如果申請失敗,那麼新開闢的空間都不存在,即爲NULL,且會返回它自己,因此外部的判斷條件是mm不能等於NULL,如果等於NULL,說明創建空間失敗了,否則,就能夠說明創建成功。

//(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
第2步:調用 setup_pgdir來申請一個頁目錄表所需的一個頁大小的內存空間
    
if (setup_pgdir(mm) != 0) {//申請頁表
        goto bad_pgdir_cleanup_mm;
}

這裏調用了一個函數setup_pgdir(kern/process/proc.c,288——299行)

static int setup_pgdir(struct mm_struct *mm) {
    struct Page *page;
    if ((page = alloc_page()) == NULL) {
        return -E_NO_MEM;
    }
    pde_t *pgdir = page2kva(page);
    memcpy(pgdir, boot_pgdir, PGSIZE);
    pgdir[PDX(VPT)] = PADDR(pgdir) | PTE_P | PTE_W;
    mm->pgdir = pgdir;
    return 0;
}
如果沒有返回0,那麼分配頁目錄表失敗,因此程序需要判斷爲0的情況,到一個錯誤的狀態。

//第3步:讀取ELF格式的文件,在內存中複製該進程所需要的代碼段等信息
    //(3) copy TEXT/DATA section, build BSS parts in binary to memory space of process
    struct Page *page;			//申請一個頁
    //(3.1) get the file header of the bianry program (ELF format)
struct elfhdr *elf = (struct elfhdr *)binary;		//獲取ELF格式文件的表頭

在bootloader啓動的過程中,已經將ucore內核和用戶代碼全部加載到內存,因爲沒有文件管理系統,我們只需要關注這個代碼在內存中的哪裏,找到了開頭就能根據它找到數據段。

    //(3.2) get the entry of the program section headers of the bianry program (ELF format)
struct proghdr *ph = (struct proghdr *)(binary + elf->e_phoff);
//(3.3) This program is valid?
    if (elf->e_magic != ELF_MAGIC) {		//這個ELF文件的格式是否是合法的?
        ret = -E_INVAL_ELF;				//返回一個ELF文件非法操作
        goto bad_elf_cleanup_pgdir;
    }

    uint32_t vm_flags, perm;
    struct proghdr *ph_end = ph + elf->e_phnum;
    for (; ph < ph_end; ph ++) {
    //(3.4) find every program section headers
        if (ph->p_type != ELF_PT_LOAD) {
            continue ;
        }
        if (ph->p_filesz > ph->p_memsz) {
            ret = -E_INVAL_ELF;
            goto bad_cleanup_mmap;
        }
        if (ph->p_filesz == 0) {
            continue ;
        }
//這個地方獲取的是文件的各個段,包括代碼段、數據段等。

//(3.5) call mm_map fun to setup the new vma ( ph->p_va, ph->p_memsz)
根據獲取的各個段的開頭,以及虛擬地址創建VMA(管理進程所認爲的合法空間)
一開始給各個段賦予了一些屬性:

        vm_flags = 0, perm = PTE_U;
        if (ph->p_flags & ELF_PF_X) vm_flags |= VM_EXEC;		//可執行屬性(代碼段)
        if (ph->p_flags & ELF_PF_W) vm_flags |= VM_WRITE;		//可讀可寫(數據段)
        if (ph->p_flags & ELF_PF_R) vm_flags |= VM_READ;
        if (vm_flags & VM_WRITE) perm |= PTE_W;
        if ((ret = mm_map(mm, ph->p_va, ph->p_memsz, vm_flags, NULL)) != 0) {
            goto bad_cleanup_mmap;
        }

//使用mm_map函數建立合法空間(kern/mm/vmm.c,159——165行)
int mm_map(struct mm_struct *mm, uintptr_t addr, size_t len, uint32_t vm_flags,
    struct vma_struct **vma_store) {
    uintptr_t start = ROUNDDOWN(addr, PGSIZE), end = ROUNDUP(addr + len, PGSIZE);
    if (!USER_ACCESS(start, end)) {
        return -E_INVAL;
    }
//關於mm_map的解釋是:

        unsigned char *from = binary + ph->p_offset;
        size_t off, size;
        uintptr_t start = ph->p_va, end, la = ROUNDDOWN(start, PGSIZE);

        ret = -E_NO_MEM;

//(3.6) alloc memory, and  copy the contents of every program section (from, from+end) to process's memory (la, la+end)
        end = ph->p_va + ph->p_filesz;
 		//(3.6.1) copy TEXT/DATA section of bianry program
//這裏是拷貝內容,memcpy是拷貝函數
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memcpy(page2kva(page) + off, from, size);//拷貝函數
            start += size, from += size;
        }

//(3.6.2) build BSS section of binary program
//執行程序的BSS段需要清空,這裏全部設置爲0
        end = ph->p_va + ph->p_memsz;
        if (start < la) {
            /* ph->p_memsz == ph->p_filesz */
            if (start == end) {
                continue ;
            }
            off = start + PGSIZE - la, size = PGSIZE - off;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);//設置爲0
            start += size;
            assert((end < la && start == end) || (end >= la && start == la));
        }
        while (start < end) {
            if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) {
                goto bad_cleanup_mmap;
            }
            off = start - la, size = PGSIZE - off, la += PGSIZE;
            if (end < la) {
                size -= la - end;
            }
            memset(page2kva(page) + off, 0, size);
            start += size;
        }
    }
//(4) build user stack memory
//除了數據段、代碼段、進程還需要用戶堆棧空間。這裏是構造用戶堆棧。
    vm_flags = VM_READ | VM_WRITE | VM_STACK;
    if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) 	{
        goto bad_cleanup_mmap;
}
//重新建立mm_map堆棧
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-2*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-3*PGSIZE , PTE_USER) != NULL);
    assert(pgdir_alloc_page(mm->pgdir, USTACKTOP-4*PGSIZE , PTE_USER) != NULL);
    
//(5) set current process's mm, sr3, and set CR3 reg = physical addr of Page Directory
//建立好映射關係
    mm_count_inc(mm);
    current->mm = mm;
    current->cr3 = PADDR(mm->pgdir);
    lcr3(PADDR(mm->pgdir));

    //(6) setup trapframe for user environment
    struct trapframe *tf = current->tf;
    memset(tf, 0, sizeof(struct trapframe));
    /* LAB5:EXERCISE1 YOUR CODE
     * should set tf_cs,tf_ds,tf_es,tf_ss,tf_esp,tf_eip,tf_eflags
     * NOTICE: If we set trapframe correctly, then the user level process can return to USER MODE from kernel. So
     *          tf_cs should be USER_CS segment (see memlayout.h)
     *          tf_ds=tf_es=tf_ss should be USER_DS segment
     *          tf_esp should be the top addr of user stack (USTACKTOP)
     *          tf_eip should be the entry point of this binary program (elf->e_entry)
     *          tf_eflags should be set to enable computer to produce Interrupt
     */

//完成一個優先級的轉變,從內核態切換到用戶態(特權級從0到3)實現部分
    tf->tf_cs = USER_CS;
    tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
    tf->tf_esp = USTACKTOP;
    tf->tf_eip = elf->e_entry;
    tf->tf_eflags = FL_IF;

*tf是一個是中斷幀的指針,總是指向內核棧的某個位置:當進程從用戶空間跳到內核空間時,中斷幀記錄了進程在被中斷前的狀態。當內核需要跳回用戶空間時,需要調整中斷幀以恢復讓進程繼續執行的各寄存器值。
其定義在(kern/trap/trap.h,60——82行)。
1、將tf_cs設置爲用戶態,這個定義在(kern/mm/memlayout.h,第21行),有一個宏定義已經定義了用戶態和內核態。
2、tf_ds=tf_es=tf_ss也需要設置爲用戶態:定義在(kern/mm/memlayout.h,第26行)
3、需要將esp設置爲用戶棧的棧頂,直接使用之前建立用戶棧時的參數USTACKTOP就可以。
4、eip是程序的入口,elf類的e_entry函數直接聲明瞭,直接使用。
5、FL_IF打開中斷。


    ret = 0;
out:
    return ret;
bad_cleanup_mmap:
    exit_mmap(mm);
bad_elf_cleanup_pgdir:
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    goto out;
}

總結:調用流程:

練習2:父進程複製自己的內存空間給子進程

 

創建子進程的函數do_fork在執行中將拷貝當前進程(即父進程)的用戶內存地址空間中的合法內容到新進程中(子進程),完成內存資源的複製。具體是通過copy_range函數(位於kern/mm/pmm.c中)實現的,請補充copy_range的實現,確保能夠正確執行。

 

Copy on Write:創建子進程的時候能夠共享父進程的內存空間,節省內存佔用。

 

copy_range函數的調用過程:do_fork()---->copy_mm()---->dup_mmap()---->copy_range()

 

首先是do_fork()的分析:(kern/process/proc.c,第373——442行)

int do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
    int ret = -E_NO_FREE_PROC;
    struct proc_struct *proc;
    if (nr_process >= MAX_PROCESS) {		//進程分配超過最大值,錯誤
        goto fork_out;
    }
    ret = -E_NO_MEM;
    //LAB4:EXERCISE2 YOUR CODE
    /*
     * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
     * MACROs or Functions:
     *   alloc_proc:   create a proc struct and init fields (lab4:exercise1)
     *   setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
     *   copy_mm:      process "proc" duplicate OR share process "current"'s mm according clone_flags
     *                 if clone_flags & CLONE_VM, then "share" ; else "duplicate"
     *   copy_thread:  setup the trapframe on the  process's kernel stack top and
     *                 setup the kernel entry point and stack of process
     *   hash_proc:    add proc into proc hash_list
     *   get_pid:      alloc a unique pid for process
     *   wakup_proc:   set proc->state = PROC_RUNNABLE
     * VARIABLES:
     *   proc_list:    the process set's list
     *   nr_process:   the number of process set
     */
    //    1. call alloc_proc to allocate a proc_struct
    //    2. call setup_kstack to allocate a kernel stack for child process
    //    3. call copy_mm to dup OR share mm according clone_flag
    //    4. call copy_thread to setup tf & context in proc_struct
    //    5. insert proc_struct into hash_list && proc_list
    //    6. call wakup_proc to make the new child process RUNNABLE
//    7. set ret vaule using child proc's pid

//這些部分和上次一模一樣,不多說了
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }

    proc->parent = current;
    assert(current->wait_state == 0);

    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
if (copy_mm(clone_flags, proc) != 0) {			
//但這裏創建內存空間的時候,是有變化的,上次這個函數什麼都不做,這次需要調用。
        goto bad_fork_cleanup_kstack;
    }
    copy_thread(proc, stack, tf);
//其他還是沒變化
    bool intr_flag;
    local_intr_save(intr_flag);
    {
        proc->pid = get_pid();
        hash_proc(proc);
        set_links(proc);
    }
    local_intr_restore(intr_flag);
    wakeup_proc(proc);
    ret = proc->pid;
fork_out:
    return ret;
bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}

其中,do_fork函數調用的copy_mm函數在實驗四中沒有實現,其他的過程和實驗四一樣,都是創建一個進程,並放入CPU中調度,而本次我們主要關注的是父子進程之間如何拷貝內存。

 

copy_mm函數(kern/process/proc.c,第309——338行)

static int copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
    struct mm_struct *mm, *oldmm = current->mm;

    /* current is a kernel thread */
    if (oldmm == NULL) {		//當前進程內存爲空,返回0,複製失敗!!
        return 0;
    }
    if (clone_flags & CLONE_VM) {		//如果共享內存標記位爲真,那麼可以共享內存
        mm = oldmm;
        goto good_mm;
    }

    int ret = -E_NO_MEM;
    if ((mm = mm_create()) == NULL) {		//如果創建地址空間失敗,報錯
        goto bad_mm;
    }
    if (setup_pgdir(mm) != 0) {	//如果創建頁表失敗報錯(kern/process/proc.c,288行)
        goto bad_pgdir_cleanup_mm;
    }

    lock_mm(oldmm);		//這是一個互斥鎖,用於避免多個進程同時訪問內存
    {
        ret = dup_mmap(mm, oldmm);		//下一層調用
    }
    unlock_mm(oldmm);

    if (ret != 0) {
        goto bad_dup_cleanup_mmap;
}
good_mm:
    mm_count_inc(mm);
    proc->mm = mm;
    proc->cr3 = PADDR(mm->pgdir);
    return 0;
bad_dup_cleanup_mmap:
    exit_mmap(mm);
    put_pgdir(mm);
bad_pgdir_cleanup_mm:
    mm_destroy(mm);
bad_mm:
    return ret;
}

我們可以看到,在這一層調用中有一個互斥鎖,用於避免多個進程同時訪問內存,在這裏進行了下一層調用:

    lock_mm(oldmm);		//這是一個互斥鎖,用於避免多個進程同時訪問內存
    {
        ret = dup_mmap(mm, oldmm);		//下一層調用
    }
    unlock_mm(oldmm);

dup_mmap函數:(kern/mm/vmm.c,191——209行)

 

首先看傳入的參數,是兩個內存mm,這是爲什麼呢?

在上一個函數copy_mm中,傳入的兩個內存叫做mm和oldmm,其中,第一個mm只是調用了mm_create()聲明,但沒有初始化,更沒有分配內容;第二個oldmm是current進程的內存空間,由此可見,前一個mm是待複製的內存,而複製的源內容在oldmm(父進程)內容中。

int dup_mmap(struct mm_struct *to, struct mm_struct *from) 
{
    assert(to != NULL && from != NULL);			//首先確保兩塊東西創建成功
    list_entry_t *list = &(from->mmap_list), *le = list;		//獲取from的首地址
    while ((le = list_prev(le)) != list) {			//對所有段都遍歷一遍
        struct vma_struct *vma, *nvma;
        vma = le2vma(le, list_link);		//獲取某一段信息,並創建到新進程中
        nvma = vma_create(vma->vm_start, vma->vm_end, vma->vm_flags);
        if (nvma == NULL) {
            return -E_NO_MEM;
        }
        insert_vma_struct(to, nvma);			//把這一個段插入到子進程內存中
        bool share = 0;
        if (copy_range(to->pgdir, from->pgdir, vma->vm_start, vma->vm_end, share) != 0) {			//調用copy_range函數
            return -E_NO_MEM;
        }
    }
    return 0;
}

copy_range函數:(kern/mm/pmm.c,506——556行)

 

在上一個函數中(dup_mmap),只是完成了新進程中的段創建,但是段中還沒有具體內容,需要在copy_range中具體複製父進程對應段中的具體內容。這個函數傳入的參數都是段指針,告訴系統應該複製內存中需要複製內容的起止地址。

 

copy range函數補充實現如下:

int copy_range(pde_t *to, pde_t *from, uintptr_t start, uintptr_t end, bool share) {
    assert(start % PGSIZE == 0 && end % PGSIZE == 0);
    assert(USER_ACCESS(start, end));
    // copy content by page unit.
    do {
        //call get_pte to find process A's pte according to the addr start
        pte_t *ptep = get_pte(from, start, 0), *nptep;		//獲取頁表內容,這裏調用的是lab2的函數,獲取頁表
        if (ptep == NULL) {
            start = ROUNDDOWN(start + PTSIZE, PTSIZE);
            continue ;
        }
        //call get_pte to find process B's pte according to the addr start. If pte is NULL, just alloc a PT
        if (*ptep & PTE_P) {
            if ((nptep = get_pte(to, start, 1)) == NULL) {
                return -E_NO_MEM;
            }
        uint32_t perm = (*ptep & PTE_USER);
        //get page from ptep
        struct Page *page = pte2page(*ptep);
//使用pte2page獲取頁表的值,相關注釋在(kern/mm/pmm.c,439行)
        // alloc a page for process B
        struct Page *npage=alloc_page();
        assert(page!=NULL);
        assert(npage!=NULL);
        int ret=0;
        /* LAB5:EXERCISE2 YOUR CODE
         * replicate content of page to npage, build the map of phy addr of nage with the linear addr start
         *
         * Some Useful MACROs and DEFINEs, you can use them in below implementation.
         * MACROs or Functions:
         *    page2kva(struct Page *page): return the kernel vritual addr of memory which page managed (SEE pmm.h)
         *    page_insert: build the map of phy addr of an Page with the linear addr la
         *    memcpy: typical memory copy function
         *
         * (1) find src_kvaddr: the kernel virtual address of page
         * (2) find dst_kvaddr: the kernel virtual address of npage
         * (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
         * (4) build the map of phy addr of  nage with the linear addr start
         */
        void * kva_src = page2kva(page);	//獲取老頁表的值
        void * kva_dst = page2kva(npage);	//獲取新頁表的值
        memcpy(kva_dst, kva_src, PGSIZE);	//複製操作
        ret = page_insert(to, npage, start, perm);
//建立子進程頁地址起始位置與物理地址的映射關係,prem是權限
        assert(ret == 0);
        }
        start += PGSIZE;
    } while (start != 0 && start < end);
    return 0;
}

練習3:閱讀分析源代碼,理解進程執行 fork/exec/wait/exit 的實現,以及系統調用的實現

 

fork:練習二和實驗四均提到並實現過:(kern/process/proc.c,第373——442行,代碼見上面的練習)

 

1、分配並初始化進程控制塊(alloc_proc 函數);

2、分配並初始化內核棧(setup_stack 函數);

3、根據 clone_flag標誌複製或共享進程內存管理結構(copy_mm 函數);

4、設置進程在內核(將來也包括用戶態)正常運行和調度所需的中斷幀和執行上下文(copy_thread 函數);

5、把設置好的進程控制塊放入hash_list 和 proc_list 兩個全局進程鏈表中;

6、自此,進程已經準備好執行了,把進程狀態設置爲“就緒”態;

7、設置返回碼爲子進程的 id 號。

 

do_execve:(kern/process/proc,652——685行)

 

首先爲加載新的執行碼做好用戶態內存空間清空準備。如果mm不爲NULL,則設置頁表爲內核空間頁表。

接下來是加載應用程序執行碼到當前進程的新創建的用戶態虛擬空間中。之後就是調用。

(在練習1中分析過)

int do_execve(const char *name, size_t len, unsigned char *binary, size_t size) {
    struct mm_struct *mm = current->mm;
    if (!user_mem_check(mm, (uintptr_t)name, len, 0)) {
        return -E_INVAL;
    }
    if (len > PROC_NAME_LEN) {
        len = PROC_NAME_LEN;
    }

    char local_name[PROC_NAME_LEN + 1];
    memset(local_name, 0, sizeof(local_name));
    memcpy(local_name, name, len);

    if (mm != NULL) {
        lcr3(boot_cr3);
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);
        }
        current->mm = NULL;
    }
    int ret;
    if ((ret = load_icode(binary, size)) != 0) {
        goto execve_exit;
    }
    set_proc_name(current, local_name);
    return 0;

execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}

do_wait函數:(kern/process/proc.c,698——755行)

 

當執行wait功能的時候,會調用系統調用SYS_wait,而該系統調用的功能則主要由do_wait函數實現,主要工作就是父進程如何完成對子進程的最後回收工作,具體的功能實現如下:

1、 如果 pid!=0,表示只找一個進程 id 號爲 pid 的退出狀態的子進程,否則找任意一個處於退出狀態的子進程;

2、 如果此子進程的執行狀態不爲PROC_ZOMBIE,表明此子進程還沒有退出,則當前進程設置執行狀態爲PROC_SLEEPING(睡眠),睡眠原因爲WT_CHILD(即等待子進程退出),調用schedule()函數選擇新的進程執行,自己睡眠等待,如果被喚醒,則重複跳回步驟 1 處執行;

3、 如果此子進程的執行狀態爲 PROC_ZOMBIE,表明此子進程處於退出狀態,需要當前進程(即子進程的父進程)完成對子進程的最終回收工作,即首先把子進程控制塊從兩個進程隊列proc_list和hash_list中刪除,並釋放子進程的內核堆棧和進程控制塊。自此,子進程才徹底地結束了它的執行過程,它所佔用的所有資源均已釋放。

do_wait(int pid, int *code_store) {
    struct mm_struct *mm = current->mm;
    if (code_store != NULL) {
        if (!user_mem_check(mm, (uintptr_t)code_store, sizeof(int), 1)) {
            return -E_INVAL;
        }
    }

    struct proc_struct *proc;
    bool intr_flag, haskid;
repeat:
    haskid = 0;
    if (pid != 0) {		//如果pid!=0,則找到進程id爲pid的處於退出狀態的子進程
        proc = find_proc(pid);
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {		
//如果子進程不處於殭屍狀態,那麼會變成睡眠狀態,因爲需要等待子進程退出,之後調用schedule函數掛起自己,選擇其他進程執行。如果爲殭屍狀態,那麼會清除該進程。
                goto found;
            }
        }
    }
    else {
        proc = current->cptr;
        for (; proc != NULL; proc = proc->optr) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                goto found;
            }
        }
    }
    if (haskid) {
        current->state = PROC_SLEEPING;
        current->wait_state = WT_CHILD;
        schedule();
        if (current->flags & PF_EXITING) {
            do_exit(-E_KILLED);
        }
        goto repeat;
    }
    return -E_BAD_PROC;
found:
    if (proc == idleproc || proc == initproc) {
        panic("wait idleproc or initproc.\n");
    }
    if (code_store != NULL) {
        *code_store = proc->exit_code;
    }
    local_intr_save(intr_flag);
    {
        unhash_proc(proc);
        remove_links(proc);
    }
    local_intr_restore(intr_flag);
    put_kstack(proc);
    kfree(proc);
    return 0;
}

do_exit函數:(kern/process/proc.c,448——499行)

 

1、先判斷是否是用戶進程,如果是,則開始回收此用戶進程所佔用的用戶態虛擬內存空間

2、設置當前進程狀態爲PROC_ZOMBIE,然後設置當前進程的退出碼爲error_code。此時這個進程已經無法再被調度了,只能等待父進程來完成最後的回收工作。

3、如果當前父進程已經處於等待子進程的狀態,即父進程的wait_state被置爲WT_CHILD,則此時就可以喚醒父進程,讓父進程來幫子進程完成最後的資源回收工作。

4、如果當前進程還有子進程,則需要把這些子進程的父進程指針設置爲內核線程init,且各個子進程指針需要插入到init的子進程鏈表中。如果某個子進程的執行狀態是 PROC_ZOMBIE,則需要喚醒 init來完成對此子進程的最後回收工作。

5、執行schedule()調度函數,選擇新的進程執行。

int do_exit(int error_code) {
    if (current == idleproc) {
        panic("idleproc exit.\n");
    }
    if (current == initproc) {
        panic("initproc exit.\n");
    } 
    struct mm_struct *mm = current->mm;
    if (mm != NULL) {		//準備回收內存,首先它應該不會爲空
        lcr3(boot_cr3);		//從用戶模式切換到內核模式
        if (mm_count_dec(mm) == 0) {
            exit_mmap(mm);
            put_pgdir(mm);
            mm_destroy(mm);		//回收頁目錄、釋放內存
        }
        current->mm = NULL;		//最後將它的內存地址指向空,完成內存的回收
    }
    current->state = PROC_ZOMBIE;	//設置殭屍狀態,等待父進程回收
    current->exit_code = error_code;
    bool intr_flag;
    struct proc_struct *proc;
    local_intr_save(intr_flag);
    {
        proc = current->parent;
        if (proc->wait_state == WT_CHILD) {		//如果父進程在等待子進程,則喚醒
            wakeup_proc(proc);
        }
        while (current->cptr != NULL) {
//如果當前進程還有子進程(孤兒進程),則需要把這些子進程的父進程指針設置爲內核線程initproc,如果某個子進程的執行狀態是PROC_ZOMBIE,則需要喚醒initproc來完成對此子進程的最後回收工作。
            proc = current->cptr;
            current->cptr = proc->optr;
            proc->yptr = NULL;
            if ((proc->optr = initproc->cptr) != NULL) {
                initproc->cptr->yptr = proc;
            }
            proc->parent = initproc;
            initproc->cptr = proc;
            if (proc->state == PROC_ZOMBIE) {
                if (initproc->wait_state == WT_CHILD) {
                    wakeup_proc(initproc);
                }
            }
        }
    }
    local_intr_restore(intr_flag);
    schedule();		//選擇其他的進程執行
    panic("do_exit will not return!! %d.\n", current->pid);
}

 

四、實驗體會和思考題

 

1、請在實驗報告中描述當創建一個用戶態進程並加載了應用程序後,CPU是如何讓這個應用程序最終在用戶態執行起來的。即這個用戶態進程被ucore選擇佔用CPU執行(RUNNING態)到具體執行應用程序第一條指令的整個經過。

 

答:詳見練習一。

 

2、Copy on Write機制

 

“Copy on Write”是指在fork一個進程時不立刻將父進程的數據段/代碼段等複製到子進程的內存空間,而是當父進程或子進程中對相關內存做出修改時,才進行復制操作。

 

實現時,在fork一個進程時,可以省去load_icode中創建新頁目錄的操作,而是直接將父進程頁目錄的地址賦給子進程,爲了防止誤操作以及辨別是否需要複製,應該將尚未完成複製的部分的訪問權限設爲只讀。

 

當執行讀操作,父進程和子進程均不受影響。但當執行寫操作時,會發生權限錯誤(因爲此時的訪問權限爲只讀)。這時候會進入到page fault的處理中去,在page fault的處理中,如果發現錯誤原因讀/寫權限問題,而訪問的段的段描述符權限爲可寫,便可以知道是由於使用COW機制而導致的,這時再將父進程的數據段、代碼段等複製到子進程內存空間上即可。

 

3、請分析fork/exec/wait/exit在實現中是如何影響進程的執行狀態的?

 

fork:如果創建新進程成功,則出現一個子進程一個父進程。在子進程中,fork函數返回0,在父進程中,fork返回新創建子進程的進程ID。

 

exit:會把一個退出碼error_code傳遞給操作系統,操作系統通過執行內核函數do_exit來完成對當前進程的退出處理,工作是回收當前進程所佔內存資源。

 

execve:完成用戶進程的創建工作。首先爲加載新的執行碼做好用戶態內存空間清空準備。再加載應用程序執行碼到當前進程的新創建的用戶態虛擬空間中。

 

wait:等待子進程的結束通知。wait_pid函數等待進程id號爲pid的子進程結束通知,讓ucore來完成對子進程的最後回收工作。

 

4、請給出ucore中一個用戶態進程的執行狀態生命週期圖(包執行狀態,執行狀態之間的變換關係,以及產生變換的事件或函數調用)。

五、運行結果

如果make grade無法滿分,嘗試註釋掉tools/grade.sh的221行到233行(在前面加上“#”)。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章