操作系統(tǒng)實驗:Lab5 用戶進程管理

清華大學操作系統(tǒng)Lab5實驗報告
課程主頁:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
實驗指導書:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab

實驗目的

  • 了解第一個用戶進程創(chuàng)建過程
  • 了解系統(tǒng)調用框架的實現(xiàn)機制
  • 了解ucore如何實現(xiàn)系統(tǒng)調用sys_fork/sys_exec/sys_exit/sys_wait來進行進程管理

實驗內容

實驗4完成了內核線程翩迈,但到目前為止辟灰,所有的運行都在內核態(tài)執(zhí)行袍镀。實驗5將創(chuàng)建用戶進程,讓用戶進程在用戶態(tài)執(zhí)行,且在需要ucore支持時,可通過系統(tǒng)調用來讓ucore提供服務锹漱。為此需要構造出第一個用戶進程,并通過系統(tǒng)調用sys_fork/sys_exec/sys_exit/sys_wait來支持運行不同的應用程序慕嚷,完成對用戶進程的執(zhí)行過程的基本管理哥牍。相關原理介紹可看附錄B毕泌。

練習0:填寫已有實驗

除了將原有Lab中的代碼轉移到Lab5之外,還需要做一些修改嗅辣。

alloc_proc中撼泛,初始化proc_struct中新加入的幾個成員變量。

        //LAB5 2015011346 : (update LAB4 steps)
            /*
             * below fields(add in LAB5) in proc_struct need to be initialized
             *       uint32_t wait_state;                        // waiting state
             *       struct proc_struct *cptr, *yptr, *optr;     // relations between processes
             */
        proc -> wait_state = 0x0;
        proc -> cptr = proc -> yptr = proc -> optr = NULL;

do_fork中澡谭,加入與進程控制有關的信息愿题。

    //LAB5 2015011346 : (update LAB4 steps)
   /* Some Functions
    *    set_links:  set the relation links of process.  ALSO SEE: remove_links:  lean the relation links of process 
    *    -------------------
    *    update step 1: set child proc's parent to current process, make sure current process's wait_state is 0
    *    update step 5: insert proc_struct into hash_list && proc_list, set the relation links of process
    */
    proc = alloc_proc();
    if (proc == NULL) {
        goto fork_out;
    }
    assert(proc -> wait_state == 0x0);
    proc -> parent = current;
    int kstack_success = setup_kstack(proc);
    if (kstack_success != 0) {
        goto bad_fork_cleanup_proc;
    }
    int copy_success = copy_mm(clone_flags, proc);
    if (copy_success != 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;

trap_dispatch中,修改timer interrupt蛙奖,在中斷時將當前正在運行的進程設置為可調度的潘酗,以便在下一個時間片重新選擇進程。

    case IRQ_OFFSET + IRQ_TIMER:
        /* LAB5 2015011346 */
        /* you should upate you lab1 code (just add ONE or TWO lines of code):
         *    Every TICK_NUM cycle, you should set current process's current->need_resched = 1
         */
        ticks++;
        if (ticks == TICK_NUM) {
            ticks = 0;
            current -> need_resched = 1;
        }

練習1:加載應用程序并執(zhí)行

load_icode函數(shù)中雁仲,在特權級為0的內核棧中創(chuàng)建新的中斷幀仔夺,通過彈出該中斷幀可以賺到特權級為3的用戶程序處執(zhí)行。

    /* LAB5:EXERCISE1 2015011346
     * 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;
請在實驗報告中描述當創(chuàng)建一個用戶態(tài)進程并加載了應用程序后攒砖,CPU是如何讓這個應用程序最終在用戶態(tài)執(zhí)行起來的缸兔。即這個用戶態(tài)進程被ucore選擇占用CPU執(zhí)行( RUNNING態(tài))到具體執(zhí)行應用程序第一條指令的整個經過。

這部分主要在load_icode函數(shù)中實現(xiàn)祭衩。

  • 為內存管理的數(shù)據(jù)結構mm分配空間并初始化灶体,代碼如下:
    //(1) create a new mm for current process
    if ((mm = mm_create()) == NULL) {
        goto bad_mm;
    }
  • 通過setup_pgdir為用戶空間創(chuàng)建頁目錄,并將內存管理數(shù)據(jù)結構mmpgdir設置為頁目錄的虛地址掐暮。
    //(2) create a new PDT, and mm->pgdir= kernel virtual addr of PDT
    if (setup_pgdir(mm) != 0) {
        goto bad_pgdir_cleanup_mm;
    }
  • 接下來將解析已經被載入內存的ELF格式的用戶代碼。解析ELF header政钟,找到用戶程序中program section headers路克。隨后通過調用mm_map將不同段的起始地址和長度記錄到虛擬內存空間管理的數(shù)據(jù)結構vma中去。接下來根據(jù)program section的header中的信息养交,找到每個program section精算,并將其中的內容拷貝到用戶進程的內存中(包括BSS section和TEXT/DATA section)。
    //(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;
        }
    }
  • 接下來通過調用mm_map函數(shù)為用戶進程的user stack分配空間:
    //(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);
  • 建立用戶進程的內存管理數(shù)據(jù)結構mm中的內容碎连,并在進程控制塊中記錄下用戶進程的頁目錄地址灰羽,將用戶進程的頁目錄地址賦給CR3寄存器。
    //(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));
  • 最后清空原來的中斷幀鱼辙,建立新的中斷幀廉嚼,代碼就是填入的那段代碼。通過iret指令從內核棧中彈出中斷幀恢復各種段寄存器的值倒戏。這時段寄存器已經指向特權級為3的段怠噪,也就說完成了到用戶進程的切換。

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

copy_range函數(shù)中完成內存資源的復制杜跷。

/* LAB5:EXERCISE2 2015011346
         * 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
         */
// (1) find src_kvaddr: the kernel virtual address of page
        uint32_t src_kvaddr = page2kva(page);
// (2) find dst_kvaddr: the kernel virtual address of npage
        uint32_t dst_kvaddr = page2kva(npage);
// (3) memory copy from src_kvaddr to dst_kvaddr, size is PGSIZE
        memcpy(dst_kvaddr, src_kvaddr, PGSIZE);
// (4) build the map of phy addr of  nage with the linear addr start
        ret = page_insert(to, npage, start, perm);
        assert(ret == 0);
請在實驗報告中簡要說明如何設計實現(xiàn)”Copy on Write 機制“傍念,給出概要設計矫夷,鼓勵給出詳細設計。

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

實現(xiàn)時阳仔,在fork一個進程時忧陪,可以省去load_icode中創(chuàng)建新頁目錄的操作,而是直接將父進程頁目錄的地址賦給子進程驳概,為了防止誤操作以及辨別是否需要復制赤嚼,應該將尚未完成復制的部分的訪問權限設為只讀。

當執(zhí)行讀操作顺又,父進程和子進程均不受影響更卒。但當執(zhí)行寫操作時,會發(fā)生權限錯誤(因為此時的訪問權限為只讀)稚照。這時候會進入到page fault的處理中去蹂空,在page fault的處理中,如果發(fā)現(xiàn)錯誤原因讀/寫權限問題果录,而訪問的段的段描述符權限為可寫上枕,便可以知道是由于使用COW機制而導致的,這時再將父進程的數(shù)據(jù)段弱恒、代碼段等復制到子進程內存空間上即可辨萍。

練習3:閱讀分析源代碼,理解進程執(zhí)行 fork/exec/wait/exit 的實現(xiàn)返弹,以及系統(tǒng)調用的實現(xiàn)锈玉。

系統(tǒng)調用共用一個中斷號(即代碼中的T_SYSCALL)。當發(fā)生沖段或異常后义起,會進入到中斷服務例程中去拉背,最終在trap_dispatch函數(shù)中調用syscall函數(shù),并通過系統(tǒng)調用號選擇應該執(zhí)行函數(shù)sys_fork/exec/wait/exit中的一個默终,這些函數(shù)會解析系統(tǒng)調用時傳入的參數(shù)椅棺,并將參數(shù)傳遞給do_fork/execv/wait/exit執(zhí)行具體操作。

請分析fork/exec/wait/exit在實現(xiàn)中是如何影響進程的執(zhí)行狀態(tài)的齐蔽?

根據(jù)上面的系統(tǒng)調用處理過程的分析两疚,我們只需了解do_fork/execve/wait/exit中的實現(xiàn)。

  • do_fork:sys_fork的相關函數(shù)肴熏。在該函數(shù)中鬼雀,首先要為子進程創(chuàng)建進程控制塊,設置好進程控制塊中的上下文的中斷幀等信息蛙吏,為子進程創(chuàng)建用戶棧源哩、內核棧等鞋吉。隨后通過wakeup_proc函數(shù)將子進程設置為RUNNABLE。之后該函數(shù)給父進程返回子進程的pid励烦,給子進程返回0谓着。隨后在ucore循環(huán)執(zhí)行進程調度schedule時,就會將子進程考慮進去坛掠。詳細說明見代碼注釋赊锚。
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. 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 wakeup_proc to make the new child process RUNNABLE
    //    7. set ret vaule using child proc's pid
    proc = alloc_proc();
    if (proc == NULL) {
        goto fork_out;
    }
    assert(proc -> wait_state == 0x0);
    proc -> parent = current;
    int kstack_success = setup_kstack(proc);
    if (kstack_success != 0) {
        goto bad_fork_cleanup_proc;
    }
    int copy_success = copy_mm(clone_flags, proc);
    if (copy_success != 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_execve:sys_exec的相關函數(shù)。sys_exec不創(chuàng)建新進程屉栓,而是用新的內容覆蓋原來的進程內存空間舷蒲。在do_execve中,首先使用exit_mmap友多、put_pgdir牲平、mm_destroy來刪除并釋放掉當前進程內存空間的頁表信息、內存管理信息域滥。隨后通過load_icode將新的用戶程序從ELF文件中加載進來執(zhí)行纵柿。如果加載失敗,則調用do_exit退出當前進程启绰。執(zhí)行sys_exec后昂儒,當前進程的狀態(tài)保持不變。詳細說明見代碼注釋委可。
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) {
// 取消vma中記錄的合法內存塊
            exit_mmap(mm);
// 刪除頁表
            put_pgdir(mm);
// 刪除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;

// 如果exec執(zhí)行不成功渊跋,則退出進程
execve_exit:
    do_exit(ret);
    panic("already exit: %e.\n", ret);
}
  • do_wait:sys_wait的相關函數(shù)。在該函數(shù)中着倾,循環(huán)查看子進程的狀態(tài)刹枉,直到一個正在等待的子進程的狀態(tài)變成Zombie狀態(tài),這時完成這個子進程的剩余資源回收工作屈呕,釋放子進程的空間。詳細說明見代碼注釋棺亭。
int
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;
// 循環(huán)詢問正在等待的子進程的狀態(tài)虎眨,直到有子進程狀態(tài)變?yōu)閆OMBIE。
repeat:
    haskid = 0;
    if (pid != 0) {
        proc = find_proc(pid);
        if (proc != NULL && proc->parent == current) {
            haskid = 1;
            if (proc->state == PROC_ZOMBIE) {
                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;

// 如果發(fā)現(xiàn)一個子進程變成了ZOMBIE镶摘,則釋放該子進程剩余的資源嗽桩。
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:sys_exit的相關函數(shù)。退出時凄敢,首先釋放掉該進程占用的一部分內存(還有一部分可能由父進程釋放)碌冶。然后將該進程標記為僵尸進程。如果它的父進程處于等待子進程退出的狀態(tài)涝缝,則喚醒父進程扑庞,將自己的子進程交給initproc處理譬重,并進行的進程調度。詳細說明見代碼注釋罐氨。
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) {
// 釋放由vma記錄的內存地址塊
            exit_mmap(mm);
// 刪除頁表
            put_pgdir(mm);
// 刪除內存管理結構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);
    {
// 如果當前進程的父進程處于等待子進程退出狀態(tài),則將父進程設置為RUNNABLE
        proc = current->parent;
        if (proc->wait_state == WT_CHILD) {
            wakeup_proc(proc);
        }
// 如果當前進程有子進程栅隐,則將子進程設置為initproc的子進程塔嬉,并完成子進程中處于僵尸狀態(tài)的進程的最后的回收工作
        while (current->cptr != NULL) {
            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);
// 執(zhí)行進程調度    
    schedule();
    panic("do_exit will not return!! %d.\n", current->pid);
}
請給出ucore中一個用戶態(tài)進程的執(zhí)行狀態(tài)生命周期圖(包執(zhí)行狀態(tài),執(zhí)行狀態(tài)之間的變換關系租悄,以及產生變換的事件或函數(shù)調用) 谨究。
狀態(tài)圖

覆蓋的知識點

  • 進程切換的全過程
  • 在父進程執(zhí)行fork時的行為
  • 子進程執(zhí)行exit后的行為

與參考答案的區(qū)別

  • 練習1:自己完成。
  • 練習2:自己完成泣棋。

總結

感覺這次實驗比之前的容易一點胶哲,官方說法應該是由于認真看了mooc和實驗指導書,但實際也可能是因為壓著DDL寫的比較有動力外傅。
同時很懷疑思考題的表述是否準確纪吮,如果不準確希望助教指正。(當然不扣分最好了萎胰,我還是認真寫了的)碾盟。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市技竟,隨后出現(xiàn)的幾起案子冰肴,更是在濱河造成了極大的恐慌,老刑警劉巖榔组,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熙尉,死亡現(xiàn)場離奇詭異,居然都是意外死亡搓扯,警方通過查閱死者的電腦和手機检痰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锨推,“玉大人铅歼,你說我怎么就攤上這事』豢桑” “怎么了椎椰?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長沾鳄。 經常有香客問我慨飘,道長,這世上最難降的妖魔是什么译荞? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任瓤的,我火速辦了婚禮休弃,結果婚禮上,老公的妹妹穿的比我還像新娘堤瘤。我一直安慰自己玫芦,他們只是感情好,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布本辐。 她就那樣靜靜地躺著桥帆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪慎皱。 梳的紋絲不亂的頭發(fā)上老虫,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音茫多,去河邊找鬼祈匙。 笑死,一個胖子當著我的面吹牛天揖,可吹牛的內容都是我干的夺欲。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼今膊,長吁一口氣:“原來是場噩夢啊……” “哼些阅!你這毒婦竟也來了?” 一聲冷哼從身側響起斑唬,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤市埋,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后恕刘,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缤谎,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年褐着,在試婚紗的時候發(fā)現(xiàn)自己被綠了坷澡。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡含蓉,死狀恐怖洋访,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情谴餐,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布呆抑,位于F島的核電站岂嗓,受9級特大地震影響,放射性物質發(fā)生泄漏鹊碍。R本人自食惡果不足惜厌殉,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一食绿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧公罕,春花似錦器紧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至罐柳,卻和暖如春掌腰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背张吉。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工齿梁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人肮蛹。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓勺择,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伦忠。 傳聞我的和親對象是個殘疾皇子省核,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容