深入理解相機七(V4L2)

和你一起終身學習,這里是程序員 Android

經典好文推薦,通過閱讀本文,您將收獲以下知識點:

一、概覽
二、流程簡介
三、關鍵結構體
四、模塊初始化
五、處理用戶空間請求

相機驅動層–V4L2框架解析

一、概覽

相機驅動層位于HAL Moudle與硬件層之間,借助linux內核驅動框架,以文件節點的方式暴露接口給用戶空間,讓HAL Module通過標準的文件訪問接口,從而能夠將請求順利地下發到內核中,而在內核中,為了更好的支持視頻流的操作,早先提出了v4l視頻處理框架,但是由于操作復雜,并且代碼無法進行較好的重構,難以維護等原因,之后便衍生出了v4l2框架。

按照v4l2標準,它將一個數據流設備抽象成一個videoX節點,從屬的子設備都對應著各自的v4l2_subdev實現,并且通過media controller進行統一管理,整個流程復雜但高效,同時代碼的擴展性也較高。

而對高通平臺而言,高通整個內核相機驅動是建立在v4l2框架上的,并且對其進行了相應的擴展,創建了一個整體相機控制者的CRM,它以節點video0暴露給用戶空間,主要用于管理內核中的Session、Request以及與子設備,同時各個子模塊都實現了各自的v4l2_subdev設備,并且以v4l2_subdev節點暴露給用戶空間,與此同時,高通還創建了另一個video1設備Camera SYNC,該設備主要用于同步數據流,保證用戶空間和內核空間的buffer能夠高效得進行傳遞。

再往下與相機驅動交互的便是整個相機框架的最底層Camera Hardware了,驅動部分控制著其上下電邏輯以及寄存器讀取時序并按照I2C協議進行與硬件的通信,和根據MIPI CSI協議傳遞數據,從而達到控制各個硬件設備,并且獲取圖像數據的目的。

V4L2英文是Video for Linux 2,該框架是誕生于Linux系統,用于提供一個標準的視頻控制框架,其中一般默認會嵌入media controller框架中進行統一管理,v4l2提供給用戶空間操作節點,media controller控制對于每一個設備的枚舉控制能力,于此同時,由于v4l2包含了一定數量的子設備,而這一系列的子設備都是處于平級關系,但是在實際的圖像采集過程中,子設備之間往往還存在著包含于被包含的關系,所以為了維護并管理這種關系,media controller針對多個子設備建立了的一個拓撲圖,數據流也就按照這個拓撲圖進行流轉。

二、流程簡介

整個對于v4l2的操作主要包含了如下幾個主要流程:


程序員Android 轉于網絡

a) 打開video設備
在需要進行視頻數據流的操作之前,首先要通過標準的字符設備操作接口open方法來打開一個video設備,并且將返回的字符句柄存在本地,之后的一系列操作都是基于該句柄,而在打開的過程中,會去給每一個子設備的上電,并完成各自的一系列初始化操作。

b) 查看并設置設備
在打開設備獲取其文件句柄之后,就需要查詢設備的屬性,該動作主要通過ioctl傳入VIDIOC_QUERYCAP參數來完成,其中該系列屬性通過v4l2_capability結構體來表達,除此之外,還可以通過傳入VIDIOC_ENUM_FMT來枚舉支持的數據格式,通過傳入VIDIOC_G_FMT/VIDIOC_S_FMT來分別獲取和獲取當前的數據格式,通過傳入VIDIOC_G_PARM/VIDIOC_S_PARM來分別獲取和設置參數。

c) 申請幀緩沖區
完成設備的配置之后,便可以開始向設備申請多個用于盛裝圖像數據的幀緩沖區,該動作通過調用ioctl并且傳入VIDIOC_REQBUFS命令來完成,最后將緩沖區通過mmap方式映射到用戶空間。

d) 將幀緩沖區入隊
申請好幀緩沖區之后,通過調用ioctl方法傳入VIDIOC_QBUF命令來將幀緩沖區加入到v4l2 框架中的緩沖區隊列中,靜等硬件模塊將圖像數據填充到緩沖區中。

e) 開啟數據流
將所有的緩沖區都加入隊列中之后便可以調用ioctl并且傳入VIDIOC_STREAMON命令,來通知整個框架開始進行數據傳輸,其中大致包括了通知各個子設備開始進行工作,最終將數據填充到V4L2框架中的緩沖區隊列中。

f) 將幀緩沖區出隊
一旦數據流開始進行流轉了,我們就可以通過調用ioctl下發VIDIOC_DQBUF命令來獲取幀緩沖區,并且將緩沖區的圖像數據取出,進行預覽、拍照或者錄像的處理,處理完成之后,需要將此次緩沖區再次放入V4L2框架中的隊列中等待下次的圖像數據的填充。

整個采集圖像數據的流程現在看來還是比較簡單的,接口的控制邏輯很清晰,主要原因是為了提供給用戶的接口簡單而且抽象,這樣方便用戶進行集成開發,其中的大部分復雜的業務處理都被V4L2很好的封裝了,接下來我們來詳細了解下V4L2框架內部是如何表達以及如何運轉的。

三、關鍵結構體

程序員Android 轉于網絡

從上圖不難看出,v4l2_device作為頂層管理者,一方面通過嵌入到一個video_device中,暴露video設備節點給用戶空間進行控制,另一方面,video_device內部會創建一個media_entity作為在media controller中的抽象體,被加入到media_device中的entitie鏈表中,此外,為了保持對所從屬子設備的控制,內部還維護了一個掛載了所有子設備的subdevs鏈表。

而對于其中每一個子設備而言,統一采用了v4l2_subdev結構體來進行描述,一方面通過嵌入到video_device,暴露v4l2_subdev子設備節點給用戶空間進行控制,另一方面其內部也維護著在media controller中的對應的一個media_entity抽象體,而該抽象體也會鏈入到media_device中的entities鏈表中。

通過加入entities鏈表的方式,media_device保持了對所有的設備信息的查詢和控制的能力,而該能力會通過media controller框架在用戶空間創建meida設備節點,將這種能力暴露給用戶進行控制。

由此可見,V4L2框架都是圍繞著以上幾個主要結構體來進行的,接下來我們依次簡單介紹下:
v4l2_device 源碼如下:

struct v4l2_device {
    struct device *dev;
#if defined(CONFIG_MEDIA_CONTROLLER)
    struct media_device *mdev;                                                                                                                         
#endif
    struct list_head subdevs;
    spinlock_t lock;
    char name[V4L2_DEVICE_NAME_SIZE];
    void (*notify)(struct v4l2_subdev *sd,
        unsigned int notification, void *arg);
    struct v4l2_ctrl_handler *ctrl_handler;
    struct v4l2_prio_state prio;
    struct kref ref;
    void (*release)(struct v4l2_device *v4l2_dev);
};

該結構體代表了一個整個V4L2設備,作為整個V4L2的頂層管理者,內部通過一個鏈表管理著整個從屬的所有的子設備,并且如果將整個框架放入media conntroller進行管理,便在初始化的時候需要將創建成功的media_device賦值給內部變量 mdev,這樣便建立了于與media_device的聯系,驅動通過調用v4l2_device_register方法和v4l2_device_unregister方法分別向系統注冊和釋放一個v4l2_device。

v4l2_subdev源碼如下:

struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)
    struct media_entity entity;
#endif
    struct list_head list;
    struct module *owner;
    bool owner_v4l2_dev;
    u32 flags;
    struct v4l2_device *v4l2_dev;
    const struct v4l2_subdev_ops *ops;
    const struct v4l2_subdev_internal_ops *internal_ops;
    struct v4l2_ctrl_handler *ctrl_handler;
    char name[V4L2_SUBDEV_NAME_SIZE];
    u32 grp_id;
    void *dev_priv;
    void *host_priv;
    struct video_device *devnode;
    struct device *dev;
    struct fwnode_handle *fwnode;
    struct list_head async_list;
    struct v4l2_async_subdev *asd;
    struct v4l2_async_notifier *notifier;
    struct v4l2_subdev_platform_data *pdata;
};

該結構體代表了一個子設備,每一個子設備都需要在初始化的時候掛載到一個總的v4l2_device上,并且將該v4l2設備賦值給內部的v4l2_dev變量,之后將自身加入到v4l2_device中的子設備鏈表中進行統一管理,這種方式提高了遍歷訪問所有子設備的效率,同時為了表達不同硬件模塊的特殊操作行為,v4l2_subdev定義了一個v4l2_subdev_ops 結構體來進行定義,其實現交由不同的硬件模塊來具體完成。其中如果使能了CONFIG_MEDIA_CONTROLLER宏,便會在media_controller中生成一個對應的media_entity,來代表該子設備,而該entity便會存入子設備結構體中的entity變量中,最后,如果需要創建一個設備節點的話,通過video_device調用標準API接口進行實現,而相應的video_device便會存入其內部devnode變量中。

video_device源碼如下:

struct video_device
{
#if defined(CONFIG_MEDIA_CONTROLLER)
    struct media_entity entity;
    struct media_intf_devnode *intf_devnode;
    struct media_pipeline pipe;
#endif
    const struct v4l2_file_operations *fops;

    u32 device_caps;

    /* sysfs */
    struct device dev;
    struct cdev *cdev;

    struct v4l2_device *v4l2_dev;
    struct device *dev_parent;

    struct v4l2_ctrl_handler *ctrl_handler;

    struct vb2_queue *queue;

    struct v4l2_prio_state *prio;

    /* device info */
    char name[32];
    int vfl_type;
    int vfl_dir;
    int minor;
    u16 num;
    unsigned long flags;
    int index;

    /* V4L2 file handles */
    spinlock_t      fh_lock;
    struct list_head    fh_list;

    int dev_debug;

    v4l2_std_id tvnorms;

    /* callbacks */
    void (*release)(struct video_device *vdev);
    const struct v4l2_ioctl_ops *ioctl_ops;
    DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);

    DECLARE_BITMAP(disable_locking, BASE_VIDIOC_PRIVATE);
    struct mutex *lock;
};

如果需要給v4l2_device或者v4l2_subdev在系統中創建節點的話,便需要實現該結構體,并且通過video_register_device方法進行創建,而其中的fops便是video_device所對應的操作方法集,在v4l2框架內部,會將video_device嵌入到一個具有特定主設備號的字符設備中,而其方法集會在操作節點時被調用到。除了這些標準的操作集外,還定義了一系列的ioctl操作集,通過內部ioctl_ops來描述。

media_device源碼如下:

struct media_device {
    /* dev->driver_data points to this struct. */
    struct device *dev;
    struct media_devnode *devnode;

    char model[32];
    char driver_name[32];
    char serial[40];
    char bus_info[32];
    u32 hw_revision;

    u64 topology_version;

    u32 id;
    struct ida entity_internal_idx;
    int entity_internal_idx_max;

    struct list_head entities;
    struct list_head interfaces;
    struct list_head pads;
    struct list_head links;

    /* notify callback list invoked when a new entity is registered */
    struct list_head entity_notify;

    /* Serializes graph operations. */
    struct mutex graph_mutex;
    struct media_graph pm_count_walk;

    void *source_priv;
    int (*enable_source)(struct media_entity *entity,
                 struct media_pipeline *pipe);
    void (*disable_source)(struct media_entity *entity);

    const struct media_device_ops *ops;
};

如果使能了CONFIG_MEDIA_CONTROLLER宏,則當v4l2_device初始化的過程中便會去創建一個media_device,而這個media_device便是整個media controller的抽象管理者,每一個v4l2設備以及從屬的子設備都會對應的各自的entity,并且將其存入media_device中進行統一管理,與其它抽象設備一樣,media_device也具有自身的行為,比如用戶可以通過訪問media節點,枚舉出所有的從屬于同一個v4l2_device的子設備,另外,在開啟數據流的時候,media_device通過將各個media_entity按照一定的順序連接起來,實現了數據流向的整體控制。

vb2_queue源碼如下:

struct vb2_queue {
    unsigned int            type;
    unsigned int            io_modes;
    struct device           *dev;
    unsigned long           dma_attrs;
    unsigned            bidirectional:1;
    unsigned            fileio_read_once:1;
    unsigned            fileio_write_immediately:1;
    unsigned            allow_zero_bytesused:1;
    unsigned           quirk_poll_must_check_waiting_for_buffers:1;

    struct mutex            *lock;
    void                *owner;

    const struct vb2_ops        *ops;
    const struct vb2_mem_ops    *mem_ops;
    const struct vb2_buf_ops    *buf_ops;

    void                *drv_priv;
    unsigned int            buf_struct_size;
    u32             timestamp_flags;
    gfp_t               gfp_flags;
    u32             min_buffers_needed;

    /* private: internal use only */
    struct mutex            mmap_lock;
    unsigned int            memory;
    enum dma_data_direction     dma_dir;
    struct vb2_buffer       *bufs[VB2_MAX_FRAME];
    unsigned int            num_buffers;

    struct list_head        queued_list;
    unsigned int            queued_count;

    atomic_t            owned_by_drv_count;
    struct list_head        done_list;
    spinlock_t          done_lock;
    wait_queue_head_t       done_wq;

    struct device           *alloc_devs[VB2_MAX_PLANES];

    unsigned int            streaming:1;
    unsigned int            start_streaming_called:1;
    unsigned int            error:1;
    unsigned int            waiting_for_buffers:1;
    unsigned int            is_multiplanar:1;
    unsigned int            is_output:1;
    unsigned int            copy_timestamp:1;
    unsigned int            last_buffer_dequeued:1;

    struct vb2_fileio_data      *fileio;
    struct vb2_threadio_data    *threadio;

#ifdef CONFIG_VIDEO_ADV_DEBUG
    /*
     * Counters for how often these queue-related ops are
     * called. Used to check for unbalanced ops.
     */
    u32             cnt_queue_setup;
    u32             cnt_wait_prepare;
    u32             cnt_wait_finish;
    u32             cnt_start_streaming;
    u32             cnt_stop_streaming;
#endif
};

在整個V4L2框架運轉過程中,最為核心的是圖像數據緩沖區的管理,而這個管理工作便是由vb2_queue來完成的,vb2_queue通常在打開設備的時候被創建,其結構體中的vb2_ops可以由驅動自己進行實現,而vb2_mem_ops代表了內存分配的方法集,另外,還有一個用于將管理用戶空間和內核空間的相互傳遞的方法集buf_ops,而該方法集一般都定義為v4l2_buf_ops這一標準方法集。除了這些方法集外,vb2_queue還通過一個vb2_buffer的數組來管理申請的所有數據緩沖區,并且通過queued_list來管理入隊狀態的所有buffer,通過done_list來管理被填充了數據等待消費的所有buffer。

vb2_buffer源碼如下:

struct vb2_buffer {
    struct vb2_queue    *vb2_queue;
    unsigned int        index;
    unsigned int        type;
    unsigned int        memory;
    unsigned int        num_planes;
    struct vb2_plane    planes[VB2_MAX_PLANES];
    u64         timestamp;

    /* private: internal use only
     *
     * state:       current buffer state; do not change
     * queued_entry:    entry on the queued buffers list, which holds
     *          all buffers queued from userspace
     * done_entry:      entry on the list that stores all buffers ready
     *          to be dequeued to userspace
     */
    enum vb2_buffer_state   state;

    struct list_head    queued_entry;
    struct list_head    done_entry;
};

該結構體代表了V4L2框架中的圖像緩沖區,當處于入隊狀態時內部queued_entry會被鏈接到vb2_queue中的queued_list中,當處于等待消費的狀態時其內部done_entry會被鏈接到vb2_queue 中的done_list中,而其中的vb2_queue便是該緩沖區的管理者。

以上便是V4L2框架的幾個核心結構體,從上面的簡單分析不難看出,v4l2_device作為一個相機內核體系的頂層管理者,內部使用一個鏈表控制著所有從屬子設備v4l2_subdev,使用vb2_queue來申請并管理所有數據緩沖區,并且通過video_device向用戶空間暴露設備節點以及控制接口,接收來自用戶空間的控制指令,通過將自身嵌入media controller中來實現枚舉、連接子設備同時控制數據流走向的目的。

四、模塊初始化

整個v4l2框架是在linux內核中實現的,所以按照內核驅動的運行機制,會在系統啟動的過程中,通過標準的module_init方式進行初始化操作,而其初始化主要包含兩個方面,一個是v4l2_device的初始化,一個是子設備的初始化,首先我們來看下v4l2_device的初始化動作的基本流程。

由于驅動的實現都交由各個平臺廠商進行實現,所有內部邏輯都各不相同,這里我們抽離出主要方法來進行梳理:

首先對于v4l2_device的初始化而言,在系統啟動的過程中,linux內核會找到module_init聲明的驅動,調用其probe方法進行探測相應設備,一旦探測成功,便表示初始化工作完成。

而在probe方法內部,主要做了以下操作:

  • 獲取dts硬件信息,初始化部分硬件設備。
  • 創建v4l2_device結構體,填充信息,通過v4l2_device_register方法向系統注冊并且創建video設備節點。
  • 創建media_device結構體,填充信息,通過media_device_register向系統注冊,并創建media設備節點,并將其賦值給v4l2_device中的mdev。
  • 創建v4l2_device的media_entity,并將其添加到media controller進行管理。

類似于v4l2_device的初始化工作,子設備的流程如下:

  • 獲取dts硬件信息,初始化子設備硬件模塊
  • 創建v4l2_subdev結構體,填充信息,通過v4l2_device_register_subdev向系統注冊,并將其掛載到v4l2_device設備中
  • 創建對應的media_entity,并通過media_device_register_entity方法其添加到media controller中進行統一管理。
  • 最后調用v4l2_device_register_subdev_nodes方法,為所有的設置了V4L2_SUBDEV_FL_HAS_DEVNODE屬性的子設備創建設備節點。

五、處理用戶空間請求

系統啟動之后,初始化工作便已經完成,現在一旦用戶想要使用圖像采集功能,便會觸發整個視頻采集流程,會通過操作相應的video節點來獲取圖像數據,一般來講,標準的V4L2框架只需要通過操作video節點即可,但是由于現在的硬件功能越來越復雜,常規的v4l2_controller已經滿足不了采集需求,所以現在的平臺廠商通常會暴露子設備的設備節點,在用戶空間直接通過標準的字符設備控制接口來控制各個設備,而現在我們的目的是梳理V4L2框架,所以暫時默認不創建子設備節點,簡單介紹下整個流程。

在操作之前,還有一個準備工作需要做,那就是需要找到哪些是我們所需要的設備,而它的設備節點是什么,此時便可以通過打開media設備節點,并且通過ioctl注入MEDIA_IOC_ENUM_ENTITIES參數來獲取v4l2_device下的video設備節點,該操作會調用到內核中的media_device_ioctl方法,而之后根據傳入的命令,進而調用到media_device_enum_entities方法來枚舉所有的設備。

整個采集流程,主要使用三個標準字符設備接口來完成,分別是用于打開設備的open方法、用于控制設備的ioctl方法以及關閉設備的close方法。

1. 打開設備(open)

一旦確認了我們需要操作的video節點是哪一個,便可以通過調用字符設備標準接口open方法來打開設備,而這個方法會首先陷入內核空間,然后調用file_operations中的open方法,再到v4l2_file_operations中的open方法,而該方法由驅動自己進行實現,其中主要包括了給各個硬件模塊上電,并且調用vb2_queue_init方法創建并初始化一個vb2_queue用于數據緩沖區的管理。

2. 控制設備(ioctl)

在打開設備之后,接下來的大部分操作都是通過ioctl方法來完成的,而在該方法中,會首先陷入到內核空間,之后調用字符設備的v4l2_fops中的v4l2_ioctl方法,而在該方法中又會去調用video_device的video_ioctl2方法,video_ioctl2方法定義了一系列video標準的方法,通過不同的命令在v4l2_ioctls中找到相應的標準方法實現,同時為了滿足用戶自定義命令的實現,在video_ioctl2方法中會去調用到之前注冊video_device時賦予的ioctl_ops中的vidioc_default方法,在該方法中加入用戶自己的控制邏輯。

在整個控制流程中,首先通過命令VIDIOC_QUERYCAP來獲取設備所具有的屬性,通過VIDIOC_G_PARM/VIDIOC_S_PARM來分別獲取和設置設備參數,在這一系列操作配置完成之后,便需要向內核申請用于數據流轉的緩沖區(Buffer),該操作通過命令VIDIOC_REQBUFS來完成,在內核部分主要調用了標準方法vb2_reqbufs,進而調用__vb2_queue_alloc來向內核申請已知個數的Buffer,并且將其存入之前創建的vb2_queue中進行管理。

申請好了Buffer之后,便可以通過傳入VIDIOC_QBUF命令將申請的Buffer入隊,具體操作最終會調用vb2_qbuf方法,而在該方法中會從vb2_queue的bufs數組中取出Buffer,將其加入queued_list鏈表中,并且更新Buffer狀態,等待數據的填充或者來自用戶空間的出隊操作。

在完成上面的操作后,整個數據流并沒有開始流轉起來,所以需要下發VIDIOC_STREAMON命令來通知整個框架開始出數據,在驅動中主要會去調用vb2_streamon方法,進而調用vb2_start_streaming方法,其中該方法會去將隊列中的的Buffer放入到相應的驅動中,等待被填充,緊接著會去調用vb2_queue.ops.start_streaming方法來通知設備開始出圖,而該方法一般由驅動自己實現,最后會調用v4l2_subdev_call(subdev, video, s_stream, mode)方法通知各個子設備開始出圖。

當有圖像產生時,會填充到之前傳入的buffe中,并且調用vb2_buffer_done方法通知vb2_queue將buffer加入到done_list鏈表中,并更新狀態為VB2_BUF_STATE_DONE。

在整個數據流開啟之后,并不會自動的將圖像傳入用戶空間,必須通過VIDIOC_DQBUF命令來從設備中讀取一個幀圖像數據,具體操作是通過層層調用會調用到vb2_dqbuf方法,而在該方法中會調用__vb2_get_done_vb方法去從done_list中獲取Buffer,如果當前鏈表為空則會等待最終數據準備好,如果有準備好的buffer便直接從done_list取出,并且將其從queued_list中去掉,最后通過__vb2_dqbuf方法將Buffer返回用戶空間。

獲取到圖像數據之后,便可以進行后期的圖像處理流程了,在處理完成之后,需要下發VIDIOC_QBUF將此次buffer重新加入queued_list中,等待下一次的數據的填充和出隊操作。

但不需要進行圖像的采集時,可以通過下發VIDIOC_STREAMOFF命令來停止整個流程,具體流程首先會調用v4l2_subdev_call(subdev, video, s_stream, 0)通知所有子設備停止出圖操作,其次調用vb2_buffer_done喚醒可能的等待Buffer的線程,同時更新Buffer狀態為VB2_BUF_STATE_ERROR,然后調用vb2_streamoff取消所有的數據流并更新vb2_queue.streaming的為disable狀態。

3. 關閉設備(close)

但確認不使用當前設備進行圖像采集操作之后,便可以調用標準方法close來關閉設備。其中主要包括了調用vb2_queue_release方法釋放了vb2_queue以及設備下電操作和相關資源的釋放。

通過上面的介紹,我相信我們已經對整個V4L2框架有了一個比較深入的認識, 然而對于一個優秀的軟件架構而言,僅僅是支持現有的功能是遠遠不夠的,隨著功能的不斷完善,勢必會出現需要進行擴展的地方,而v4l2在設計之初便很好的考慮到了這一點,所以提供了用于擴展的方法集,開發者可以通過加入自定的命令來擴充整個框架,高通在這一點上做的非常好,在v4l2框架基礎上,設計出了一個獨特的KMD框架,提供給UMD CSL進行訪問的接口。

原文鏈接:https://blog.csdn.net/u012596975/article/details/107137555

至此,本篇已結束。轉載網絡的文章,小編覺得很優秀,歡迎點擊閱讀原文,支持原創作者,如有侵權,懇請聯系小編刪除,歡迎您的建議與指正。同時期待您的關注,感謝您的閱讀,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容