日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

Redis 网络模型 -- 阻塞非阻塞IO、IO多路复用、epoll详解

發(fā)布時間:2024/1/1 数据库 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Redis 网络模型 -- 阻塞非阻塞IO、IO多路复用、epoll详解 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

引言

本篇前半部分屬于知識點,后半部分的[手撕面答環(huán)節(jié)],以問題展開,應(yīng)對面試場景作答,盡量簡短,可以在學(xué)習(xí)了前置知識后,嘗試自己作答復(fù)述喔。

本篇先簡單介紹常見的IO模型,還未深入具體Redis中的應(yīng)用,可以把這節(jié)當(dāng)做【操作系統(tǒng)】來啃hhh

🎨本篇腦圖速覽

🎯常見的幾種網(wǎng)絡(luò)模型?

阻塞 IO

  • 過程 1:應(yīng)用程序想要去讀取數(shù)據(jù),他是無法直接去讀取磁盤數(shù)據(jù)的,他需要先到內(nèi)核里邊去等待內(nèi)核操作硬件拿到數(shù)據(jù),這個等待數(shù)據(jù)就緒的過程便是過程1。

  • 過程 2:內(nèi)核態(tài)準(zhǔn)備好了,開始拷貝數(shù)據(jù)給用戶緩沖區(qū),便是過程2。

用戶去讀取數(shù)據(jù)時,會去先發(fā)起 recvform 一個命令,去嘗試從內(nèi)核上加載數(shù)據(jù),如果內(nèi)核沒有數(shù)據(jù),那么用戶就會等待,此時內(nèi)核會去從硬件上讀取數(shù)據(jù),內(nèi)核讀取數(shù)據(jù)之后,會把數(shù)據(jù)拷貝到用戶態(tài),并且返回 ok,整個過程,都是阻塞等待的,這就是阻塞 IO

也就是兩個過程都阻塞的話,便是阻塞IO

總結(jié)如下:

顧名思義,阻塞 IO 就是兩個階段都必須阻塞等待:

階段一:

  • 用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
  • 此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
  • 此時用戶進程也處于阻塞狀態(tài)

階段二:

  • 數(shù)據(jù)到達并拷貝到內(nèi)核緩沖區(qū),代表已就緒
  • 將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
  • 拷貝過程中,用戶進程依然阻塞等待
  • 拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)

流程圖

非阻塞 IO

顧名思義,非阻塞 IO 的 recvfrom 操作會立即返回結(jié)果而不是阻塞用戶進程。

階段一:

  • 用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
  • 此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
  • 返回異常給用戶進程
  • 用戶進程收到 error 后,再次嘗試讀取【忙輪詢】
  • 循環(huán)往復(fù),直到數(shù)據(jù)就緒

階段二:

  • 將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
  • 拷貝過程中,用戶進程依然阻塞等待
  • 拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)

可以看到,非阻塞 IO 模型中,用戶進程在第一個階段是非阻塞,第二個階段是阻塞狀態(tài)。雖然是非阻塞,但性能并沒有得到提高。而且忙等機制會導(dǎo)致 CPU 空轉(zhuǎn),CPU 使用率暴增。

信號驅(qū)動

信號驅(qū)動 IO 是與內(nèi)核建立 SIGIO 的信號關(guān)聯(lián)并設(shè)置回調(diào),當(dāng)內(nèi)核有 FD 就緒時,會發(fā)出 SIGIO 信號通知用戶,期間用戶應(yīng)用可以執(zhí)行其它業(yè)務(wù),無需阻塞等待。

階段一:

  • 用戶進程調(diào)用 sigaction ,注冊信號處理函數(shù)
  • 內(nèi)核返回成功,開始監(jiān)聽 FD
  • 用戶進程不阻塞等待,可以執(zhí)行其它業(yè)務(wù)
  • 當(dāng)內(nèi)核數(shù)據(jù)就緒后,回調(diào)用戶進程的 SIGIO 處理函數(shù)

階段二:

  • 收到 SIGIO 回調(diào)信號
  • 調(diào)用 recvfrom ,讀取
  • 內(nèi)核將數(shù)據(jù)拷貝到用戶空間
  • 用戶進程處理數(shù)據(jù)

缺點

當(dāng)有大量 IO 操作時,信號較多,SIGIO 處理函數(shù)不能及時處理可能導(dǎo)致信號隊列溢出,而且內(nèi)核空間與用戶空間的頻繁信號交互性能也較低。

異步 IO

這種方式,不僅僅是用戶態(tài)在試圖讀取數(shù)據(jù)后,不阻塞,而且當(dāng)內(nèi)核的數(shù)據(jù)準(zhǔn)備完成后,也不會阻塞

兩個過程都不阻塞

他會由內(nèi)核將所有數(shù)據(jù)處理完成后,由內(nèi)核將數(shù)據(jù)寫入到用戶態(tài)中,然后才算完成,所以性能極高,不會有任何阻塞,全部都由內(nèi)核完成,可以看到,異步 IO 模型中,用戶進程在兩個階段都是非阻塞狀態(tài)。

缺點

得做好限流,不然無腦的給內(nèi)核去干,相當(dāng)于領(lǐng)導(dǎo)不管用戶死活,一股腦塞

🎯Java中常見的IO模型

BIO

上文的阻塞IO

NIO

上文的非阻塞IO

AIO

其實就是上文的異步模型

🎯什么是IO多路復(fù)用

定義 & 流程

當(dāng)用戶進程調(diào)用了select,那么整個進程會被阻塞,而同時,內(nèi)核會"監(jiān)視"所有select負責(zé)的socket,當(dāng)任何一個socket中的數(shù)據(jù)準(zhǔn)備好了,select就會返回。這個時候用戶進程再調(diào)用read操作,將數(shù)據(jù)從內(nèi)核拷貝到用戶進程。

這個模型和阻塞IO的模型其實并沒有太大的不同,事實上還更差一些。因為這里需要使用兩個系統(tǒng)調(diào)用(select和recvfrom),而阻塞IO只調(diào)用了一個系統(tǒng)調(diào)用(recvfrom)。

  • 但是,用select的優(yōu)勢在于它可以同時處理多個連接。所以,如果系統(tǒng)的連接數(shù)不是很高的話,使用select/epoll的web server不一定比使用多線程的阻塞IO的web server性能更好,可能延遲還更大;select/epoll的優(yōu)勢并不是對單個連接能處理得更快,而是在于能處理更多的連接。

🎯IO多路復(fù)用的三種實現(xiàn)方式

目前流程的多路復(fù)用 IO 實現(xiàn)主要包括四種: select、poll、epoll、kqueue。下表是他們的一些重要特性的比較:

IO 模型相對性能關(guān)鍵思路操作系統(tǒng)JAVA 支持情況
select較高Reactorwindows/Linux支持,Reactor 模式 (反應(yīng)器設(shè)計模式)。Linux 操作系統(tǒng)的 kernels 2.4 內(nèi)核版本之前,默認(rèn)使用 select;而目前 windows 下對同步 IO 的支持,都是 select 模型
poll較高ReactorLinuxLinux 下的 JAVA NIO 框架,Linux kernels 2.6 內(nèi)核版本之前使用 poll 進行支持。也是使用的 Reactor 模式
epollReactor/ProactorLinuxLinux kernels 2.6 內(nèi)核版本及以后使用 epoll 進行支持;Linux kernels 2.6 內(nèi)核版本之前使用 poll 進行支持;另外一定注意,由于 Linux 下沒有 Windows 下的 IOCP 技術(shù)提供真正的 異步 IO 支持,所以 Linux 下使用 epoll 模擬異步 IO
kqueueProactorLinux目前 JAVA 的版本不支持

select

select 是 Linux 最早的 I/O 多路復(fù)用技術(shù):

linux 中,一切皆文件,socket 也不例外,我們把需要處理的數(shù)據(jù)封裝成 FD,然后在用戶態(tài)時創(chuàng)建一個 fd_set 的集合(這個集合的大小是要監(jiān)聽的那個 FD 的最大值 + 1,但是大小整體是有限制的 ),這個集合的長度大小是有限制的,同時在這個集合中,標(biāo)明出來我們要控制哪些數(shù)據(jù)。

具體流程

用戶態(tài) :

  • 創(chuàng)建 fd_set 集合,包括要監(jiān)聽的 讀事件、寫事件、異常事件的集合
  • 確定要監(jiān)聽的 fd_set 集合
  • 將要監(jiān)聽的集合作為參數(shù)傳入 select () 函數(shù)中,select 中會將 集合復(fù)制到內(nèi)核 buffer
  • 內(nèi)核態(tài):

  • 內(nèi)核線程在得到集合后,遍歷該集合
  • 沒有數(shù)據(jù)就緒,就休眠
  • 當(dāng)數(shù)據(jù)來時,線程被喚醒,然后再次遍歷集合,標(biāo)記就緒的 fd 然后將整個集合,復(fù)制回用戶 buffer 中
  • 用戶線程遍歷集合,找到就緒的 fd ,再發(fā)起讀請求。
  • 🎈源碼&流程

    🎐不足之處

  • select無法得知具體是哪個fd準(zhǔn)備就緒了,每次都需要遍歷一遍fd_set,效率很低
  • 需要進行 2 次「遍歷」文件描述符集合,一次是在內(nèi)核態(tài)里,一個次是在用戶態(tài)里 ,而且還會發(fā)生 2 次「拷貝」文件描述符集合,先從用戶空間傳入內(nèi)核空間,由內(nèi)核修改后,再傳出到用戶空間中。

  • 集合大小固定為 1024 ,也就是說最多維持 1024 個 socket,在海量數(shù)據(jù)下,不夠用
  • 需要將整個fd_set從用戶空間拷貝到內(nèi)核空間,select結(jié)束后還需要再次拷貝回用戶空間,涉及到 用戶態(tài)和內(nèi)核態(tài)的切換,非常影響性能
  • poll

    poll 模式對 select 模式做了簡單改進,但性能提升不明顯。

    具體流程:

  • 創(chuàng)建 pollfd 數(shù)組,向其中添加關(guān)注的 fd 信息,數(shù)組大小自定義
  • 調(diào)用 poll 函數(shù),將 pollfd 數(shù)組拷貝到內(nèi)核空間,轉(zhuǎn)鏈表存儲,無上限
  • 內(nèi)核遍歷 fd ,判斷是否就緒
  • 數(shù)據(jù)就緒或超時后,拷貝 pollfd 數(shù)組到用戶空間,返回就緒 fd 數(shù)量 n
  • 用戶進程判斷 n 是否大于 0:【好像不重要】
  • 大于 0 則遍歷 pollfd 數(shù)組找到就緒的 fd
  • 與 select 對比

    大小方面:

    • select 模式中的 fd_set 大小固定為 1024,而 pollfd 在內(nèi)核中采用鏈表理論上無上限,但實際上不能這么做,因為的監(jiān)聽 FD 越多,每次遍歷消耗時間也越久,性能反而會下降

    🎈epoll

    epoll 模式是對 select 和 poll 的改進,它提供了三個函數(shù):eventpoll 、epoll_ctl 、epoll_wait

    • eventpoll 函數(shù)內(nèi)部包含了兩個東西 :

      • 紅黑樹 :用來記錄所有的 fd
      • 鏈表 : 記錄已就緒的 fd 、
    • epoll_ctl 函數(shù) ,將要監(jiān)聽的 fd 添加到 紅黑樹 上去,并且給每個 fd 綁定一個監(jiān)聽函數(shù),當(dāng) fd 就緒時就會被觸發(fā),這個監(jiān)聽函數(shù)的操作就是 將這個 fd 添加到 鏈表中去

    • epoll_wait 函數(shù),就緒等待。一開始,用戶態(tài) buffer 中創(chuàng)建一個空的 events 數(shù)組,當(dāng)就緒之后,我們的回調(diào)函數(shù)會把 fd 添加到鏈表中去

      • 當(dāng)函數(shù)被調(diào)用的時候,會去檢查鏈表(當(dāng)然這個過程需要參考配置的等待時間,可以等一定時間,也可以一直等)
        • 如果鏈表中沒有 fd ,則 fd 會從紅黑樹被添加到鏈表中,此時再將鏈表中的的 fd 復(fù)制到用戶態(tài)的空 events中,并且返回對應(yīng)的操作數(shù)量,用戶態(tài)此時收到響應(yīng)后,會從 events 中拿到已經(jīng)準(zhǔn)備好的數(shù)據(jù),在調(diào)用 讀方法 去拿數(shù)據(jù)。

    🎈🎈總結(jié)

    select 模式存在的三個問題:

    • 能監(jiān)聽的 FD 最大不超過 1024
    • 每次 select 都需要把所有要監(jiān)聽的 FD 都拷貝到內(nèi)核空間
    • 每次都要遍歷所有 FD 來判斷就緒狀態(tài)

    poll 模式的問題:

    • poll 利用鏈表解決了 select 中監(jiān)聽 FD 上限的問題,但依然要遍歷所有 FD,如果監(jiān)聽較多,性能會下降

    epoll 模式中如何解決這些問題的?

    • 基于 epoll 實例中的紅黑樹保存要監(jiān)聽的 FD,理論上無上限 ,而且增刪改查效率都非常高,性能不會隨監(jiān)聽
    • 每個 FD 只需要執(zhí)行一次 epoll_ctl 添加到紅黑樹,以后每次 epol_wait 無需傳遞任何參數(shù),無需重復(fù)拷貝 FD 到內(nèi)核空間
    • 利用 ep_poll_callback 機制來監(jiān)聽 FD 狀態(tài),無需遍歷所有 FD,因此性能不會隨監(jiān)聽的 FD 數(shù)量增多而下降

    🎯邊緣觸發(fā)和水平觸發(fā)

    epoll 支持兩種事件觸發(fā)模式,分別是邊緣觸發(fā)(edge-triggered,ET)和水平觸發(fā)(level-triggered,LT)

    • 使用邊緣觸發(fā)模式時,當(dāng)被監(jiān)控的 Socket 描述符上有可讀事件發(fā)生時,服務(wù)器端只會從 epoll_wait 中蘇醒一次,即使進程沒有調(diào)用 read 函數(shù)從內(nèi)核讀取數(shù)據(jù),也依然只蘇醒一次,因此我們程序要保證一次性將內(nèi)核緩沖區(qū)的數(shù)據(jù)讀取完;

    • 使用水平觸發(fā)模式時,當(dāng)被監(jiān)控的 Socket 上有可讀事件發(fā)生時,服務(wù)器端不斷地從 epoll_wait 中蘇醒,直到內(nèi)核緩沖區(qū)數(shù)據(jù)被 read 函數(shù)讀完才結(jié)束,目的是告訴我們有數(shù)據(jù)需要讀取;

    這個過程是用戶空間去讀內(nèi)核空間

    水平觸發(fā)的意思是只要滿足事件的條件,比如內(nèi)核中有數(shù)據(jù)需要讀,就一直不斷地把這個事件傳遞給用戶;而邊緣觸發(fā)的意思是只有第一次滿足條件的時候才觸發(fā),之后就不會再傳遞同樣的事件了。

    如果使用水平觸發(fā)模式,當(dāng)內(nèi)核通知文件描述符可讀寫時,接下來還可以繼續(xù)去檢測它的狀態(tài),看它是否依然可讀或可寫。所以在收到通知后,沒必要一次執(zhí)行盡可能多的讀寫操作。

    邊緣觸發(fā)注意點

    如果使用邊緣觸發(fā)模式,I/O 事件發(fā)生時只會通知一次,而且我們不知道到底能讀寫多少數(shù)據(jù),所以在收到通知后應(yīng)盡可能地讀寫數(shù)據(jù),以免錯失讀寫的機會。

    因此,我們會循環(huán)從文件描述符讀寫數(shù)據(jù)【圖中的④操作使用循環(huán)】,那么如果文件描述符是阻塞的,沒有數(shù)據(jù)可讀寫時,進程會阻塞在讀寫函數(shù)那里,程序就沒辦法繼續(xù)往下執(zhí)行。

    所以,邊緣觸發(fā)模式一般和非阻塞 I/O 搭配使用,程序會一直執(zhí)行 I/O 操作,直到系統(tǒng)調(diào)用(如 read 和 write)返回錯誤,錯誤類型為 EAGAIN 或 EWOULDBLOCK。

    一般來說,邊緣觸發(fā)的效率比水平觸發(fā)的效率要高,因為邊緣觸發(fā)可以減少 epoll_wait 的系統(tǒng)調(diào)用次數(shù),系統(tǒng)調(diào)用也是有一定的開銷的的,畢竟也存在上下文的切換。

    select/poll 只有水平觸發(fā)模式,epoll 默認(rèn)的觸發(fā)模式是水平觸發(fā),但是可以根據(jù)應(yīng)用場景設(shè)置為邊緣觸發(fā)模式。


    🍿🍿🍿手撕面答環(huán)節(jié) -- 這是一條分割線

    劃掉的部分屬于melo復(fù)述時,發(fā)送的疏漏之處/答錯的地方hhh

    🍔select,poll,epoll的區(qū)別

    select

    用戶注冊了自己需要監(jiān)聽的設(shè)備,記錄在一個fd數(shù)組里邊,拷貝給內(nèi)核態(tài)服務(wù)端,服務(wù)端那邊若準(zhǔn)備好了,會修改fd數(shù)組中對應(yīng)設(shè)備的位置,值改為1,并且把整個fd數(shù)組拷貝回用戶態(tài)

    實際上服務(wù)端還要遍歷一遍fd數(shù)組,標(biāo)記就緒的fd為1,拷貝回用戶態(tài)

    用戶態(tài)再遍歷一遍fd數(shù)組,找到其中值為1的,說明準(zhǔn)備好了,可以開始拷貝了。

    不足之處

    涉及到多次拷貝,用戶態(tài)和內(nèi)核態(tài)的切換

    poll

    跟select的區(qū)別主要在于,不是用fd數(shù)組了,而是用一個鏈表,理論上可以無限節(jié)點,但本質(zhì)上,節(jié)點數(shù)量越多,效率自然隨著降低,有沒有能夠解決這種節(jié)點數(shù)影響效率的限制呢?這個時候epoll就出來了,紅黑樹。

    更具體一點是,用戶態(tài)仍然是fd數(shù)組,轉(zhuǎn)到內(nèi)核態(tài)才變?yōu)殒湵泶鎯?/p>

    epoll

    把要監(jiān)聽的設(shè)備,都注冊到一棵紅黑樹上邊,并給每個節(jié)點綁定監(jiān)聽函數(shù),但服務(wù)端準(zhǔn)備就緒時,會觸發(fā)監(jiān)聽函數(shù),把該節(jié)點拷貝到fd數(shù)組上邊【是就緒鏈表上邊】,并且返回給用戶態(tài)【注意只返回準(zhǔn)備好了的設(shè)備,這是跨時代的進步】

    優(yōu)點

    select/poll 每次操作時都傳入整個 socket 集合給內(nèi)核,而 epoll 因為在內(nèi)核維護了紅黑樹,可以保存所有待檢測的 socket ,所以只需要傳入一個待檢測的 socket,減少了內(nèi)核和用戶空間大量的數(shù)據(jù)拷貝和內(nèi)存分配。

    🍔🎐邊緣觸發(fā)為何建議搭配非阻塞IO?

    多路復(fù)用 API 返回的事件并不一定可讀寫的【select() 可能會將一個 socket 文件描述符報告為 "準(zhǔn)備讀取",而后續(xù)的讀取塊卻沒有。例如,當(dāng)數(shù)據(jù)已經(jīng)到達,但經(jīng)檢查后發(fā)現(xiàn)有錯誤的校驗和而被丟棄時,就會發(fā)生這種情況

    虛晃一槍,以為準(zhǔn)備好了要給你數(shù)據(jù)了,但這時被丟棄了【又變成還沒準(zhǔn)備好的狀態(tài)】,我們還傻傻的一直在等待讀取

    如果使用阻塞 I/O, 那么在調(diào)用 read/write 時則會發(fā)生程序阻塞,

    非阻塞 I/O的話,會忙等輪詢,直到系統(tǒng)調(diào)用(如 read 和 write)返回錯誤,錯誤類型為 EAGAIN 或 EWOULDBLOCK。

    阻塞IO:當(dāng)你去讀一個阻塞的文件描述符時,如果在該文件描述符上沒有數(shù)據(jù)可讀,那么它會一直阻塞(通俗一點就是一直卡在調(diào)用函數(shù)那里),直到有數(shù)據(jù)可讀。當(dāng)你去寫一個阻塞的文件描述符時,如果在該文件描述符上沒有空間(通常是緩沖區(qū))可寫,那么它會一直阻塞,直到有空間可寫。

    非阻塞IO:當(dāng)你去讀寫一個非阻塞的文件描述符時,不管可不可以讀寫,它都會立即返回,返回成功說明讀寫操作完成了,返回失敗會設(shè)置相應(yīng)errno狀態(tài)碼,根據(jù)這個errno可以進一步執(zhí)行其他處理。它不會像阻塞IO那樣,卡在那里不動!!!

    另一種答案

    由于ET模式下,需要while循環(huán)調(diào)用read和wirte,直到最后返回特定的錯誤類型才退出循環(huán)。

    如果采用非阻塞IO,則可能會在最后一次本應(yīng)該跳出循環(huán)的read調(diào)用阻塞住。

    🍔epoll的ET和LT有什么區(qū)別

    ET:edge trigger 邊緣觸發(fā),指的是當(dāng)socket準(zhǔn)備好了,服務(wù)端只蘇醒一次,所以用戶緩沖區(qū)要一次性把內(nèi)核緩沖區(qū)讀完,nginx就是采用的ET

    LT:level-trigger 水平觸發(fā),socket準(zhǔn)備好了,服務(wù)端會不斷蘇醒,直到用戶緩沖區(qū)把內(nèi)核緩沖區(qū)讀完了,redis就是采用的LT

    🍔邊緣觸發(fā)如何保證數(shù)據(jù)讀完

    while循環(huán)讀寫,直到最后一次返回特定的錯誤類型【EAGAIN錯誤】

    🍔ET模式下的accept問題

    在某一時刻,有多個連接同時到達,服務(wù)器的 TCP 就緒隊列瞬間積累多個就緒連接,由于是邊緣觸發(fā)模式,epoll 只會通知一次,accept 只處理一個連接,導(dǎo)致 TCP 就緒隊列中剩下的連接都得不到處理。在這種情形下,我們應(yīng)該如何有效的處理呢?

    解決的方法是:解決辦法是用 while 循環(huán)包住 accept 調(diào)用,處理完 TCP 就緒隊列中的所有連接后再退出循環(huán)。

    如何知道是否處理完就緒隊列中的所有連接呢?

    • accept 返回 -1 并且 errno 設(shè)置為 EAGAIN 就表示所有連接都處理完。

    🍔epoll讀到一半又有新事件來了怎么辦?

    避免在主進程epoll再次監(jiān)聽到同一個可讀事件,可以把對應(yīng)的描述符設(shè)置為EPOLL_ONESHOT,效果是監(jiān)聽到一次事件后就將對應(yīng)的描述符從監(jiān)聽集合中移除,也就不會再被追蹤到。讀完之后可以再把對應(yīng)的描述符重新手動加上。

    總結(jié)

    以上是生活随笔為你收集整理的Redis 网络模型 -- 阻塞非阻塞IO、IO多路复用、epoll详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。