Hugepages详解
原文地址:https://www.tuicool.com/articles/vYZJ3i3
IBM的創始人Thomas J. Watson曾經說: “全世界只需要5臺電腦就足夠了”。
Microsoft的創始人Bill Gates曾在一次演講中說:“個人用戶的計算機內存只需640K?”。
Intel創始人之一Gordon Moore曾經說:“當價格不變時,集成電路上可容納的晶體管數目,約每隔18個月便會增加一倍,性能也會增加一倍”。
前面兩句話在今天看來實際上是十分荒謬的,而最后那條就是著名的摩爾定律。
hugepages的出現同樣也是因為摩爾定律導致了主機內存越來越大,最后不得不對于標準pages的改進以提高效率。要了解hugepages首先必須清楚Unix/Linux內存的分配機制。本文從入門的角度介紹Linux的內存分配機制,當然這一部分內容相信很多人都在計算機體系結構這門課程中學到過,這里就重新溫習一下。
早期因為程序非常簡單并且使用的內存也非常小,程序的內存地址都是使用程序地址對應物理地址這種這種直接映射的模式,通俗的說就是物理地址是在程序中寫死的。想想使用匯編直接操作物理地址是多么壯觀的工程呀。對于一個大型的軟件系統來說,這部分工作幾乎是不可能完成的。后來開始使用“段頁式”的方式來管理內存的尋址。用一個時髦的詞來說就是引入了“虛擬化”技術?,F代操作系統都會使用受保護的虛擬地址模式(?protected virtual address mode?)來管理內存,Linux下分為三類地址:邏輯地址(Logic Address)、線性地址(Linear Address)與物理地址(Physics Address)。這三者的對應關系如下所示:????
簡單的說就是邏輯地址通過?分段機制映射?轉化為線性地址,然后線性地址通過?分頁機制映射轉化為物理地址。因為這里是介紹hugepages,所以我這里不打算介紹分段機制,主要對分頁機制做簡單的闡述。 事實上,地址映射的過程遠遠不止上圖中這么簡單。例如在分頁機制映射中,一般需要經過四級映射,也就是將線性地址映射為物理地址的過程中,需要從內存中讀取至少四次頁目錄表(Page Directory)和頁表 (Page Table)。眾所周知,CPU寄存器與內存的訪問速率相差至少一個數量級,所以如果使用傳統的分頁機制的開銷將會非常大。所以在現代的X86架構中,集成了一種被成為TLB(?Translation Lookaside Buffer?)的硬件級緩存。TLB讀寫速度非常快,基本和CPU寄存器的速率在一個數量級,而TLB中保緩存了最近使用過的線性地址和物理地址的映射關系。如下圖所示:
?
那么將線性地址映射為物理地址的過程如下:首先到TLB中找到這個條目是否存在,如果不存在則為TLB miss,然后使用頁表的方式進行尋址,最后把這個映射關系更新到TLB中以備下次使用。由于TLB是大小有限,而一旦出現TLB miss,則其查詢的代價很高,所以現代CPU架構基本都進行了一些優化以提高地址映射的效率。例如:
- 線性地址到物理地址轉換一開始就選擇同時在TLB和頁表進行查詢,而不經過TLB查找是否成功的等待;
- 使用多級TLB以及軟TLB,
- 在CPU context swtch的時候不flush整個TLB。
下面用例子來說明為什么使用傳統的 4k大小的頁表相比hugepages對大內存的管理效率會很低。在x86平臺,一條PTE的大小為4Byte;而在x86_64平臺,一條PTE的大小為8Byte。 以下這種場景并不罕見:
Linux x86_64, SGA大小為100G,使用常規的4k的page,連接到數據庫的進程數約1000。page table一共需要100×1024×1024K/4K=26214400條PTE;
那么26214400條PTE需要100×1024×1024K/4K×8Byte=209715200Byte=200M;
從而1000個進程需要100×1024×1024K/4K×8Byte×1000=209715200000Byte=200G。
計算出來的結果真是令人崩潰,但是事實上在你沒有崩潰以前,數據庫就已經因為沒有內存而崩潰了。同樣條件下,如果使用2M的hugepages進行對比,則以上的計算方法變為:
page table一共需要100×1024M/2M=51200條PTE;
那么51200條PTE需要100×1024M/2M×8Byte=409600Byte=400K;
從而1000個進程需要100×1024M/2M×8Byte×1=409600Byte=400K。
綜上,可以看到同樣是1000個進程,同樣是管理100G的SGA,結果卻大不相同。使用傳統的4k大小的page開銷竟然會達到驚人的200G;而使用2M的hugepages,開銷只有400K。 這其中不僅僅只是對于因為單個進程而言,2M page需要的PTE小于4K ?page的PTE,最大的一個原因是在于使用4K page的話,有多少進程使用SGA,就需要多少套PTE,相反如果使用2M page則無論有多少進程使用SGA,共享的都是同一套PTE。
眾所周知,x86是32位的,所以默認情況下,其可尋址的空間為2的32次方——4G。在X86設計之初,4G內存似乎是一個遙不可及的天文數字,但是摩爾定律打破了這一切,所以軟硬件的設計和開發商必須想出一個對策來解決4G以上不可尋址的問題。注意:這里沒有說4G以上的內存不可尋址,而是說4G以上的地址空間不可尋址,這兩者實際上有區別的。例如4G內存的CPU在32bit的Windows(非server版本)能識別到的內存一般在3G左右。?這其中的主要是主板或者操作系統的限制。因為計算機上一些其他的設備同樣需要可尋址,而這一部分地址需要從總的可尋址空間中預留,例如BIOS芯片的ROM,顯卡上的顯存(RAM)和BIOS(ROM),以及PCI、PCI-E設備上的RAM和ROM都需要占用一定的可尋址的空間。這個叫?MMIO?(Memory-mapped I/O)。這里有點off the topic了,所以不再贅述,要了解更詳細的原因和機制,請參考以下鏈接:(?RAM limit?PCI hole?,????Conventional memory?,????3GB barrier?)
CPU的設計者碰到了難題了:既要解決4G以上可尋址,又要兼容已有的架構和程序,那該怎么辦呢?一種最可行的方式就是通過增加擴展地址來解決4G以上的尋址問題。于是Intel引入一個workaround?PAE?(Physical Address Extensions)機制:增加20位擴展地址,將原來的32位的物理地址擴展為52位,那么可尋址的空間就增加到了2的52次方——4PB,但是實際上x86只使用了其中的36位,也就是X86實際可尋址的物理地址為64G,而轉化的過程為操作系統通過使用頁表將4GB的地址空間映射到大小為64GB的物理地址空間。盡管物理地址為52位,但是線性地址還是32位,所以在這種架構下單個程序/進程使用的內存限制實際上仍然還是4G。?????注:?PSE?(Page Size Extension)使得用戶可以使用4M的頁表。?PAE?(Physical Address Extension)使得32位的系統就能夠使用接近64GB的內存的一種技術。如果PAE和PSE同時使用,則只能使用2M的頁表。x86_64默認使用PAE的擴展——??long mode?。
通過上圖可知:是否使用分頁是通過CR0寄存器的PG表示來控制的,如果只使用傳統的分段模式,則將CR0.PG置為0, 如果啟用分頁則需要將PG設置為1。使用page的大小則是由IA32_EFER寄存器LME標志以及CR4寄存器的PAE標志和PSE標志控制的,其中IA32_EFER.LME標志控制是否是否啟用IA-32e模式,而CR4.PAE標志為控制是否啟用PAE, CR4.PSE標志控制是否啟用PSE。但是并不是這三者的任意組合都是存在的,現實中存在的情況為以下幾種: 當前x86/x86_64架構支持的page的大小包括: 4k, 2M, 4M, 1G (操作系統到目前還不支持1G的page table) 以下以x86_64的Linux為例,說明page的大小是如何得到的:在內核源代碼include/asm-x86_64/pgtable.h文件中,可以找到如下宏定義:
...可知使用long mode的page的大小為:
1 << 21 = 2097152這個移位運算的結果,請參考?http://www.wolframalpha.com/input/?i=1%3C%3C21
需要注意的是page table的大小用戶無法自定義,在Linux中即使修改這個宏定義,重新編譯內核,也無法修改page的大小。
| Architecture | Page Size | Huge Page Size | Large Page Size | 
| i386 | 4?KB | 4M (2M in PAE mode) | 1?GB | 
| IA-64 | 4?KB | 4K, 8K, 64K, 256K, 1M, 4M, 16M, 256M | - | 
| ppc64 | 4?KB | - | 16M | 
| sparc | 8?KB | - | 8K, 64K, 4M, 256M, 2G | 
在Exadata甚至所有的大內存Linux平臺, hugepages的重要性怎么強調都不過分。那么為什需要配置hugepages呢?使用hugepages的好處,總結起來包括如下五個方面:
Not swappable?(使得?SGA?不可交換)
為啥SGA是可以被換到虛擬內存的???Bug?160033?-?Kernel swaps out Oracle instead of releasing cache 這個bug最終關閉的狀態為Not a bug, 但是同時在這個bug中提到: ? The basic design of the VM in RHEL 4 and RHEL 5 will not allow a complete fix for this issue, only tweaks to make it behave better most of the time.In the upstream kernel (and for RHEL 6), this problem has been addressed with the split LRU VM, which was merged in 2.6.28 and continues to get small fixes and tweaks.? In RHEL 6 this issue should be resolved. Oracle SGA區的內存被swap到磁盤只有到Linux Kernel2.6.28或者RHEL 6以后才通過引入新的機制得到解決。而一旦SGA區被換出內存會造成很大的性能抖動。所以在這個問題徹底解決之前,絕大多數版本的Linux依然需要通過hugepages來固定SGA區。(Linux平臺并不支持通過設置lock_sga和參數的方法來固定SGA, pre_page_sga這個參數存在太多的弊端已經強烈不推薦使用)
Relief of TLB pressure:?(減輕?TLB?的壓力)
我們知道TLB是直接緩存虛擬地址到物理地址的緩存表,用于提升性能,省去查找page table減少開銷,但是如果出現的大量的TLB miss,必然會給系統的性能帶來較大的負面影響,尤其對于連續的讀操作。從第二篇文章中我們知道如果使用hugepages能大量減少PTE的數量,也就意味著訪問同樣多的內容需要的PTE會更少,而通常TLB的槽位是有限的,一般只有512個,所以更少的PTE也就意味著更高的TLB的命中率。
Decreased page table overhead:?(減少頁表的開銷)
在前面的內容中我們通過計算,對4k page和hugepages的頁表開銷的進行對照發現,使用hugepages可以極大的減少維護頁表的開銷。這篇文章中也提到了一個計算方法:
Each page table entry can be as large as 64 bytes and if we are trying to handle 50GB of RAM, the pagetable will be approximately 800MB in size which is practically will not fit in 880MB size lowmem (in 2.4 kernels – the page table is not necessarily in lowmem in 2.6 kernels) considering the other uses of lowmem. When 95% of memory is accessed via 256MB hugepages, this can work with a page table of approximately 40MB in total.
注意這里說PTE為64Bytes是一種最極端的情況,考慮的是極值,所以實際上這種估算方法并不是太合理。前面說過:一般來說x86的PTE的大小為4Bytes, x86_64的PTE的大小為8Bytes,這里說的PTE為64Bytes實際上指的是由8個PTE組成的PTEG(PTE group),在?《Microprocessor 8085, 8086》?這本書的425頁中提到A?PTEG?contains eight Page Table Entries (PTEs) of eight bytes each
Eliminated page table lookup overhead:?(減少頁表查詢的開銷)
PTE的數量減少,那么使得很多頁表的查詢就不需要了,并且更少的PTE使得頁表的查詢更快。如果TLB miss,則可能需要額外三次內存讀取操作才能將線性地址翻譯為物理地址。
Faster overall memory performance:?(提升內存訪問的整體性能)
現代操作系統都是使用虛擬內存管理,每一次對內存的訪問實際上都是由兩次抽象的內存操作組成。如果只要使用更少的頁面,那么原本在頁表訪問的瓶頸也得以避免。
來看一個具體的問題。
某客戶新上線的Oracle數據庫系統,運行在Linux x86_64平臺上,主機配置較高,32核+120G內存,SGA設置90G左右,但是每當數據庫運行大約一周以后,前臺應用就會變得異常緩慢,經常假死,有時新連接都沒有響應,如果DBA不加干涉殺掉部分local=NO的進程,過一段時間就可能有節點被驅逐。 經過簡單的分析,當前數據庫主機有如下一些特征:
- 前臺響應緩慢或者新連接無法建立時,CPU占用率并不高,但是奇怪的是有一個系統進程kswapd0占據了單核CPU的100%,其它進程的CPU占用率都控制在單核的各位數;
- 通過free命令來查看剩余內存,發現所剩的內存已經不多,通過sar -B和vmstat查看發現有較為嚴重的page in和page out。
- 問題發生時刻,連接數有一定程度的增加,但是基本都是呈緩慢線性的方式增加,沒有劇增的情況。從oracle進程來看,每個連接占的CPU和內存資源都差不多;
- 從/proc/meminfo來看,頁表占用了內存的絕大部分; 操作系統重啟的時候,在/var/logs/message中有類似的信息:
這個問題并不復雜:本質就是一個內存耗盡的問題,但是另客戶非常不解的是:按照規劃,物理內存應該是綽綽有余的,那為什么還出現了內存耗竭的情況呢? 從前面的例子可以看到,因為沒有配置hugepages,導致了隨著連接數的增加頁表急劇膨脹,并且SGA被頻繁的從swap交換空間換入/換出。在Linux中, ?kswapd是負責內核頁面交換管理的一個守護進程,它的職責是保證Linux內存管理操作的高效。當物理內存不夠時,它就會變得非常aggressive,就像上文中所說的能占用單核CPU的100%. ? kswapd 進程負責確保內存空間總是在被釋放中,它監控內核中的pages_high和pages_low閥值。如果空閑內存的數值低于pages_low,則每次 kswapd 進程啟動掃描并嘗試釋放32個free pages.并一直重復這個過程,直到空閑內存的數值高于 pages_high。kswapd 進程完成以下幾個操作:
- 如果該頁處于未修改狀態,則將該頁放置回空閑列表中.
- 如果該頁處于已修改狀態并可備份回文件系統,則將頁內容寫入到磁盤.
- 如果該頁處于已修改狀態但沒有任何磁盤備份,則將頁內容寫入到swap device.
注:?每次?kswapd?進程啟動掃描并嘗試釋放?32?個?free pages??不再適用于RHEL 5.4以后的版本。 以上方框中的內容來自Linux System and Performance?Monitoring, 原文鏈接已不可考。當然也可以參考MOS文檔kswapd / krefilld Process Consumes All the CPU Resources [ID 272249.1]。
在Linux系統中,有兩種方式可以用來使用hugepages。一種是2.6內核就已經引入的Hugetlbfs虛擬文件系統,還有一種方式就是從2.6.38版本開始(RHEL 6)引入的THP(Transparent Hugepages).
在現實世界中,hugetlbfs主要用于數據庫,需要專門的進行配置以及應用程序的代碼支持,而THP則可用于更廣泛的應用程序,一切都交給操作系統來完成,也不再需要額外的配置,所以對于應用程序是透明的。 我們首先來看hugetlbfs。hugetlbfs是一個虛擬文件系統,它運行在內核層,不與磁盤空間相對應。如果需要使用hugetlbfs,則需要在編譯Linux內核的時候在file system這個配置欄中勾選CONFIG_HUGETLB_PAGE和CONFIG_HUGETLBFS選項(如果勾選了CONFIG_HUGETLB_PAGE則CONFIG_HUGETLBFS會自動勾選)。內核編譯完成并啟動操作系統以后,將hugetlbfs文件系統掛載到特定的目錄(不同的發行版目錄可能不一樣,Red Hat一般mount在/dev/hugepages),則hugetlbfs就可以訪問了。完成以后可以通過
cat /proc/filesystems | grep tlb來查看hugetlbfs是否已經開啟。因為RHEL/OL提供二進制內核的rpm包,所以這些操作在RHEL/OL都不需要做。RHEL/OL上惟一需要做的就是在nr_hugepages中輸入需要使用的hugepages的數量,例如:
echo 25000 > /proc/sys/vm/nr_hugepages當然我們都知道/proc文件系統的內容只對當前生效,如果需要重啟后依然有效則需要把vm.nr_hugepages?= 25000這個條目加入到/etc/sysctl.conf內核配置文件中,然后重啟操作系統或者使用sysctl -p 刷新內核參數配置才能生效。 如果一個程序要使用hugetlbfs,那么用戶只能使用mmap 標準的SYSV函數shmget/ shmat來調用/訪問它。如果使用mmap調用,則在mmap函數的第四個參數帶入的標志位為MAP_HUGETLB,具體實例請參考Linux內核文檔:?https://www.kernel.org/doc/Documentation/vm/map_hugetlb.c?,
如果使用shmget接口來調用,則需要則shmget的第三個參數的位置帶入SHM_HUGETLB參數(shmat沒有這個參數, 所以通過listener新建一個連接無法獲知是否使用了hugepages??蓞⒖磗hmat的man頁面:??http://linux.die.net/man/2/shmat?)。具體實例請參考Linux內核文?https://www.kernel.org/doc/Documentation/vm/hugepage-shm.c?。可能有同學覺得知道這個對于一個dba毫無意義,因為可能我永遠不會進行與hugepages開發相關的任何工作,但是實際上卻不是這樣的。
Linux平臺上無法從操作系統層面得知數據庫是不是真正使用了hugepages。那么這個時候我們通過strace跟蹤數據庫的啟動過程(啟動以后無法看到)發現,其調用了shmget, 并且第三個參數為SHM_HUGETLB標志。這樣我們就可以斷定數據庫使用了hugepages。例如:如果kernel.shmmax為32G,通過strace跟蹤就能看到類似的調用信息。(可以看出shmget的第二個參數為申請的內存大小,單位為Bytes)
$ strace -fo strace.out $ sqlplus / as sysdba SQL>startup nomount; $ grep HUGETLB strace.out | less shmget(0xxxxxxxxx, 13625196544, IPC_CREAT|IPC_EXCL|SHM_HUGETLB|0660) ...那么能否可以通過/proc/meminfo中的信息來確認數據庫使用了hugepages,我這里舉例說明:以下是一個Exadata用戶的hugepages信息:
#cat /proc/meminfo | grep -i hugepage HugePages_Total: 13007 HugePages_Free: 11813 HugePages_Rsvd: 3372 HugePages_Surp: 0 Hugepagesize: 2048 kB從上面的信息可以得出以下結論:當前hugepages一共實際使用了13007-11813=1194個pages (不包括預留的),預留了3372個pages。永遠不會使用達到了11813-3372=8841個pages。每個page大小為2M,也就是說有接近16.5G的hugepages內存被浪費了(因為hugepages無法swap,所以不能被其它程序所使用)。換句話說:當前系統使用中或者即將被使用的hugepages的總和為HugePages_Total – HugePages_Free + HugePages_Rsvd=13007-11813+3372=4566個。產生這樣令人困惑的結果是因為用戶為了省資源,在這臺主機上配置了多個數據庫實例,但是只有一個實例使用了hugepages。 之所以要提這個事情,是因為在Oracle 11.2.0.3版本以前,如果當前操作系統配置了hugepages,但是數據庫實例設置的SGA大小如果比hugepages總的大小還要大,則SGA無法使用hugepages的內存,然后會去使用其它非hugepages的內存,并且在alert中也沒有任何信息,進而導致系統內存占用率高,數據庫性能非常糟糕。
Kevin Closson?在他的blog中有抱怨過這個問題,但是他卻沒說這實際上是一個Bug 12654172: EXHAUST THE AVAILABLE HUGEPAGES FOR ALLOCATING A REALM。當然11.2.0.3以后的版本,會在alert日志提示hugepages沒有使用的警告信息。 目前在Linux的主流版本,依然無法像其它平臺那樣使用操作系統工具來獲取到數據庫是否使用了hugepages。例如:在Solaris中可以使用pmap -s來查看,在AIX中可以使用vmstat來查看,在RHEL/OL 6可以使用cat /proc/<pid>/smaps來查看。
配置hugepages
接下來講述如何在RHEL/OL/SLES如何設置hugepages,Exadata后期的版本onecommand默認為數據庫配置了hugepages,早期的版本配置方式與RHEL/OL 類似。 在RHEL/OL上,大致有如下步驟:
1?)???首先需要配置?memlock
在/etc/security/limits.conf最后添加:
oracle hard memlock lock_value oracle soft memlock lock_value其中上述lock_value使用比SGA值略大的一個值代替。
2?)估算?hugepages?的大小
在RHEL/OL平臺,如果是數據庫沒有啟動,則可以按照如下公式大致估算所需要的hugepages數目: #hugepages=(SGA(MB)+(20k * # of Oracle processes running)/1024) / hugepage_size (MB) 例如:如果sga的大小為64G,運行的oracle進程數為1000, 那么則大致需要32777個hugepages。RedHat Consultant?Scott Croft認為計算的時候?應該加上PGA的值?,但是pga是無法用到hugetlbfs的,所以這里沒有算到pga的值。 當然在數據庫實例沒有啟動的時候只能是估算,如果Oracle數據庫正在運行,則可以得到更準確的值。以下腳本根據數據庫啟動以后的的共享內存段總和除上hugepages的大小,計算出來所需的hugepages的數量。
# # hugepages_settings.sh # # Linux bash script to compute values for the # recommended HugePages/HugeTLB configuration # # Note: This script does calculation for all shared memory # segments available when the script is run, no matter it # is an Oracle RDBMS shared memory segment or not. # Check for the kernel version #查看Oracle Kernel的版本,因為2.4和2.6使用的hugepages的參數是不一樣的; #2.4使用vm.hugetlb_pool,而2.6使用vm.nr_hugepages。 KERN='uname -r | awk -F. '{ printf("%d.%d\n",$1,$2); }'' # Find out the HugePage size #查找Hugepages的大小,x86非PAE為4096,x86+PAE以及x86_64為2048,注意# 這里單位為K。 HPG_SZ=`grep Hugepagesize /proc/meminfo | awk {'print $2'}` # Start from 1 pages to be on the safe side and guarantee 1 free HugePage #保證至少有1個page,也就是計數從1開始,MOS文檔401749.1的初始計數從0開始。 NUM_PG=1 # Cumulative number of pages required to handle the running shared memory segments #循環計算一共需要多少hugepages #ipcs -m | awk {'print $5'} | grep "[0-9][0-9]*"的結果是列出所有的shared memory的大#小,單位為Bytes;echo "$SEG_BYTES/($HPG_SZ*1024)" | bc -q 為將shared memory處理單 #個page的大小,得到單個shared memory所需的hugepages的數量。將所有的shared memory #循環累加,最終得到總的hugepages的數量。 for SEG_BYTES in `ipcs -m | awk {'print $5'} | grep "[0-9][0-9]*"` do MIN_PG='echo "$SEG_BYTES/($HPG_SZ*1024)" | bc -q' if [ $MIN_PG -gt 0 ]; then NUM_PG=`echo "$NUM_PG+$MIN_PG+1" | bc -q` fi done # Finish with results #根據不同的內核,提示設置不同的hugepages參數 case $KERN in '2.4') HUGETLB_POOL=`echo "$NUM_PG*$HPG_SZ/1024" | bc -q`; echo "Recommended setting: vm.hugetlb_pool = $HUGETLB_POOL" ;; '2.6') echo "Recommended setting: vm.nr_hugepages = $NUM_PG" ;; *) echo "Unrecognized kernel version $KERN. Exiting." ;; esac # End3?)?設置?hugepages?內核參數
運行上述hugepages_settings.sh,得到一個值。將這個值寫入/etc/sysctl.conf, 例如:
4?)?禁用?AMM?特性
因為Oracle自動內存管理的AMM特性與hugetlbfs不兼容,所以需要禁用這個特性,但是可以使用SGA自動管理。具體做法將MEMORY_TARGET/MEMORY_MAX_TARGET設置為0,并且手工指定SGA_TARGET/SGA_MAX_SIZE/PGA_AGGREGATE_TARGET的值。
5?)重啟主機和數據庫確認生效
設置完成以后,最好重啟主機(不是必須的,但是建議)和數據庫(必須)使得hugepages生效。如果使用的Linux是SLES (SUSE Linux Enterprise Server) ,則Oracle提供了一個名為orarun的rpm包,可以用于自動配置其hugepages。 This package creates the user and the groups for Oracle, sets the Oracle environment variables, sets kernel parameters to values recommended by Oracle, and provides for automated start and stop of Oracle components at system start and stop time. SLES 11的orarun包可以在以下ftp站點找到:?http://ftp.novell.com/partners/oracle/sles-10/?SLES 11的orarun包可以在以下ftp站點找到:??http://ftp.novell.com/partners/oracle/sles-11/??
總結
以上是生活随笔為你收集整理的Hugepages详解的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: SPOJ7258 SUBLEX - Le
- 下一篇: 长线思维模式
