不只是“切圖仔”——進(jìn)程、線程和協(xié)程

<meta name="source" content="lake">

前言

軟件開發(fā)中各類知識(shí)都是具有一定相關(guān)性的,前端開發(fā)雖然大部分時(shí)間都是在寫頁面、做交互,但是除了頁面開發(fā)之外,我們也可以掌握一些網(wǎng)絡(luò)、操作系統(tǒng)、后端、數(shù)據(jù)庫等其他的知識(shí),擴(kuò)充自己的知識(shí)面。我一直傾向于多的掌握各類知識(shí),領(lǐng)會(huì)它們之間的聯(lián)系,做一個(gè)有見識(shí)的人,然后再去深挖背后的原理和細(xì)節(jié)。在學(xué)習(xí)時(shí)先建立起完善的知識(shí)結(jié)構(gòu),然后再追本溯源,找到知識(shí)的源頭,“要****識(shí)廬山真面目,不應(yīng)身在此山中****”。

今天就一起來了解下進(jìn)程、線程和協(xié)程,目標(biāo)是弄明白下面幾個(gè)問題:

  1. 程序、進(jìn)程、線程、協(xié)程是什么?
  2. 程序運(yùn)行的基本過程?
  3. 為什么要有進(jìn)程、線程、協(xié)程?
  4. 如何使用進(jìn)程、線程和協(xié)程?

基本概念

程序

計(jì)算機(jī)程序是指一組指示電子計(jì)算機(jī)或其他具有消息處理能力設(shè)備每一步動(dòng)作的指令,通常用某種程序設(shè)計(jì)語言編寫,運(yùn)行于某種目標(biāo)體系結(jié)構(gòu)上。——《維基百科》

我們都知道程序其實(shí)就是一系列的指令,用一種計(jì)算機(jī)程序設(shè)計(jì)語言編寫,然后用編譯器或者解釋器翻譯成機(jī)器語言。指令可以分為操作數(shù)據(jù),對(duì)應(yīng)編程語言中的算法數(shù)據(jù)結(jié)構(gòu)。

進(jìn)程

程序相當(dāng)于是一個(gè)名詞,描述了一件事如何去做,而進(jìn)程是程序運(yùn)行的真正實(shí)例。當(dāng)下達(dá)了運(yùn)行程序的命令后,操作系統(tǒng)會(huì)把程序相關(guān)的內(nèi)容和資源加載到內(nèi)存中,這些資源就是進(jìn)程。進(jìn)程是資源分配的基本單位。

微信截圖_20201226225004.png

一個(gè)進(jìn)程通常包括或者說擁有下面這些資源:

  • 那個(gè)程序的可執(zhí)行機(jī)器代碼的一個(gè)在存儲(chǔ)器的映像。
  • 分配到的存儲(chǔ)器(通常是虛擬的一個(gè)存儲(chǔ)器區(qū)域)。存儲(chǔ)器的內(nèi)容包括可執(zhí)行代碼、特定于進(jìn)程的資料(輸入、輸出)、調(diào)用堆棧、堆棧(用于保存運(yùn)行時(shí)運(yùn)輸中途產(chǎn)生的資料)。
  • 分配給該進(jìn)程的資源的操作系統(tǒng)描述符,諸如文件描述符(Unix術(shù)語)或文件句柄(Windows)、資料源和資料終端。
  • 安全特性,諸如進(jìn)程擁有者和進(jìn)程的權(quán)限集(可以容許的操作)。
  • 處理器狀態(tài)(內(nèi)文),諸如寄存器內(nèi)容、物理存儲(chǔ)器尋址等。當(dāng)進(jìn)程正在運(yùn)行時(shí),狀態(tài)通常存儲(chǔ)在寄存器,其他情況在存儲(chǔ)器?!?a target="_blank">《維基百科》

虛擬內(nèi)存

進(jìn)程的創(chuàng)建或者說程序的加載是由操作系統(tǒng)的加載器來完成的。內(nèi)存資源是有限的,所以程序不是一下子全部加載進(jìn)內(nèi)存,而是使用了虛擬內(nèi)存。操作系統(tǒng)會(huì)為程序創(chuàng)建一個(gè)連續(xù)的虛擬地址空間,內(nèi)存會(huì)被分成很多頁,然后通過頁表記錄虛擬內(nèi)存到物理內(nèi)存之間的映射,每一頁對(duì)應(yīng)物理內(nèi)存中的一塊(頁幀)。操作系統(tǒng)會(huì)記錄程序入口地址(虛擬地址), 等到程序要開始運(yùn)行時(shí),從該地址中取指令開始運(yùn)行。此時(shí)進(jìn)程處于就緒狀態(tài)

內(nèi)存中以字節(jié)為存儲(chǔ)單位,每一個(gè)字節(jié)分配一個(gè)物理地址。以 32 位 CPU 位例,可以表示 232個(gè)地址,也就是 4G * 1B = 4GB 的物理空間(這也是為什么 32 位 CPU 最多支持 4G 內(nèi)存。通過 PAE 可以擴(kuò)展到 64GB)。虛擬內(nèi)存和物理內(nèi)存會(huì)被分為很多塊,按每塊 4KB 計(jì)算,一個(gè)頁表應(yīng)該有 1M 個(gè)頁表項(xiàng)。要想表示 1M 個(gè)頁表項(xiàng),需要 20 位,在 x86 中頁表項(xiàng)除了塊地址外還包含其他信息總共 32 位,也就是 4B,因此一個(gè)頁表的大小就是 4MB。

虛擬地址和物理地址.png

進(jìn)程在執(zhí)行時(shí) CPU 需要將虛擬地址轉(zhuǎn)換成物理地址,轉(zhuǎn)換主要通過 MMU(內(nèi)存管理單元) 來完成。MMU 是CPU的一部分,每個(gè)處理器核心都有。每次轉(zhuǎn)換,MMU首先在 TLB(轉(zhuǎn)譯后備緩沖區(qū),快表) 中檢查現(xiàn)有的緩存。如果沒有命中,根據(jù) CR3 寄存器,Table Walk Unit 將從內(nèi)存中的頁表查詢。

虛擬地址轉(zhuǎn)換.jpg

進(jìn)程的切換

我們都知道,程序并不完全是同時(shí)運(yùn)行的,而是操作系統(tǒng)按照一定的調(diào)度算法輪流讓進(jìn)程獲取 CPU 時(shí)間來執(zhí)行的。當(dāng)一個(gè)進(jìn)程的執(zhí)行時(shí)間到了就會(huì)掛起該進(jìn)程,切換到另一個(gè)進(jìn)程運(yùn)行。當(dāng)一個(gè)進(jìn)程獲取到 CPU 時(shí)間開始執(zhí)行時(shí),進(jìn)程就處于運(yùn)行狀態(tài)。有時(shí)候,正在進(jìn)行的進(jìn)程由于發(fā)生某個(gè)事件而暫時(shí)無法繼續(xù)執(zhí)行時(shí),便放棄處理機(jī)而處于暫停狀態(tài),這種暫停狀態(tài)叫阻塞進(jìn)程阻塞,此時(shí)進(jìn)程處于等待狀態(tài)。

AryWDI.png

進(jìn)程在切換時(shí),需要先保留當(dāng)前進(jìn)程的現(xiàn)場(chǎng),然后恢復(fù)另一個(gè)進(jìn)程的現(xiàn)場(chǎng),這一過程叫做進(jìn)程上下文切換。進(jìn)程的上下文切換,主要可以分為兩個(gè)部分

  1. 虛擬地址空間的切換。
  2. 線程上下文切換(見下文)。

前面提到操作系統(tǒng)會(huì)為每個(gè)進(jìn)程分配一個(gè)虛擬地址空間,CPU 執(zhí)行指令時(shí),拿到的地址都是虛擬地址,然后 MMU 獲取物理地址。操作系統(tǒng)需要給每個(gè)進(jìn)程設(shè)置虛擬地址空間,在進(jìn)程調(diào)度過程中,需要同時(shí)切換虛擬地址空間,否則 CPU 通過 MMU 獲取到的物理地址就不正確,切換也就是把不同頁表的地址放入到 MMU 中。

關(guān)于線程的切換部分在后面線程調(diào)度的過程中再具體講述。

進(jìn)程間通信

  1. 管道pipe:管道是一種半雙工的通信方式,數(shù)據(jù)只能單向流動(dòng),而且只能在具有親緣關(guān)系的進(jìn)程間使用。進(jìn)程的親緣關(guān)系通常是指父子進(jìn)程關(guān)系。例如:node.js 中 fork 進(jìn)程,linux 中管道符“|”。
  2. 命名管道FIFO:有名管道也是半雙工的通信方式,但是它允許無親緣關(guān)系進(jìn)程間的通信。
  3. 消息隊(duì)列MessageQueue:消息隊(duì)列是由消息的鏈表,存放在內(nèi)核中并由消息隊(duì)列標(biāo)識(shí)符標(biāo)識(shí)。消息隊(duì)列克服了信號(hào)傳遞信息少、管道只能承載無格式字節(jié)流以及緩沖區(qū)大小受限等缺點(diǎn)。
  4. 共享存儲(chǔ)SharedMemory:共享內(nèi)存就是映射一段能被其他進(jìn)程所訪問的內(nèi)存,這段共享內(nèi)存由一個(gè)進(jìn)程創(chuàng)建,但多個(gè)進(jìn)程都可以訪問。共享內(nèi)存是最快的 IPC 方式,它是針對(duì)其他進(jìn)程間通信方式運(yùn)行效率低而專門設(shè)計(jì)的。它往往與其他通信機(jī)制,如信號(hào)兩,配合使用,來實(shí)現(xiàn)進(jìn)程間的同步和通信。
  5. 信號(hào)量Semaphore:信號(hào)量是一個(gè)計(jì)數(shù)器,可以用來控制多個(gè)進(jìn)程對(duì)共享資源的訪問。它常作為一種鎖機(jī)制,防止某進(jìn)程正在訪問共享資源時(shí),其他進(jìn)程也訪問該資源。因此,主要作為進(jìn)程間以及同一進(jìn)程內(nèi)不同線程之間的同步手段。
  6. 套接字Socket:套解口也是一種進(jìn)程間通信機(jī)制,與其他通信機(jī)制不同的是,它可用于不同及其間的進(jìn)程通信。
  7. 信號(hào) ( sinal ) : 信號(hào)是一種比較復(fù)雜的通信方式,用于通知接收進(jìn)程某個(gè)事件已經(jīng)發(fā)生。

線程

線程(英語:thread)是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位。大部分情況下,它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)。

線程是獨(dú)立調(diào)度和分派的基本單位?!?a target="_blank">《維基百科》

程序就是一系列的指令,在程序運(yùn)行的過程中 CPU 按照順序執(zhí)行指令,當(dāng)執(zhí)行某個(gè)指令所需要的的資源未就緒時(shí),執(zhí)行就會(huì)陷入阻塞。為了提高 CPU 利用率,可以將指令的執(zhí)行分為多段,讓某一段指令執(zhí)行阻塞時(shí),可以切換到另一段流程繼續(xù)執(zhí)行。這一段指令流就是線程。因此,可以說線程是程序執(zhí)行的基本單位。

用戶線程和內(nèi)核線程

根據(jù)操作系統(tǒng)內(nèi)核是否對(duì)線程可感知,可以把線程分為內(nèi)核線程用戶線程。

在用戶線程中,有關(guān)線程管理的所有工作都由應(yīng)用程序完成,內(nèi)核意識(shí)不到線程的存在。對(duì)于系統(tǒng)內(nèi)核而言,其實(shí)就是一個(gè)單線程的進(jìn)程在運(yùn)行。

在內(nèi)核線程中,內(nèi)核線程建立和銷毀都是由操作系統(tǒng)負(fù)責(zé)、通過系統(tǒng)調(diào)用完成的。線程管理的所有工作由內(nèi)核完成,應(yīng)用程序沒有進(jìn)行線程管理的代碼,只有一個(gè)到內(nèi)核級(jí)線程的編程接口。內(nèi)核為進(jìn)程及其內(nèi)部的每個(gè)線程維護(hù)上下文信息,調(diào)度也是在內(nèi)核基于線程架構(gòu)的基礎(chǔ)上完成。

線程模型.png

用戶線程和內(nèi)核線程之間根據(jù)根據(jù)實(shí)現(xiàn)可以是一對(duì)一、多對(duì)一、多對(duì)多的關(guān)系。

線程的切換

在了解線程切換之前需要先理解一下用戶態(tài)和內(nèi)核態(tài)。

在 Linux 中,按照特權(quán)等級(jí),把進(jìn)程的運(yùn)行空間分為內(nèi)核空間和用戶空間,分別對(duì)應(yīng)著下圖中, CPU 特權(quán)等級(jí)的 Ring 0 和 Ring 3。

  • 內(nèi)核空間(Ring 0)具有最高權(quán)限,可以直接訪問所有資源;
  • 用戶空間(Ring 3)只能訪問受限資源,不能直接訪問硬件設(shè)備,必須通過系統(tǒng)調(diào)用陷入到內(nèi)核中,才能訪問這些特權(quán)資源。
image.png

進(jìn)程既可以在用戶空間運(yùn)行,又可以在內(nèi)核空間中運(yùn)行。進(jìn)程在用戶空間運(yùn)行時(shí),被稱為進(jìn)程的用戶態(tài),而陷入內(nèi)核空間的時(shí)候,被稱為進(jìn)程的內(nèi)核態(tài)。

當(dāng)需要訪問系統(tǒng)資源時(shí),我們都需要系統(tǒng)調(diào)用來進(jìn)行,也就是進(jìn)入內(nèi)核態(tài)。其實(shí)就是完成某些操作的代碼放到了內(nèi)核中,用戶代碼沒辦法直接訪問操作系統(tǒng)資源,需要調(diào)用內(nèi)核暴露的接口來進(jìn)行,相當(dāng)于一個(gè)內(nèi)核庫。

內(nèi)核線程的管理是由內(nèi)核完成的,因此當(dāng)線程切換時(shí)還會(huì)涉及到用戶態(tài)到內(nèi)核態(tài)之間的切換,其實(shí)可以理解為需要執(zhí)行內(nèi)核線程調(diào)度和切換的代碼。

線上的切換就包括用戶態(tài)和內(nèi)核態(tài)的切換以及 CPU 硬件上下文的切換兩部分。硬件上下文的切換很容易理解,就是 CPU 寄存器中數(shù)據(jù)需要緩存起來,然后 PC(程序計(jì)數(shù)器) 切換到新的指令開始執(zhí)行,等到線程恢復(fù)運(yùn)行時(shí)恢復(fù)之前緩存的數(shù)據(jù)繼續(xù)執(zhí)行指令。

線程鎖

多個(gè)線程對(duì)同一競(jìng)態(tài)資源的搶奪會(huì)引發(fā)線程安全問題。競(jìng)態(tài)資源是對(duì)多個(gè)線程可見的共享資源,主要包括全局(非const)變量、靜態(tài)(局部)變量、堆變量、資源文件等。

通過鎖機(jī)制,能夠保證在多核多線程環(huán)境中,在某一個(gè)時(shí)間點(diǎn)上,只能有一個(gè)線程進(jìn)入臨界區(qū)代碼,從而保證臨界區(qū)中操作數(shù)據(jù)的一致性。

依據(jù)鎖的特性、鎖的設(shè)計(jì)、鎖的狀態(tài)常見的分類如下:

  • 樂觀鎖、悲觀鎖:樂觀鎖認(rèn)為一個(gè)線程去拿數(shù)據(jù)的時(shí)候不會(huì)有其他線程對(duì)數(shù)據(jù)進(jìn)行更改,所以不會(huì)上鎖,而是在更新數(shù)據(jù)的判斷是否被修改;悲觀鎖認(rèn)為一個(gè)線程去拿數(shù)據(jù)時(shí)一定會(huì)有其他線程對(duì)數(shù)據(jù)進(jìn)行更改。所以一個(gè)線程在拿數(shù)據(jù)的時(shí)候都會(huì)順便加鎖,這樣別的線程此時(shí)想拿這個(gè)數(shù)據(jù)就會(huì)阻塞。
  • 自旋鎖、互斥鎖:自旋鎖的線程一直在那循環(huán)檢測(cè)鎖標(biāo)志位,全程消耗 cpu,起始開銷雖然低于互斥鎖,但隨著持鎖時(shí)間加鎖開銷是線性增長。當(dāng)一個(gè)線程獲得互斥鎖后,其他線程會(huì)進(jìn)入隨便狀態(tài),由操作系統(tǒng)調(diào)度喚醒并獲取鎖。
  • 獨(dú)享鎖、共享鎖:字面意思。
  • 公平鎖、非公平鎖:公平鎖中多個(gè)線程相互競(jìng)爭時(shí)要排隊(duì),多個(gè)線程按照申請(qǐng)鎖的順序來獲取鎖;非公平鎖中多個(gè)線程相互競(jìng)爭時(shí),先嘗試插隊(duì),插隊(duì)失敗再排隊(duì)。

協(xié)程

協(xié)程可以理解為用戶線程,工作的方式很像是線程池。協(xié)程的切換完全是在用戶空間進(jìn)行,由我們自己編寫的代碼來控制。協(xié)程在切換時(shí),只有 CPU 上下文的切換,相比較線程切換涉及到用戶空間和內(nèi)核空間的切換并且需要操作系統(tǒng)老大來調(diào)度,協(xié)程切換的開銷比線程切換要小得多。同時(shí),有好必有壞,協(xié)程也有如下的缺點(diǎn):

  • 無法利用多核資源。
  • 一個(gè)協(xié)程如果阻塞會(huì)導(dǎo)致整個(gè)線程掛起。

程序執(zhí)行的基本過程

計(jì)算機(jī)組成.png
  1. 程序就是一堆指令和數(shù)據(jù),平時(shí)躺在硬盤里。
  2. 當(dāng)開始運(yùn)行該程序時(shí),操作系統(tǒng)會(huì)通過虛擬內(nèi)存技術(shù)為程序分配虛擬的內(nèi)存空間,創(chuàng)建好頁面等各種信息,此時(shí)一個(gè)進(jìn)程就起來了。此時(shí)程序代碼依舊在硬盤中。
  3. 當(dāng)操作系統(tǒng)通過調(diào)度輪到該進(jìn)程執(zhí)行的時(shí)候,就把他的頁表地址放到MMU中,程序入口地址放到 PC 中。CPU 開始執(zhí)行指令。
  4. 此時(shí)指令還在內(nèi)存中還沒有程序的指令,會(huì)觸發(fā)缺頁中斷,然后由異常中斷程序負(fù)責(zé)從外存在中加載指令和數(shù)據(jù)到內(nèi)存中。
  5. 程序的指令就這樣一點(diǎn)點(diǎn)的被加載到內(nèi)存中,由 CPU 負(fù)責(zé)執(zhí)行,執(zhí)行的一系列指令就是線程。

JavaScript 中的基本使用

多進(jìn)程

單個(gè) Node.js 實(shí)例運(yùn)行在單個(gè)線程中。 為了充分利用多核系統(tǒng),有時(shí)需要啟用一組 Node.js 進(jìn)程去處理負(fù)載任務(wù)。

const cluster = require('cluster')
const http = require('http')
const numCPUs = require('os').cpus().length

if (cluster.isMaster) {
  console.log(`主進(jìn)程 ${process.pid} 正在運(yùn)行`)

  // 衍生工作進(jìn)程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作進(jìn)程 ${worker.process.pid} 已退出`)
  })
} else {
  // 工作進(jìn)程可以共享任何 TCP 連接。
  // 在本例子中,共享的是 HTTP 服務(wù)器。
  http
    .createServer((req, res) => {
      res.writeHead(200)
      res.end('你好世界\n')
    })
    .listen(8000)

  console.log(`工作進(jìn)程 ${process.pid} 已啟動(dòng)`)
}
cluster進(jìn)程信息.png

多線程

我們都知道 JavaScript 是單線程的,既然是單線程的,在某個(gè)特定的時(shí)刻只有特定的代碼能夠被執(zhí)行,并阻塞其它的代碼。JavaScript 通過事件和回調(diào)實(shí)現(xiàn)異步任務(wù)。當(dāng)所有同步任務(wù)執(zhí)行完成后,就會(huì)依次取出異步的任務(wù)開始執(zhí)行。具體可以參考JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop。

function foo() {
  console.log('first')
  setTimeout(function () {
    console.log('second')
  }, 5)
}

console.time()
for (let i = 0; i < 5000; i++) {
  foo()
}
console.timeEnd()
image.png

Web Worker 的作用,就是為 JavaScript 創(chuàng)造多線程環(huán)境,允許主線程創(chuàng)建 Worker 線程,將一些任務(wù)分配給后者運(yùn)行。在主線程運(yùn)行的同時(shí),Worker 線程在后臺(tái)運(yùn)行,兩者互不干擾。等到 Worker 線程完成計(jì)算任務(wù),再把結(jié)果返回給主線程。這樣的好處是,一些計(jì)算密集型或高延遲的任務(wù),被 Worker 線程負(fù)擔(dān)了,主線程(通常負(fù)責(zé) UI 交互)就會(huì)很流暢,不會(huì)被阻塞或拖慢。

協(xié)程

Generator 函數(shù)是協(xié)程在 ES6 的實(shí)現(xiàn),最大特點(diǎn)就是可以交出函數(shù)的執(zhí)行權(quán)(即暫停執(zhí)行)。

function* fun(a) {
  console.log('a', a)
  const b = yield a + 1
  console.log('b', b)
  const c = yield b + 2
  console.log('c', c)
  return c
}

var gen = fun(1)
console.log('next', gen.next(4))
console.log('next', gen.next(5))
console.log('next', gen.next(6))

通過生成器可以使用類似用戶級(jí)線程,控制任務(wù)處理的流程。下面是生產(chǎn)者-消費(fèi)者的一個(gè)簡單例子。

const BUFFER_MAX_SIZE = 10
const buffer = []

function block() {
  return Math.random() < 0.1
}

function* produce() {
  let count = 0
  while (true) {
    if (buffer.length >= BUFFER_MAX_SIZE || block()) {
      yield count
      count = 0
    } else {
      const item = Math.round(Math.random() * 100)
      console.log('生產(chǎn) item:' + item)
      buffer.push(item)
      count++
    }
  }
}

function* consume() {
  let count = 0
  while (true) {
    if (buffer.length <= 0 || block()) {
      yield count
      count = 0
    } else {
      const item = buffer.shift()
      console.log('消費(fèi) item:' + item)
      count++
    }
  }
}

function main() {
  const producer = produce()
  const consumr = consume()
  let i = 0
  while (i < 10) {
    if (Math.random() > 0.5) {
      console.log('開始生產(chǎn)')
      console.log(`此次生產(chǎn)了 ${producer.next().value} 個(gè)`)
    } else {
      console.log('開始消費(fèi)')
      console.log(`此次消費(fèi)了 ${consumr.next().value} 個(gè)`)
    }
    i++
  }
  console.log(buffer)
}

main()

參考鏈接

  1. 知乎頁表
  2. 一文讓你明白CPU上下文切換
  3. 操作系統(tǒng)是個(gè)大騙子?
  4. 程序執(zhí)行過程
  5. 深入理解linux內(nèi)存管理之頁表管理
  6. 進(jìn)程的切換過程
  7. 頁目錄項(xiàng)和頁表項(xiàng)
  8. 進(jìn)程間通信IPC (InterProcess Communication)
  9. 為什么應(yīng)該在 Linux 上使用命名管道
  10. 進(jìn)程間通信及使用場(chǎng)景
  11. 線程的3種實(shí)現(xiàn)方式--內(nèi)核級(jí)線程, 用戶級(jí)線程和混合型線程
  12. 用戶態(tài)和內(nèi)核態(tài)的理解和區(qū)別
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容