零拷貝 - 用戶態(tài)分析

翻譯轉(zhuǎn)載自:https://blog.biezhi.me/2019/01/zero-copy-user-mode-perspective.html

現(xiàn)在幾乎所有人都聽過 Linux 下的零拷貝技術(shù)捧弃,但我經(jīng)常遇到對這個問題不能深入理解的人赠叼。所以我寫了這篇文章,來深入研究這些問題违霞。本文通過用戶態(tài)程序的角度來看零拷貝嘴办,因此我有意忽略了內(nèi)核級別的實現(xiàn)。

什么是 “零拷貝” 买鸽?

為了更好的理解這個問題涧郊,我們首先需要了解問題本身。來看一個網(wǎng)絡(luò)服務(wù)的簡單運行過程眼五,在這個過程中將磁盤的文件讀取到緩沖區(qū)妆艘,然后通過網(wǎng)絡(luò)發(fā)送給客戶端。下面是示例代碼:

read(file, tmp_buf, len); 
write(socket, tmp_buf, len);

這個例子看起來非常簡單看幼,你可能會認(rèn)為只有兩次系統(tǒng)調(diào)用不會產(chǎn)生太多的系統(tǒng)開銷批旺。實際上并非如此,在這兩次調(diào)用之后诵姜,數(shù)據(jù)至少被拷貝了 4 次汽煮,同時還執(zhí)行了很多次 用戶態(tài)/內(nèi)核態(tài) 的上下文切換。(實際上這個過程是非常復(fù)雜的棚唆,為了解釋我盡可能保持簡單)為了更好的理解這個過程暇赤,請查看下圖中的上下文切換,圖片上部分展示上下文切換過程宵凌,下部分展示拷貝操作鞋囊。

  1. 程序調(diào)用 read 產(chǎn)生一次用戶態(tài)到內(nèi)核態(tài)的上下文切換。DMA 模塊從磁盤讀取文件內(nèi)容摆寄,將其拷貝到內(nèi)核空間的緩沖區(qū)失暴,完成第 1 次拷貝坯门。
  2. 數(shù)據(jù)從內(nèi)核緩沖區(qū)拷貝到用戶空間緩沖區(qū)微饥,之后系統(tǒng)調(diào)用 read 返回,這回導(dǎo)致從內(nèi)核空間到用戶空間的上下文切換古戴。這個時候數(shù)據(jù)存儲在用戶空間的 tmp_buf 緩沖區(qū)內(nèi)欠橘,可以后續(xù)的操作了。
  3. 程序調(diào)用 write 產(chǎn)生一次用戶態(tài)到內(nèi)核態(tài)的上下文切換现恼。數(shù)據(jù)從用戶空間緩沖區(qū)被拷貝到內(nèi)核空間緩沖區(qū)肃续,完成第 3 次拷貝黍檩。但是這次數(shù)據(jù)存儲在一個和 socket 相關(guān)的緩沖區(qū)中,而不是第一步的緩沖區(qū)始锚。
  4. write 調(diào)用返回刽酱,產(chǎn)生第 4 個上下文切換。第 4 次拷貝在 DMA 模塊將數(shù)據(jù)從內(nèi)核空間緩沖區(qū)傳遞至協(xié)議引擎的時候發(fā)生瞧捌,這與我們的代碼的執(zhí)行是獨立且異步發(fā)生的棵里。你可能會疑惑:“為何要說是獨立、異步姐呐?難道不是在 write 系統(tǒng)調(diào)用返回前數(shù)據(jù)已經(jīng)被傳送了殿怜?write 系統(tǒng)調(diào)用的返回,并不意味著傳輸成功——它甚至無法保證傳輸?shù)拈_始曙砂。調(diào)用的返回头谜,只是表明以太網(wǎng)驅(qū)動程序在其傳輸隊列中有空位,并已經(jīng)接受我們的數(shù)據(jù)用于傳輸鸠澈≈妫可能有眾多的數(shù)據(jù)排在我們的數(shù)據(jù)之前。除非驅(qū)動程序或硬件采用優(yōu)先級隊列的方法笑陈,各組數(shù)據(jù)是依照FIFO的次序被傳輸?shù)?上圖中叉狀的 DMA copy 表明這最后一次拷貝可以被延后)末荐。

mmap

如你所見,上面的數(shù)據(jù)拷貝非常多新锈,我們可以減少一些重復(fù)拷貝來減少開銷甲脏,提升性能。作為一名驅(qū)動程序開發(fā)人員妹笆,我的工作圍繞著擁有先進特性的硬件展開块请。某些硬件支持完全繞開內(nèi)存,將數(shù)據(jù)直接傳送給其他設(shè)備拳缠。這個特性消除了系統(tǒng)內(nèi)存中的數(shù)據(jù)副本墩新,因此是一種很好的選擇,但并不是所有的硬件都支持窟坐。此外海渊,來自于硬盤的數(shù)據(jù)必須重新打包(地址連續(xù))才能用于網(wǎng)絡(luò)傳輸,這也引入了某些復(fù)雜性哲鸳。為了減少開銷臣疑,我們可以從消除內(nèi)核緩沖區(qū)與用戶緩沖區(qū)之間的拷貝開始。

減少數(shù)據(jù)拷貝的一種方法是將 read 調(diào)用改為 mmap徙菠。例如:

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);

為了方便你理解讯沈,請參考下圖的過程。

  1. mmap 調(diào)用導(dǎo)致文件內(nèi)容通過 DMA 模塊拷貝到內(nèi)核緩沖區(qū)婿奔。然后與用戶進程共享緩沖區(qū)缺狠,這樣不會在內(nèi)核緩沖區(qū)和用戶空間之間產(chǎn)生任何拷貝问慎。
  2. write 調(diào)用導(dǎo)致內(nèi)核將數(shù)據(jù)從原始內(nèi)核緩沖區(qū)拷貝到與 socket 關(guān)聯(lián)的內(nèi)核緩沖區(qū)中。
  3. 第 3 次數(shù)據(jù)拷貝發(fā)生在 DMA 模塊將數(shù)據(jù)從 socket 緩沖區(qū)傳遞給協(xié)議引擎時挤茄。

通過調(diào)用 mmap 而不是 read如叼,我們已經(jīng)將內(nèi)核拷貝數(shù)據(jù)操作減半。當(dāng)傳輸大量數(shù)據(jù)時穷劈,效果會非常好薇正。然而,這種改進并非沒有代價囚衔;使用 mmap + write 方式存在一些隱藏的陷阱挖腰。當(dāng)內(nèi)存中做文件映射后調(diào)用 write,與此同時另一個進程截斷這個文件時练湿。此時 write 調(diào)用的進程會收到一個 SIGBUS 中斷信號猴仑,因為當(dāng)前進程訪問了非法內(nèi)存地址。這個信號默認(rèn)情況下會殺死當(dāng)前進程并生成 dump 文件——而這對于網(wǎng)絡(luò)服務(wù)器程序而言不是最期望的操作肥哎。有兩種方式可用于解決該問題:

第一種方法是處理收到的 SIGBUS 信號辽俗,然后在處理程序中簡單地調(diào)用 return。通過這樣做篡诽,write 調(diào)用會返回它在被中斷之前寫入的字節(jié)數(shù)崖飘,并且將全局變量 errno 設(shè)置為成功。我認(rèn)為這是一個治標(biāo)不治本的解決方案杈女。因為收到 SIGBUS 信號表示程序發(fā)生了嚴(yán)重的錯誤朱浴,我不推薦使用它作為解決方案。

第二種方式應(yīng)用了文件租借(在Microsoft Windows系統(tǒng)中被稱為“機會鎖”)达椰。這才是解勸前面問題的正確方式翰蠢。通過對文件描述符執(zhí)行租借,可以同內(nèi)核就某個特定文件達成租約啰劲。從內(nèi)核可以獲得讀/寫租約梁沧。當(dāng)另外一個進程試圖將你正在傳輸?shù)奈募財鄷r,內(nèi)核會向你的進程發(fā)送實時信號——RT_SIGNAL_LEASE蝇裤。該信號通知你的進程廷支,內(nèi)核即將終止在該文件上你曾獲得的租約。這樣栓辜,在write調(diào)用訪問非法內(nèi)存地址恋拍、并被隨后接收到的SIGBUS信號殺死之前,write系統(tǒng)調(diào)用就被RT_SIGNAL_LEASE信號中斷了啃憎。write的返回值是在被中斷前已寫的字節(jié)數(shù)芝囤,全局變量errno設(shè)置為成功。下面是一段展示如何從內(nèi)核獲得租約的示例代碼辛萍。

if (fcntl(fd, F_SETSIG, RT_SIGNAL_LEASE) == -1) {
    perror("kernel lease set signal");
    return -1;
}
/* l_type can be F_RDLCK F_WRLCK */
if (fcntl(fd, F_SETLEASE, l_type)) {
    perror("kernel lease set type");
    return -1;
}

在對文件進行映射前悯姊,應(yīng)該先獲得租約,并在結(jié)束 write 操作后結(jié)束租約贩毕。這是通過在 fcntl 調(diào)用中指定租約類型為 F_UNLCK 來實現(xiàn)的悯许。

Sendfile

在內(nèi)核的 2.1 版本中,引入了 sendfile 系統(tǒng)調(diào)用辉阶,目的是簡化通過網(wǎng)絡(luò)和兩個本地文件之間的數(shù)據(jù)傳輸先壕。sendfile 的引入不僅減少了數(shù)據(jù)拷貝,還減少了上下文切換谆甜±牛可以這樣使用它:

sendfile(socket, file, len);

同樣的,為了理解起來方便规辱,可以看下圖的調(diào)用過程谆棺。

  1. sendfile 調(diào)用會使得文件內(nèi)容通過 DMA 模塊拷貝到內(nèi)核緩沖區(qū)。然后罕袋,內(nèi)核將數(shù)據(jù)拷貝到與 socket 關(guān)聯(lián)的內(nèi)核緩沖區(qū)中改淑。
  2. 第 3 次拷貝發(fā)生在 DMA 模塊將數(shù)據(jù)從內(nèi)核 socket 緩沖區(qū)傳遞到協(xié)議引擎時。

你可能想問當(dāng)我們使用 sendfile 調(diào)用傳輸文件時有另一個進程截斷會發(fā)生什么浴讯?如果我們沒有注冊任何信號處理程序朵夏,sendfile 調(diào)用只會返回它在被中斷之前傳輸?shù)淖止?jié)數(shù),并且全局變量 errno 被設(shè)置為成功榆纽。

但是仰猖,如果我們在調(diào)用 sendfile 之前從內(nèi)核獲得了文件租約,那么行為和返回狀態(tài)完全相同奈籽。我們會在sendfile 調(diào)用返回之前收到一個 RT_SIGNAL_LEASE 信號亮元。

到目前為止,我們已經(jīng)能夠避免讓內(nèi)核產(chǎn)生多次拷貝唠摹,但我們還有一次拷貝爆捞。這可以避免嗎勾拉?當(dāng)然,在硬件的幫助下成肘。為了避免內(nèi)核完成的所有數(shù)據(jù)拷貝双霍,我們需要一個支持收集操作的網(wǎng)絡(luò)接口染坯。這僅僅意味著等待傳輸?shù)臄?shù)據(jù)不需要在內(nèi)存中单鹿;它可以分散在各種存儲位置仲锄。在內(nèi)核 2.4 版本中儒喊,修改了 socket 緩沖區(qū)描述符以適應(yīng)這些要求 - 在 Linux 下稱為零拷貝币呵。這種方法不僅減少了多個上下文切換掸驱,還避免了處理器完成的數(shù)據(jù)拷貝毕贼。對于用戶的程序不用做什么修改鬼癣,所以代碼仍然如下所示:

sendfile(socket, file, len);

為了更好地了解所涉及的過程待秃,請查看下圖

  1. sendfile 調(diào)用會導(dǎo)致文件內(nèi)容通過 DMA 模塊拷貝到內(nèi)核緩沖區(qū)。
  2. 沒有數(shù)據(jù)被復(fù)制到 socket 緩沖區(qū)。相反培廓,只有關(guān)于數(shù)據(jù)的位置和長度信息的描述符被附加到 socket 緩沖區(qū)肩钠。DMA 模塊將數(shù)據(jù)直接從內(nèi)核緩沖區(qū)傳遞到協(xié)議引擎当纱,從而避免了剩余的最終拷貝惫东。

因為數(shù)據(jù)實際上仍然是從磁盤復(fù)制到內(nèi)存,從內(nèi)存復(fù)制到總線徐矩,所以有人可能會認(rèn)為這不是真正的零拷貝滤灯。但從操作系統(tǒng)的角度來看鳞骤,這是零拷貝豫尽,因為內(nèi)核緩沖區(qū)之間的數(shù)據(jù)不會產(chǎn)生多余的拷貝。使用零拷貝時榴嗅,除了避免拷貝外嗽测,還可以獲得其他性能優(yōu)勢唠粥,比如更少的上下文切換,更少的 CPU 高速緩存污染以及不會產(chǎn)生 CPU 校驗和計算养涮。

現(xiàn)在我們知道了什么是零拷貝贯吓,把前面的理論通過編碼來實踐悄谐。你可以從 http://www.xalien.org/articles/source/sfl-src.tgz下載源碼们陆。解壓源碼需要執(zhí)行 tar -zxvf sfl-src.tgz坪仇,然后編譯代碼并創(chuàng)建一個隨機數(shù)據(jù)文件 data.bin,接下來使用 make 運行皆刺。

查看頭文件:

/* sfl.c sendfile example program
Dragan Stancevic <
header name                 function / variable
-------------------------------------------------*/
#include <stdio.h>          /* printf, perror */
#include <fcntl.h>          /* open */
#include <unistd.h>         /* close */
#include <errno.h>          /* errno */
#include <string.h>         /* memset */
#include <sys/socket.h>     /* socket */
#include <netinet/in.h>     /* sockaddr_in */
#include <sys/sendfile.h>   /* sendfile */
#include <arpa/inet.h>      /* inet_addr */
#define BUFF_SIZE (10*1024) /* size of the tmp buffer */

除了 socket 操作需要的頭文件 <sys/socket.h><netinet/in.h> 之外望伦,我們還需要 sendfile 調(diào)用的頭文件 - <sys/sendfile.h>

/* are we sending or receiving */
if(argv[1][0] == 's') is_server++;
/* open descriptors */
sd = socket(PF_INET, SOCK_STREAM, 0);
if(is_server) fd = open("data.bin", O_RDONLY);

同樣的程序既可以充當(dāng) 服務(wù)端/發(fā)送者屯伞,也可以充當(dāng) 客戶端/接受者珠移。這里我們接收一個命令提示符參數(shù)钧惧,通過該參數(shù)將標(biāo)志 is_server 設(shè)置為以 發(fā)送方模式 運行巧婶。我們還打開了 INET 協(xié)議族的流套接字。作為在服務(wù)端運行的一部分,我們需要某種類型的數(shù)據(jù)傳輸?shù)娇蛻舳朔9矗源蜷_我們的數(shù)據(jù)文件(data.bin)。由于我們使用 sendfile 來傳輸數(shù)據(jù)场刑,所以不用讀取文件的實際內(nèi)容將其存儲在程序的緩沖區(qū)中铐懊。這是服務(wù)端地址:

/* clear the memory */
memset(&sa, 0, sizeof(struct sockaddr_in));
/* initialize structure */
sa.sin_family = PF_INET;
sa.sin_port = htons(1033);
sa.sin_addr.s_addr = inet_addr(argv[2]);

我們重置了服務(wù)端地址結(jié)構(gòu)并分配了端口和 IP 地址。服務(wù)端的地址作為命令行參數(shù)傳遞鹏溯,端口號寫死為 1033,選擇這個端口號是因為它是一個允許訪問的端口范圍。

下面是服務(wù)端執(zhí)行的代碼分支:

if(is_server){
    int client; /* new client socket */
    printf("Server binding to [%s]\n", argv[2]);
    if(bind(sd, (struct sockaddr *)&sa,
                      sizeof(sa)) < 0){
        perror("bind");
        exit(errno);
    }
}

作為服務(wù)端,我們需要為 socket 描述符分配一個地址僧须。這是通過系統(tǒng)調(diào)用 bind 實現(xiàn)的,它為 socket 描述符(sd)分配一個服務(wù)器地址(sa):

if(listen(sd,1) < 0){
    perror("listen");
    exit(errno);
}

因為我們正在使用流套接字,所以我們必須接受傳入連接并設(shè)置連接隊列大小。我將緩沖壓隊列設(shè)置為 1,但對于等待接受的已建立連接析恢,一般會將緩沖值要設(shè)置的更高一些。在舊版本的內(nèi)核中,緩沖隊列用于防止 syn flood 攻擊。由于系統(tǒng)調(diào)用 listen 已經(jīng)修改為 僅為已建立的連接設(shè)置參數(shù)逆巍,所以不使用這個調(diào)用的緩沖隊列功能。內(nèi)核參數(shù) tcp_max_syn_backlog代替了保護系統(tǒng)免受 syn flood 攻擊的角色:

if((client = accept(sd, NULL, NULL)) < 0){
    perror("accept");
    exit(errno);
}

accept 調(diào)用從掛起連接隊列上的第一個連接請求創(chuàng)建一個新的 socket 連接。調(diào)用的返回值是新創(chuàng)建的連接的描述符; socket 現(xiàn)在可以進行讀、寫或輪詢/select 了:

if((cnt = sendfile(client,fd,&off,
                          BUFF_SIZE)) < 0){
    perror("sendfile");
    exit(errno);
}
printf("Server sent %d bytes.\n", cnt);
close(client);

在客戶端 socket 描述符上建立連接,我們可以開始將數(shù)據(jù)傳輸?shù)竭h(yuǎn)端。通過 sendfile 調(diào)用來實現(xiàn)懦胞,該調(diào)用是在 Linux 下通過以下方式原型化的:

extern ssize_t
sendfile (int __out_fd, int __in_fd, off_t *offset,
          size_t __count) __THROW;
  • 前兩個參數(shù)是文件描述符。
  • 第 3 個參數(shù)指向 sendfile 開始發(fā)送數(shù)據(jù)的偏移量胀糜。
  • 第四個參數(shù)是我們要傳輸?shù)淖止?jié)數(shù)颅拦。

為了使 sendfile 傳輸使用零拷貝功能,你需要從網(wǎng)卡獲得內(nèi)存收集操作支持僚纷。還需要實現(xiàn)校驗和的協(xié)議的校驗和功能矩距,通過 TCP 或 UDP。如果你的 NIC 已過時不支持這些功能怖竭,你也可以使用 sendfile 來傳輸文件,不同之處在于內(nèi)核會在傳輸之前合并緩沖區(qū)陡蝇。

移植性問題

通常痊臭,sendfile 系統(tǒng)調(diào)用的一個問題是缺少標(biāo)準(zhǔn)實現(xiàn),就像開放系統(tǒng)調(diào)用一樣登夫。Linux广匙、Solaris 或 HP-UX 中 的 Sendfile 實現(xiàn)完全不同。這對于想通過代碼實現(xiàn)零拷貝的開發(fā)人員而言是個問題恼策。

其中一個實現(xiàn)差異是 Linux 提供了一個 sendfile 接口鸦致,用于在兩個文件描述符(文件到文件)和(文件到socket)之間傳輸數(shù)據(jù)。另一方面涣楷,HP-UX 和 Solaris 只能用于文件到 socket 的提交分唾。

第二個區(qū)別是 Linux 沒有實現(xiàn)向量傳輸。Solaris sendfile 和 HP-UX sendfile 有一些擴展參數(shù)狮斗,可以避免與正在傳輸?shù)臄?shù)據(jù)添加頭部的開銷绽乔。

展望

Linux 下的零拷貝實現(xiàn)離最終實現(xiàn)還有點距離,并且很可能在不久的將來發(fā)生變化碳褒。要添加更多功能折砸,例如看疗,sendfile 調(diào)用不支持向量傳輸,而 Samba 和 Apache 等服務(wù)器必須使用設(shè)置了 TCP_CORK 標(biāo)志的多個sendfile 調(diào)用睦授。這個標(biāo)志告訴系統(tǒng)在下一個 sendfile 調(diào)用中會有更多數(shù)據(jù)通過两芳。TCP_CORKTCP_NODELAY 不兼容,并且在我們想要在數(shù)據(jù)前添加或附加標(biāo)頭時使用去枷。這是一個完美的例子盗扇,其中向量調(diào)用將消除對當(dāng)前實現(xiàn)所強制的多個 sendfile 調(diào)用和延遲的需要。

當(dāng)前 sendfile 中一個相當(dāng)令人不快的限制是它在傳輸大于2GB的文件時無法使用沉填。如此大小的文件在今天并不罕見疗隶,并且在出路時復(fù)制所有數(shù)據(jù)相當(dāng)令人失望。因為在這種情況下sendfile和mmap方法都不可用翼闹,所以sendfile64在未來的內(nèi)核版本中會非常方便斑鼻。

總結(jié)

盡管有一些缺點,不過通過 sendfile 來實現(xiàn)零拷貝也很有用猎荠,我希望你在閱讀本文后可以開始在你的程序中使用它坚弱。如果想對這個主題有更深入的興趣,請留意我的第二篇文章关摇,標(biāo)題為 “零拷貝 - 內(nèi)核態(tài)分析”荒叶,我將在零拷貝的內(nèi)核內(nèi)部挖掘更多內(nèi)容。

英文原文:http://www.linuxjournal.com/article/6345

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末输虱,一起剝皮案震驚了整個濱河市些楣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宪睹,老刑警劉巖愁茁,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異亭病,居然都是意外死亡鹅很,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門罪帖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來促煮,“玉大人,你說我怎么就攤上這事整袁〔こ荩” “怎么了?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵葬项,是天一觀的道長泞当。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么襟士? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任盗飒,我火速辦了婚禮,結(jié)果婚禮上陋桂,老公的妹妹穿的比我還像新娘逆趣。我一直安慰自己,他們只是感情好嗜历,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布宣渗。 她就那樣靜靜地躺著,像睡著了一般梨州。 火紅的嫁衣襯著肌膚如雪痕囱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天暴匠,我揣著相機與錄音鞍恢,去河邊找鬼。 笑死每窖,一個胖子當(dāng)著我的面吹牛帮掉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窒典,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蟆炊,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瀑志?” 一聲冷哼從身側(cè)響起涩搓,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎后室,沒想到半個月后缩膝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡岸霹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了将饺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贡避。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖予弧,靈堂內(nèi)的尸體忽然破棺而出刮吧,到底是詐尸還是另有隱情,我是刑警寧澤掖蛤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布杀捻,位于F島的核電站,受9級特大地震影響蚓庭,放射性物質(zhì)發(fā)生泄漏致讥。R本人自食惡果不足惜仅仆,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望垢袱。 院中可真熱鬧墓拜,春花似錦、人聲如沸请契。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽爽锥。三九已至涌韩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氯夷,已是汗流浹背臣樱。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留肠槽,地道東北人擎淤。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像秸仙,于是被迫代替她去往敵國和親嘴拢。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

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