協程

對于協程做一個整體的描述谨胞,從概念、原理蒜鸡、實現三個方面敘述胯努。側重有棧協程。

1 概覽

1.1 什么是協程

有很多與協程相關的名字:輕量級線程逢防、微線程叶沛、纖程、協程忘朝、goroutine等等灰署。其本質,都是大學操作系統(tǒng)課程上學到的用戶態(tài)線程局嘁。

后文統(tǒng)一使用協程來描述它溉箕。

協程,能讓你以同步的方式編寫高性能的異步代碼悦昵,而不用像使用epoll一樣維護大量狀態(tài)機和回調函數约巷;其性能在大多業(yè)務場景(計算密集型業(yè)務以外的場景),都會優(yōu)于操作系統(tǒng)線程旱捧,其優(yōu)勢主要是以下幾點:

  1. 協程往往在同一線程內執(zhí)行独郎,需要同步語義的場景踩麦,不需要依賴操作系統(tǒng)的互斥鎖等系統(tǒng)調用,用一個全局的bool變量作為鎖即可氓癌,同步操作開銷極低
  2. 協程棧往往比較小谓谦,只有幾十到幾百KB(無棧協程甚至可能只有幾KB),相比于線程默認的8MB贪婉,同樣的配置反粥,能夠啟動大量的協程,而不影響系統(tǒng)的性能
  3. 協程的切換由用戶自行實現疲迂,只需要切換寄存器和用戶棧才顿,十分輕量

1.2 有棧協程和無棧協程

協程可以劃分為兩類

  • 有棧協程
  • 無棧協程

有棧協程,在創(chuàng)建時尤蒿,除了創(chuàng)建用于管理協程的對象郑气,還會預先在heap/mmap上分配固定大小(比如128KB)的椦兀空間尾组,后續(xù)協程棧切換時,只需要修改%rsp和%rbp寄存器示弓,將其由程序的函數椈淝龋空間,指向協程自己的位于堆上的椬嗍簦空間即可跨跨。有棧協程相對比較靈活,能夠在程序任何位置囱皿,隨時隨地進行suspend和resume勇婴。

無棧協程,在創(chuàng)建時铆帽,只分配管理協程的對象咆耿,并不會去heap/mmap上分配協程的椀铝拢空間爹橱,無棧協程與主程序共用同一個棧,其布局與普通的函數調用棧一致窄做。很明顯愧驱,這讓無棧協程的內存占用更加小,它不需要預先分配獨立的椡终担空間组砚,只是在suspend時,才在堆上按需分配空間掏颊,把主程序棧中需要保存的變量restore起來糟红。但這也帶來了更多的限制艾帐,無棧協程不能隨時進行suspend,必須在協程的top-level函數中才能進行進行suspend操作盆偿。(且完全體的無棧協程一般需要編譯器原生支持柒爸。)

有棧協程和無棧協程都支持協程的嵌套調用,都支持將協程綁定到不同線程使用(雖然一般不會這樣做)事扭。

1.3 協程庫

協程其實和函數調用有些像捎稚,其思路就是手動能夠保存函數當前的上下文,將執(zhí)行權讓給其他函數求橄,并能在恰當的場合今野,恢復被保存函數的執(zhí)行。

當前c/c++中罐农,第三方有棧協程的實現比較多条霜,主流的協程庫,都是參考 setjmp/longjmp 或者 swapcontext 兩個系列的系統(tǒng)調用實現的啃匿,比如libco蛔外,boost coroutine。

無棧協程溯乒,一般不需要匯編操作夹厌,在suspend時,需要能夠準確保存要切換的變量裆悄,比如asio stackless coroutine矛纹、C++20引入的協程。

2 函數棧幀和寄存器約定:x64 UNIX - System V ABI

如前文所說光稼,協程調用和函數調用十分類似或南,要理解和實現協程,首先要對函數調用過程的棧布局和寄存器使用約定有所了解艾君。一般linux下的棧幀布局為System V采够,通過readelf命令可以查看elf文件的ABI。

# readelf -h a.out
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400410
  Start of program headers:          64 (bytes into file)
  Start of section headers:          7160 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         36
  Section header string table index: 35

如果你對進程的內存布局不太了解冰垄,建議先去google一下蹬癌,再繼續(xù)閱讀。

函數虹茶,主要有函數棧幀逝薪、寄存器、指令(匯編)幾部分蝴罪。

2.1 函數棧幀

下圖是一個典型的x64函數棧幀布局董济,相對清晰的描繪了函數的棧幀布局。

image.png

當一個函數被調用要门,首先要做的是按照ABI規(guī)范虏肾,保存caller函數的相關寄存器廓啊;然后從ABI約定的寄存器和棧中獲取函數的參數,執(zhí)行函數代碼封豪;執(zhí)行完成后崖瞭,將返回值存入指定寄存器,恢復caller函數的相關寄存器撑毛,并從棧幀中獲取函數的返回地址书聚,ret到caller函數繼續(xù)執(zhí)行。

下圖對于%rsp和%rbp的一些細節(jié)做了補充(不要糾結return address屬于caller函數還是callee函數藻雌,這個只是角度問題雌续,不重要)。%rsp寄存器一般存放棧頂指針胯杭,顧名思義驯杜,指向函數棧的頂端;%rbp寄存器一般存放當前函數的棧幀起始地址做个,作為基址定位棧幀中的其他變量鸽心。

image.png

通過callq指令進入callee函數時,其%rsp指向caller函數的return address居暖。在樸素情形下顽频,接下來的幾條指令一般是將%rbp寄存器入棧,然后將%rbp的值設為%rsp寄存器中的地址太闺。在函數指定retq指令返回之前糯景,會將%rbp指針出棧,此時%rsp指針重新指向caller函數的return address省骂,調用retq指令返回caller函數蟀淮,完成一輪函數調用。

開啟了一定級別的編譯優(yōu)化后钞澳,上述提到的%rbp寄存器怠惶,往往不在作為棧幀頂部指針,而是作為通用寄存器來使用轧粟。這可以節(jié)省兩條指令策治,并多出一個通用寄存器。棧幀內尋址操作直接使用%rsp指針進行逃延。

The conventional use of %rbp as a frame pointer for the stack frame may be avoided by using
%rsp (the stack pointer) to index into the stack frame. This technique saves two instructions in
the prologue and epilogue and makes one additional general-purpose register (%rbp) available.

另一個優(yōu)化是%rsp览妖,在入棧轧拄、出棧變量時揽祥,需要兩條指令修改%rsp指針的值,這兩條指令同樣可以優(yōu)化檩电。這涉及到上圖最下面的一個區(qū)域red zone拄丰,先給出x64 System V ABI中對其的描述:

The 128-byte area beyond the location pointed to by %rsp is considered to
be reserved and shall not be modified by signal or interrupt handlers. Therefore,
functions may use this area for temporary data that is not needed across function
calls. In particular, leaf functions may use this area for their entire stack frame,
rather than adjusting the stack pointer in the prologue and epilogue. This area is
known as the red zone.

簡而言之府树,程序可以假定,這128字節(jié)一定不會被其他線程料按、信號奄侠、中斷等異步修改,所以可以直接通過%rsp使用這128字節(jié)的內存载矿,而不需要增減%rsp指針垄潮。但red zone在函數調用過程會被覆蓋,所以這一優(yōu)化闷盔,一般用于葉子函數(即不會調用其他函數的函數)弯洗。

2.2 寄存器

下圖是x64 System V ABI中對寄存器的一個概覽。

image.png

拋開寄存器的用途不談逢勾,從寄存器的易失性(上圖最后一列)角度考慮牡整,寄存器可以分成易失性寄存器和非易失性寄存器。

易失性寄存器可以看作臨時寄存器溺拱,在函數調用過程被調函數可以隨意修改覆蓋逃贝,不需要保留其值。

非易失性寄存器在函數調用過程后迫摔,返回原函數時沐扳,需要保證其值與原來一致,被調函數需要保存其原值句占,并負責恢復迫皱。

根據x64 System V ABI,x64的非易失性寄存器包括:

  • %rbp, %rbx, %r12, %r13, %r14, %r15
  • %rsp
  • mxcsr
  • x87 CW

x64 System V ABI規(guī)范中有關非易失性寄存器的相關描述:

Registers %rbp, %rbx and %r12 through %r15 “belong” to the calling function
and the called function is required to preserve their values.
In other words, a called function must preserve these registers’ values for its caller.
Remaining registers “belong” to the called function.

The control bits of the MXCSR register are callee-saved (preserved across
calls), while the status bits are caller-saved (not preserved). The x87 status word
register is caller-saved, whereas the x87 control word is callee-saved.

易失性寄存器一般為參數傳遞寄存器辖众、結果返回寄存器卓起、臨時寄存器等;非易失性寄存器包括棧指針寄存器凹炸、幀指針寄存器戏阅、規(guī)定的一些callee-save寄存器、以及幾個特殊寄存器啤它。

有關各寄存器的詳細用途奕筐,內容比較多,在此不做展開变骡,可以參考官方ABI規(guī)范离赫,描述的很詳細:System V Application Binary Interface AMD64 Architecture Processor Supplement Version 1.0

2.3 指令(匯編)

這里我們搞一個簡單的實例,用下面的程序來展示塌碌。

int fuck(int l, int r) {
    return l+r;
}

int main(){
    int a(1);
    int b(2);
    int c = fuck(a, b);
    return 0;
}

函數對應的匯編代碼如下(編譯時渊胸,沒有加任何優(yōu)化參數),代碼后附詳細描述台妆。

(gdb) disassemble main
Dump of assembler code for function main():
   0x0000000000400511 <+0>: push   %rbp
   0x0000000000400512 <+1>: mov    %rsp,%rbp
   0x0000000000400515 <+4>: sub    $0x10,%rsp
   0x0000000000400519 <+8>: movl   $0x1,-0x4(%rbp)
   0x0000000000400520 <+15>:    movl   $0x2,-0x8(%rbp)
   0x0000000000400527 <+22>:    mov    -0x8(%rbp),%edx
   0x000000000040052a <+25>:    mov    -0x4(%rbp),%eax
   0x000000000040052d <+28>:    mov    %edx,%esi
   0x000000000040052f <+30>:    mov    %eax,%edi
   0x0000000000400531 <+32>:    callq  0x4004fd <fuck(int, int)>
   0x0000000000400536 <+37>:    mov    %eax,-0xc(%rbp)
   0x0000000000400539 <+40>:    mov    $0x0,%eax
   0x000000000040053e <+45>:    leaveq
   0x000000000040053f <+46>:    retq
End of assembler dump.

在main函數中:

  1. 保存%rbp寄存器的值(幀指針)翎猛,然后將%rbp指向%rsp(當前的棧指針)
  2. 然后為局部變量a和b分配空間并初始化胖翰,將棧指針下移16字節(jié)(因為棧幀指針要求16字節(jié)對齊,所以雖然我們用了12字節(jié)切厘,仍然要分配16字節(jié))
  3. 因為a和b都是integer類型的變量萨咳,根據ABI,前六個整形參數使用%rdi,%rsi, %rdx, %rcx, %r8 and %r9進行傳參疫稿,所以這里將a和b的值培他,分別賦給%edi和%esi寄存器(e表示對應寄存器的低32bit位置)
  4. 因為沒用用到caller-save的寄存器,接下來直接通過callq指令調用將return address壓棧遗座,并轉到fuck函數執(zhí)行
  5. 執(zhí)行fuck函數內容靶壮,在fuck函數末尾通過retq指令將return address從棧中取出,并返回到main函數
  6. 從%eax寄存器中取出函數的返回結果员萍,存入變量c對應的椞诮担空間
  7. 在函數結束前,通過leaveq指令(等同于mov %rbp,%rsp然后pop %rbp)碎绎,將%rsp寄存器指向%rbp(當前的幀指針)螃壤,并從棧中彈出函數開頭保存的舊幀指針,賦給%rbp寄存器
  8. 調用retq返回caller函數
(gdb) disassemble fuck
Dump of assembler code for function fuck(int, int):
   0x00000000004004fd <+0>: push   %rbp
   0x00000000004004fe <+1>: mov    %rsp,%rbp
   0x0000000000400501 <+4>: mov    %edi,-0x4(%rbp)
   0x0000000000400504 <+7>: mov    %esi,-0x8(%rbp)
   0x0000000000400507 <+10>:    mov    -0x8(%rbp),%eax
   0x000000000040050a <+13>:    mov    -0x4(%rbp),%edx
   0x000000000040050d <+16>:    add    %edx,%eax
   0x000000000040050f <+18>:    pop    %rbp
   0x0000000000400510 <+19>:    retq
End of assembler dump.

在fuck函數中:

  1. 保存%rbp寄存器的值(幀指針)筋帖,然后將%rbp指向%rsp(當前的棧指針)
  2. 為a和b在棧上分配空間(這里因為fuck函數是leaf function奸晴,所以直接使用了%rsp棧指針下的128字節(jié)red zone,省略了增減%rsp寄存器的步驟)日麸,并從%edi和%esi寄存器中取出參數a和b的值寄啼,存入棧中
  3. 執(zhí)行運算,并將結果存入%eax寄存器(%rax寄存器用于存儲函數的第一個整型返回值)
  4. 從棧中恢復%rbp集群器的值
  5. 調用retq返回caller函數

上面的描述對于我們在棧幀和寄存器兩節(jié)代箭,提到的棧指針%rsp墩划、幀指針%rbp, red zone、局部變量分配嗡综、函數參數傳遞寄存器乙帮、函數結果返回寄存器等要點都有體現。

唯一不太明朗的是棧幀布局中的return address的位置极景,我們打個斷點察净,確認下這一點。

在程序的第2行盼樟,也就是fuck函數內部打一個斷點氢卡。按匯編代碼來看,此時晨缴,%rbp寄存器已經入棧译秦,棧頂第一個值為舊%rbp寄存器的值,棧頂第二個值應該為我們的函數返回地址,也就是main函數中callq指令下一條指令的地址:0x0000000000400536诀浪。

(gdb) disassemble /m fuck
Dump of assembler code for function fuck(int, int):
1   int fuck(int l, int r) {
   0x00000000004004fd <+0>: push   %rbp
   0x00000000004004fe <+1>: mov    %rsp,%rbp
   0x0000000000400501 <+4>: mov    %edi,-0x4(%rbp)
   0x0000000000400504 <+7>: mov    %esi,-0x8(%rbp)

2       return l+r;
   0x0000000000400507 <+10>:    mov    -0x8(%rbp),%eax
   0x000000000040050a <+13>:    mov    -0x4(%rbp),%edx
   0x000000000040050d <+16>:    add    %edx,%eax

3   }
   0x000000000040050f <+18>:    pop    %rbp
   0x0000000000400510 <+19>:    retq

End of assembler dump.
(gdb) b 2
Breakpoint 1 at 0x400507: file main.cc, line 2.
(gdb) r
Breakpoint 1, fuck (l=1, r=2) at main.cc:2
2       return l+r;

輸出下棧頂第二個8字節(jié)位置的值,為0x00400536延都,確實是main函數下一條指令的地址雷猪,與我們預期的一致。

(gdb) x /x $rsp+0x8
0x7fffffffe3b8: 0x00400536

至此晰房,函數棧幀和寄存器相關的內容已經全部理清求摇。

本節(jié)涉及的gdb命令:

# 打印函數匯編代碼
disassemble func_name

# 帶代碼打印函數匯編代碼
disassemble /m func_name

# 打印寄存器列表
info registers

# 上面命令的輸出不包括浮點寄存器和向量寄存器的內容,要打印所有寄存器殊者,使用
info all-registers

# 打印rsp寄存器的值(/x表示以十六進制打佑刖场)
p /x $rsp

# 打印指定地址處的值
x /x $rsp
x /x $rsp+0x8

3 協程實現

理解了前文的函數棧幀和寄存器,對于協程的實現猖吴,會感覺相當自然摔刁。本節(jié)會說明如何實現協程,并給出一個完備的協程切換函數的實現海蔽。

在寄存器一節(jié)我們知道了共屈,x64的非易失性寄存器。

在協程切換時党窜,我們需要保存的上下文拗引,除了這些非易失性寄存器,還有協程未來恢復后要執(zhí)行的下一條指令的地址幌衣,即next instruction矾削。如下:

  • %rbp, %rbx, %r12, %r13, %r14, %r15
  • %rsp
  • mxcsr
  • x87 CW
  • next instruction

下面是具體實現的協程切換函數:

  • coctx_save保存當前協程的上下文
  • coctx_resume恢復指定協程的上下文
  • coctx_swap存儲當前上下文,并切換到指定協程執(zhí)行豁护。

coctx_save和coctx_resume僅用于展示如何實現協程上下文的保存和恢復哼凯;并不能真實的用于協程切換。如果確實要使用楚里,需要處理一些細節(jié)挡逼,coctx_save執(zhí)行后悠就,ctx中保存的是調用coctx_save后的返回地址邀窃,但這個地址之后,往往會執(zhí)行coctx_resume以切換到其他協程撬呢,所以這里需要通過一些機制吝梅,保證在當前協程被重新切換回來時虱疏,不會再執(zhí)行一次coctx_resume。

coctx_swap通過將save和resume放到一個函數中苏携,規(guī)避了這個問題做瞪,保存的返回地址就是協程接下來應該執(zhí)行的代碼地址。

這里僅展示了協程切換函數的實現,如果需要實現一套能夠使用的協程庫装蓬,還需要維護協程的棧著拭、狀態(tài)等信息,并需要一個全局的調度器牍帚,某個協程執(zhí)行完成或者切出時儡遮,由調度器決定下一個執(zhí)行的協程。如果要在實際程序中使用暗赶,調度器往往要基于epoll實現鄙币,并要有定時器、hook系統(tǒng)調用等功能蹂随。

coctx_op.h

/* C++ needs to know that types and declarations are C, not C++.  */
#ifdef   __cplusplus
# define __M_BEGIN_DECLS  extern "C" {                                            
# define __M_END_DECLS }
#else
# define __M_BEGIN_DECLS
# define __M_END_DECLS
#endif

#ifndef _COCTX_OP_H
#define _COCTX_OP_H   1
__M_BEGIN_DECLS

typedef unsigned char      byte1_t;
typedef unsigned short     byte2_t;
typedef unsigned int       byte4_t;
typedef unsigned long long byte8_t;

typedef struct {
    byte8_t retaddr;
    byte8_t registers[7];
    byte4_t mxcsr;
    byte2_t x87cw;
    byte2_t padding; // not used, just padding explictly
} coctx_t;

extern int coctx_save(coctx_t *old_ctx);

extern int coctx_resume(coctx_t *new_ctx);

extern int coctx_swap(coctx_t *old_ctx, coctx_t *new_ctx);

__M_END_DECLS
#endif /* coctx_op.h  */

coctx_op.S

#define _retaddr 0
#define _rsp 8
#define _rbp 16
#define _rbx 24
#define _r12 32
#define _r13 40
#define _r14 48
#define _r15 56
#define _mxcsr 64
#define _x87 68

    .text
    .globl coctx_save 
    .type  coctx_save, @function
coctx_save:
    /* save retaddr */
    movq (%rsp), %r11
    movq %r11, _retaddr(%rdi)
    /* save rsp after pop retaddr */
    leaq 8(%rsp), %r11
    movq %r11, _rsp(%rdi)
    /* save callee-save registers */
    movq %rbp, _rbp(%rdi)
    movq %rbx, _rbx(%rdi)
    movq %r12, _r12(%rdi)
    movq %r13, _r13(%rdi)
    movq %r14, _r14(%rdi)
    movq %r15, _r15(%rdi)
    stmxcsr _mxcsr(%rdi)
    fnstcw  _x87(%rdi)
    /* return code 0 */
    movl $0, %eax
    /* pop retaddr from stack to %rip and return */
    retq
    .size   coctx_save, .-coctx_save

    .globl coctx_resume 
    .type  coctx_resume, @function
coctx_resume:
    /* resume rsp and retaddr*/
    movq _rsp(%rdi), %rsp
    movq _retaddr(%rdi), %r11
    pushq %r11
    /* resume callee-save registers */
    movq _rbp(%rdi), %rbp
    movq _rbx(%rdi), %rbx
    movq _r12(%rdi), %r12
    movq _r13(%rdi), %r13
    movq _r14(%rdi), %r14
    movq _r15(%rdi), %r15
    ldmxcsr _mxcsr(%rdi)
    fldcw  _x87(%rdi)
    /* return code 0 */
    movl $0, %eax
    /* pop retaddr from stack to %rip and return */
    retq
    .size   coctx_resume, .-coctx_resume

    .globl coctx_swap 
    .type  coctx_swap, @function
coctx_swap:
    /* save retaddr */
    movq (%rsp), %r11
    movq %r11, _retaddr(%rdi)
    /* save rsp after pop retaddr */
    leaq 8(%rsp), %r11
    movq %r11, _rsp(%rsp)
    /* save callee-save registers */
    movq %rbp, _rbp(%rdi)
    movq %rbx, _rbx(%rdi)
    movq %r12, _r12(%rdi)
    movq %r13, _r13(%rdi)
    movq %r14, _r14(%rdi)
    movq %r15, _r15(%rdi)
    stmxcsr _mxcsr(%rdi)
    fnstcw  _x87(%rdi)

    /* resume rsp and retaddr*/
    movq _rsp(%rsi), %rsp
    movq _retaddr(%rsi), %r11
    pushq %r11
    /* resume callee-save registers */
    movq _rbp(%rsi), %rbp
    movq _rbx(%rsi), %rbx
    movq _r12(%rsi), %r12
    movq _r13(%rsi), %r13
    movq _r14(%rsi), %r14
    movq _r15(%rsi), %r15
    ldmxcsr _mxcsr(%rsi)
    fldcw  _x87(%rsi)
    /* return code 0 */
    movl $0, %eax
    /* pop retaddr from stack to %rip and return */
    retq
    .size   coctx_swap, .-coctx_swap

注:有關x87 CW和mxcsr寄存器十嘿,我沒有做深入的研究,僅用一般情形下的參數作為其初始值進行初始化岳锁,并隨coctx切換而保存绩衷,想要了解這兩個寄存器的具體信息,可以參考下面這些文章激率。
https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol1/o_7281d5ea06a5b67a-197.html
https://docs.roguewave.com/en/totalview/2019/html/index.html#page/Reference_Guide/Intelx86MXSCRRegister_2.html
https://stackoverflow.com/questions/50308366/storing-the-x87-fpu-control-word
https://xem.github.io/minix86/manual/intel-x86-and-64-manual-vol1/o_7281d5ea06a5b67a-214.html
https://wiki.osdev.org/SSE

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末唇聘,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子柱搜,更是在濱河造成了極大的恐慌迟郎,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件聪蘸,死亡現場離奇詭異宪肖,居然都是意外死亡,警方通過查閱死者的電腦和手機健爬,發(fā)現死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門控乾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娜遵,你說我怎么就攤上這事蜕衡。” “怎么了设拟?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵慨仿,是天一觀的道長。 經常有香客問我纳胧,道長镰吆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任跑慕,我火速辦了婚禮万皿,結果婚禮上摧找,老公的妹妹穿的比我還像新娘。我一直安慰自己牢硅,他們只是感情好蹬耘,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著减余,像睡著了一般综苔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上佳励,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天休里,我揣著相機與錄音蛆挫,去河邊找鬼赃承。 笑死,一個胖子當著我的面吹牛悴侵,可吹牛的內容都是我干的瞧剖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼可免,長吁一口氣:“原來是場噩夢啊……” “哼抓于!你這毒婦竟也來了?” 一聲冷哼從身側響起浇借,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤捉撮,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后妇垢,有當地人在樹林里發(fā)現了一具尸體巾遭,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年闯估,在試婚紗的時候發(fā)現自己被綠了灼舍。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡涨薪,死狀恐怖骑素,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情刚夺,我是刑警寧澤献丑,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站侠姑,受9級特大地震影響阳距,放射性物質發(fā)生泄漏。R本人自食惡果不足惜结借,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一筐摘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦咖熟、人聲如沸圃酵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郭赐。三九已至,卻和暖如春确沸,著一層夾襖步出監(jiān)牢的瞬間捌锭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工罗捎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留观谦,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓桨菜,卻偏偏與公主長得像豁状,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子倒得,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353