首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

即時 Java 第 1 部分:使用 Java 語言建構即時系統

     編輯:Java 相關知識

因多項重要原因,Java 語言在即時系統應用非常有限。原因包括 Java 語言設計固有的不確定性效能影響(如動態類別載入),以及 Java 執行環境(JRE)本身的不確定性效能影響(如垃圾收集器、本地碼編譯)。即時 Java 規範(Real-time Specification for Java,RTSJ)是一種開放規範,進一步增強 Java 語言開放性,使其能構建即時系統。實現 RTSJ 需作業系統、JRE 與 Java 類別庫(JCL)支援。本文探究使用 Java 實現即時系統面臨的挑戰,介紹能應對這些挑戰的開發工具包與執行環境。本系列後續文章將更深入解釋本文介紹的概念與技術。

即時需求

即時(Real-time,RT)是廣義術語,描述需與真實世界同步的應用。如反應緩慢的使用者介面無法滿足一般使用者正常即時需求,此類應用稱為軟即時應用。「應用程式對滑鼠點擊反應時間不超過 0.1 秒」表述更明確。若無法滿足此需求屬軟故障:應用可繼續執行,雖使用者不滿但仍可使用。反之,必須嚴格滿足即時同步需求的應用稱為硬即時應用。如控制飛機方向的應用不能有任何原因延遲,否則導致災難性後果。RT 應用含義很大程度上取決於應用容錯程度,即偏離即時需求何種程度被視為錯誤。

RT 需求另一關鍵因素是回應時間。對編寫硬或軟即時應用的程式設計師,理解回應時間約束至關重要。要求滿足 1 微秒硬回應的技術與滿足 100 毫秒硬回應的技術截然不同。實踐中,要使回應時間小於幾十微秒,需組合定製軟硬體,很可能沒有(或極薄)作業系統層。

最後,健全即時應用設計者通常需可計量確定的效能特徵,以便設計滿足回應時間需求的應用。不可預測效能影響極其嚴重,致使系統無法滿足應用回應時間要求,因此難以(或根本不可能)正確設計應用。大多數即時執行環境設計者投入大量精力減少不確定性效能影響,滿足最大範圍即時應用回應時間需求。

即時 Java 應用面臨的挑戰

在通用作業系統中,執行通用 JVM 上的標準 Java 應用只能期望滿足幾百毫秒級別的軟即時需求。原因涉及語言幾個基本面向:執行緒管理、類別載入、即時(JIT)編譯活動、垃圾收集(GC)。應用設計者可減輕部分問題,但需付出大量工作。

執行緒管理

標準 Java 語言未為執行緒排程與優先權提供任何保證。必須在精確時間回應事件的應用無法確保不會在高優先權執行緒前排程低優先權執行緒。為彌補此缺點,程式設計師需將應用劃分為一組子應用,讓作業系統在不同優先權執行。此劃分增加事件開銷,使事件間通訊更困難。

類別載入

一致的 JVM 必須延遲載入類別,直到程式首次引用該類別。根據被載入類別所在介質(磁碟或其他)速度、類別大小、類別載入器本身開銷,載入時間不同。載入類別延遲通常高達 10 毫秒。若需載入幾十或幾百個類別,載入時間本身會引起很長意外延遲。仔細設計應用使啟動時載入所有類別,但必須手動完成,因 Java 語言規範不讓 JVM 提前執行此步驟。

垃圾收集

應用開發中 GC 益處——包含指標安全、避免記憶體洩漏、使開發者免於編寫定製記憶體管理工具——已獲充分證明。但對使用 Java 的硬即時程式設計師,GC 是另一挫折原因。當 Java 堆耗盡仍無法滿足分配請求時自動進行垃圾收集。應用本身也能觸發垃圾收集。

一方面,GC 對 Java 程式設計師很好。在 C/C++ 等語言中,因需明確管理記憶體引發的錯誤是最難診斷問題。部署應用時檢驗此類錯誤同樣是基本難題。Java 程式模型主要優點:由 JVM 而非應用執行記憶體管理,為應用程式設計師卸下此負擔。

另一方面,傳統垃圾收集器可導致長時間延遲,應用程式設計師幾乎不可能預測此時間。幾百毫秒延遲不罕見。在應用層解決此問題唯一方法:透過建立一組重用物件阻止 GC,確保不會耗盡 Java 堆記憶體。換言之,程式設計師放棄 JVM 管理記憶體優勢,改由親自明確管理記憶體解決問題。實踐中此法通常不奏效,因阻止程式設計師使用 JDK 與其他類別供應商提供的眾多類別庫,這些可能建立大量暫時物件最終填滿堆。

編譯

將 Java 程式碼編譯為本地碼引發與類別載入類似問題。大多數現代 JVM 先解釋 Java 方法,再僅將頻繁執行方法編譯成本地碼。延遲編譯促成快速啟動,減少應用執行期間編譯數量。但使用解釋程式碼執行任務與編譯程式碼執行任務在時間上有巨大差異。對硬即時應用,因無法預測何時發生編譯,導致極大不確定性,無法有效規劃應用行為。對類別載入,可透過應用啟動階段使用 Compiler 類別以程式設計方式編譯方法減輕問題,但維護此法非常乏味且易出錯。

Java 即時規範

RTSJ 創建旨在解決 Java 語言一些限制,這些限制阻止其在即時執行環境廣泛應用。RTSJ 處理幾個問題領域:排程、記憶體管理、執行緒、同步、計時、時鐘、非同步事件處理。

排程

即時系統需嚴格控制執行緒排程方式並保證排程確定性:即給定相同條件下排程執行緒。雖 JCL 定義執行緒優先權概念,但傳統 JVM 不需執行優先權。同樣,非即時 Java 實作通常使用循環搶佔式排程,依不可預測排程順序排程。RTSJ 規範中,即時執行緒需真正優先權,以及具優先權繼承支援的固定優先權搶佔式排程器。此排程確保最高優先權活動執行緒總是被執行,並能持續執行直到自願釋放 CPU 或被更高優先權執行緒搶佔。優先權繼承確保高優先權執行緒所需資源被低優先權執行緒佔用時,避免優先權反轉。優先權反轉在即時系統嚴重,將在即時 Linux 中詳細討論。

記憶體管理

雖某些即時系統能容忍垃圾收集器引起延遲,但許多情況延遲不可接受。為支援不能容忍 GC 中斷的任務,RTSJ 定義不朽(immortal)與作用域(scoped)記憶體區域,補充標準 Java 堆。若垃圾收集器需釋放堆記憶體,這些區域允許任務使用記憶體而不中斷任務。分配在不朽記憶體物件可被所有執行緒存取且永不被收集。因永不收集,不朽記憶體是有限資源,必須謹慎使用。程式設計師可控制作用域記憶體區域建立與銷毀。每個作用域區域按最大空間分配,可用於物件分配。為確保物件間引用完整性,RTSJ 定義管理一個記憶體區域(堆、不朽、作用域)物件如何引用其他區域物件的規則。更多規則定義作用域記憶體物件何時終結、何時可重用區域。因這些複雜性,建議僅對不能容忍 GC 暫停組件使用不朽與作用域記憶體。

執行緒

RTSJ 增加對兩個新執行緒類別支援,為使用即時行為 RealtimeThread 與 NoHeapRealtimeThread (NHRT) 執行任務提供基礎。兩類別為優先權、週期性行為、處理程式時限(超過時觸發程式)、非堆記憶體區域使用提供支援。NHRT 無法存取堆,故與其他執行緒不同,極少被 GC 中斷或搶佔。即時系統通常用高優先權 NHRT 執行延遲需求最嚴格任務。用 RealtimeThread 執行能適應垃圾收集器延遲需求任務,用一般 Java 執行緒執行其他任務。因 NHRT 無法存取堆,使用這些執行緒必須非常謹慎。例如即使使用標準 JCL 容器類也必須小心,避免容器類無意中為堆建立暫時或內部物件。

同步

即時系統必須小心管理同步,防止高優先權執行緒等待低優先權執行緒。RTSJ 包含優先權繼承支援,可在同步發生時管理,並提供執行緒通訊能力,而不需透過無等待(wait-free)讀寫佇列實現同步。

計時與時鐘

即時系統需比標準 Java 程式碼提供時鐘更高解析度時鐘。新的 HighResolutionTime 與 Clock 類別封裝這些計時服務。

非同步事件處理

即時系統通常管理並回應非同步事件。RTSJ 包含非同步事件處理支援,觸發事件來源包含大量含計時器原始碼、作業系統信號、超過時限、其他應用定義事件。

IBM WebSphere Real Time

實現 RTSJ 需底層作業系統與 JRE 組件廣泛支援。2006 年 8 月發布的 IBM WebSphere Real Time 與 RTSJ 完全相容,包含新技術旨在改進即時系統執行行為、減少應用設計者建立即時系統必須工作量。圖 1 展示 WebSphere Real Time 組件簡化表示:

WebSphere Real Time 架構概覽

WebSphere Real Time 基礎是 IBM 跨平台 J9 技術。Linux 作業系統應用開源即時補丁,提供基本即時服務支援即時行為,特別支援 RTSJ 規範要求行為。大幅強化 GC 技術支援 1 毫秒暫停時間。JIT 編譯可用於軟即時場景,編譯在不需執行高優先權任務時發生。同樣引入新 AOT(Ahead-of-time)編譯技術(圖 1 未顯示),為不適合 JIT 編譯系統提供硬即時效能。接下來幾節介紹各技術;本系列後續文章詳細闡述各技術運作原理。

即時 Linux

WebSphere Real Time 運行在定製完全開源 Linux 版本上。其中幾處更改用以創建即時 Java 環境。更改提供完全搶佔式核心、執行緒中斷處理程式、高解析度計時器、優先權繼承、健全互斥鎖(mutex)。

完全搶佔式核心

即時 Java 執行緒使用固定優先權排程實現,又稱靜態優先權排程,使用先進先出(FIFO)排程策略。標準 Linux 核心提供軟即時行為,雖無保證高優先權執行緒搶佔低優先權執行緒等待時間上限,但可粗略估算為幾十微秒。即時 Linux 中,幾乎每個核心行為都是搶佔式,因此減少低優先權執行緒被搶佔時間,允許高優先權執行緒執行。剩下不能被搶佔的臨界區極短且可確定執行。這三個等級執行順序改善即時排程延遲,可粗略測量為幾十微秒。

減少延遲的執行緒中斷處理程式

幾乎所有中斷處理程式轉換為運行在進程中的核心執行緒。因處理程式變為使用者可配置、可排程實體,可像其他進程被搶佔與優先化,延遲變得更短且更具確定性。

高解析度計時器

高解析度時鐘與計時器提供更高解析度與準確性。即時 Java 將此特性用於高解析度睡眠與定時等待。Linux 高解析度計時器使用高精度 64 位元資料型別實現。與傳統 Linux 不同(其時鐘計時器依賴低解析度系統滴答,限制計時器事件粒度),即時 Linux 使用獨立可編程高解析度計時器事件,可實現每微秒內計時。

優先權繼承

優先權繼承技術可避免常見優先權反轉問題。圖 2 最上圖表解釋最簡單優先權反轉範例,涉及三個執行緒:高(H)、中(M)、低(L)優先權。設想 H、M 最初等待事件觸發處於睡眠,L 活動並持有鎖。若 H 被喚醒處理事件,將搶佔 L 並開始執行。考慮若 H 在 L 持有鎖處阻塞會發生什麼。因只有 L 釋放鎖後 H 才能處理,H 被阻塞,L 再次執行。若此時 M 被事件觸發,M 將搶佔 L 並執行視其需求時間。此情況稱優先權反轉,因 M 在 H 前執行,即使 H 優先權比 M 高。

優先權反轉與優先權繼承示例

即時 Linux 透過名為優先權繼承(也稱優先權借出)策略防止優先權反轉,圖 2 底部圖表解釋。當 H 在 L 持有鎖處阻塞時,H 將其優先權給 L,保證在 L 釋放 H 需要鎖前,比 H 優先權低任務都不能搶佔 L。當 L 釋放鎖後,L 優先權恢復初始值,H 可處理而無需等待 L 完成。應用設計者應盡量避免高優先權執行緒所需資源被低優先權執行緒佔有,但優先權繼承機制增強健全性,防止優先權反轉發生。

健全互斥鎖與 rt-mutexes

快速使用者空間互斥(futexes)支援 Linux pthread 互斥鎖。Futexes 優化獲得非競爭鎖時間,不依賴核心;只有競爭鎖需核心干涉。健全互斥鎖解決持有鎖應用崩潰後正確清理鎖問題。同樣,rt-mutexes 將優先權繼承協定擴展至健全互斥鎖,允許即時 JVM 透過 pthread 程式庫依賴優先權繼承行為。

確定性垃圾收集

假設即時作業系統(如即時 Linux)為即時行為提供基礎,可構建 JVM 其他主要部分呈現即時行為。GC 是 JVM 造成不確定行為主要原因之一,但此不確定性可透過謹慎設計與使用即時 Linux 特性減輕。

GC 暫停不確定性影響嚴重破壞即時應用在特定時限內完成任務能力。多數 GC 實作將妨礙即時應用實現延遲目標,只有同步需求寬鬆大型任務才承受得起 GC 技術引起延遲。RTSJ 解決方案:引入程式設計師管理的記憶體分配,使用不朽與作用域記憶體區域及 NHRT,但此方案對 Java 應用設計者成為巨大負擔。

WebSphere Real Time 讓程式設計師依賴 RTSJ 記憶體區域(若願意),但建議僅對延遲需求極度嚴格任務使用此法。對允許 GC 暫停 1 毫秒級任務,IBM 創建確定性 GC 技術,使程式設計師從輕鬆編寫自動記憶體管理與具可預測效能管理任務獲益。

IBM 確定性 GC 技術基於兩個簡單前提:

  • 沒有 GC 暫停超出最大上限
  • GC 消耗時間不多於給定時限,透過控制該時限內暫停次數實現

牢記這兩前提並用之管理 GC 行為將顯著增加應用實現即時目標可能性。

Metronome GC

WebSphere Real Time 使用 Metronome GC 實現 JVM 確定性低暫停時間 GC 行為。Metronome GC 使用基於時間排程方法,在固定排程中交叉執行收集器與應用(GC 稱應用為 mutator,因從垃圾收集器角度看,應用行為就是隨時間改變活動物件圖表)。

針對時間而非分配率排程原因:應用執行期間分配率通常不均勻。完成 GC 工作對分配是負擔,GC 暫停很可能不均勻分配,降低 GC 行為確定性。透過基於時間排程,Metronome GC 可實現一致、確定、有限暫停時間。且因不需語言擴展或修改現有程式碼,一般 Java 應用可透明使用 Metronome 並受益於確定性特徵。

Metronome 將時間分為離散量子(quanta),約 500 微秒但不超過 1 毫秒,專門用於 GC 或應用。雖量子極短,若將一些量子用於執行 GC 任務,應用仍感覺較長暫停時間,影響即時時限。為更好支援即時時限,Metronome 將量子分配給 GC,而應用應具最小時間百分比。此百分比稱 utilization,為使用者提供參數。任何時間區間內,用於應用量子數不得少於指定 utilization。預設 utilization 70%:任何 10 毫秒時限內,至少 7 毫秒專門用於應用。

使用者可啟動應用時設定 utilization 值。圖 3 展示較長時限內應用 utilization 範例。注意垃圾收集器活動狀態下對應時間量子的週期性下降。圖 3 整個時限內,應用 utilization 保持指定 70%(0.7)或以上。

Utilization 圖表示例

圖 4 演示使用 Metronome 技術後 GC 暫停時間確定性。僅小部分暫停超過 500 微秒。

GC 暫停時間直方圖

為保持較短單次 GC 暫停時間,Metronome 在堆內使用寫屏障(write barrier)及相關亞結構(metastructure)追蹤活動與可能死亡物件。追蹤活動物件需一些 GC 量子確定應保持哪些物件活動、哪些應回收。因追蹤工作與程式執行交叉進行,應用透過執行載入或儲存將某些物件「隱藏」,GC 可能丟失這些物件追蹤。

隱藏活動物件不一定是惡意應用程式碼結果。此現象常見,因應用不知垃圾收集器行為。為確保收集器不丟失物件,GC 與 VM 互相協作,追蹤物件間鏈接,因它們是透過儲存應用執行操作建立與銷毀。應用執行儲存操作前執行寫屏障完成此追蹤。寫屏障目的:若儲存導致隱藏活動物件,則記錄物件鏈接狀態變化。這些寫屏障表示平衡確定性行為的效能與記憶體佔用開銷。

大型物件分配對許多 GC 策略是棘手事。許多情況堆存在太多碎片無法存放大物件(如陣列)。必然引發長暫停整理碎片或壓縮,堆將許多小自由記憶體區域整合為大自由區域滿足大型分配請求。Metronome 使用新兩級物件模型用於 arraylets 陣列。arraylets 將大陣列分為小陣列,使無碎片整理堆更容易滿足大陣列分配請求。arraylets 物件第一級稱 spine,包含指標列表指向陣列較小部分稱 leaves。每個 leaf 大小相同,簡化查找陣列特定元素計算,使收集器更容易找到合適自由空間分配每個 leaf。將陣列分成較小不連接部分,使陣列可分配在許多小自由區域(堆常有此區域),無需壓縮。

與傳統 STW 垃圾收集器不同,Metronome 在整個應用生命週期連續執行 GC 過程,前者使用 GC 週期概念表示垃圾收集開始與結束。整個應用生命週期內,保證應用 utilization,其值高於不需大量 GC 工作情況下 utilization 最小值。隨收集器將找到自由記憶體歸還應用,自由記憶體量不斷上下變化。

即時中的本地碼編譯

大多數現代 JVM 結合使用解釋與編譯程式碼執行。為消除解釋高效能代價,JIT 編譯器選擇將頻繁執行程式碼直接轉換為 CPU 本地指令。Java 語言動態特性通常將編譯器操作作為程式執行,而非程式執行前步驟(C++、Fortran 屬後者)。JIT 編譯器選擇編譯程式碼,使其花費編譯時間很可能透過程式碼效能改進獲得補償。動態編譯行為最頂層,傳統 JIT 編譯器使用大量推測性優化,利用正在執行程式動態特性,對應用執行某階段或許正確,但不一定適合整個應用執行階段。若對特性假設稍後變錯,此優化「未完成」。

傳統非即時環境中,程式執行時編譯程式碼效果很好,因大部分編譯器操作對應用效能透明。然而即時環境中,JIT 編譯器引入不可預知執行行為,嚴重影響最壞情況執行時間分析。但此環境中編譯程式碼效能優勢仍重要,因能使更複雜任務在較短時間完成。

WebSphere Real Time 引入兩種方案在不同折中點平衡兩需求。第一方案使用 JIT 編譯器,在較低非即時優先級運行,已修改為極少執行積極推測性優化。非即時優先級操作使作業系統保證編譯器不干擾即時任務執行。但程式碼效能隨時間改變是不確定影響,使此方案更適合軟即時環境而非硬即時。

對於硬即時環境,WebSphere Real Time 引入 AOT 供應用程式設計。儲存於 JAR 檔 Java 類別檔可透過簡單命令列預編譯為 Java eXEcutable (JXE) 檔。透過將 JXE 檔(而非原始 JAR)指定至應用類別路徑,應用獲得調用,執行 AOT 編譯程式碼——而非經解釋位元組碼或 JIT 編譯本地碼。首個 WebSphere Real Time 發布版中,使用 AOT 程式碼意味著不使用 JIT 編譯器,有兩主要好處:較低記憶體消耗、不受來自 JIT 編譯執行緒與取樣執行緒動態效能影響(後者用於識別頻繁執行程式碼)。

如何使用 AOT 程式碼

圖 5 展示使用 AOT 程式碼時 WebSphere Real Time 如何執行 Java 程式碼:左上方如同任何 Java 開發專案,開發者將原始碼編譯成類別檔,綁定至 JAR,再由 AOT 使用 jxeinajar 工具編譯。此工具可編譯 JAR 中所有類別所有方法,也可根據基於 JIT 取樣程式執行輸出選擇性編譯某些方法,該程式識別需編譯最重要方法。jxeinajar 編譯 JAR 方法並構造 JXE 檔,包含原始 JAR 內容與 AOT 編譯器產生本地碼。執行程式時,JXE 檔可直接取代 JAR 檔。若使用 -Xnojit 選項呼叫 JVM,將載入類別路徑中 JXE 檔 AOT 編譯程式碼(依 Java 語言規則)。執行期間,從 JAR 載入方法或從 JXE 載入未編譯方法將被解釋;從 JXE 載入編譯方法作為本地碼執行。圖 5 中同樣需用 -Xrealtime 命令列選項指定應呼叫即時 VM,此選項僅 WebSphere Real Time 可用。

AOT 程式碼缺點

雖 AOT 程式碼能產生更確定效能,但同樣有缺點。儲存 AOT 程式碼的 JXE 檔通常比存放類別檔 JAR 檔大許多,因本地碼通常比類別檔位元組碼密度小。執行本地碼還需各種補充資料,描述如何綁定至 JVM、如何捕獲異常等以執行程式碼。第二缺點:雖 AOT 編譯程式碼比解釋程式碼快,但比 JIT 編譯程式碼慢得多。最後:解釋方法轉換為編譯方法所用時間(反之亦然)比從其他解釋方法呼叫一解釋方法(或編譯方法呼叫編譯方法)要長。具活動 JIT 編譯器 JVM 中,透過不斷編譯編譯程式碼「邊緣部分」直到轉換次數不影響效能,最終消除此代價。具 AOT 編譯程式碼無 JIT 編譯器 JVM 中,轉換次數由編譯至 JXE 方法決定。因此通常建議 AOT 編譯整個應用及應用依賴 Java 程式庫類別。雖擴展編譯方法數量影響記憶體消耗,但效能獲益往往比增加記憶體消耗更重要。

AOT 程式碼通常比 JIT 程式碼執行慢,因 Java 語言本身特性。Java 語言需程式在首次引用類別時解析類別。因程式執行前編譯,AOT 編譯器必須謹慎對待所編譯程式碼引用的類別、欄位、方法。AOT 編譯程式碼通常比 JIT 慢,因 JIT 優勢在於應用解析大量此類引用後才編譯。然而 JIT 編譯器還必須注意抵消編譯程式消耗時間,因這些時間延長程式執行時間。因此 JIT 編譯器不會用相同優化程度編譯所有程式碼。AOT 應用無此限制,故能使用更積極編譯技術,有時可產生優於 JIT 程式碼效能。此外 AOT 能編譯更多方法,故 AOT 編譯也能比 JIT 產生更好效能。雖然如此,通常 AOT 編譯程式碼速度慢於 JIT。

為避免不確定性效能影響,WebSphere Real Time 提供的 JIT 編譯器或 AOT 編譯器都不使用現代 JIT 常採用的積極推測性優化。執行此類優化通常能顯著改善效能,但不適合即時環境。此外,為全面支援 RTSJ 與 Metronome 垃圾收集器,引入一些開銷至編譯程式碼,傳統編譯器無需執行此程式碼。鑑於以上原因,即時環境編譯程式碼執行速度通常比非即時環境慢。

未來發展方向

在可預測效能與原始吞吐量方面,需更多工作加快即時 Java 環境。必須對 Java 語言實現兩關鍵領域改進,使其在即時應用領域獲成功:

  • 為運行傳統作業系統而希望獲得更好預測性用戶提供即時技術
  • 使即時技術簡單易用

面向軟即時

WebSphere Real Time 許多特性對以傳統作業系統為目標程式設計師非常有用。增量 GC 與基於優先權執行緒必定在大量應用獲得應用,即使只能提供軟即時效能而不能保證硬即時實現。如對多數開發者,提供可預測效能而無不可預測 GC 延遲的應用伺服器極具吸引力。同樣,使用合理排程目標,使應用運行高優先權並良好監控的 Java 執行緒將簡化 Java 伺服器開發。

令即時更簡單

對開發者,將 Java 語言優點引入即時系統創建帶來巨大優勢。但始終有改善處,我們不斷開發新特性進一步簡化即時程式設計。可造訪 IBM alphaWorks 站點試用我們研究的即時執行緒研究技術,能使開發者管理使用極頻繁事件,而這些事件對變化或延遲要求很高。透過預載入、預初始化、預編譯程式碼處理事件,然後使用比 RTSJ 中 NHRT 更少繁瑣限制運行獨立於垃圾收集器程式碼,實現更具確定性行為。還將發現名為 TuningFork 工具,用於追蹤從作業系統到 JVM 再到應用內部路徑,能更輕鬆執行詳細效能分析。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved