Linux Epoll 一網打盡

前言

epoll同樣是linux上的IO多路復用的一種實現,內核在實現時使用的數據結構相比select要復雜,但原理上并不復雜,我們力求在下面的描述里抽出主干,理清思路。

epoll也利用了上文中介紹過的Linux中的重要數據結構 wait queue, 有了上面select的基礎,其實epoll就沒那么復雜了。

通過閱讀本文 ,你除了可以了解到epoll的原理外,還可以搞清epoll存不存在驚群問題,LT和 ET模式在實現上有什么 區別,epoll和select相比有什么不同, epoll是如何處理多核并發的等等問題 。當然內容難免有疏漏之處,請大家多多指證。

主要數據結構

eventpoll

epoll操作最重要的數據結構,封裝了所有epoll操作涉及到的數據結構:

struct eventpoll {
    // 用于鎖定這個eventpoll數據結構,
    // 在用戶空間多線程操作這個epoll結構,比如調用epoll_ctl作add, mod, del時,用戶空間不需要加鎖保護
    // 內核用這個mutex幫你搞定
    struct mutex mtx;

    // 等待隊列,epoll_wait時如果當前沒有拿到有效的事件,將當前task加入這個等待隊列后作進程切換,等待被喚醒
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    // eventpoll對象在使用時都會對應一個struct file對象,賦值到其private_data,
    // 其本身也可以被 poll, 那也就需要一個wait queue
    wait_queue_head_t poll_wait;

    // 所有有事件觸發的被監控的fd都會加入到這個列表
    struct list_head rdllist;

    /* Lock which protects rdllist and ovflist */
    rwlock_t lock;

    // 所有被監控的fd使用紅黑樹來存儲
    struct rb_root_cached rbr;

    //  當將ready的fd復制到用戶進程中,會使用上面的 lock鎖鎖定rdllist,
    //  此時如果有新的ready狀態fd, 則臨時加入到 ovflist表示的單鏈表中
    struct epitem *ovflist;

    // 會autosleep準備的喚醒源
    struct wakeup_source *ws;

    /* The user that created the eventpoll descriptor */
    struct user_struct *user;

    // linux下一切皆文件,epoll實例被創建時,同時會創建一個file, file的private_data
    // 指向當前這個eventpoll結構
    struct file *file;

    /* used to optimize loop detection check */
    int visited;
    struct list_head visited_list_link;

#ifdef CONFIG_NET_RX_BUSY_POLL
    /* used to track busy poll napi_id */
    unsigned int napi_id;
#endif
};

我們將 上面結構體中的 poll_wait單提出來說一下,正如注釋中所說的,有了這個成員變量,那這個eventpoll對應的struct file也可以被poll,那我們也就可以將這個 epoll fd 加入到另一個epoll fd中,也就是實現了epoll的嵌套。

另外,在下面的講解中我們暫時不涉及epoll嵌套的問題。

epitem

由上面的介紹我們知道每一個被 epoll監控的句柄都會保存在eventpoll內部的紅黑樹上(eventpoll->rbr),ready狀態的句柄也會保存在eventpoll內部的一個鏈表上(eventpoll->rdllist), 實現時會將每個句柄封裝在一個結構中,即epitem:

struct epitem {
    // 用于構建紅黑樹
    union {
        /* RB tree node links this structure to the eventpoll RB tree */
        struct rb_node rbn;
        /* Used to free the struct epitem */
        struct rcu_head rcu;
    };

    // 用于將當前epitem鏈接到eventpoll->rdllist中
    struct list_head rdllink;

    //用于將當前epitem鏈接到"struct eventpoll"->ovflist這個單鏈表中
    struct epitem *next;

    /* The file descriptor information this item refers to */
    struct epoll_filefd ffd;

    /* Number of active wait queue attached to poll operations */
    int nwait;

    /* List containing poll wait queues */
    struct list_head pwqlist;

    // 對應的eventpoll對象
    struct eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct list_head fllink;

    /* wakeup_source used when EPOLLWAKEUP is set */
    struct wakeup_source __rcu *ws;

    // 需要關注的讀,寫事件等
    struct epoll_event event;
};
epoll_event

調用epoll_ctl時傳入的最后一個參數,主要是用來告訴內核需要其監控哪些事件。我們先來看其定義

  • 在kernel源碼中的定義:

    struct epoll_event {
      __poll_t events;
      __u64 data;
    } EPOLL_PACKED;
    
  • 在glic中的定義:

    typedef union epoll_data
    {
      void *ptr;
      int fd;
      uint32_t u32;
      uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event
    {
      uint32_t events;    /* Epoll events */
      epoll_data_t data;  /* User data variable */
    } __EPOLL_PACKED;
    

    乍一看,為什么這兩種定義不一樣,這怎么調用啊?

    我們先來看下glic中的定義,它將epoll_event.data定義為epoll_data_t類型,而epoll_data_t被定義為union類型,其能表示的最大值類型為uinit64_t,這與kernel源碼中的定義__u64 data是一致的,其實這個data成員變量部分kernel在實現時根本不會用到,它作為user data在epoll_wait返回時通過epoll_event原樣返回到用戶空間,聲明成 union對使用者來說自由發揮的空間就大多了,如果使用fd,你可以把當前要監控的socket fd賦值給它,如果使用void* ptr,那你可以將任意類型指針給它......

主要函數

epoll_create

創建一個epoll的實例,Linux里一切皆文件,這里也不例外,返回一個表示當前epoll實例的文件描述符,后續的epoll相關操作,都需要傳入這個文件描述符。

其實現位于 fs/eventpoll.cSYSCALL_DEFINE1(epoll_create, int, size), 具體實現 static int do_epoll_create(int flags):

static int do_epoll_create(int flags)
{
    int error, fd;
    struct eventpoll *ep = NULL;
    struct file *file;

    /* Check the EPOLL_* constant for consistency.  */
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);

   // 目前flags只支持 EPOLL_CLOEXEC 這一種,如果傳入了其他的,返回錯誤
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;
    /*
     * Create the internal data structure ("struct eventpoll").
     */
    error = ep_alloc(&ep);
    if (error < 0)
        return error;
    /*
     * Creates all the items needed to setup an eventpoll file. That is,
     * a file structure and a free file descriptor.
     */
    fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
    if (fd < 0) {
        error = fd;
        goto out_free_ep;
    }
    file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep,
                 O_RDWR | (flags & O_CLOEXEC));
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto out_free_fd;
    }
    ep->file = file;
    fd_install(fd, file);
    return fd;

out_free_fd:
    put_unused_fd(fd);
out_free_ep:
    ep_free(ep);
    return error;
}

主要分以下幾步:

  • 校驗傳入參數flags, 目前僅支持 EPOLL_CLOEXEC 一種,如果是其他的,立即返回失敗;

  • 調用ep_alloc, 創建 eventpoll結構體;

  • 在當前task的打開文件打描述符表中獲取一個fd;

  • 使用 anon_inode_getfile創建一個 匿名inode的struct file, 其中會使用 file->private_data = priv將第二步創建的eventpoll對象賦值給struct fileprivate_data 成員變量。

    關于匿名inode作者也沒有找到太多的資料,可以簡單理解為其沒有對應的dentry, 在目錄下ls看不到這類文件 ,其被close后會自動刪除,比如 使用O_TMPFILE選項來打開的就是這類文件;

  • 將第三步中的fd和第四步中的struct file結合起來,放入當前task的打開文件描述符表中;

epoll_ctl

從一個fd添加到一個eventpoll中,或從中刪除,或如果此fd已經在eventpoll中,可以更改其監控事件。

我們在下面的源碼中添加了必要的注釋:

SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
        struct epoll_event __user *, event)
{
    int error;
    int full_check = 0;
    struct fd f, tf;
    struct eventpoll *ep;
    struct epitem *epi;
    struct epoll_event epds;
    struct eventpoll *tep = NULL;

    error = -EFAULT;
    // ep_op_has_event() 其實就是判斷當前的op不是 EPOLL_CTL_DEL操作, 
    // 如果 是EPOLL_CTL_ADD 或  EPOLL_CTL_MOD,
    //  將event由用戶態復制到內核態
    // 
    if (ep_op_has_event(op) &&
        copy_from_user(&epds, event, sizeof(struct epoll_event)))
        goto error_return;

    error = -EBADF;
    f = fdget(epfd);
    if (!f.file)
        goto error_return;

    /* Get the "struct file *" for the target file */
    tf = fdget(fd);
    if (!tf.file)
        goto error_fput;

    // 被添加的fd必須支持poll方法
    error = -EPERM;
    if (!file_can_poll(tf.file))
        goto error_tgt_fput;

    /*
     Linux提供了autosleep的電源管理功能
     如果當前系統支持 autosleep功能,支持休眠,
     那么我們 允許用戶傳入EPOLLWAKEUP標志;
     如果當前系統不支持這樣的電源管理功能,但用戶還是傳入了EPOLLWAKEUP標志,
     那么我們將此標志從flags中去掉
    */
    if (ep_op_has_event(op))
        ep_take_care_of_epollwakeup(&epds);

    error = -EINVAL;
    // epoll不能自己監控自己
    if (f.file == tf.file || !is_file_epoll(f.file))
        goto error_tgt_fput;

    /*
    EPOLLEXCLUSIVE是為了解決某個socket有事件發生時的驚群問題
   所謂驚群,簡單講就是把一個socket fd加入到多個epoll中時,如果此socket有事件發生,
   會同時喚醒多個在此socket上等待的task
   
   目 前僅允許在EPOLL_CTL_ADD操作時傳入EPOLLEXCLUSIVE標志,且傳入此標志時不允許
   epoll嵌套監聽
    */
    if (ep_op_has_event(op) && (epds.events & EPOLLEXCLUSIVE)) {
        if (op == EPOLL_CTL_MOD)
            goto error_tgt_fput;
        if (op == EPOLL_CTL_ADD && (is_file_epoll(tf.file) ||
                (epds.events & ~EPOLLEXCLUSIVE_OK_BITS)))
            goto error_tgt_fput;
    }

    /*
     * At this point it is safe to assume that the "private_data" contains
     * our own data structure.
     */
    ep = f.file->private_data;
    
    /*
    這里處理將一個epoll fd添加到當前epoll的嵌套情況,
    特別是要檢測是否有環形epoll監聽情況,類似于A監聽B, B又監聽A
    我們先略過
    */

    // 查看對應的epitme是否已經在紅黑樹上存在,即是否已經添加過
    epi = ep_find(ep, tf.file, fd);

    error = -EINVAL;
    switch (op) {
    case EPOLL_CTL_ADD:
        if (!epi) {
            epds.events |= EPOLLERR | EPOLLHUP;
            // 將當前fd加入紅黑樹,我們在下面重點講
            error = ep_insert(ep, &epds, tf.file, fd, full_check);
        } else
            error = -EEXIST;
        if (full_check)
            clear_tfile_check_list();
        break;
    case EPOLL_CTL_DEL:
        if (epi)
            error = ep_remove(ep, epi);
        else
            error = -ENOENT;
        break;
    case EPOLL_CTL_MOD:
        if (epi) {
            if (!(epi->event.events & EPOLLEXCLUSIVE)) {
                epds.events |= EPOLLERR | EPOLLHUP;
                error = ep_modify(ep, epi, &epds);
            }
        } else
            error = -ENOENT;
        break;
    }
    ......
    return error;
}

這個函數主要作以下幾件事:

  • 先將epoll_event(上面已有介紹,保存著需要監控的事件)從用戶空間復制到內核空間。

    我們看來針對某個socket, 這種用戶空間到內核空間的復制只需一次,不像select,每次調用都要復制;

  • 先由傳入的epoll fd和被監聽的socket fd獲取到其對應的文件句柄 struct file,針對文件句柄和傳入的flags作邊界條件檢測;

  • 針對epoll嵌套用法,作單獨檢測,檢測是否有環形epoll監聽情況,類似于A監聽B, B又監聽A, 這部分我們先略過;

  • 針對 EPOLL_CTL_ADD EPOLL_CTL_DEL EPOLL_CTL_MOD分別作處理。

ep_insert

這個函數是真正將待監聽的fd加入到epoll中去。下面我們將這個函數的實現拆解,分段來看一下其是如何實現的。

  • 作max_user_watches檢驗

      user_watches = atomic_long_read(&ep->user->epoll_watches);
      if (unlikely(user_watches >= max_user_watches))
          return -ENOSPC;
    

    內核對系統中所有(是所有,所有使用了epoll的進程)使用epoll監聽fd所消耗的內存作了限制, 且這個限制是針對當前linux user id的。32位系統為了監控注冊的每個文件描述符大概占90字節,64位系統上占160字節。

    可以通過 /proc/sys/fs/epoll/max_user_watches來查看和設置 。

    默認情況下每個用戶下epoll為注冊文件描述符可用的內存是內核可使用內存的1/25。

  • 初始化epitem

    這個epitem前面說過,它會被掛在epoll的紅黑樹上。

      if (!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
          return -ENOMEM;
    
      /* Item initialization follow here ... */
      INIT_LIST_HEAD(&epi->rdllink);
      INIT_LIST_HEAD(&epi->fllink);
      INIT_LIST_HEAD(&epi->pwqlist);
      epi->ep = ep;
      ep_set_ffd(&epi->ffd, tfile, fd);
      epi->event = *event;
      epi->nwait = 0;
      epi->next = EP_UNACTIVE_PTR;
      if (epi->event.events & EPOLLWAKEUP) {
          error = ep_create_wakeup_source(epi);
          if (error)
              goto error_create_wakeup_source;
      } else {
          RCU_INIT_POINTER(epi->ws, NULL);
      }
    

    a. 這個epitem里會保存監控的fd及其事件,所屬的eventpoll等;

    b. 如果events里設置了EPOLLWAKEUP, 還需要為autosleep創建一個喚醒源 ep_create_wakeup_source

  • 獲取當前被監聽的fd上是否有感 興趣的事件發生,同時生成新的eppoll_entry對象并添加到被監聽的socket fd的等待隊列中

        epq.epi = epi;
      init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
      revents = ep_item_poll(epi, &epq.pt, 1);
    

    下面是 ep_item_poll的實現:

    static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
                   int depth)
    {
      struct eventpoll *ep;
      bool locked;
    
      pt->_key = epi->event.events;
      if (!is_file_epoll(epi->ffd.file))
          return vfs_poll(epi->ffd.file, pt) & epi->event.events;
    }
    

    如果你讀過上面的select分析部分,就會看到一個熟悉的身影 vfs_poll, 它會調用 ep_ptable_queue_proc將當前被監聽的socket fd加入到等待隊列中:

    static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
                   poll_table *pt)
    {
      struct epitem *epi = ep_item_from_epqueue(pt);
      struct eppoll_entry *pwq;
    
      if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
          init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
          pwq->whead = whead;
          pwq->base = epi;
          if (epi->event.events & EPOLLEXCLUSIVE)
              add_wait_queue_exclusive(whead, &pwq->wait);
          else
              add_wait_queue(whead, &pwq->wait);
          list_add_tail(&pwq->llink, &epi->pwqlist);
          epi->nwait++;
      } else {
          /* We have to signal that an error occurred */
          epi->nwait = -1;
      }
    }
    

    這里有兩點比較重要:

    a. init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);如果這個被監聽的socket上有事件發生,這個回調 ep_poll_callback將被調用, 我們后面會講這個回調里作了哪些事情, 這個回調很重要;

    b. 如果設置了 EPOLLEXCLUSIVE, 將使用add_wait_queue_exclusive添加到等待隊列。意思是說,如果一個socket fd被添加到了多個epoll中進行監控,設置了這個參數后,這個fd上有事件發生時,只會喚醒被添加到的第一個epoll里,避免驚群。

  • 添加到 epoll的紅黑樹上

    ep_rbtree_insert(ep, epi);
    
  • 如果上面調用 ep_item_poll時,立即返回了準備好的事件,我們這里要作喚醒的操作

      if (revents && !ep_is_linked(epi)) {
          list_add_tail(&epi->rdllink, &ep->rdllist);
          ep_pm_stay_awake(epi);
    
          /* Notify waiting tasks that events are available */
          if (waitqueue_active(&ep->wq))
              wake_up(&ep->wq);
          if (waitqueue_active(&ep->poll_wait))
              pwake++;
      }
    
        .....
          if (pwake)
          ep_poll_safewake(&ep->poll_wait);
    

    a. 將當前 epi加入到eventpoll的rdllist中;

    b. 如果當前eventpoll處于wait狀態,就喚醒它;

    c. 如果當前的eventpoll被嵌套地加入到了另外的poll中,且處于wait狀態,就喚醒它。

ep_poll_callback

被監聽的socket fd上有事件發生時,這個回調被觸發, 然后喚醒epoll_wait被調用時加入到eventpoll等待隊列中的task,下面會放張圖來解釋其功能。

ep_events_available

我們首先來看一下函數ep_events_available,它的功能是檢測當前epoll上是否已經收集了有效的事件:

static inline int ep_events_available(struct eventpoll *ep)
{
    return !list_empty_careful(&ep->rdllist) ||
        READ_ONCE(ep->ovflist) != EP_UNACTIVE_PTR;
}

按這個邏輯只有rdllist不為空或者ovflist != EP_UNACTIVE_PTR,那么就有有效的事件,前一個條件好理解,ovflist這個我們先在這里埋個坑,后面我們來填它~

ep_poll

這個函數是epoll_wait在內核里的具體實現。我們把它的實現分解來看。

  • 準備好超時時間

    if (timeout > 0) {
          struct timespec64 end_time = ep_set_mstimeout(timeout);
    
          slack = select_estimate_accuracy(&end_time);
          to = &expires;
          *to = timespec64_to_ktime(end_time);
      } else if (timeout == 0) {
          timed_out = 1;
    
          write_lock_irq(&ep->lock);
          eavail = ep_events_available(ep);
          write_unlock_irq(&ep->lock);
    
          goto send_events;
      }
    

    a. 如果用戶設置了超時時間, 作相應的初始化;

    b. 如果timeout == 0, 表時此次調用立即返回, 此時首先獲取當前是否已有有效的事件ready, 然后goto 到send_events, 這部分是將有效的events復制到用戶空間,我們后面會詳述。

  • 將當前task加入到此eventpoll的等待隊列中

    if (!waiter) {
          waiter = true;
          init_waitqueue_entry(&wait, current);
    
          spin_lock_irq(&ep->wq.lock);
          __add_wait_queue_exclusive(&ep->wq, &wait);
          spin_unlock_irq(&ep->wq.lock);
      }
    

    我們前面在select部分已經介紹過wait queue, 這里就是將當前task加入到eventpoll的等待隊列,接下來當前task將會被調度走,然后等待從eventpoll的等待隊列中被喚醒。這里用了__add_wait_queue_exclusive, 是說針對同一個eventpoll, 可能在不同的進程(線程)調用epoll_wait, 此時eventpoll的等待隊列里將會有多個task, 為避免驚群,我們每次只喚醒一個task。

  • 無限循環體

    for (;;) {
          set_current_state(TASK_INTERRUPTIBLE);
    
          if (fatal_signal_pending(current)) {
              res = -EINTR;
              break;
          }
    
          eavail = ep_events_available(ep);
          if (eavail)
              break;
          if (signal_pending(current)) {
              res = -EINTR;
              break;
          }
    
          if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) {
              timed_out = 1;
              break;
          }
      }
    

    這個無限循環體退出的條件:

    a. 有signal發生,被中斷會退出;

    b. 有ready的事件,會退出;

    c. 用戶設置的超時時間到達,會退出;

    否則當前 task將被 schedule_hrtimeout_range調度走。

  • 有ready的事件復制到用戶空間

      if (!res && eavail &&
          !(res = ep_send_events(ep, events, maxevents)) && !timed_out)
          goto fetch_events;
    
  • ep_pollep_poll_callback的處理有些復雜,我下面用張圖來說明一下:

epoll.png

幾點說明如下:

  1. 實際中可能同時有多個socket有事件到來,此時ep_poll_callback會并發被調用,因此將epi添中到eventpoll->rdllik時,均采用原子操作;

  2. ep_scan_ready_list中一旦開始向用戶空間復制events, eventpoll->rdllink就不能再有新的添加,此時如果ep_poll_callback被調用,當前的epi會被添加到eventpoll->ovflist中, ovflist是個單鏈表,這個添加操作很有意思,每次新的epi都被原子添加到鏈接頭:

    static inline bool chain_epi_lockless(struct epitem *epi)
    {
       struct eventpoll *ep = epi->ep;
    
       /* Check that the same epi has not been just chained from another CPU */
       if (cmpxchg(&epi->next, EP_UNACTIVE_PTR, NULL) != EP_UNACTIVE_PTR)
           return false;
    
       /* Atomically exchange tail */
       epi->next = xchg(&ep->ovflist, epi);
    
       return true;
    }
    
  3. ep_send_events_proc才是真正實現將events復制到用戶空間。

    雖然當socket fd有事件到來時,會通過ep_poll_callback來喚醒epoll_wait所在的task, 后者遍歷rdllist即可,但在遍歷時,還是通過ep_item_poll(內部會調用vs_poll, 最終調用到tcp_poll)來獲取關注的事件是否發生,所有poll機制很重要;

  4. 對于水平觸發方式,在首次調用ep_item_poll后,會再次將這個epi加入到eventpoll->rdllist這個就緒列表中,這會導致兩種情況出現:

    a. 如果針對同一個eventpoll同時調用了多個 epoll_wait, 此時另一個調用epoll_wait的task將被喚醒,這不能被稱之為epoll_wait的驚群,反而是并發處理的體現;

    b. 如果只有一個epoll_wait, 那下次這個epoll_wait再次被調用時,不會進入到上面的無限循化邏輯,也不會被調度走,而是直接又一次進入到ep_send_events中,直到在這個socket fd上poll不到關注的事情,它就不會再被加入到rdllist中。你可以將這個水平觸發方式理解成是完全輪詢的一種實現;

    聰明的你讀到這里一定會發現對于水平觸發,即使是socket fd上已經沒有關注的事件發生了,它還是要多用一次poll來確認,這是一處性能損失的點,但監聽的socket少的話這也不是什么大問題。

總結

這里先講上點上面沒有提及的內容

  • epoll模型中ep_poll執行時如果當前沒有有效的events,當前task會被調度走,后續有socket fd有事件發生,ep_poll_callback被調用,將當前的socket fd 添加到rdllist中,再喚醒前面的task, 然后ep_poll再一次被調度執行,鎖定住rdllist后開始向用戶空間復制,由次可以看出來每次epoll_wait返回的events就是從第一次ep_poll_callback調用執行喚醒到ep_poll所有task被真正喚醒開始執行這段時間內,所收集中的socket fd。如果同時有大量的socket fd是活躍狀態,那么這里可能需要多次調用epoll_wait,效率上是個問題;
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容