Glibc 線程資源分配與釋放-----線程棧

標簽(空格分隔): Glibc, Thread媒峡, 線程棧


前言

前幾天自己寫了一段基于線程模型的網(wǎng)絡程序半哟,即主線程對每個連接請求創(chuàng)建一個工作線程,工作線程處理接下來所有業(yè)務氯檐。主線程希望工作線程完成后自動結束并釋放資源(請別問我這么 Low 的代碼用來干嘛糯崎,我就自己寫兩行來玩的 v)河泳。這個時候容易犯一個錯(大神請繞道)沃呢,那就是既不調(diào)join操作(因為主線程并不關注工作線程什么時候結束),也不將工作線程detach拆挥。這個錯誤的后果就是導致資源泄露薄霜,當然解決這個問題最簡單的方法是將工作線程設置為detach狀態(tài),系統(tǒng)就會自動完成資源的回收纸兔。顯然如果本篇博客只是想描述怎么解決這個問題黄锤,那么顯然沒有寫一篇文章的必要。本文真正要描述的是線程的資源是怎么自動釋放的食拜,這毫無疑問涉及到線程有哪些資源以及是如何管理的問題。 在此之前,需要說明一下,本文中描述所描述的適用于 Linux 系統(tǒng),x86_64 平臺为严,至于其它平臺是否適用我也不知道,哈哈。

背景

本節(jié)線程模型的內(nèi)容來自 Linux 線程模型的比較:LinuxThreads 和 NPTL悬钳。

對 Linux 有所了解就會知道滞诺, Linux 內(nèi)核并不能真正支持線程淋叶,而是通過進程間共享資源(內(nèi)存空間、文件等)的方式模擬線程凝赛,又被稱之為輕量級進程(LWP)直晨。最早 LinuxThreads 項目希望在用戶空間模擬對線程的支持。LinuxThreads 采用的是一對一的線程模型,為了解決信號處理慨丐、調(diào)度和進程間同步原語方面的問題捅暴, LinuxThreads 引入了一個管理線程,以滿足響應終止信號殺死整個進程瓶埋,完成線程結束后的內(nèi)存回收等任務挤悉。但是管理線程的引入也帶來系統(tǒng)伸縮性與性能的問題。并且讯柔, LinuxThreads 并不符合 POSIX 標準慈格。

NPTL 的出現(xiàn)改變了 LinuxThreads 尷尬的現(xiàn)狀美莫。不過赐劣,NPTL 不僅僅是一個用戶態(tài)的線程庫塌计,同時它也對系統(tǒng)內(nèi)核做了一定的要求魁衙, 因此有時在談論 Linux 內(nèi)核沒有線程概念時并不十分準確炮姨,例如為了支持 nptl 線程內(nèi)核 task_struct 是引入了 pid 與 tgid 的區(qū)別俄认, 因而準確的說法應該是內(nèi)核在調(diào)度的時候沒有線程的概念梭依,這都是題外話了。NPTL 作為 Linux 線程的新的實現(xiàn)部念,它移除了 LinuxThreads 中的管理線程,因而其在 NUMA 與 SMP 系統(tǒng)上更好的伸縮性與同步機制。此外缸匪,NPTL 是符合 POSIX 需求的翁狐, glibc2.3.5 開始就全面使用 NPTL 模型了,所在現(xiàn)在使用的 Linux 線程模型都是已經(jīng) NPTL 了凌蔬。 本文中描述的資源管理都是指 NPTL 模型中的資源管理露懒。更多的關于 LinuxThreads 與 NPTL 的內(nèi)容可以參考 Linux 線程模型的比較:LinuxThreads 和 NPTL闯冷。

此外需要說明一點, 無論是 LinuxThreads 還是 NPTL, 它們都使用了一對一的線程模型懈词,也即一個用戶態(tài)線程對應一個內(nèi)核態(tài)LWP蛇耀,線程的調(diào)度是由內(nèi)核完成的。

線程內(nèi)核資源

線程資源可以粗略地分為兩類坎弯,內(nèi)核資源(例如 task_struct)以及用戶態(tài)內(nèi)存資源(主要是線程棧)纺涤。在 Linux 平臺上,進程的內(nèi)核資源釋放是通過父進程使用 wait 系統(tǒng)調(diào)用完成的抠忘,如果父進程沒有調(diào)用該操作撩炊,就會出現(xiàn)僵尸進程,直到父進程結束崎脉。對于線程而言拧咳,Linux 還提供了內(nèi)核自動釋放的功能。參考 glibc-2.25 源碼描述 (sysdeps/unix/sysv/linux/createthread.c)

const int clone_flags = 
     (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
               | CLONE_SIGHAND | CLONE_THREAD
               | CLONE_SETTLS | CLONE_PARENT_SETTID
               | CLONE_CHILD_CLEARTID
               | 0);

上述代碼是 glibc 在調(diào)用 clone 創(chuàng)建線程時傳入的 flag 參數(shù)囚灼,在本文中我們需要注意三個參數(shù): CLONE_THREAD骆膝, CLONE_PARENT_SETTID,CLONE_CHILD_CLEARTID啦撮。后面兩個參數(shù)與后面講述線程棧的釋放有關谭网。 關于 CLONE_THREAD 參數(shù)的描述如下:

When a CLONE_THREAD thread terminates, the thread that created it using clone() is not sent a SIGCHLD (or other termination) signal; nor can the status of such a thread be obtained using wait(2).

這段說明,當使用 CREATE_THREAD 參數(shù)創(chuàng)建線程后赃春,此線程結束時不會發(fā)送 SIGCHLD 信號,而且不能使用 wait 獲得其狀態(tài)劫乱,其間接地說明了织中,內(nèi)核在某個時機自動釋放了該線程的內(nèi)核資源,而至于是否有其它方式獲得該線程的狀態(tài)衷戈,以后再討論這個問題狭吼。

線程棧的管理

對于多線程程序而言,堆資源是共享的殖妇,所有的線程都使用一個堆區(qū)刁笙。但是棧區(qū)是獨立的,每個線程都必須有自己的獨立的棧區(qū)谦趣,那么這些棧區(qū)是如何管理的呢疲吸?

線程棧的布局

在討論線程棧的布局的時候,涉及到一個十分重要的數(shù)據(jù)結構 struct pthread前鹅。它存儲了線程的相關信息擔任線程的管理功能摘悴。其數(shù)據(jù)結構比較復雜,再這里我們只展示幾個與本文討論內(nèi)容相關的變量舰绘,完整的內(nèi)容可以從 nptl/descr.h 文件中查看蹂喻。

/* This descriptor's link on the `stack_used' or `__stack_user' list.  */
list_t list;

/* Thread ID - which is also a 'is this thread descriptor (and
     therefore stack) used' flag.  */
pid_t tid;

list 用于將此結構體掛于雙鏈表中葱椭,這也是 Linux 內(nèi)核中十分常見的一種數(shù)據(jù)結構。 tid 存儲了線程的 ID 值口四。 從代碼中的注釋也可以看出 list 和 tid 都將用于線程棧的管理孵运。

struct pthread 是用于用戶態(tài)描述線程的數(shù)據(jù)結構,那么顯然每個 pthread 都唯一對應一個線程蔓彩。那么這個變量是存儲在哪里的掐松,答案是線程棧內(nèi)存塊的高地址空間中的(這里以 x86 棧向下增長的方式為例)。也就是說粪小,創(chuàng)建線程時為每個線程分配了一塊內(nèi)存大磺,然后這塊內(nèi)存一部分存儲了 pthread 變量,剩下的內(nèi)存才是真正的線程棧探膊。熟悉 Linux 內(nèi)核棧 結構的人會對這種方式比較熟悉杠愧。下圖展示了 x86 上線程棧的簡要布局:

線程棧布局圖

Talk is cheap, show me the code.

在創(chuàng)建線程的函數(shù) __pthread_create_2_1 中(nptl/pthrea_create.c),調(diào)用 ALLOCATE_STACK 宏用于分配線程棧逞壁,該宏即函數(shù) allcate_stack (nptl/allocatestack.c)流济。

struct pthread *pd;
...
/* The user provided some memory.  Let's hope it matches the
size...  We do not allocate guard pages if the user provided
the stack.  It is the user's responsibility to do this if it is wanted.  */
#if TLS_TCB_AT_TP
      pd = (struct pthread *) ((uintptr_t) stackaddr
                   - TLS_TCB_SIZE - adj);
#elif TLS_DTV_AT_TP
      pd = (struct pthread *) (((uintptr_t) stackaddr
                - __static_tls_size - adj)
                   - TLS_PRE_TCB_SIZE);
#endif

這段代碼是用戶自己提供內(nèi)存塊用作線程棧時的代碼,此處 stackaddr 指向所分配內(nèi)存塊的高地址腌闯。因此绳瘟,從代碼中可以看出來,無論從哪個分支編譯姿骏,pd 都指向該內(nèi)存塊高地址端一塊內(nèi)存糖声。換句話說在線程棧內(nèi)存塊中存儲了一個 pthread 對象。 至于這其中復雜的地址預留策略分瘦,例如對齊等蘸泻,就不在此細說,有興趣可以直接去閱讀代碼嘲玫。nptl 自動分配線程棧的處理代碼是類似的悦施,其注釋說明的已經(jīng)非常清楚了,如下所示:

/* Place the thread descriptor at the end of the stack.  */

#if TLS_TCB_AT_TP
      pd = (struct pthread *) ((char *) mem + size - coloring) - 1;
#elif TLS_DTV_AT_TP
      pd = (struct pthread *) ((((uintptr_t) mem + size - coloring
                    - __static_tls_size)
                    & ~__static_tls_align_m1)
                   - TLS_PRE_TCB_SIZE);
#endif

線程棧的管理結構

glibc 中使用了鏈表的形式來管理所有內(nèi)存棧去团,其中定義了兩個全局變量(nptl/allocatestack.c):

/* List of queued stack frames.  */
static LIST_HEAD (stack_cache);

/* List of the stacks in use.  */
static LIST_HEAD (stack_used);

而 LIST_HEAD 定義(include/list.h):

/* Define a variable with the head and tail of the list.  */
# define LIST_HEAD(name) \
  list_t name = { &(name), &(name) }

可以看出抡诞,上面的代碼定義了兩個鏈表頭, stack_cache 用于存放沒有使用的棧內(nèi)存土陪,而 stack_used 是正在使用的棧內(nèi)存塊昼汗。

前面提到 pthread 是存儲在分配的棧內(nèi)存塊中的,同時 pthread 中存在一個管理變量 list旺坠, 該變量即可將棧內(nèi)存塊掛載到不同的鏈表中乔遮。 如果內(nèi)存棧在使用過程中時,則內(nèi)存塊被放入 stack_used 隊列中; 當線程結束后取刃,該內(nèi)存塊被移入 stack_cache 隊列中蹋肮,可以供下次創(chuàng)建線程時直接使用出刷。

線程棧的分配

創(chuàng)建線程時,既可以由用戶自己分配內(nèi)存作為線程的棧區(qū)坯辩,也可以由庫自動為線程分配棧區(qū)馁龟。這里我們看一下線程分配棧內(nèi)存的過程。

在 allocatestack 函數(shù)中漆魔,當用戶沒有傳入棧區(qū)內(nèi)存地址時坷檩,庫首先會調(diào)用 get_cached_stack 函數(shù)嘗試從緩存中分配一塊內(nèi)存:

...

/* Search the cache for a matching entry.  We search for the
     smallest stack which has at least the required size.  Note that
     in normal situations the size of all allocated stacks is the
     same.  As the very least there are only a few different sizes.
     Therefore this loop will exit early most of the time with an
     exact match.  */
  list_for_each (entry, &stack_cache)
    {
      struct pthread *curr;

      curr = list_entry (entry, struct pthread, list);
      if (FREE_P (curr) && curr->stackblock_size >= size)
    {
      if (curr->stackblock_size == size)
        {
          result = curr;
          break;
        }

      if (result == NULL
          || result->stackblock_size > curr->stackblock_size)
        result = curr;
    }
    }

...

 /* Dequeue the entry.  */
  stack_list_del (&result->list);

其中主要邏輯很簡單,就是從 stack_cache 中找到一個空閑的棧內(nèi)存改抡, 其中 FREE_P 用于判斷是否空閑矢炼。事實上該宏就是判斷 pthread 結構中 tid 值是否小于或等于 0, 若是則該塊地址是空閑的阿纤。 并將該 內(nèi)存塊從列表中取出來句灌。

/* Check whether the stack is still used or not.  */
#define FREE_P(descr) ((descr)->tid <= 0)

如果沒有空閑的內(nèi)存塊,那么就需要調(diào)用 mmap 去重新分配內(nèi)存了欠拾。

mem = mmap (NULL, size, prot,
              MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

為線程成功獲得一塊內(nèi)存塊后胰锌,按前面分析會掛入 stack_used 列表中。這一步驟也是在 allocate_stack 函數(shù)中完成的藐窄,如下:

/* Prepare to modify global data.  */
lll_lock (stack_cache_lock, LLL_PRIVATE);

/* And add to the list of stacks in use.  */
stack_list_add (&pd->list, &stack_used);

lll_unlock (stack_cache_lock, LLL_PRIVATE);

如上资昧,即完成了線程棧的分配。

線程棧的釋放

線程棧的釋放我們需要搞清楚下面兩個問題:

  • 由誰釋放荆忍?
    對于非 detach 的線程格带,這個問題答案十分明顯,線程的棧區(qū)將由調(diào)用 Join 操作的線程來完成釋放东揣。但是對于 detach 線程践惑,這個問題就不是那么清楚了。 沒有其它線程來顯式的釋放棧區(qū)嘶卧,那么這個棧區(qū)的釋放只能交由線程自己來完成。也就是說凉袱,一個線程需要自己釋放自己正在使用的棧內(nèi)存塊芥吟。這聽上去就是胡扯嘛,正在用怎么能釋放呢专甩。但是仔細想一下钟鸵,如果棧區(qū)沒有使用了,那么線程已經(jīng)結束涤躲,它更沒辦法去釋放自己的棧內(nèi)存了棺耍。這時就需要 Linux 內(nèi)核的支持了。

  • 怎么釋放种樱?
    事實上蒙袍,線程釋放自己的棧區(qū)也并非真正意義上的釋放該內(nèi)存塊俊卤,而是將該內(nèi)存塊從 stack_used 移除,放入 stack_cache 鏈表中害幅, 同時修改標志位消恍,而將內(nèi)存真正的釋放操作推遲到其它線程中完成。釋放過程被分為如下步驟:(1) 將棧內(nèi)存塊從 stack_used 取下放入 stack_cache 列表中以现。(2) 釋放 stack_cache 中已結束線程的棧內(nèi)存塊狠怨。這里是否已結束是根據(jù) pthread tid 位是否被清零來業(yè)判斷的。(3) 線程結束時邑遏, 由內(nèi)核清除標志位(tid)佣赖, 這一步驟是由內(nèi)核完成的,當線程結束時记盒,內(nèi)核會自動將tid清零憎蛤,這就意味著一旦 tid 被清零就意味著線程已經(jīng)結束。需要注意:前兩步是由線程完成孽鸡,而每三步是由內(nèi)核來完成的蹂午。
    可以看出,針對自己的棧內(nèi)存彬碱,每個線程只是將其放入 stack_cache 鏈表中豆胸,而該內(nèi)存塊真正的釋放操作是由別的線程來完成的。所以會存在這樣一個時間段巷疼,線程正在使用過程中卻已經(jīng)被放到 stack_cache 鏈表中了晚胡,而線程真正結束的標志是由 Linux 內(nèi)核來完成的,只由 tid 被清零的棧內(nèi)存才可能被真正的釋放掉。

當用戶執(zhí)行完用戶指定的函數(shù)后旦袋,進入清理工作跌宛。整個線程的入口函數(shù)是 START_THREAD_DEFN(nptl/pthread_create.c)
,該宏定義為:

#def START_THREAD_DEFN \
    static void __attribute__ ((noreturn)) start_thread(void)

所以遣妥,其實該宏其實是一個函數(shù)的簽名。在這個函數(shù)中攀细,調(diào)用用戶提供的函數(shù)(pd->start_routine(pd->arg))箫踩。 下面 THREAD_SETMEM 宏的作用是執(zhí)行函數(shù)的結果存儲在 pthread 的 result 變量中。

/* Run the code the user provided.  */
#ifdef CALL_THREAD_FCT
      THREAD_SETMEM (pd, result, CALL_THREAD_FCT (pd));
#else
      THREAD_SETMEM (pd, result, pd->start_routine (pd->arg));
#endif

當用戶函數(shù)執(zhí)行完后谭贪, start_thread 函數(shù)會進行清理工作境钟。如果發(fā)現(xiàn)線程是 detach 狀態(tài),則會主動進行資源的釋放俭识,否則將等待 join 操作來釋放:

...
 /* If the thread is detached free the TCB.  */
  if (IS_DETACHED (pd))
    /* Free the TCB.  */
    __free_tcb (pd);
...

真正的釋放操作發(fā)生在 __deallocate_stack 函數(shù)中慨削,

void
internal_function
__deallocate_stack (struct pthread *pd)
{
  lll_lock (stack_cache_lock, LLL_PRIVATE);

  /* Remove the thread from the list of threads with user defined
     stacks.  */
  stack_list_del (&pd->list);

  /* Not much to do.  Just free the mmap()ed memory.  Note that we do
     not reset the 'used' flag in the 'tid' field.  This is done by
     the kernel.  If no thread has been created yet this field is
     still zero.  */
  if (__glibc_likely (! pd->user_stack))
    (void) queue_stack (pd);
  else
    /* Free the memory associated with the ELF TLS.  */
    _dl_deallocate_tls (TLS_TPADJ (pd), false);

  lll_unlock (stack_cache_lock, LLL_PRIVATE);
}


/* Add a stack frame which is not used anymore to the stack.  Must be called with the cache lock held.  */
static inline void
__attribute ((always_inline))
queue_stack (struct pthread *stack)
{
  /* We unconditionally add the stack to the list.  The memory may
     still be in use but it will not be reused until the kernel marks
     the stack as not used anymore.  */
  stack_list_add (&stack->list, &stack_cache);

  stack_cache_actsize += stack->stackblock_size;
  if (__glibc_unlikely (stack_cache_actsize > stack_cache_maxsize))
    __free_stacks (stack_cache_maxsize);
}

這一幕何其熟悉,首先將將內(nèi)存塊從 stack_used 鏈表中移除(stack_list_del (&pd->list););再調(diào)用 queue_stack 函數(shù)將其添加到 stack_cache 鏈表中缚态。 如上完成了第一步了磁椒。

glibc 允許緩存一部分內(nèi)存塊,只有當內(nèi)存塊的大小超過 stack_cache_maxsize 時才會釋放掉一部分內(nèi)存塊猿规,這也就是為什么會有分配階段的 get_cached_stack 的操作了衷快。 具體的釋放過程如下:

/* Free stacks until cache size is lower than LIMIT.  */
void
__free_stacks (size_t limit)
{
  /* We reduce the size of the cache.  Remove the last entries until
     the size is below the limit.  */
  list_t *entry;
  list_t *prev;

  /* Search from the end of the list.  */
  list_for_each_prev_safe (entry, prev, &stack_cache)
    {
      struct pthread *curr;

      curr = list_entry (entry, struct pthread, list);
      if (FREE_P (curr))
    {
      /* Unlink the block.  */
      stack_list_del (entry);

      /* Account for the freed memory.  */
      stack_cache_actsize -= curr->stackblock_size;

      /* Free the memory associated with the ELF TLS.  */
      _dl_deallocate_tls (TLS_TPADJ (curr), false);

      /* Remove this block.  This should never fail.  If it does
         something is really wrong.  */
      if (munmap (curr->stackblock, curr->stackblock_size) != 0)
        abort ();

      /* Maybe we have freed enough.  */
      if (stack_cache_actsize <= limit)
        break;
    }
    }
}

該函數(shù)過程就是就是遍歷 stack_cache 鏈表,從中判斷使用該內(nèi)存的線程是否結束(FREE_P)姨俩,即內(nèi)存塊中 pthread 的 tid 值是否被清零蘸拔,并釋放掉一部分內(nèi)存(munmap)。其中包含了 TLS 內(nèi)存釋放的操作环葵,本文中暫不做討論调窍。

當前線程結束時的 tid 操作是怎么完成的呢?希望你還記得前面說過的 clone 系統(tǒng)調(diào)用時傳入的 flag 參數(shù) CLONE_PARENT_SETTID 與 CLONE_CHILD_CLEARTID张遭。這兩個參數(shù)的說明如下:

CLONE_CHILD_CLEARTID (since Linux 2.5.49)
    Clear (zero) the child thread ID at the location ctid in child memory when the child exits, and do a wakeup on the futex at that address.  The address involved may be changed by the set_tid_address(2) system call.  This is used by threading libraries.
   
CLONE_PARENT_SETTID (since Linux 2.5.49)
    Store the child thread ID at the location ptid in the parent's memory.  (In Linux 2.5.32-2.5.48 there was a flag CLONE_SETTID that did this.)  The store operation completes before clone() returns control to user space.

簡單來說邓萨, CLONE_PARENT_SETTID 參數(shù)要求內(nèi)核在 clone 操作完成前將父進程空間的某個指定內(nèi)存位置填上子線程的 ID 值; CLONE_CHILD_CLEARTID 則要求內(nèi)核在線程結束后將子線程空間的某個指定內(nèi)存位置處的值清零菊卷。當然缔恳,針對線程而言都是在同一個內(nèi)存空間中。那么 glibc 在調(diào)用 clone 傳入的參數(shù)是怎么樣的呢洁闰? 如下歉甚,

if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
                    clone_flags, pd, &pd->tid, tp, &pd->tid)
            == -1))

這里 ARCH_CLONE 是 glibc 對底層做的一層封裝,它是直接使用的 ABI 接口扑眉,代碼是用匯編語言寫的纸泄,x86_64 平臺的代碼在 (sysdeps/unix/sysv/linux/x86_64/clone.S) 文件中, 感興趣可以自己去看腰素。你會發(fā)現(xiàn)其實就是就是調(diào)用了 linux 提供的 clone 接口聘裁。所以也可以直接參考 Linux 手冊上對 clone 函數(shù)的描述,此宏與 clone 參數(shù)是一樣的弓千。 我們可以看出此處衡便,函數(shù)兩次傳入的都子線程 pthread 中 tid 值,以讓內(nèi)核在線程開始時設置線程 ID 以及線程結束時清除其 ID 值洋访。這樣此線程的棧內(nèi)存塊就可以被隨后的線程釋放了砰诵。

綜上,我們就分析完了線程棧的釋放過程捌显。

除了本文描述的線程棧,線程資源還應該包括 TLS 等总寒。在本文中扶歪,我們并沒有分析這些資源是怎么管理的。這方面內(nèi)容留做以后的工作吧。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末善镰,一起剝皮案震驚了整個濱河市妹萨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炫欺,老刑警劉巖乎完,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異品洛,居然都是意外死亡树姨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門桥状,熙熙樓的掌柜王于貴愁眉苦臉地迎上來帽揪,“玉大人,你說我怎么就攤上這事辅斟∽” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵士飒,是天一觀的道長查邢。 經(jīng)常有香客問我,道長酵幕,這世上最難降的妖魔是什么扰藕? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮裙盾,結果婚禮上实胸,老公的妹妹穿的比我還像新娘。我一直安慰自己番官,他們只是感情好庐完,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著徘熔,像睡著了一般门躯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上酷师,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天讶凉,我揣著相機與錄音,去河邊找鬼山孔。 笑死懂讯,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的台颠。 我是一名探鬼主播褐望,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瘫里?” 一聲冷哼從身側響起实蔽,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎谨读,沒想到半個月后局装,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡劳殖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年铐尚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闷尿。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡塑径,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出填具,到底是詐尸還是另有隱情统舀,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布劳景,位于F島的核電站誉简,受9級特大地震影響,放射性物質發(fā)生泄漏盟广。R本人自食惡果不足惜闷串,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望筋量。 院中可真熱鬧烹吵,春花似錦、人聲如沸桨武。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽呀酸。三九已至凉蜂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間性誉,已是汗流浹背窿吩。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留错览,地道東北人纫雁。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像倾哺,于是被迫代替她去往敵國和親先较。 傳聞我的和親對象是個殘疾皇子携冤,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容