08 I/O 设备管理

从这一章开始,我们探讨的话题就会与前面的话题稍有不同——我们将开始探讨操作系统对于设备的管理以及它为用户程序提供服务的作用。这一章中,我们将先介绍操作系统对于 I/O 的抽象与优化;然后,我们将重点介绍两种存储设备、并对比其优缺点,为下一章有关文件系统的内容作铺垫。

无时无刻不需要使用的 I/O 设备

我们在使用计算机的过程中无时无刻不需要使用 I/O 设备——无论是鼠标、键盘、屏幕,还是 USB 设备、音响、耳机,亦或是藏在计算机内部的磁盘、固态硬盘,都属于 I/O 设备。没有这些设备,我们既不能向计算机输入值、也不能看到计算机的输出值、甚至无法存储我们需要的数据。这些 I/O 设备拥有各自不同的使用方法、数据形式、存储空间和读写速度,我们这一节中就先来看一看这些设备如何能够被分类,然后再来学习操作系统在简化这些 I/O 设备的使用方面起到的作用。

I/O 设备的分类主要可以根据下面两个标准:设备的信息交换单位、设备的共享属性、设备的读写速度和读写能力。

分类

  • 从设备的信息交换单位上说

一些设备的读取单位是一个字节,它们被称为 字符设备(character device)
一些设备的读取单位是一个字符块(也就是说你不能只从中读取一个字节),其中可能包含了 512 字节或更多,这些设备被称为 块设备(block device)

字符设备主要包括鼠标、键盘这些本来就没有那么多数据可以被传输的设备;后者主要包括磁盘等设备。

字符设备与块设备有一个很明显的不同,那就是字符设备不能够被寻址。不能被寻址导致的结果就是我们不能随机访问设备中的任意一个位置。

  • 从设备的共享属性上说

设备被分为独占设备(dedicated device)共享设备(shared device)
不要被共享设备的名字给骗了,共享设备并不能同时被多个进程使用,我们之所以叫它共享设备是因为它允许几个线程交替地读写信息(就像几个线程可以交替地在处理器上运行一样)。磁带就是一种典型的独占设备。作为一个古老的存储设备,它成本低廉、存储量大,但是读取数据时必须花长时间将磁带转到指定位置才能读取,因此它不适于被多个线程交替使用(有可能出现一个线程还没有转到指定位置,下一个线程又想向反方向旋转磁带的情况)。

不同设备的读写速度和读写能力差异也是很大的。所谓读写能力指的是有一些设备只允许读取数据,有一些设备只允许写入数据,也另一些设备既允许读取也允许写入设备。只允许读取数据的就是输入设备,如键盘和鼠标;只允许写入数据的就是输出设备,如音响或打印机;而既允许输入又允许输出的设备就可能是像磁盘、固态硬盘这样的存储设备。这些设备的读取速度也各有不同,如键盘、鼠标的读取速度就远慢于磁盘的读取速度,而磁盘的读取速度又慢于同为存储设备的固态硬盘。

从上面的分类中我们可以看出,各类硬件设备的差别是很大的,我们既需要针对某一硬件的特殊性处理其 I/O,又需要一个统一的界面方便用户进程在不考虑每个硬件的特殊性的前提下仍然能够使用这些硬件。接下来的几节里,我们会同时讲解这两种需求的满足方法。

上一节中我们已经看到了,不同的 I/O 设备有很多不同的特征,但它们也有共同点,那就是每个设备都含有一个控制本设备的 设备控制器(device controller)

设备控制器里一般都包含了 4 个寄存器:

  • 一个输入数据寄存器
  • 一个输出数据寄存器
  • 一个代表设备状态(忙或空闲)的寄存器
  • 一个存放指令的寄存器

前两个寄存器的作用是很显然的,它们分别被用来存放系统写入设备的数据和系统需要从设备中读取的数据;
第三个寄存器在设备控制器在设备和寄存器之间搬运数据时会被设为忙,其余时间为闲;
最后一个寄存器中有多个代表不同含义的位,其中包括指令就绪位、读位、写位等等。

为了在系统中以一个统一的界面表示所有不同的硬件,每个系统中都有一个对于硬件的抽象,这就是 设备驱动(device driver)。这个设备驱动就是我们一般会用光盘或辅助软件安装的驱动软件,它拥有关于其管理的设备的知识,同时又提供操作系统要求的标准界面。操作系统需要执行的所有 I/O 请求都通过这个设备驱动执行。

一个典型的 I/O 流程可能是这样的:系统在需要将一个数据写入设备时,执行驱动程序,设备驱动先等待设备的状态变为空闲,然后再将指令寄存器中的写位设为 1 ,将需要写入的数据写入输入数据寄存器,然后将指令寄存器中的指令就绪位设为 1 。设备控制器发现指令就绪后会将状态设为忙,然后将数据从寄存器中复制到设备的相关位置。结束操作后,设备控制器会将指令就绪位设为空,将状态设为空闲,重新开始等待 I/O 请求。

你可以看到,在这个流程的开头,系统需要等待设备将状态设为空闲,而在这段时间内系统不能进行任何有用的工作。这种方式叫做 轮询式(polling) I/O。这种轮询式 I/O 是不理想的,因为我们希望系统在等待设备空闲时可以做一些有用的工作。为了实现这个需求,我们转而采用中断式(interrupt) I/O

在中断式 I/O 中,处理器发出完成某项 I/O 的指令给设备驱动,设备驱动就会控制设备开始 I/O 操作,于此同时处理器会继续执行其它与这个 I/O 操作无关的指令,等待 I/O 发出中断。在获得 I/O 的中断后,再进入中断处理函数,将数据存入内存或进行其它处理数据的操作。

需要注意的是,即使在中断式 I/O 中,处理器仍然会直接参与到数据搬运中,而且数据搬运的单位是一个寄存器,最多不超过几个字节,这种数据搬运方式对于磁盘这种块设备是十分不利的。如果我们一次要搬运一个页面进入内存,那么我们可能需要很多次中断才能搬运完所有数据。之前我们已经提到过,一个中断发生时,系统中会出现上下文切换,耗费大量时间,因此我们不能有太多中断,否则它会干扰任务的正常运行。我们也不想在中断处理程序中搬运大量的数据,因为在一个中断程序中停留太久可能导致我们错过其它优先级更低的中断的时机(试想,如果一个设备在搬运数据时另一个设备也产生了数据,而我们没有去搬运,那么就可能出现数据溢出和丢失的情况)。为了避免这样的问题,我们就需要下一种 I/O 的实现方法:直接存储器存取(Direct Memory Access)

直接存储器存取仍然需要使用中断的机制,它与中断式 I/O 唯一的差别是处理器不再参与搬运数据到内存中的过程。处理器在生成 I/O 指令时使用的是 DMA 指令,其中包括两个指针和一个数字。两个指针分别代表被移动数据的来源和移动目标地址,数字表示移动的数据块大小。设备驱动会将这一指令传送给指令的相关设备,而这个设备则会启动 DMA 控制器,开始一个 DMA 请求。设备会将数据的每个字节发送给 DMA 控制器,而 DMA 控制器会将数据直接通过总线(bus)写入内存。DMA 控制器完成请求后,会产生一个中断,这时处理器再执行中断处理程序,唤醒被阻塞的任务或执行其它处理方式。

你可以看到 DMA 相对于中断式 I/O 大大减少了中断产生的次数和处理器在中断处理程序中所花的时间。DMA 的问题在于,当 DMA 控制器占用总线搬运数据时,处理器就不能使用总线访问内存了(但它仍然能够访问高速缓存)。这种占用被称为 cycle stealing,对于系统的效率有一定的不良影响,但总体上来讲 DMA 还是加快了 I/O 处理的速度。

DMA 还有一种变种,就是 通道式(channel) I/O 。通道是一种特殊的处理器,只被用于 I/O 操作,因此通道也被称作 I/O处理器( I/O processor)。在支持通道式 I/O 的系统中,所有 I/O 操作都会直接交由通道处理,处理器完全不参与到 I/O 操作的过程中,这与 DMA 中处理器提供 DMA 指令的模式有所不同。不仅如此,DMA 控制器与设备的数量关系是 1:1 的,因此硬件数量增多后成本过高,而通道与设备的数量是 1:n 。当然,通道的成本也并不低,因而通道一般只在大型数据交互中被使用。

练习

详解中断

我们在前两节中已经提到,中断打断了原本在运行的用户空间代码,因而我们不希望一个中断运行太久。但是 DMA 并不能提升所有设备的效率,因此有时我们必须在中断处理函数中搬运数据;即使在加入 DMA 之后,我们仍然需要在 I/O 处理函数中处理数据。为了避免在 I/O 处理函数中做大量的工作,我们想要只在处理函数中完成需要在短时间内完成、要求禁用中断的部分,把剩余的部分推迟到未来执行。这种思想使得 Linux 以及很多其它系统都采用了一种将 I/O 处理分为两个阶段的设计思路。

I/O 处理的第一个阶段是我们在注册中断时输入的中断处理函数,这一阶段中我们首先会向发出中断的硬件承认我们收到了中断请求,然后如果我们收到的是有输入数据的中断,那么我们就需要在这一阶段将数据从硬件的缓冲中复制到内存中,否则新到来的数据可能会覆盖现在的数据,我们就会面临数据丢失的风险。这一部分在 Linux 中被称为上半部分(top half)

第一阶段的运行环境会在一定程度上禁用中断,即使它没有禁用所有的中断,它也会禁用和自己中断号码相同的所有中断,因此保持这一阶段的简短是很重要的。第二阶段则不同——它会在允许所有中断的环境下运行,因此如果有更重要的任务需要抢占处理器,系统就可以立刻抢占处理器运行那个任务。为了满足上面的需求,我们需要基于一种新的机制来实现第二阶段,也就是 下半部分(bottom half)

Linux 的下半部分主要基于三种机制实现——软中断(softirq),小任务(tasklet)和工作队列(work queue),其中小任务是基于软中断实现的。这三种机制的详细实现超过了我们这节课的范围,但我们还是会大致地覆盖一下这一部分内容,给你对于这部分内容大概的认识。对更具体的实现感兴趣的同学可以阅读 Robert Love 的《Linux 内核设计与实现》 第八章。

softirq 和 tasklet 这两种机制都是在中断的环境下运行的,也就是说它们不归属于任何一个进程,不能够在运行过程中睡眠。它们的差异是同类的(针对同一中断事件的) softirq 可以在不同的处理器上同时运行,但 tasklet 不能够做到这一点。softirq 分为几个优先级不同的类型(一般来讲,开发者不会随便建立一个新的 softirq,但是 tasklet 的自由度就大许多),所有 tasklet 都被映射到其中两个类型上。

中断处理函数在运行完毕之前会标记它对应的下半部分为需要运行,这样当我们执行完中断处理函数、即将返回用户空间时,我们就可以查看系统中是否有未处理的 softirq,如果有则按其优先级一个个处理,每处理完一个都查看此时系统中是否有优先级更高的用户进程,如果有则使该进程运行。tasklet 的运行也是在上述的过程中运行的——当我们发现 tasklet 映射的两个 softirq 类型有未处理的请求时,则执行这两类 softirq 的处理函数。处理函数会遍历未处理的 tasklet 的队列,处理每个 tasklet。

work queue 与前两种机制有明显的不同,那就是它能够在线程的环境下、而不是终端的环境下运行下半部分的处理函数,这也是它的一大优势。每个处理器都有一个 work queue 和一个专门用来处理 work queue 的线程,这些线程会像一般的线程一样被调度,执行自己 work queue 中每个函数。

在 softirq 机制中也有一种类似 work queue 线程的线程,那就是 ksoftirq。一些 I/O 的下半部分工作量非常庞大,其 softirq 会自己标记自己为还需要运行,这时为了避免 softirq 运行时间过长使用户代码产生饥饿,我们就不会处理新产生的 softirq,直到系统中积攒的 softirq 数量过多时再用每个处理器中专门用来处理这种情况的线程,ksoftirq,来处理多余的 softirq。

缓冲和假脱机

在前面几节中我们已经看到,不同的设备有不同的数据单位、传输速度等等,因此我们需要引入轮询、中断、DMA 等不同的 I/O 实现方式来协调 I/O 时机;但除了协调时机以外,我们还需要确保,在 I/O 操作完成前,被输入或输出的数据能够被存放在一个地方,这就是 缓冲(buffer)。。缓冲技术的概念很简单,那就是把需要被写入设备的输入数据或传出设备的输出数据暂时存储在一个区域中,使得设备能够在不丢失数据的情况下完成数据传输。它有几种实现方法:单缓冲,双缓冲和环装缓冲。

  • 单缓冲(single buffer)
    实现中,系统给每个 I/O 操作分配一个缓冲区,系统和设备同时使用这个缓存区。这种缓冲实现的坏处是,系统与设备同时修改缓冲区时很可能出现互斥问题,且载入数据时不能够一次性载入所有数据、而需要一点一点载入(如果缓冲区由一个数组实现,那么我们就需要一项一项载入)。为了能够一次性载入数据,我们可以选择使用双缓冲。

  • 双缓冲(double buffer)
    实现中,系统给每个 I/O 操作分配两个缓冲区 A、B,这样当一个设备有输入数据时,它就可以向 A 写入数据,直至将 A 填满,这时它就将 A、B 缓冲区对调,系统开始读取 A 缓冲区中的数据,它再继续向 B 缓冲区中写入数据,以此类推。这种双缓冲在计算机图像中较为常见,它能够使得用户不看到每一个像素的变化,而能直接看到整体的变化,使得用户体验更好。

  • 环状缓冲(circular buffer)
    来实现缓冲区。顾名思义,环状缓冲基于一个环形的数据结构,其中包含两个指针,一个指向下一个被读取的区域,一个指向下一个被写入的区域。当有数据被写入或读取时,对应的指针就会向后移动;当读指针在写指针移动方向上的后一个位置时,缓冲区就装满了。

环状缓冲的好处是对于大小固定的缓冲,它的任何一个操作所需的时间都是常数,与缓冲区大小无关;与之相对的,某些单缓冲的实现可能要求我们在每读取出一个数据时就移动后面所有的数据项,因此需要的时间与缓冲区长度成比例。

缓冲与我们之前提过的高速缓存(cache)有些类似,但它们并不相同。高速缓存的目的在于存储经常使用的数据,提高系统效率,是一种牺牲空间来换取时间的做法;缓冲则是为了弥补不同设备间数据单位大小差异、读取速度差异而存在的,其目的并不完全是提升效率。缓冲与高速缓存也有被联用的情况,内核缓冲就是这样一个例子。在 Linux 的write()系统调用中,为了将针对连续的磁盘分区的 I/O 操作集中到一起、提高效率,内核中会设置一个缓冲区,专门用来存储需要写入磁盘的数据,几秒种后再将整个缓冲区内的数据按照效率最高的方式写入磁盘;这个缓冲区同时又被用作一个高速缓存——在系统收到从磁盘中读取文件的 I/O 请求时,它会先查看需要被读取的文件是否存在于这个缓冲区中,以此来提高读取文件的效率。

假脱机技术

一种与缓冲类似的思想是 假脱机技术(Spooling)。在本章的第一节中,我们提到了设备分为独占设备和共享设备两种;打印机就属于独占设备。但是我们知道,在一个打印作业正在被打印的时候,我们仍然可以给打印机分配其它任务,这就是通过假脱机技术实现的。假脱机技术依赖的是一个叫做 spool 的缓冲区,专门用来存储针对某个独占设备的 I/O 请求;每次独占设备完成一个请求后,都会从缓冲区中取出下一个 I/O 请求来执行。由此可见,假脱机技术实际上只是一种特殊的缓冲,它被用来弥补同时只能完成一个任务的设备与可能在这段时间内产生多个任务的系统之间的速度差异。

练习

下面有关 I/O 的说法哪三个是正确的?


持久性存储设备

在学完了有关 I/O 总体实现的内容以后,让我们来认识一类较为特殊的 I/O 设备,也就是持久性存储设备,以为我们下一章学习有关文件系统的内容做好准备。在任何计算机中,我们都需要存储大量数据;我们希望在关机后重新打开计算机时,存储的数据仍然能够被读取和使用,这就要求被用来存储这些数据的设备在断电后仍然能够维持数据的完整性。内存,也就是 Dynamic Random-Access Memory,虽然速度很快,但断电后不能维持数据的存储,因此它不能被应用于这种数据存储。我们需要的是 持久性存储(persistent storage)

常见的持久性存储设备包括

  • 磁带(magnetic tape)
  • 磁盘(magnetic disk)
  • 固态硬盘(Solid-State Drive,SSD)

磁带作为一种独占式设备,在读取某一特定位置的数据时速度较慢,因此一般的计算机不会使用磁带作为存储设备;但磁带相对于磁盘和固态硬盘也有明显的优势——它的容量很大,且成本低廉。因此磁带经常被一些需要存储大量数据的公司(如 YouTube)用于存储不经常被使用的数据。

磁盘和固态硬盘是我们生活中经常使用的计算机中较为常见的持久性存储设备。它们两者也各有利弊,我们下面就来具体地看一看它们的优缺点。

磁盘

磁盘在计算机中比固态硬盘更早被应用,它的形状就如下图所示,一个计算机中可以有多层磁盘共同组成一个存储设备,每层磁盘上有一个或多个读写磁头,用于读取和写入数据。磁盘被以同心圆的形式分为了多个 磁道(track),每个磁道又分为多个 扇区(sector),这些扇区正是存储数据时可被分配的最小的存储区单位,它们的大小经常为 512 KB。在磁盘中,每个扇区都有一个逻辑编号,方便抽象数据在磁盘中的位置。


在向某一个文件中读取或写入数据时,读写磁头会先找到数据所在的磁道,然后再移动到数据所在的扇区、搬运数据。在磁头完成这一步骤的过程中,磁盘一直会以恒定的速度高速旋转,因此从磁盘读取数据的延迟时间与磁盘的旋转速度有关。


固态硬盘

与磁盘相对应的另一种持久性存储设备是固态硬盘。固态硬盘中数据的存储是通过晶体管实现的,因此在读取数据时,它无需进行类似于寻找磁道或寻找扇区的操作。当读取数据的位置较为随机时,它的延迟时间远小于磁盘的延迟时间。但它也有劣势——几年前,固态硬盘的造价相对于磁盘明显较为高昂,但这一缺点现在已不再明显;另一更为明显的缺点是,每次写入数据时,它都必须将原有的数据消除,且消除的数据大小往往远大于一个页面,因此这一过程花费的时间是它实际写入数据时间的 10 倍到 100 倍左右。

为了避免消除操作产生的延迟,许多闪存(flash storage)都采用了Flash Translation Layer(FTL)这种方法。FTL 与虚拟内存相似,都是将虚拟地址映射到物理地址的方法。在这种方法下,固态硬盘中始终保留有一些已经清空的页面,可供写入新的数据;每次需要写入数据时,FTL 就将需要被写入的页面映射到这样一个已经清空的页面上,这样一次清空所需的时间就分摊到了所有被清空页面的写入时间上,其平均写入时间就大大减少了。

固态硬盘的最后一个缺点是它的晶体管在消除数据的过程中会逐渐被损耗,因此一个硬盘大约在百万次使用后就会报废。除此之外,当我们连续多次读取同一个区域的数据时,这个区域周围的区域就会受到影响,产生数据错误。为了解决这些问题,一些固态硬盘引入了 Wear Leveling 的机制来监测硬盘中不同区域的损耗程度;当一个页面被使用次数较多时,Wear Leveling 机制就会借用 FTL 将它映射到一个新的物理页面上,以防止原来的物理页面失效;一些算法也会将经常被使用的页面周围的页面移动到其它区域,防止出现数据错误。另外,当一个物理页面失效后,硬盘固件会将这一页面标注为失效,之后再也不使用这个页面,以防止出现错误。

从上面的比较中我们可以看出,在固态硬盘与磁盘的成本日益逼近的今天,固态硬盘的快速随机读取能力容易使其受青睐,但它提供的存储容量大小和读写次数限制往往不及磁盘,因此磁盘直到今天仍然是大部分笔记本、台式计算机的首选。在本章的后几节和下一章中我们仍然会基于磁盘来讨论读写速度的优化和文件系统的设计。

这一节中我们介绍了磁盘的基本原理,但我们没有具体讲解当系统同时收到多个针对磁盘的 I/O 操作请求时、系统将如何处理这些请求。系统对于这些请求的处理方式就涉及到了磁盘的调度算法;

磁盘的调度算法

磁盘在完成 I/O 请求时总是需要经过寻找磁道和寻找扇区这两个步骤,使其速度变慢。为了提高磁盘 I/O 请求的效率、避免提出 I/O 请求的进程或线程等待过长时间,当操作系统面临多个针对磁盘的 I/O 请求时,它应该选择一个合适的磁盘调度算法,使得所有 I/O 请求的平均等待时间最短。这一节中,我们就来介绍几种常见的磁盘调度算法,它们各自都有优点和缺陷,我们也会一一分析。

练习

下面有关磁盘 I/O 请求的说法中哪两个是正确的?


磁盘优化

来学习几种优化磁盘的方法。从前面的章节中我们可以得知,磁盘的劣势主要在随机读取时显现出来,但当我们从连续的扇区中读取数据时,磁盘的读取速度就不会受到寻道和旋转时间的影响。下面的几种优化方式大多都是围绕着这个目标实现的。

我们知道,在系统读取完一个磁道上的所有数据、向下一个磁道移动时,磁盘仍然在高速旋转。如果两个相邻磁道上扇区的逻辑编号分布完全相同,那么磁头在移动至下一个磁道时就会错过数据的起始点。为了让磁头可以正好移动至下一个磁道的数据起始位置,我们需要在设计扇区的分布时将磁道切换时间内磁盘旋转的角度考虑进去。这种优化方法就叫做 磁道偏移(track skewing)。下图阐释了这一应用这一方法后相邻磁道上扇区的分布情况:

图中的磁盘沿逆时针方向旋转,磁头自外向内遍历磁道、读取或写入数据。在这种情况下,内侧的磁道数据起始的位置需要比外侧磁道在旋转方向上偏移一定数量的扇区,在图中我们选择的偏移数量是三个扇区。

除了这种方式之外,我们还需要考虑,当磁盘某一扇区出现故障时,我们需要能够保留原有数据在磁盘上位置的连续性,因此我们需要在距离原扇区较近的位置选择一个扇区将数据复制进去。为了能实现这一目标,一些磁盘固件采用了 保留扇区(sector sparing) 的机制,在每个区域都保留一些空扇区,一旦出现故障就选择旋转方向上离错误扇区最近的空扇区来存储故障扇区的数据。这种机制保证了磁盘在出现一些故障的情况下仍然能高速运转。

不过,这种机制也有它的缺点——当硬件不向操作系统报告硬件故障的出现时,操作系统就不知道硬件可能存在隐患。磁盘部分扇区出现故障可能意味着整个磁盘都已经到了该当寿终正寝的时候,但由于操作系统无法获得这些信息,系统以及使用系统的用户都无法得知这一点。

除了上述的优化方式以外,磁盘上还存在一个缓冲区,称为 磁道缓冲(Track buffering),它可以被用来优化读写操作。

在优化读取数据的操作时,磁盘固件会将处于一个 I/O 请求的扇区附近的扇区一并读取到磁道缓冲中。由于好的文件系统中、同一个目录下的文件大都在相邻的扇区中,这一操作是符合用户的行为的——用户很可能希望查看多个相关联的文件。

在优化写入数据的操作时,磁盘会将需要被写入的数据暂时存放在缓冲中,并向操作系统报告数据已写入完毕,一段时间后再将缓冲区中所有的数据写入磁盘中。这一优化方式虽然能减少 I/O 请求的等待时间,但它也有一定的风险——磁道缓冲区本身不是持久性存储设备,因此如果在数据被写入计算机前突然断电,数据就会全部丢失。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容