Lab4\5:進程和線程

進程的定義

進程是指一個具備必定獨立功能的程序在一個數據集合上的一次動態執行過程數據結構

源代碼在通過編譯連接以後生成了可執行文件,再由操做系統進行加載而且進行一些堆棧的分配纔是進程併發

進程控制塊

操做系統管理控制進程運行所用的信息集合函數

  • 操做系統用PCB來描述進程的基本狀況以及運行變化的過程
  • PCB是進程存在的惟一標誌ui

    每一個進程都在操做系統中有一個對應的PCBthis

進程控制塊主要包含的就是進程的標識信息,處理機現場保存和進程控制信息spa

控制信息操作系統

  1. 調度和狀態信息
  2. 進程間通訊信息
  3. 存儲管理信息
  4. 進程所用資源
  5. 有關數據結構鏈接信息

進程的生命週期

  • 進程建立線程

    用戶請求建立一個新進程,正在運行的進程執行了建立進程的系統調用,而且加入到就緒隊列指針

  • 進程執行rest

    內核對就緒隊列進行調度,到執行該進程

  • 進程等待

    運行中的進程可能會進入阻塞狀態,好比進行IO的等待或者須要的數據沒有到達

  • 進程搶佔

    運行中的進程可能時間片被用完或者高優先級進程被喚醒致使了進程被搶佔進入阻塞狀態

  • 進程喚醒

    被阻塞須要的資源能夠被知足就可能被喚醒進入就緒隊列

  • 進程結束

    進程完成任務或者被迫結束

線程的定義

線程是進程的一部分,描述指令流執行狀態。它是進程中的指令執行流的最小單元,是CPU調度的基本單位。

  • 進程做爲資源分配角色

進程由一組相關資源構成,包括地址空間(代碼段、數據段)、打開的文件等各類資源

  • 線程做爲處理機調度角色

線程描述在進程資源環境中的指令流執行狀態

一個進程中能夠同時存在多個線程,各個線程之間能夠併發地執行,各個線程之間能夠共享地址空間和文件等資源

線程的實現方式

用戶線程

用戶線程是基於在用戶態本身實現的線程庫函數來完成對線程的管理,包括線程的建立、終止、同步和調度

這樣內核並不知道用戶線程的存在,因此每一個線程控制塊都由線程庫函數來維護,也不要在用戶態和內核態進行切換,開銷會較小,可是對於若是線程發生阻塞的話,因爲操做系統並不知道用戶級線程的狀況,因此會形成整個進程的阻塞,而且除非當前運行線程主動放棄,它所在進程的其餘線程沒法搶佔CPU

內核線程

由內核經過系統調用實現的線程機制,由內核完成線程的建立、終止和管理

由內核維護PCB和TCB,線程執行系統調用而被阻塞不影響其餘線程,線程的建立、終止和切換相對較大,以線程爲單位進行CPU時間分配

進程控制

進程切換

暫停當前運行進程,從運行狀態變成其餘狀態,調度另外一個進程從就緒狀態變成運行狀態

要完成進程切換就須要對切換前的進程進行進程上下文的保存,而且在切換後對進程上下文的恢復

進程控制塊:內核爲每一個進程維護了對應的進程控制塊(PCB),內核將相同狀態的進程的PCB放置在同一隊列

進程建立

fork/exec

  • fork() 建立一個繼承的子進程

    複製父進程的全部變量和內存,複製父進程的全部CPU寄存器(有一個寄存器例外),fork()執行過程對於子進程而言,是在調用時間對父進程地址空間的一次複製,對於父進程fork() 返回child PID, 對於子進程返回值爲0

  • fork()的地址空間複製

    fork()執行過程對於子進程而言,是在調用時間對父進程地址空間的一次複製,對於父進程fork() 返回child PID, 對於子進程返回值爲0

代碼實現

分配並初始化一個進程控制塊

struct proc_struct {
    enum proc_state state;                      // Process state
    int pid;                                    // Process ID
    int runs;                                   // the running times of Proces
    uintptr_t kstack;                           // Process kernel stack
    volatile bool need_resched;                 // bool value: need to be rescheduled to release CPU?
    struct proc_struct *parent;                 // the parent process
    struct mm_struct *mm;                       // Process's memory management field
    struct context context;                     // Switch here to run process
    struct trapframe *tf;                       // Trap frame for current interrupt
    uintptr_t cr3;                              // CR3 register: the base addr of Page Directroy Table(PDT)
    uint32_t flags;                             // Process flag
    char name[PROC_NAME_LEN + 1];               // Process name
    list_entry_t list_link;                     // Process link list 
    list_entry_t hash_link;                     // Process hash list
};

static struct proc_struct *alloc_proc(void) {

    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) {
        proc->state = PROC_UNINIT;  //設置進程爲未初始化狀態
        proc->pid = -1;             //未初始化的的進程id爲-1
        proc->runs = 0;             //初始化時間片
        proc->kstack = 0;           //內存棧的地址
        proc->need_resched = 0;     //是否須要調度設爲不須要
        proc->parent = NULL;        //父節點設爲空
        proc->mm = NULL;            //虛擬內存設爲空
        memset(&(proc->context), 0, sizeof(struct context));//上下文的初始化
        proc->tf = NULL;            //中斷幀指針置爲空
        proc->cr3 = boot_cr3;       //頁目錄設爲內核頁目錄表的基址
        proc->flags = 0;            //標誌位
        memset(proc->name, 0, PROC_NAME_LEN);//進程名
    }
    return proc;
}

proc_struct便是進程控制塊的結構體,alloc_proc函數來負責分配一個新的struct proc_struct結構,根據提示咱們須要初始化一些變量

爲新建立的內核線程分配資源

建立內核線程的主要工做由do_fork來完成資源的分配

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;
    //1:調用alloc_proc()函數申請內存塊,若是失敗,直接返回處理
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
    //2.將子進程的父節點設置爲當前進程
    proc->parent = current;
    //3.調用setup_stack()函數爲進程分配一個內核棧
    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
    //4.調用copy_mm()函數複製父進程的內存信息到子進程
    if (copy_mm(clone_flags, proc) != 0) {
        goto bad_fork_cleanup_kstack;
    }
    //5.調用copy_thread()函數複製父進程的中斷幀和上下文信息
    copy_thread(proc, stack, tf);
    //6.將新進程添加到進程的hash列表中
    bool intr_flag;
    local_intr_save(intr_flag);
    {
        proc->pid = get_pid();
        hash_proc(proc); //創建映射
        nr_process ++;  //進程數加1
        list_add(&proc_list, &(proc->list_link));//將進程加入到進程的鏈表中
    }
    local_intr_restore(intr_flag);
    //      7.一切就緒,喚醒子進程
    wakeup_proc(proc);
    //      8.返回子進程的pid
    ret = proc->pid;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}
  • 1.分配並初始化進程控制塊(alloc_proc 函數);
  • 2.分配並初始化內核棧(setup_stack 函數);
  • 3.根據 clone_flag標誌複製或共享進程內存管理結構(copy_mm 函數);
  • 4.設置進程在內核(未來也包括用戶態)正常運行和調度所需的中斷幀和執行上下文
    (copy_thread函數);
  • 5.把設置好的進程控制塊放入hash_list 和proc_list 兩個全局進程鏈表中;
  • 6.自此,進程已經準備好執行了,把進程狀態設置爲「就緒」態;
  • 7.設置返回碼爲子進程的 id號。

proc_run完成進程切換

先看調度函數,就只是一個簡單FIFO模型

void
schedule(void) {
    bool intr_flag;
    list_entry_t *le, *last;
    struct proc_struct *next = NULL;
    local_intr_save(intr_flag);
    {
        current->need_resched = 0;
        last = (current == idleproc) ? &proc_list : &(current->list_link);
        le = last;
        do {
            if ((le = list_next(le)) != &proc_list) {
                next = le2proc(le, list_link);
                if (next->state == PROC_RUNNABLE) {
                    break;
                }
            }
        } while (le != last);
        if (next == NULL || next->state != PROC_RUNNABLE) {
            next = idleproc;
        }
        next->runs ++;
        if (next != current) {
            proc_run(next);
        }
    }
    local_intr_restore(intr_flag);
}
  • 一、設置當前內核線程 current->need_resched 爲 0;
  • 二、在 proc_list 隊列中查找下一個處於就緒態的線程或進程 next;
  • 三、找到這樣的進程後,就調用 proc_run 函數,保存當前進程 current 的執行現場(進程上下文),恢復新進程的執行現場,完成進程切換。

調度完成由proc_run來切換和開始執行進程

void proc_run(struct proc_struct *proc) {
    if (proc != current) {
        bool intr_flag;
        struct proc_struct *prev = current, *next = proc;
        local_intr_save(intr_flag);
        {
            current = proc;
            load_esp0(next->kstack + KSTACKSIZE);
            lcr3(next->cr3);
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}
  • 一、讓 current 指向 next 內核線程 initproc;
  • 二、設置任務狀態段 ts 中特權態 0 下的棧頂指針 esp0 爲 next 內核線程 initproc 的內核棧的棧頂,即 next->kstack + KSTACKSIZE ;
  • 三、設置 CR3 寄存器的值爲 next 內核線程 initproc 的頁目錄表起始地址 next->cr3,這其實是完成進程間的頁表切換;
  • 四、由 switch_to函數完成具體的兩個線程的執行現場切換,即切換各個寄存器,當 switch_to 函數執行完「ret」指令後,就切換到 initproc 執行了。
switch_to:                      # switch_to(from, to)
    # save from's registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    movl %esp, 4(%eax)
    movl %ebx, 8(%eax)
    movl %ecx, 12(%eax)
    movl %edx, 16(%eax)
    movl %esi, 20(%eax)
    movl %edi, 24(%eax)
    movl %ebp, 28(%eax)

    # restore to's registers
    movl 4(%esp), %eax          # not 8(%esp): popped return address already
                                # eax now points to to
    movl 28(%eax), %ebp
    movl 24(%eax), %edi
    movl 20(%eax), %esi
    movl 16(%eax), %edx
    movl 12(%eax), %ecx
    movl 8(%eax), %ebx
    movl 4(%eax), %esp
    pushl 0(%eax)               # push eip
    ret

這些指令完成了保存前一個進程的其餘 7 個寄存器到 context 中的相應域中。至此前一個進程的執行現場保存完畢。

再日後是恢復向一個進程的執行現場,這其實就是上述保存過程的逆執行過程,即從 context 的高地址的域 ebp 開始,逐一把相關域的值賦值給對應的寄存器。

加載應用程序並執行

完成應用程序的加載的函數是do_evecve,其中最主要的是load_icode,這個函數用來將ELF可執行二進制文件加載到當前內存中來

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);
}

首先爲加載新的執行碼作好用戶態內存空間清空準備。若是mm不爲NULL,則設置頁表
爲內核空間頁表,且進一步判斷mm的引用計數減1後是否爲0,若是爲0,則代表沒有進
程再須要此進程所佔用的內存空間,爲此將根據mm中的記錄,釋放進程所佔用戶空間內
存和進程頁表自己所佔空間。最後把當前進程的mm內存管理指針爲空。因爲此處的
initproc是內核線程,因此mm爲NULL,整個處理都不會作

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

    int ret = -E_NO_MEM;
    struct mm_struct *mm;
    //(1) create a new mm for current process
    if ((mm = mm_create()) == NULL) {
        goto bad_mm;
    }
    //(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
    if (setup_pgdir(mm) != 0) {
        goto bad_pgdir_cleanup_mm;
    }
    //(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;
    //(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) {
        ret = -E_INVAL_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)
        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;
        }
        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
        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
        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);
            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;
    }
    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
     */
    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;
    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;
}
  • 調用mm_create函數來申請進程的內存管理數據結構mm所需內存空間,並對mm進行初
    始化;

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

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

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

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

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

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

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息