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閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容