淺談 Linux 內核之 CPU 緩存

一、什麼是 CPU 緩存

1. CPU 緩存的來歷

當計算機處理數據時,CPU(中央處理器)是負責執行指令和進行計算的核心組件。然而,CPU 與內存之間的通信速度存在着較大差異。內存存儲了大量的數據,但是 CPU 無法直接從內存中讀取數據,而是需要通過數據總線進行通信。這個過程需要花費一定的時間。

爲了解決這個問題,CPU 緩存應運而生。CPU 緩存是位於 CPU 內部的一小塊高速存儲器,它用於臨時存儲 CPU 頻繁訪問的數據和指令。CPU 緩存的訪問速度比內存快得多,因爲它位於 CPU 芯片上,與 CPU 內部電路直接相連。

CPU 緩存通過減少 CPU 與內存之間的數據傳輸次數來提高計算機的性能。當 CPU 需要讀取數據時,它首先檢查緩存中是否存在這些數據。如果數據已經緩存在 CPU 緩存中,CPU 就可以直接從緩存中讀取,無需訪問內存,從而大大提高了訪問速度。這類似於在您的書桌上放置一些您經常使用的物品,以便您可以更快地獲取它們,而不必每次都去其他地方尋找。

CPU 緩存通常分爲多個層級,如 L1、L2 和 L3 緩存。L1 緩存是最接近 CPU 核心的一級緩存,速度最快但容量最小。L2 緩存和 L3 緩存容量較大,速度稍慢一些,但仍比內存快得多。不同層級的緩存之間形成了一個層次結構,以便更好地利用緩存空間並提高緩存命中率。

總而言之,CPU 緩存的目的是爲了提高計算機的運行速度。它通過暫時存儲頻繁使用的數據和指令,減少 CPU 與內存之間的數據傳輸次數,從而加快數據訪問速度,提高計算機的整體性能。

2. CPU 緩存的概念

假設你是一位辦公室職員,每天需要處理大量的文件和文件夾。你的辦公桌上只有有限的空間,而你的文件櫃則放置在離你辦公桌較遠的地方。每當你需要一個文件時,你就必須走到文件櫃那裏去取,然後再回到辦公桌上進行處理。這樣的來回取文件的過程會花費一些時間。

爲了提高你的工作效率,你決定在辦公桌上放置一個小的文件夾,裏面放置一些你經常使用的文件。這樣,當你需要這些文件時,你就可以直接從文件夾裏拿出來,而無需每次都去文件櫃那裏取。這樣一來,你可以更快地獲取文件並更快地完成工作。

在計算機中,CPU 就像你的辦公桌,而內存就像文件櫃。內存存儲了大量的數據,但是 CPU 無法直接從內存中讀取數據,需要通過數據總線進行通信,這個過程需要花費一定的時間。爲了解決這個問題,就引入了 CPU 緩存,就像你的辦公桌上的小文件夾一樣。

CPU 緩存是一個小而快速的存儲器,位於 CPU 內部。它用於暫時存儲 CPU 頻繁訪問的數據和指令。當 CPU 需要讀取數據時,它首先檢查緩存中是否存在這些數據。如果數據已經緩存在 CPU 緩存中,CPU 就可以直接從緩存中讀取,無需訪問內存,從而大大提高了訪問速度。

就像你的辦公桌上的小文件夾有不同的層次一樣,CPU 緩存也被劃分爲多個層級,如 L1、L2 和 L3 緩存。L1 緩存是最接近 CPU 核心的一級緩存,速度最快但容量最小。L2 緩存和 L3 緩存容量較大,速度稍慢一些,但仍比內存快得多。這些層級的緩存之間形成了一個層次結構,以便更好地利用緩存空間並提高緩存命中率。

通過使用 CPU 緩存,計算機可以更快地讀取和處理數據,提高計算機的整體性能。這就好像你在辦公桌上放置一個小文件夾,以便更快地獲取經常使用的文件,從而更高效地完成工作。

3. CPU 緩存的意義

當我們使用計算機進行各種任務時,CPU(中央處理器)扮演着非常重要的角色。它負責執行各種計算和指令,讓我們的計算機能夠完成我們需要的工作。然而,CPU 的運算速度非常快,而內存(RAM)的運算速度相對較慢。

假如說您是一名老師,在教室裏有很多學生,每個學生都在提問問題。這裏有一個問題:如果一個學生問一個非常常見的問題,您可能已經回答了很多次,那麼您可能會立刻知道答案並且快速地回答他。這個學生的問題就像 CPU 在執行任務時要頻繁訪問的數據和指令。

現在假設這裏還有一些學生提出了不太常見的問題,你之前從未回答過。對於這些問題,您可能需要稍微思考一下,或者去書櫃找到相關的資料,然後再給他們答案。這裏的問題就像是 CPU 在執行任務時需要訪問較慢的內存,因爲內存相對於 CPU 的速度較慢。

CPU 緩存就是一種像您的腦海裏存放一些最常見問題答案的小抽屜。當學生提出常見問題時,您可以迅速地從這個小抽屜中取出答案,而無需再去查找或思考。這樣,您可以更快地回答問題。類似地,CPU 緩存是一個位於 CPU 內部的小快速存儲器,它存放着 CPU 頻繁訪問的數據和指令。

通過使用 CPU 緩存,CPU 可以快速地從緩存中取得常見數據和指令,而不必每次都去訪問較慢的內存。這樣就節省了大量的時間,提高了計算機的運行速度。而且,CPU 緩存通常分爲多個層級,每個層級都有不同的大小和速度,就像您可能有一個專門存放常見問題答案的小抽屜,還有一個稍大一點的抽屜放着不那麼常見的問題答案。這樣的設計使得 CPU 緩存能夠更好地適應不同類型的任務,並提供更高效的數據訪問方式。

所以,CPU 緩存的意義在於加快 CPU 的運行速度,使計算機能夠更快地完成各種任務,提高我們的計算機使用體驗。就像您作爲老師能更快回答學生問題一樣,CPU 緩存也讓 CPU 更迅速地處理數據和指令,使計算機運行更加高效。

滿足的兩個侷限性原理是指計算機中的兩個重要原則:時間侷限性(Temporal Locality)和空間侷限性(Spatial Locality)。這兩個原理有助於優化計算機的性能和效率。

  1. 時間侷限性(Temporal Locality):這個原理指的是當計算機訪問某個數據或指令時,接下來很可能會再次訪問相同的數據或指令。簡單來說,如果你在做某個任務時,剛剛使用過的數據或指令很可能會在不久後再次被使用到。這個原理可以類比爲你在讀一本書時,剛剛閱讀過的頁面很可能在接下來的幾頁中會再次提到。

時間侷限性的重要性在於,計算機中的緩存系統可以利用這個原理。當 CPU 訪問某個數據時,緩存會將這個數據存儲在高速的緩存中,這樣當 CPU 再次需要這個數據時,就可以直接從緩存中讀取,而不必再次訪問慢速的內存。這樣可以極大地加快數據的訪問速度,提高計算機的性能。

  1. 空間侷限性(Spatial Locality):這個原理指的是當計算機訪問某個數據或指令時,接下來很可能會訪問與之相鄰的數據或指令。簡單來說,如果你在整理書櫃時,剛剛取出的一本書後面很可能是你接下來要整理的書,因此你可以將這兩本書放在手邊,以便更快地找到它們。

空間侷限性的原理同樣適用於計算機中的緩存系統。當 CPU 訪問某個數據時,緩存不僅會將這個數據存儲在緩存中,還會將與之相鄰的數據也存儲起來。這樣,當 CPU 需要訪問相鄰的數據時,可以直接從緩存中獲取,而不必再次訪問內存。這種預取相鄰數據的方式同樣可以提高計算機的性能,減少訪問內存的次數。

時間侷限性和空間侷限性原則在計算機中起到了優化性能和提高效率的作用。通過利用這兩個原則,緩存系統能夠預測和存儲 CPU 接下來可能訪問的數據和指令,從而加快數據的訪問速度,提高計算機的整體運行效率。就像你在整理書櫃時可以事先將相關的書放在一起,以便更快地找到它們一樣。

二、CPU 的三級緩存

1. CPU 的三級緩存

當談到 CPU 緩存時,通常會提到三個級別的緩存,即 L1 緩存、L2 緩存和 L3 緩存。讓我們以一所學校的圖書館爲例來解釋這三個級別的緩存。

比如說,你是一名學生,你經常需要在圖書館借閱和歸還書籍。現在呢,圖書館裏有三個不同級別的書架。

L1 緩存就像是你手邊的小書架,上面擺放着你最常用的書籍。這些書籍非常接近你,因此你可以輕鬆而快速地取出它們並進行閱讀。這就是爲什麼 L1 緩存的訪問速度非常快的原因。它是 CPU 內部最快的緩存層,它存儲着 CPU 最常使用的數據和指令。

接下來是 L2 緩存,就像是你的房間裏的書架。這個書架比你手邊的小書架大,可以存放更多的書籍。雖然它離你的位置有一點點距離,但你仍然可以相對容易地取得你需要的書籍。這是因爲 L2 緩存位於離 CPU 更遠但仍然相對快速的位置,它可以存儲比 L1 緩存更多的數據和指令。

最後是 L3 緩存,它就像是整個學校圖書館的大書架。這個書架非常大,可以容納大量的書籍。它離你的位置相對較遠,所以取得書籍可能需要花費更多的時間。但是,由於它可以容納更多的書籍,所以即使它的速度相對較慢,你仍然可以在需要時找到你需要的書籍。L3 緩存通常位於 CPU 芯片上,而且不是所有的處理器都有 L3 緩存。

在這個圖書館的例子中,你是 CPU,書籍是數據和指令,而三個級別的書架就是 CPU 緩存的三個級別。L1 緩存是最快、容量最小的,L2 緩存速度稍慢一些但容量更大,而 L3 緩存則更大容量但速度相對較慢。

爲什麼有這種層次結構的緩存呢?因爲更靠近 CPU 的緩存層次可以更快地訪問數據,但容量有限。如果所有數據都放在最快的 L1 緩存中,那麼容量會很快不夠。因此,較慢但容量更大的 L2 和 L3 緩存就像

是擴展的存儲空間,可以存儲更多的數據,儘管訪問速度稍慢。

總的來說,這三個級別的緩存構成了一種層次結構,以滿足 CPU 對數據和指令的不同訪問需求。通過這種層次結構,CPU 可以快速訪問頻繁使用的數據和指令,並在需要時逐級向下查找,從而提高計算機的整體性能。
下面是三級緩存的處理速度參考表:

image

下圖是 Intel Core i5-4285U 的 CPU 三級緩存示意圖:

image

就像數據庫緩存一樣,獲取數據時首先會在最快的緩存中找數據,如果緩存沒有命中 (Cache miss) 則往下一級找, 直到三級緩存都找不到時,那只有向內存要數據了。一次次地未命中,代表取數據消耗的時間越長。

2. 帶有高速緩存 CPU 執行計算的流程

1、程序以及數據被加載到主內存
2、指令和數據被加載到 CPU 的高速緩存
3、CPU 執行指令,把結果寫到高速緩存
4、高速緩存中的數據寫回主內存

目前流行的多級緩存結構如下圖:

image

三、CPU 緩存一致性協議 (MESI)

當計算機系統中有多個 CPU 同時訪問共享內存時,CPU 緩存一致性協議(MESI)起着非常重要的作用。MESI 是一種用於保持多個 CPU 緩存中數據一致性的協議。

有兩個人(CPU)正在合作完成一項任務,並且他們共享一張紙(共享內存),上面需要記錄一些信息。每個人都有自己的備忘錄(緩存),他們可以在備忘錄上寫下他們需要的信息,並且可以隨時參考備忘錄上的信息,而不必每次都去查看紙上的內容。

現在,假設其中一個人(CPU1)想要修改紙上的信息,他會先查看自己的備忘錄(緩存),如果備忘錄上沒有這個信息,他就會去查看另一個人(CPU2)的備忘錄(緩存)。如果 CPU2 的備忘錄中有這個信息,那麼 CPU1 會告訴 CPU2:“嘿,我要修改這個信息,你的備忘錄中的那個信息已經過時了。” 然後 CPU2 會把他的備忘錄上的信息標記爲無效,表示它已經過時了。

接着,CPU1 會把他自己的備忘錄上的信息修改爲新的內容,並且將這個新的信息更新到紙上。然後他會告訴 CPU2:“我已經修改了紙上的信息,你可以把你的備忘錄上的信息更新爲最新的了。” 這樣,兩個人的備忘錄和紙上的信息就保持一致了。

MESI 協議就是通過類似的方式來保持多個 CPU 緩存的數據一致性。每個 CPU 緩存有四種狀態:修改(Modified)、獨佔(Exclusive)、共享(Shared)和無效(Invalid)。當一個 CPU 要讀取或修改共享內存中的數據時,它首先會檢查自己的緩存中的狀態。如果數據在自己的緩存中是有效的且處於共享狀態,那麼它可以直接使用緩存中的數據。如果數據在自己的緩存中無效或者處於獨佔狀態,那麼它就需要向其他 CPU 發送請求,獲取最新的數據。

當一個 CPU 要修改共享內存中的數據時,它會先將緩存中的數據狀態修改爲修改狀態,並且把這個修改的消息發送給其他 CPU。其他 CPU 接收到這個消息後,會將自己的緩存中對應的數據狀態修改爲無效狀態,表示它們的數據已經過時了。然後,修改數據的 CPU 可以安全地更新共享內存中的數據,並且將其他 CPU 的緩存中對應的數據狀態修改爲無效。

通過這種方式,MESI 協議保證了多個 CPU 之間共享數據的一致性。它確保了在多個 CPU 同時訪問共享內存時,數據的修改能夠正確地同步和更新,避免了數據不一致的情況發生。就像兩個人通過備忘錄和紙張的協作來保持信息一致一樣,MESI 協議通過 CPU 緩存的狀態轉換和消息交互來保持多個 CPU 緩存中的數據一致。

1. MESI 協議中的狀態

在 MESI 協議中,每個 CPU 緩存的數據狀態可以有以下四種狀態:

  1. 修改(Modified):當一個 CPU 將共享內存中的數據加載到自己的緩存中後,如果修改了這個數據,那麼該數據就處於修改狀態。修改狀態表示該 CPU 是該數據的唯一擁有者,並且數據與內存中的數據不一致。如果其他 CPU 想要讀取或修改該數據,需要先與擁有修改狀態的 CPU 進行通信,確保數據的一致性。

  2. 獨佔(Exclusive):當一個 CPU 將共享內存中的數據加載到自己的緩存中後,如果沒有修改該數據,那麼該數據就處於獨佔狀態。獨佔狀態表示該 CPU 是該數據的唯一擁有者,但數據與內存中的數據是一致的。其他 CPU 可以讀取該數據,但需要通過緩存一致性協議來確保數據的一致性。

  3. 共享(Shared):當多個 CPU 將共享內存中的數據加載到各自的緩存中後,且數據未被修改,那麼該數據就處於共享狀態。共享狀態表示多個 CPU 共享該數據,並且數據與內存中的數據是一致的。其他 CPU 可以讀取該數據,而不需要進行額外的通信。

  4. 無效(Invalid):當一個 CPU 將共享內存中的數據加載到自己的緩存中後,如果其他 CPU 已經修改了該數據,那麼該數據就處於無效狀態。無效狀態表示該 CPU 的緩存中的數據已過期,不可用。當其他 CPU 需要讀取或修改該數據時,必須先與擁有最新數據的 CPU 進行通信,獲取最新的數據。

這些狀態的轉換是根據 CPU 緩存對數據的讀寫操作以及與其他 CPU 之間的通信來進行的。MESI 協議通過有效管理緩存中數據的狀態,確保多個 CPU 之間共享數據的一致性和正確性。

CPU 中每個緩存行(Cache line) 使用 4 種狀態進行標記,使用 2bit 來表示:

注意:對於 M 和 E 狀態而言總是精確的,他們在和該緩存行的真正狀態是一致的,而 S 狀態可能是非一致的。如果一個緩存將處於 S 狀態的緩存行作廢了,而另一個緩存實際上可能已經獨享了該緩存行,但是該緩存卻不會將該緩存行升遷爲 E 狀態,這是因爲其它緩存不會廣播他們作廢掉該緩存行的通知,同樣由於緩存並沒有保存該緩存行的 copy 的數量,因此(即使有這種通知)也沒有辦法確定自己是否已經獨享了該緩存行。

從上面的意義看來 E 狀態是一種投機性的優化:如果一個 CPU 想修改一個處於 S 狀態的緩存行,總線事務需要將所有該緩存行的 copy 變成 invalid 狀態,而修改 E 狀態的緩存不需要使用總線事務。

MESI 狀態轉換圖:

下圖表示了當一個緩存行 (Cache line) 的調整的狀態的時候,另外一個緩存行 (Cache line) 需要調整的狀態。

舉個示例:

假設 cache 1 中有一個變量 x = 0 的 Cache line 處於 S 狀態 (共享)。
那麼其他擁有 x 變量的 cache 2、cache 3 等 x 的 Cache line 調整爲 S 狀態(共享)或者調整爲 I 狀態(無效)。

2. 多核緩存協同操作

(1) 內存變量

假設有三個 CPU A、B、C,對應三個緩存分別是 cache a、b、c。在主內存中定義了 x 的引用值爲 0。

(2) 單核讀取

執行流程是:

(3) 雙核讀取

執行流程是:

(4) 修改數據

執行流程是:

(5) 同步數據

那麼執行流程是:

3. CPU 存儲模型簡介

當多個 CPU 同時訪問緩存中的共享數據時,爲了保證數據的一致性,引入了 MESI 協議。這個協議定義了緩存行的四種狀態,用來表示數據在不同 CPU 的緩存中的狀態。當一個 CPU 對緩存進行讀取或寫入操作時,可能會導致緩存中的數據狀態變得不一致。爲了解決這個問題,緩存控制器需要監聽本地和遠程操作,並對相關的緩存行狀態進行相應的修改,以保證數據在多個緩存之間的一致性。

然而,保證緩存一致性需要進行消息傳遞,而這個過程需要時間。狀態的切換會引入更多的延遲,並且某些狀態的切換需要特殊的處理,可能會阻塞處理器的執行。這些問題會給系統的穩定性和性能帶來一些挑戰。

爲了解決這些問題,引入了存儲緩存(Store Buffer)和無效隊列(Invalidate Queue)。存儲緩存是一種特殊的緩存區域,用於暫時存儲將要寫入緩存的數據。當 CPU 執行寫操作時,它將數據先存儲到存儲緩存中,而不是直接寫入緩存。這樣做的好處是,CPU 可以繼續執行後續的指令,而無需等待寫操作完成。存儲緩存會在合適的時機將數據寫入緩存,並確保數據的一致性。

無效隊列是用於處理無效狀態的緩存行的隊列。當一個 CPU 將緩存行標記爲無效時,它將該信息添加到無效隊列中。其他 CPU 可以檢查無效隊列,獲取關於緩存行狀態的更新信息,並進行相應的處理。

通過引入存儲緩存和無效隊列,可以避免處理器因爲等待遠程緩存狀態的確認而被阻塞,從而減少時間的浪費。這樣處理器可以繼續執行其他指令,提高了性能。

(1) 存儲緩存

在沒有存儲緩存的情況下,當 CPU 要寫入一個數值時,可能會面臨以下情況:

  1. 如果這個數值不在 CPU 的緩存中,那麼 CPU 需要發送一個信號給其他部件,告訴它們需要讀取和失效(無效化)這個數值的狀態。然後 CPU 需要等待這個信號的響應,然後才能將這個數值寫入到緩存中。

  2. 如果這個數值在 CPU 的緩存中,並且它的狀態是獨佔(Exclusive)的,那麼 CPU 可以直接修改這個數值。

  3. 如果這個數值在 CPU 的緩存中,但它的狀態是共享(Shared)的,那麼 CPU 需要發送一個使其他 CPU 感知到這個更改的信號,然後等待其他 CPU 的響應,然後才能進行修改。

在這些情況下,很可能會涉及到 CPU 與其他 CPU 進行通信,並且需要等待它們的回覆。這會浪費很多時鐘週期。爲了提高效率,可以採用異步的方式進行處理:首先將要寫入的數值存儲到一個緩衝區(Buffer)中,然後發送通信信號,等待信號被響應後,再將數值應用到緩存中。並且,這個緩衝區還可以讓 CPU 讀取數值。這個緩衝區就是存儲緩衝區(Store Buffer)。而不需要等待對某個數值的寫入指令完成才繼續執行下一條指令,CPU 可以直接從存儲緩衝區中讀取這個數值的值。這種優化稱爲存儲轉發(Store Forwarding)。

簡而言之,存儲緩存的作用是提高 CPU 寫入數據時的效率。它通過使用存儲緩衝區來暫時存儲待寫入的數據,異步地進行通信和操作,以減少 CPU 與其他部件之間的等待時間,從而提高計算機的整體性能。

(2) 無效隊列

在多個 CPU 之間進行數據同步時,一個 CPU 可能會發送一個 "Invalidate"(使無效)的信號給其他 CPU,以通知它們某個特定的數據已經發生了變化。然而,如果接收到 "Invalidate" 信號的 CPU 立即採取行動去與其他 CPU 同步數據,那麼這個過程可能會導致相當長的時鐘週期延遲。

爲了解決這個問題,接收到 "Invalidate" 信號的 CPU 並不會立即採取行動,而是將這個 "Invalidate" 信號放入一個叫做 "Invalidate Queue"(使無效隊列)的隊列中,並立即發送響應信號。等到適當的時機,CPU 再去處理這個 "Invalidate Queue" 中的 "Invalidate" 信號,並進行相應的處理操作。

換個方式理解,我們可以把這個過程類比成一個小區內的信箱系統。當有人給你寄來一封信件("Invalidate" 信號),你並不會立即停下手頭的工作去處理它。相反,你會將信件放入你的個人信箱("Invalidate Queue"),並立即回覆發送方說你已經收到了。然後,當你有空閒時間時,你會去檢查你的信箱,並處理裏面的信件。

通過使用 "Invalidate Queue",CPU 可以更加高效地處理接收到的 "Invalidate" 信號,而不會立即中斷當前的工作。這樣,CPU 可以根據自己的時間表進行合理的調度,以最大程度地提高處理效率。

總之,通過使用 "Invalidate Queue",接收到 "Invalidate" 信號的 CPU 可以在合適的時機進行數據同步處理,而不會立即中斷當前工作。這種優化方法可以減少時鐘週期延遲,提高 CPU 的整體性能。

四、亂序執行

計算機內部有一個很重要的部分叫做 CPU(中央處理器),它是負責執行計算和指令的核心。有一種特性叫做亂序執行,它可以讓 CPU 更加高效地工作。亂序執行意味着 CPU 不必按照指令在程序中的順序依次執行,而是根據情況自動地重新安排指令的執行順序。

想象一下,您是一名廚師,面前有一張食譜,上面列着各種不同的烹飪步驟。通常情況下,您可能會按照食譜上的順序,依次完成每個步驟。但是,有時候某些步驟可能會佔用很長時間,例如等待水煮沸或者烤箱預熱。如果您按照食譜的順序等待每個步驟完成,可能會導致整個烹飪過程變得非常慢。

這時,您可能會聰明地採用亂序執行的方式。也就是說,您可以在等待某個步驟完成的同時,開始做其他不依賴於該步驟的工作。例如,您可以準備其他需要切割的食材,或者開始準備下一道菜的材料。這樣,您能夠更加高效地利用時間,使整個烹飪過程更快完成。

同樣地,CPU 的亂序執行也是爲了更高效地利用時間。當 CPU 執行一條指令時,有時會遇到需要等待某些數據準備就緒的情況。如果按照指令在程序中的順序等待每個數據就緒,CPU 的工作效率可能會降低。亂序執行允許 CPU 在等待某些數據就緒的同時,開始執行其他不依賴於這些數據的指令。這樣,CPU 就能更加充分地利用時間,提高整體的計算速度。

亂序執行需要 CPU 內部的一些智能調度和判斷機制來決定指令的執行順序。這些機制能夠分析指令之間的依賴關係,判斷哪些指令可以並行執行,以及在等待某些數據就緒時可以執行哪些其他指令。通過亂序執行,CPU 能夠更好地利用計算資源,提高計算機的性能。

總而言之,亂序執行是 CPU 的一種智能特性,它使得 CPU 可以不按照指令在程序中的順序依次執行,而是根據情況自動重新安排指令的執行順序。這樣可以更高效地利用時間,提高計算機的運行速度。就像廚師在烹飪過程中聰明地安排工作順序一樣,亂序執行讓 CPU 更加聰明地處理指令,使計算機工作更快更高效。

CPU 執行亂序主要有以下幾種:

總而言之,CPU 的亂序執行優化指的是處理器爲提高運算速度而做出違背代碼原有順序的優化。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/wbziOxoWt6mG_DCltDHL1Q