Linux高階應用CpuMemSets在Linux中的實現(轉)
Linux高階應用CpuMemSets在Linux中的實現(轉)[@more@] 一、 前言
非一致性記憶體訪問(Non-Uniform Memory Access)結構是分散式共享記憶體(Distributed Shared Memory)體系結構的主要分支,它透過結合分散式記憶體技術和單一系統映像(SSI)技術,實現了SMP 系統的易程式設計性和 MPP系統的易擴充套件性的折中,已成為當今高效能伺服器的主流體系結構之一。目前國外著名的伺服器廠商都先後推出了基於 NUMA 架構的高效能伺服器,如HP的Superdome、SGI 的 Altix 3000、Origin 3000、IBM 的 x440、NEC 的 TX7、AMD 的Opteron 等。
隨著NUMA架構的高效能伺服器被逐漸推廣,系統軟體針對這種分散式共享記憶體架構的特點,在排程器、儲存管理和使用者級介面等方面進行了大量的最佳化工作。例如,SGI的Origin 3000 ccNUMA系統在許多領域得到了廣泛應用,是個非常成功的系統,為了最佳化Origin 3000的效能,SGI的IRIX作業系統在其上實現了CpuMemSets,透過將應用與處理器和記憶體的繫結,充分發揮NUMA系統本地訪存的優勢。Linux社群在自己的NUMA專案中也實現了CpuMemSets,並且在SGI的Altix 3000的伺服器中得到實際應用。
本文將以 SGI 的 ProPack v2.2 為研究物件,分析 CpuMemSets 在Linux-2.4.20 中的具體實現。CpuMemSets 是 SGI 進行的一個開放原始碼專案,由針對 Linux2.4 核心的補丁、使用者庫、python 模組和 runon 等命令共四部分組成,以實現處理器和記憶體塊的分割槽為目標,控制系統資源(處理器、記憶體塊)面向核心、任務以及虛擬儲存區的分配,為 dplace、RunOn 等 NUMA 工具提供支援,最終最佳化 Linux 系統的 NUMA 效能。
二、 相關工作
分割槽技術(Partition)最初出現在大型機(MainFrame)上,如今被廣泛應用到伺服器領域,支援在單個伺服器上執行一個作業系統的多個例項或者多個作業系統的多個例項,主要特點是"機器獨立、屏障可靠、單點管理"。在分割槽技術支援下,當前多臺伺服器執行的多個作業系統就可以在同一地點的一臺伺服器上同時執行,優於在一個組織中四處分散用多個伺服器來支援不同的作業系統,從而有效地實現了伺服器整合。支援分割槽技術的伺服器可以當作應用伺服器,執行Windows平臺供市場部門使用;同時還可以執行Linux系統供工程部門使用。還可以在大多數使用者執行Windows 2000 Advanced Server系統的同時,在另一個分割槽中為發展組測試其它作業系統;或者所有節點都應用在一個作業系統環境下。各種分割槽實現技術的主要差別體現在分割槽故障隔離手段(硬體或軟體)、分割槽資源粒度、分割槽資源靈活性以、虛擬分割槽資源以及對動態分割槽重構的支援等方面。典型的有IBM的LPAR和DLAPAR(AIX 5L 5.1)、HP的nPartitions和vPartitions(HP-UX 11i)、SUN的Dynamic Domains(Solaris 8)、以及Compaq的Alpha Servers(Tru64 Unix 5.1)。但是,針對NUMA系統採用的分割槽技術與NUMA系統本身具有的單系統映像優勢是矛盾的。
從使用者的角度來看,NUMA系統提供了對本地主存和遠端主存訪問的透明性;但是,從效能的角度來看,由於儲存模組物理上分佈在不同的節點引起的儲存訪問延遲不一致現象,對系統的效能也帶來了較大的影響。在這類系統中,一個節點對遠端節點儲存訪問的延遲通常比本地訪問延遲高1到2個數量級。頁遷移與頁複製是對資料進行動態區域性性最佳化的主要方法之一。其實質是一種預測技術,根據收集到的資訊預測將來對頁面的訪問情況,然後作出遷移或複製頁面的決策。採用適當的頁複製與頁遷移策略可以減小cache容量和衝突失效,平衡遠端和本地訪問延遲的不一致,達到最佳化NUMA系統效能的目的。但是現有的頁遷移與頁複製策略大都過分依賴於體系結構和特殊的硬體支援,開銷比較大,通用性也不好。
在NUMA結構的多處理器系統中,一個任務可以在任何一個處理器上執行,然而任務在各種情況的執行會被中斷;被中斷的任務在恢復執行的時候,如果選擇恢復在另外一個處理器上執行,就會導致它失去原有的處理器cache資料。我們知道,訪問cache資料只需要幾個納秒,而訪問主存需要大約50納秒。這時處理器執行的速度處在訪問主存的級別上,直到任務執行了足夠的時間,任務執行所需要的資料重新充滿該處理器的cache為止。為解決這個問題,系統可以採用處理器親近排程策略排程每個節點上的任務:系統記錄下最後執行這個任務的處理器並維持這種關係,在恢復執行被中斷的任務時,儘量恢復在最後執行這個任務的處理器上執行。但是,由於應用程式的特點各有不同,而且工作集具有動態屬性,處理器親近排程的作用是有限的。
使用者是系統的使用者,也是效能的評判者,他們最清楚應用對系統的需求和評價指標。在一個大的NUMA系統中,使用者往往希望控制一部分處理器和記憶體給某些特殊的應用。CpuMemSets允許使用者更加靈活的控制(它可以重疊、劃分系統的處理器和記憶體),允許多個程式將系統看成一個單系統映像,並且不需要重啟系統,保障某些處理器和記憶體資源在不同的時間分配給指定的應用;也是對分割槽技術、頁遷移和親近排程策略的有益補充。
三、 系統實現
在介紹CpuMemSets在Linux-2.4.20中的具體實現之前,我們首先說明CpuMemSets涉及的幾個基本概念,包括:
處理器:指承載任務排程的物理處理器,但是不包括DMA裝置、向量處理器等專用處理器;
記憶體塊:在SMP、UP系統中,所有的記憶體塊與所有處理器的距離相等,因此不存在差別;但是在NUMA系統中,可以按照與處理器的距離對記憶體塊劃分等價類。此外,CpuMemSets不考慮具有速度差異的特殊儲存器,如輸入輸出裝置快取、幀快取等。
任務:一個任務,在任一時刻,或者等待事件、資源,或者被中斷,或者在處理器上執行。
虛擬儲存區:核心為每個任務維護的多個虛擬地址區域,可為多個任務共享。位於虛擬儲存區內的頁,或者沒有分配,或者已分配但被換出,或者已分配且在記憶體中。可以指定允許分配給某個虛擬儲存區的記憶體塊以及分配順序。
CpuMemSets為Linux提供了將系統服務和應用繫結在指定的處理器上進行排程、在指定的結點上分配記憶體的機制。CpuMemSets在已有的Linux排程和記憶體分配程式碼基礎上增加了cpumemmap(cmm)和cpumemset(cms)兩層結構,底層的cpumemmap層提供一個簡單的對映對,實現系統的處理器號與應用的處理器號、系統的記憶體塊號與應用的記憶體塊號的對映。這種對映不一定是單射,一個系統號可以對應多個應用號。上層的cpumemset層負責說明允許把任務排程到哪些應用處理器號所對應的處理器上執行、可以從哪些應用記憶體塊號所對應的記憶體塊中為相應的核心或虛擬儲存區分配記憶體頁,也就是說,指定可供核心、任務、虛擬儲存區使用的資源集合。在這種兩層結構中,資源的系統號供核心執行排程和記憶體分配時使用;而資源的應用號供使用者程式指定本應用的資源集合時使用。系統號在啟動期間全系統範圍內有效,而應用號僅僅相對於共享同一個cmm的所有使用者程式有效。而且,由於負載平衡和熱插拔引發的資源物理編號的變化對應用號是不可見的。
Linux的程式排程和記憶體分配在保持現有程式碼正常運轉的基礎上,新增了對CpuMemSets的支援,使用"系統處理器號"和"系統記憶體塊號"以及其他資料結構如cpus_allowed和mems_allowed等實現資源的分割槽。此外,CpuMemSets的API提供了對cpusets、dplace、runon、psets、MPI、OpenMP、nodesets的支援,並且提供/proc介面以顯示cmm和 cms的結構、設定以及與任務、虛擬儲存區、核心的連線關係、系統資源號和應用資源號等資訊。下面我們分別對cpumemmap和cpumemset、程式排程和記憶體分配、以及API這三個部分進行詳細分析:
3.1 cmm&cms
3.1.1 資料結構
cpumemmap和cpumemset的資料結構如下所示,具體定義在include/linux/cpumemset.h中。Cpumemmap中的scpus和smems域分別指向一組系統處理器號和一組系統記憶體塊號,實現應用的資源號(陣列下標)與系統的資源號(陣列元素值)的對映。Cpumemset中的acpus域指向一組應用處理器號,而amems域指向一組cms_memory_list_t型別的記憶體塊列表。每個記憶體塊列表描述了一組應用記憶體塊號(mems)以及享有該列表的一組應用處理器號(cpus)。記憶體塊分配策略由cpumemset中的policy域決定,預設使用本地優先方式。Cpumemset透過cmm域與相應的cpumemmap建立關聯。兩個資料結構中的counter域的作用將在後文介紹。
【include/linux/cpumemset.h】
84 typedef struct cpumemmap {
85 int nr_cpus; /* number of cpus in map */
86 int nr_mems; /* number of mems in map */
87 cms_scpu_t *cpus; /* array maps application to system cpu num */
88 cms_smem_t *mems; /* array maps application to system mem num */
89 atomic_t counter; /* reference counter */
90 } cpumemmap_t;
92 typedef struct cpumemset {
93 cms_setpol_t policy; /* CMS_* policy flag :Memory allocation policy */
94 int nr_cpus; /* Number of cpus in this CpuMemSet */
95 int nr_mems; /* Number of Memory Lists in this CpuMemSet */
96 cms_acpu_t *cpus; /* The 'nr_cpus' app cpu nums in this set */
97 cms_memory_list_t *mems; /* Array 'nr_mems' Memory Lists */
98 unsigned long mems_allowed; /* memory_allowed vector used by vmas */
99 cpumemmap_t *cmm; /* associated cpumemmap */
100 atomic_t counter; /* reference counter */
101 } cpumemset_t;
76 typedef struct cms_memory_list {
77 int nr_cpus; /* Number of cpu's sharing this memory list */
78 int nr_mems; /* Number of memory nodes in this list */
79 cms_acpu_t *cpus; /* Array of cpu's sharing this memory list */
80 cms_amem_t *mems; /* Array of 'nr_mems' memory nodes */
81 } cms_memory_list_t;
以一個包含四個節點的NUMA系統為例簡單說明上述資料結構的使用。假設每個節點由四個節點和一個記憶體塊組成,16個處理器的系統號依次是:c0(0)、c1(1)、… …、c15(15),4個記憶體塊的系統號依次是mb0(0)、mb1(1)、mb2(2)、mb3(3)。構造一個只包含第2和第3節點的處理器和記憶體塊cpumemmap,並且把某個應用繫結在奇數號的處理器上,同時以本地優先的方式分配記憶體塊。資料結構如圖1所示:
圖1 CpuMemSets的資料結構使用示例
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
3.1.2 基本操作
多個任務可以同時對指定的cms&cmm執行訪問、掛接、釋放或替換的操作。這些操作都是透過下列例程執行:
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
控制程式碼結構定義如下:
【include/linux/cpumemset.h】
417 typedef struct cmshandle {
418 cpumemset_t *set;
419 cpumemmap_t *map;
420 int error;
421 } cmshandle_t;
上述例程透過鎖機制以及對cmm&cms中的引用計數counter的管理,保證了對CpuMemSets進行並行操作的完整性。每個cms中的counter記錄著:使用者的引用計數(指向這個cms的任務、虛擬儲存區、核心總數)和控制程式碼的引用計數(臨時指向這個cms的控制程式碼的總數)。每個cmm中的counter記錄著:cms的引用計數(指向這個cmm的cms總數)和控制程式碼的引用計數(臨時指向這個cmm的控制程式碼的總數)。
替換CpuMemSets的操作分為下列三個步驟:
1. 呼叫合適的cmsGetHandle*()安全的獲得cms和cmm的控制程式碼;
2. 構造新的cms和cmm;
3. 呼叫合適的cmsExchange*()安全的更換上述的新舊cms和cmm。
訪問任務或虛擬儲存區的cms和cmm的操作分為下列三個步驟:
1. 呼叫合適的cmsGetHandle*()安全的獲得cms和cmm的控制程式碼;
2. 呼叫cms和cmm,此期間無法保證原來的任務或虛擬儲存區仍然使用這些cms和cmm;
3. 呼叫cmsRelease()釋放控制程式碼。
掛接、放棄控制程式碼的操作步驟:
1. 呼叫合適的cmsGetHandle*()或cmsNewHandle*()安全的獲得cms和cmm的控制程式碼;
2. 呼叫合適的cmsAttachNew*()或cmsDiscard()。
3.1.3 基本設定
核心擁有自己的kernel_cms。核心在start_kernel()的開頭(build_all_ zonelists()之後,trap_init()之前)首先呼叫cms_cmm_static_init()為kernel_cms構造靜態的初始cmm&cms,其中只包含執行本核心的處理器和所在節點的記憶體塊,並且將kernel_cms -> mems_allowed賦值-1UL,允許核心在cpu_init()過程中使用所有記憶體塊。然後,核心在start_kernel()的結尾執行cms_cmm_init(),建立cmm和cms快取記憶體,為kernel_cms構造包含所有處理器和記憶體塊的cmm&cms,並傳遞給init_task。如果系統啟動時設定了cpumemset_minimal引數,則使用的是cms_cmm_static_init()構造的最小集。
每個程式都擁有兩個cms:current_cms影響自己的處理器分配和虛擬儲存區建立, child_cms繼承給由自己fork的子程式。每個新建程式的current_cms和child_cms都繼承自父程式的child_cms。
【include/linux/sched.h】
296 struct task_struct {
:
325 cpumask_t cpus_allowed;
:
429 cpumemset_t * current_cms;
430 cpumemset_t * child_cms;
431 /* stash mems_allowed of most recent vma to page fault here */
432 unsigned long mems_allowed;
:
462 };
【kernel/fork.c】
620 int do_fork(unsigned long clone_flags, unsigned long stack_start,
621 struct pt_regs *regs, unsigned long stack_size)
622 {
:
751 SET_CHILD_CMS(p, current);
:
871 }
【include/linux/cpumemset.h】
173 /*
174 * Set child's current, child and alloc cpumemset
175 * from parent's child_cms (used in fork).
176 */
177
178 #define SET_CHILD_CMS(child, parent) do { 179 cpumemset_t *cms = (parent)->child_cms; 180 CMS_TRACE_HOOK("fork task", cms); 181 atomic_inc(&cms->counter); 182 (child)->current_cms = cms; 183 update_cpus_allowed(child); 184 atomic_inc(&cms->counter); 185 (child)->child_cms = cms; 186 } while (0)
每個虛擬儲存區都擁有各自的vm_mems_allowed位向量。新建虛擬儲存區的vm_mems_ allowed透過cms_current_mems_allowed()繼承自建立者任務current_cms的mems_allowed。對於被掛接的已存在的虛擬儲存區,如mmap的記憶體物件和共享記憶體區,則繼承自掛接程式current_cms的mems_allowed。cms的mems_allowed位向量由mems_allowed_build(cms)根據cms中的所有記憶體塊列表來構造。
【include/linux/mm.h】
45 struct vm_area_struct {
:
77 unsigned long vm_mems_allowed; /* cpumemset managed memory placement */
:
81 };
【kernel/cpumemset.c】
1538 static unsigned long
1539 mems_allowed_build(cpumemset_t *cms)
1540 {
1541 int i;
1542 unsigned long mems_allowed = 0;
1543
1544 for (i = 0; i < cms->nr_mems; i++)
1545 mems_allowed |= mems_allowed_value(cms, cms->mems + i);
1546 return mems_allowed;
1547 }
3.2 程式排程及記憶體分配
核心呼叫update_cpus_allowed(struct task_struct *p)根據任務的current_cms的處理器列表更改它的cpus_allowed位向量,從而影響該任務的處理器排程。
【kernel/cpumemset.c】
585 void
586 update_cpus_allowed(struct task_struct *p)
587 {
588 #ifdef CONFIG_SMP
589 int i;
590 cpumemset_t *cms = p->current_cms;
591 cpumask_t cpus_allowed = CPU_MASK_NONE;
592
593 for (i = 0; i < cms->nr_cpus; i++)
594 __set_bit(cms->cmm->cpus[cms->cpus], &cpus_allowed);
595 if (any_online_cpu((cpumask_t*)&cpu_online_map) < NR_CPUS) {
:
609 set_cpus_allowed(p, &cpus_allowed);
610 }
611 #endif
612 }
核心根據虛擬儲存區的vm_mems_allowed位向量為任務分配記憶體,如果是在中斷上下文中,虛擬儲存區的記憶體分配則依賴於kernel_cms的mems_allowed。宏CHECK_MEMS_ALLOWED(mems_allowed, zone)負責檢查zone所在的節點是否落在mems_allowed設定的記憶體塊集合內。
【mm/memory.c】
1383 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
1384 unsigned long address, int write_access)
1385 {
:
1390 /*
1391 * We set the mems_allowed field of the current task as
1392 * the one pointed by the faulting vma. The current
1393 * process will then use the correct mems_allowed mask
1394 * if a new page has to be allocated.
1395 */
1396 if(!in_interrupt())
1397 current->mems_allowed = vma->vm_mems_allowed;
:
1417 }
【mm/page_alloc.c】
334 struct page * __alloc_pages(..)
:
343 if (in_interrupt())
344 mems_allowed = kernel_cms->mems_allowed;
345 else
346 mems_allowed = current->mems_allowed;
347 if (mems_allowed == 0) {
348 printk(KERN_DEBUG "workaround zero mems_allowed in alloc_pages ");
349 mems_allowed = -1UL;
350 }
:
if(!CHECK_MEMS_ALLOWED(mems_allowed, z))
continue;
:
450 }
【include/linux/cpumemset.h】
194 /* Used in __alloc_pages() to see if we can allocate from a node */
195 #define CHECK_MEMS_ALLOWED(mems_allowed, zone) 196 ((1UL << (zone)->zone_pgdat->node_id) & (mems_allowed))
為虛擬儲存區分配頁時,如果當前執行該任務的處理器包含在該虛擬儲存區的cms中,則從該處理器的記憶體塊列表中分配,否則從該虛擬儲存區的cms定義的CMS_DEFAULT_CPU的記憶體塊列表中分配。
3.3 API設計
CpuMemSets提供了一系列的核心級和應用級的程式設計介面,分別定義在核心的include/linux/cpumemset.h檔案和庫程式碼CpuMemSets/cpumemsets.h檔案中(如表2所示)。
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
透過呼叫使用者級介面對cmm&cms進行設定將導致核心的排程器和記憶體分配器所使用的系統位向量如cpus_allowed和mems_allowed的變化,從而使核心排程程式碼使用新的系統處理器號、記憶體分配程式碼從新記憶體塊中分配記憶體頁;但是,原先從舊記憶體塊中分配的記憶體頁將不會發生遷移,除非強制採用其他手段。具體來說,系統在cmsAttachNewTask()、cmsExchangeTask()和cmsExchangePid()過程中,執行update_cpus_allowed()根據任務的current_cms的當前處理器列表更改它的cpus_allowed位向量;在cms_set()過程中,執行mems_allowed_build()根據任務的current_cms的當前記憶體塊列表更改當前虛擬儲存區或任務、核心的mems_allowed位向量。
【kernel/cpumemset.c】
1661 static int
1662 cms_set(unsigned long *preq, char *rec, int size, target_option cmm_or_cms)
:
1713 if (choice == CMS_VMAREA) {
:
1772 vma->vm_mems_allowed = mems_allowed_build(newhan.set);
:
1792 } else {
:
1822 newhan.set->mems_allowed = mems_allowed_build(newhan.set);
:
1840 }
:
1842 }
在許可權保護方面,只有根權使用者才可以修改核心所使用的cms&cmm和任何任務的cms&cmm;而一般使用者只能修改自身的任務和虛擬儲存區所擁有的cms&cmm;具有相同uid的任務之間可以互相修改對方的cms&cmm。只有根權使用者才可以擴充套件自身的cmm,一般使用者只可以縮小自己的cmm。
【kernel/cpumemset.c】
409 cmshandle_t
410 cmsGetHandlePid(int choice, pid_t pid, int do_check_perm)
411 {
:
425 if (pid &&
426 do_check_perm &&
427 (current->euid ^ p->suid) &&
428 (current->euid ^ p->uid) &&
429 (current->uid ^ p->suid) &&
430 (current->uid ^ p->uid) &&
431 !capable(CAP_SYS_ADMIN))
432 han.error = -EPERM;
:
442 }
【kernel/cpumemset.c】
1478 /*
1479 * Unless you have CAP_SYS_ADMIN capability, you can only shrink cmm.
1480 */
1481
1482 static int
1483 cmm_restrict_checking(cpumemmap_t *oldmap, cpumemmap_t *newmap)
1484 {
1485 int i;
1486
1487 if (capable(CAP_SYS_ADMIN))
1488 return 0;
1489
1490 /* newmap must be a subset of oldmap */
1491 for (i = 0; i < newmap->nr_cpus; i++)
1492 if (!foundin (newmap->cpus, oldmap->cpus, oldmap->nr_cpus))
1493 return -EINVAL;
1494 for (i = 0; i < newmap->nr_mems; i++)
1495 if (!foundin (newmap->mems, oldmap->mems, oldmap->nr_mems))
1496 return -EINVAL;
1497 return 0;
1498 }
四、 使用舉例
示例1:顯示當前任務current_cms中的處理器
/*
* sample1 - display current cpumemset cpus
*
* Compile with:
* cc sample1.c -o sample1 -lcpumemsets
* Displays on stdout the number and a list of the cpus
* on which the current process is allowed to execute.
*/
#include "cpumemsets.h"
main()
{
int i;
cpumemset_t *pset;
pset = cmsQueryCMS(CMS_CURRENT, (pid_t)0, (void *)0);
if (pset == (cpumemset_t *)0) {
perror("cmsQueryCMS");
exit (1);
}
printf("Current CpuMemSet has %d cpu(s): ", pset->nr_cpus);
for (i = 0; i < pset->nr_cpus; i++)
printf("%s%d", (i > 0 ? ", " : ""), pset->cpus);
printf("0);
exit(0);
}
示例2:設定當前任務的子任務僅執行在0號處理器上,並啟動sh執行
/*
* sample2 - change child cpumemset cpus to just cpu 0
*
* Compile with:
* cc sample2.c -o sample2 -lcpumemsets
* Change the cpus which the child task is allowed
* execute on to just cpu 0. Start a subshell,
* instead of just exiting, so that the user has
* the opportunity to verify that the change occurred.
*/
#include
main()
{
int i;
cpumemset_t *pset;
pset = cmsQueryCMS(CMS_CHILD, (pid_t)0, (void *)0);
if (pset == (cpumemset_t *)0) {
perror("cmsQueryCMS");
exit (1);
}
pset->nr_cpus = 1;
free(pset->cpus);
pset->cpus = (cms_acpu_t *) malloc(sizeof(cms_acpu_t));
pset->cpus[0] = (cms_acpu_t)0;
if (cmsSetCMS(
CMS_CHILD, (pid_t)0, (void *)0, (size_t)0, pset) < 0) {
perror ("cmsSetCMS");
exit(1);
}
cmsFreeCMS (pset);
printf ("Invoking subshell running on cpu 0.0);
execl("/bin/sh", "sh", 0);
exit (2);
}
先執行示例2,再執行示例1:
# PS1='Sub> ' ./sample2
Invoking subshell running on cpu 0.
Sub> ./sample1
Current CpuMemSet has 1 cpu(s):
0
Sub> exit
五、 小結
CpuMemSets透過在已有的Linux排程和記憶體分配程式碼基礎上增加cpumemmap(cmm)和cpumemset(cms)兩層結構,為Linux提供了將系統服務和應用繫結在指定的處理器上進行排程、在指定的結點上分配記憶體的機制。從資料結構和控制機制上看,目前的實現比較簡單實用,但是仍然有進一步最佳化的空間。但是,CpuMemSets僅僅是提供了一套挖掘本地訪存優勢、最佳化Linux系統的NUMA效能的機制,如何基於這種支援手段制定合適的NUMA系統最佳化策略需要進行更多更深入的研究。
非一致性記憶體訪問(Non-Uniform Memory Access)結構是分散式共享記憶體(Distributed Shared Memory)體系結構的主要分支,它透過結合分散式記憶體技術和單一系統映像(SSI)技術,實現了SMP 系統的易程式設計性和 MPP系統的易擴充套件性的折中,已成為當今高效能伺服器的主流體系結構之一。目前國外著名的伺服器廠商都先後推出了基於 NUMA 架構的高效能伺服器,如HP的Superdome、SGI 的 Altix 3000、Origin 3000、IBM 的 x440、NEC 的 TX7、AMD 的Opteron 等。
隨著NUMA架構的高效能伺服器被逐漸推廣,系統軟體針對這種分散式共享記憶體架構的特點,在排程器、儲存管理和使用者級介面等方面進行了大量的最佳化工作。例如,SGI的Origin 3000 ccNUMA系統在許多領域得到了廣泛應用,是個非常成功的系統,為了最佳化Origin 3000的效能,SGI的IRIX作業系統在其上實現了CpuMemSets,透過將應用與處理器和記憶體的繫結,充分發揮NUMA系統本地訪存的優勢。Linux社群在自己的NUMA專案中也實現了CpuMemSets,並且在SGI的Altix 3000的伺服器中得到實際應用。
本文將以 SGI 的 ProPack v2.2 為研究物件,分析 CpuMemSets 在Linux-2.4.20 中的具體實現。CpuMemSets 是 SGI 進行的一個開放原始碼專案,由針對 Linux2.4 核心的補丁、使用者庫、python 模組和 runon 等命令共四部分組成,以實現處理器和記憶體塊的分割槽為目標,控制系統資源(處理器、記憶體塊)面向核心、任務以及虛擬儲存區的分配,為 dplace、RunOn 等 NUMA 工具提供支援,最終最佳化 Linux 系統的 NUMA 效能。
二、 相關工作
分割槽技術(Partition)最初出現在大型機(MainFrame)上,如今被廣泛應用到伺服器領域,支援在單個伺服器上執行一個作業系統的多個例項或者多個作業系統的多個例項,主要特點是"機器獨立、屏障可靠、單點管理"。在分割槽技術支援下,當前多臺伺服器執行的多個作業系統就可以在同一地點的一臺伺服器上同時執行,優於在一個組織中四處分散用多個伺服器來支援不同的作業系統,從而有效地實現了伺服器整合。支援分割槽技術的伺服器可以當作應用伺服器,執行Windows平臺供市場部門使用;同時還可以執行Linux系統供工程部門使用。還可以在大多數使用者執行Windows 2000 Advanced Server系統的同時,在另一個分割槽中為發展組測試其它作業系統;或者所有節點都應用在一個作業系統環境下。各種分割槽實現技術的主要差別體現在分割槽故障隔離手段(硬體或軟體)、分割槽資源粒度、分割槽資源靈活性以、虛擬分割槽資源以及對動態分割槽重構的支援等方面。典型的有IBM的LPAR和DLAPAR(AIX 5L 5.1)、HP的nPartitions和vPartitions(HP-UX 11i)、SUN的Dynamic Domains(Solaris 8)、以及Compaq的Alpha Servers(Tru64 Unix 5.1)。但是,針對NUMA系統採用的分割槽技術與NUMA系統本身具有的單系統映像優勢是矛盾的。
從使用者的角度來看,NUMA系統提供了對本地主存和遠端主存訪問的透明性;但是,從效能的角度來看,由於儲存模組物理上分佈在不同的節點引起的儲存訪問延遲不一致現象,對系統的效能也帶來了較大的影響。在這類系統中,一個節點對遠端節點儲存訪問的延遲通常比本地訪問延遲高1到2個數量級。頁遷移與頁複製是對資料進行動態區域性性最佳化的主要方法之一。其實質是一種預測技術,根據收集到的資訊預測將來對頁面的訪問情況,然後作出遷移或複製頁面的決策。採用適當的頁複製與頁遷移策略可以減小cache容量和衝突失效,平衡遠端和本地訪問延遲的不一致,達到最佳化NUMA系統效能的目的。但是現有的頁遷移與頁複製策略大都過分依賴於體系結構和特殊的硬體支援,開銷比較大,通用性也不好。
在NUMA結構的多處理器系統中,一個任務可以在任何一個處理器上執行,然而任務在各種情況的執行會被中斷;被中斷的任務在恢復執行的時候,如果選擇恢復在另外一個處理器上執行,就會導致它失去原有的處理器cache資料。我們知道,訪問cache資料只需要幾個納秒,而訪問主存需要大約50納秒。這時處理器執行的速度處在訪問主存的級別上,直到任務執行了足夠的時間,任務執行所需要的資料重新充滿該處理器的cache為止。為解決這個問題,系統可以採用處理器親近排程策略排程每個節點上的任務:系統記錄下最後執行這個任務的處理器並維持這種關係,在恢復執行被中斷的任務時,儘量恢復在最後執行這個任務的處理器上執行。但是,由於應用程式的特點各有不同,而且工作集具有動態屬性,處理器親近排程的作用是有限的。
使用者是系統的使用者,也是效能的評判者,他們最清楚應用對系統的需求和評價指標。在一個大的NUMA系統中,使用者往往希望控制一部分處理器和記憶體給某些特殊的應用。CpuMemSets允許使用者更加靈活的控制(它可以重疊、劃分系統的處理器和記憶體),允許多個程式將系統看成一個單系統映像,並且不需要重啟系統,保障某些處理器和記憶體資源在不同的時間分配給指定的應用;也是對分割槽技術、頁遷移和親近排程策略的有益補充。
三、 系統實現
在介紹CpuMemSets在Linux-2.4.20中的具體實現之前,我們首先說明CpuMemSets涉及的幾個基本概念,包括:
處理器:指承載任務排程的物理處理器,但是不包括DMA裝置、向量處理器等專用處理器;
記憶體塊:在SMP、UP系統中,所有的記憶體塊與所有處理器的距離相等,因此不存在差別;但是在NUMA系統中,可以按照與處理器的距離對記憶體塊劃分等價類。此外,CpuMemSets不考慮具有速度差異的特殊儲存器,如輸入輸出裝置快取、幀快取等。
任務:一個任務,在任一時刻,或者等待事件、資源,或者被中斷,或者在處理器上執行。
虛擬儲存區:核心為每個任務維護的多個虛擬地址區域,可為多個任務共享。位於虛擬儲存區內的頁,或者沒有分配,或者已分配但被換出,或者已分配且在記憶體中。可以指定允許分配給某個虛擬儲存區的記憶體塊以及分配順序。
CpuMemSets為Linux提供了將系統服務和應用繫結在指定的處理器上進行排程、在指定的結點上分配記憶體的機制。CpuMemSets在已有的Linux排程和記憶體分配程式碼基礎上增加了cpumemmap(cmm)和cpumemset(cms)兩層結構,底層的cpumemmap層提供一個簡單的對映對,實現系統的處理器號與應用的處理器號、系統的記憶體塊號與應用的記憶體塊號的對映。這種對映不一定是單射,一個系統號可以對應多個應用號。上層的cpumemset層負責說明允許把任務排程到哪些應用處理器號所對應的處理器上執行、可以從哪些應用記憶體塊號所對應的記憶體塊中為相應的核心或虛擬儲存區分配記憶體頁,也就是說,指定可供核心、任務、虛擬儲存區使用的資源集合。在這種兩層結構中,資源的系統號供核心執行排程和記憶體分配時使用;而資源的應用號供使用者程式指定本應用的資源集合時使用。系統號在啟動期間全系統範圍內有效,而應用號僅僅相對於共享同一個cmm的所有使用者程式有效。而且,由於負載平衡和熱插拔引發的資源物理編號的變化對應用號是不可見的。
Linux的程式排程和記憶體分配在保持現有程式碼正常運轉的基礎上,新增了對CpuMemSets的支援,使用"系統處理器號"和"系統記憶體塊號"以及其他資料結構如cpus_allowed和mems_allowed等實現資源的分割槽。此外,CpuMemSets的API提供了對cpusets、dplace、runon、psets、MPI、OpenMP、nodesets的支援,並且提供/proc介面以顯示cmm和 cms的結構、設定以及與任務、虛擬儲存區、核心的連線關係、系統資源號和應用資源號等資訊。下面我們分別對cpumemmap和cpumemset、程式排程和記憶體分配、以及API這三個部分進行詳細分析:
3.1 cmm&cms
3.1.1 資料結構
cpumemmap和cpumemset的資料結構如下所示,具體定義在include/linux/cpumemset.h中。Cpumemmap中的scpus和smems域分別指向一組系統處理器號和一組系統記憶體塊號,實現應用的資源號(陣列下標)與系統的資源號(陣列元素值)的對映。Cpumemset中的acpus域指向一組應用處理器號,而amems域指向一組cms_memory_list_t型別的記憶體塊列表。每個記憶體塊列表描述了一組應用記憶體塊號(mems)以及享有該列表的一組應用處理器號(cpus)。記憶體塊分配策略由cpumemset中的policy域決定,預設使用本地優先方式。Cpumemset透過cmm域與相應的cpumemmap建立關聯。兩個資料結構中的counter域的作用將在後文介紹。
【include/linux/cpumemset.h】
84 typedef struct cpumemmap {
85 int nr_cpus; /* number of cpus in map */
86 int nr_mems; /* number of mems in map */
87 cms_scpu_t *cpus; /* array maps application to system cpu num */
88 cms_smem_t *mems; /* array maps application to system mem num */
89 atomic_t counter; /* reference counter */
90 } cpumemmap_t;
92 typedef struct cpumemset {
93 cms_setpol_t policy; /* CMS_* policy flag :Memory allocation policy */
94 int nr_cpus; /* Number of cpus in this CpuMemSet */
95 int nr_mems; /* Number of Memory Lists in this CpuMemSet */
96 cms_acpu_t *cpus; /* The 'nr_cpus' app cpu nums in this set */
97 cms_memory_list_t *mems; /* Array 'nr_mems' Memory Lists */
98 unsigned long mems_allowed; /* memory_allowed vector used by vmas */
99 cpumemmap_t *cmm; /* associated cpumemmap */
100 atomic_t counter; /* reference counter */
101 } cpumemset_t;
76 typedef struct cms_memory_list {
77 int nr_cpus; /* Number of cpu's sharing this memory list */
78 int nr_mems; /* Number of memory nodes in this list */
79 cms_acpu_t *cpus; /* Array of cpu's sharing this memory list */
80 cms_amem_t *mems; /* Array of 'nr_mems' memory nodes */
81 } cms_memory_list_t;
以一個包含四個節點的NUMA系統為例簡單說明上述資料結構的使用。假設每個節點由四個節點和一個記憶體塊組成,16個處理器的系統號依次是:c0(0)、c1(1)、… …、c15(15),4個記憶體塊的系統號依次是mb0(0)、mb1(1)、mb2(2)、mb3(3)。構造一個只包含第2和第3節點的處理器和記憶體塊cpumemmap,並且把某個應用繫結在奇數號的處理器上,同時以本地優先的方式分配記憶體塊。資料結構如圖1所示:
圖1 CpuMemSets的資料結構使用示例
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
3.1.2 基本操作
多個任務可以同時對指定的cms&cmm執行訪問、掛接、釋放或替換的操作。這些操作都是透過下列例程執行:
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
控制程式碼結構定義如下:
【include/linux/cpumemset.h】
417 typedef struct cmshandle {
418 cpumemset_t *set;
419 cpumemmap_t *map;
420 int error;
421 } cmshandle_t;
上述例程透過鎖機制以及對cmm&cms中的引用計數counter的管理,保證了對CpuMemSets進行並行操作的完整性。每個cms中的counter記錄著:使用者的引用計數(指向這個cms的任務、虛擬儲存區、核心總數)和控制程式碼的引用計數(臨時指向這個cms的控制程式碼的總數)。每個cmm中的counter記錄著:cms的引用計數(指向這個cmm的cms總數)和控制程式碼的引用計數(臨時指向這個cmm的控制程式碼的總數)。
替換CpuMemSets的操作分為下列三個步驟:
1. 呼叫合適的cmsGetHandle*()安全的獲得cms和cmm的控制程式碼;
2. 構造新的cms和cmm;
3. 呼叫合適的cmsExchange*()安全的更換上述的新舊cms和cmm。
訪問任務或虛擬儲存區的cms和cmm的操作分為下列三個步驟:
1. 呼叫合適的cmsGetHandle*()安全的獲得cms和cmm的控制程式碼;
2. 呼叫cms和cmm,此期間無法保證原來的任務或虛擬儲存區仍然使用這些cms和cmm;
3. 呼叫cmsRelease()釋放控制程式碼。
掛接、放棄控制程式碼的操作步驟:
1. 呼叫合適的cmsGetHandle*()或cmsNewHandle*()安全的獲得cms和cmm的控制程式碼;
2. 呼叫合適的cmsAttachNew*()或cmsDiscard()。
3.1.3 基本設定
核心擁有自己的kernel_cms。核心在start_kernel()的開頭(build_all_ zonelists()之後,trap_init()之前)首先呼叫cms_cmm_static_init()為kernel_cms構造靜態的初始cmm&cms,其中只包含執行本核心的處理器和所在節點的記憶體塊,並且將kernel_cms -> mems_allowed賦值-1UL,允許核心在cpu_init()過程中使用所有記憶體塊。然後,核心在start_kernel()的結尾執行cms_cmm_init(),建立cmm和cms快取記憶體,為kernel_cms構造包含所有處理器和記憶體塊的cmm&cms,並傳遞給init_task。如果系統啟動時設定了cpumemset_minimal引數,則使用的是cms_cmm_static_init()構造的最小集。
每個程式都擁有兩個cms:current_cms影響自己的處理器分配和虛擬儲存區建立, child_cms繼承給由自己fork的子程式。每個新建程式的current_cms和child_cms都繼承自父程式的child_cms。
【include/linux/sched.h】
296 struct task_struct {
:
325 cpumask_t cpus_allowed;
:
429 cpumemset_t * current_cms;
430 cpumemset_t * child_cms;
431 /* stash mems_allowed of most recent vma to page fault here */
432 unsigned long mems_allowed;
:
462 };
【kernel/fork.c】
620 int do_fork(unsigned long clone_flags, unsigned long stack_start,
621 struct pt_regs *regs, unsigned long stack_size)
622 {
:
751 SET_CHILD_CMS(p, current);
:
871 }
【include/linux/cpumemset.h】
173 /*
174 * Set child's current, child and alloc cpumemset
175 * from parent's child_cms (used in fork).
176 */
177
178 #define SET_CHILD_CMS(child, parent) do { 179 cpumemset_t *cms = (parent)->child_cms; 180 CMS_TRACE_HOOK("fork task", cms); 181 atomic_inc(&cms->counter); 182 (child)->current_cms = cms; 183 update_cpus_allowed(child); 184 atomic_inc(&cms->counter); 185 (child)->child_cms = cms; 186 } while (0)
每個虛擬儲存區都擁有各自的vm_mems_allowed位向量。新建虛擬儲存區的vm_mems_ allowed透過cms_current_mems_allowed()繼承自建立者任務current_cms的mems_allowed。對於被掛接的已存在的虛擬儲存區,如mmap的記憶體物件和共享記憶體區,則繼承自掛接程式current_cms的mems_allowed。cms的mems_allowed位向量由mems_allowed_build(cms)根據cms中的所有記憶體塊列表來構造。
【include/linux/mm.h】
45 struct vm_area_struct {
:
77 unsigned long vm_mems_allowed; /* cpumemset managed memory placement */
:
81 };
【kernel/cpumemset.c】
1538 static unsigned long
1539 mems_allowed_build(cpumemset_t *cms)
1540 {
1541 int i;
1542 unsigned long mems_allowed = 0;
1543
1544 for (i = 0; i < cms->nr_mems; i++)
1545 mems_allowed |= mems_allowed_value(cms, cms->mems + i);
1546 return mems_allowed;
1547 }
3.2 程式排程及記憶體分配
核心呼叫update_cpus_allowed(struct task_struct *p)根據任務的current_cms的處理器列表更改它的cpus_allowed位向量,從而影響該任務的處理器排程。
【kernel/cpumemset.c】
585 void
586 update_cpus_allowed(struct task_struct *p)
587 {
588 #ifdef CONFIG_SMP
589 int i;
590 cpumemset_t *cms = p->current_cms;
591 cpumask_t cpus_allowed = CPU_MASK_NONE;
592
593 for (i = 0; i < cms->nr_cpus; i++)
594 __set_bit(cms->cmm->cpus[cms->cpus], &cpus_allowed);
595 if (any_online_cpu((cpumask_t*)&cpu_online_map) < NR_CPUS) {
:
609 set_cpus_allowed(p, &cpus_allowed);
610 }
611 #endif
612 }
核心根據虛擬儲存區的vm_mems_allowed位向量為任務分配記憶體,如果是在中斷上下文中,虛擬儲存區的記憶體分配則依賴於kernel_cms的mems_allowed。宏CHECK_MEMS_ALLOWED(mems_allowed, zone)負責檢查zone所在的節點是否落在mems_allowed設定的記憶體塊集合內。
【mm/memory.c】
1383 int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
1384 unsigned long address, int write_access)
1385 {
:
1390 /*
1391 * We set the mems_allowed field of the current task as
1392 * the one pointed by the faulting vma. The current
1393 * process will then use the correct mems_allowed mask
1394 * if a new page has to be allocated.
1395 */
1396 if(!in_interrupt())
1397 current->mems_allowed = vma->vm_mems_allowed;
:
1417 }
【mm/page_alloc.c】
334 struct page * __alloc_pages(..)
:
343 if (in_interrupt())
344 mems_allowed = kernel_cms->mems_allowed;
345 else
346 mems_allowed = current->mems_allowed;
347 if (mems_allowed == 0) {
348 printk(KERN_DEBUG "workaround zero mems_allowed in alloc_pages ");
349 mems_allowed = -1UL;
350 }
:
if(!CHECK_MEMS_ALLOWED(mems_allowed, z))
continue;
:
450 }
【include/linux/cpumemset.h】
194 /* Used in __alloc_pages() to see if we can allocate from a node */
195 #define CHECK_MEMS_ALLOWED(mems_allowed, zone) 196 ((1UL << (zone)->zone_pgdat->node_id) & (mems_allowed))
為虛擬儲存區分配頁時,如果當前執行該任務的處理器包含在該虛擬儲存區的cms中,則從該處理器的記憶體塊列表中分配,否則從該虛擬儲存區的cms定義的CMS_DEFAULT_CPU的記憶體塊列表中分配。
3.3 API設計
CpuMemSets提供了一系列的核心級和應用級的程式設計介面,分別定義在核心的include/linux/cpumemset.h檔案和庫程式碼CpuMemSets/cpumemsets.h檔案中(如表2所示)。
javascript:if(this.width>screen.width-600)this.style.width=screen.width-600;">
透過呼叫使用者級介面對cmm&cms進行設定將導致核心的排程器和記憶體分配器所使用的系統位向量如cpus_allowed和mems_allowed的變化,從而使核心排程程式碼使用新的系統處理器號、記憶體分配程式碼從新記憶體塊中分配記憶體頁;但是,原先從舊記憶體塊中分配的記憶體頁將不會發生遷移,除非強制採用其他手段。具體來說,系統在cmsAttachNewTask()、cmsExchangeTask()和cmsExchangePid()過程中,執行update_cpus_allowed()根據任務的current_cms的當前處理器列表更改它的cpus_allowed位向量;在cms_set()過程中,執行mems_allowed_build()根據任務的current_cms的當前記憶體塊列表更改當前虛擬儲存區或任務、核心的mems_allowed位向量。
【kernel/cpumemset.c】
1661 static int
1662 cms_set(unsigned long *preq, char *rec, int size, target_option cmm_or_cms)
:
1713 if (choice == CMS_VMAREA) {
:
1772 vma->vm_mems_allowed = mems_allowed_build(newhan.set);
:
1792 } else {
:
1822 newhan.set->mems_allowed = mems_allowed_build(newhan.set);
:
1840 }
:
1842 }
在許可權保護方面,只有根權使用者才可以修改核心所使用的cms&cmm和任何任務的cms&cmm;而一般使用者只能修改自身的任務和虛擬儲存區所擁有的cms&cmm;具有相同uid的任務之間可以互相修改對方的cms&cmm。只有根權使用者才可以擴充套件自身的cmm,一般使用者只可以縮小自己的cmm。
【kernel/cpumemset.c】
409 cmshandle_t
410 cmsGetHandlePid(int choice, pid_t pid, int do_check_perm)
411 {
:
425 if (pid &&
426 do_check_perm &&
427 (current->euid ^ p->suid) &&
428 (current->euid ^ p->uid) &&
429 (current->uid ^ p->suid) &&
430 (current->uid ^ p->uid) &&
431 !capable(CAP_SYS_ADMIN))
432 han.error = -EPERM;
:
442 }
【kernel/cpumemset.c】
1478 /*
1479 * Unless you have CAP_SYS_ADMIN capability, you can only shrink cmm.
1480 */
1481
1482 static int
1483 cmm_restrict_checking(cpumemmap_t *oldmap, cpumemmap_t *newmap)
1484 {
1485 int i;
1486
1487 if (capable(CAP_SYS_ADMIN))
1488 return 0;
1489
1490 /* newmap must be a subset of oldmap */
1491 for (i = 0; i < newmap->nr_cpus; i++)
1492 if (!foundin (newmap->cpus, oldmap->cpus, oldmap->nr_cpus))
1493 return -EINVAL;
1494 for (i = 0; i < newmap->nr_mems; i++)
1495 if (!foundin (newmap->mems, oldmap->mems, oldmap->nr_mems))
1496 return -EINVAL;
1497 return 0;
1498 }
四、 使用舉例
示例1:顯示當前任務current_cms中的處理器
/*
* sample1 - display current cpumemset cpus
*
* Compile with:
* cc sample1.c -o sample1 -lcpumemsets
* Displays on stdout the number and a list of the cpus
* on which the current process is allowed to execute.
*/
#include "cpumemsets.h"
main()
{
int i;
cpumemset_t *pset;
pset = cmsQueryCMS(CMS_CURRENT, (pid_t)0, (void *)0);
if (pset == (cpumemset_t *)0) {
perror("cmsQueryCMS");
exit (1);
}
printf("Current CpuMemSet has %d cpu(s): ", pset->nr_cpus);
for (i = 0; i < pset->nr_cpus; i++)
printf("%s%d", (i > 0 ? ", " : ""), pset->cpus);
printf("0);
exit(0);
}
示例2:設定當前任務的子任務僅執行在0號處理器上,並啟動sh執行
/*
* sample2 - change child cpumemset cpus to just cpu 0
*
* Compile with:
* cc sample2.c -o sample2 -lcpumemsets
* Change the cpus which the child task is allowed
* execute on to just cpu 0. Start a subshell,
* instead of just exiting, so that the user has
* the opportunity to verify that the change occurred.
*/
#include
main()
{
int i;
cpumemset_t *pset;
pset = cmsQueryCMS(CMS_CHILD, (pid_t)0, (void *)0);
if (pset == (cpumemset_t *)0) {
perror("cmsQueryCMS");
exit (1);
}
pset->nr_cpus = 1;
free(pset->cpus);
pset->cpus = (cms_acpu_t *) malloc(sizeof(cms_acpu_t));
pset->cpus[0] = (cms_acpu_t)0;
if (cmsSetCMS(
CMS_CHILD, (pid_t)0, (void *)0, (size_t)0, pset) < 0) {
perror ("cmsSetCMS");
exit(1);
}
cmsFreeCMS (pset);
printf ("Invoking subshell running on cpu 0.0);
execl("/bin/sh", "sh", 0);
exit (2);
}
先執行示例2,再執行示例1:
# PS1='Sub> ' ./sample2
Invoking subshell running on cpu 0.
Sub> ./sample1
Current CpuMemSet has 1 cpu(s):
0
Sub> exit
五、 小結
CpuMemSets透過在已有的Linux排程和記憶體分配程式碼基礎上增加cpumemmap(cmm)和cpumemset(cms)兩層結構,為Linux提供了將系統服務和應用繫結在指定的處理器上進行排程、在指定的結點上分配記憶體的機制。從資料結構和控制機制上看,目前的實現比較簡單實用,但是仍然有進一步最佳化的空間。但是,CpuMemSets僅僅是提供了一套挖掘本地訪存優勢、最佳化Linux系統的NUMA效能的機制,如何基於這種支援手段制定合適的NUMA系統最佳化策略需要進行更多更深入的研究。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-952573/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- CpuMemSets在Linux作業系統中的實現(轉)Linux作業系統
- Linux在實際中的應用Linux
- 【轉】Linux下history命令配置及高階應用Linux
- LINUX動態連結庫高階應用(轉)Linux
- 超實用的 Linux 高階命令!Linux
- linux中gcc的應用(轉)LinuxGC
- Linux在企業中的應用尚不成熟 (轉)Linux
- Flutter進階:在應用中實現 Hero(飛行) 動畫Flutter動畫
- 在應用程式中實現RAS撥號 (轉)
- 在LINUX中實現流量控制器(轉)Linux
- Linux在中小型企業中的全面應用方案(轉)Linux
- python中list列表的高階應用 高階函式Python函式
- 在PB應用中實現聲音與動畫 (轉)動畫
- 無線技術在Linux作業系統中的應用(轉)Linux作業系統
- Linux系統在儲存技術中的幾項應用(轉)Linux
- Linux的非同步IO(AIO)在Oracle中應用Linux非同步AIOracle
- Linux高階命令Linux
- Linux 系統中隨機數在 KVM 中的應用Linux隨機
- javascript的高階應用JavaScript
- NetMeeting高階應用 (轉)
- Linux環境下的高階列印系統(轉)Linux
- 在Linux系統下FTP的配置與應用(轉)LinuxFTP
- css高階應用三種方法實現多行省略CSS
- 在Linux中,如何實現負載均衡?Linux負載
- React 中的高階元件及其應用場景React元件
- 遠端除錯在Linux車機中的應用除錯Linux
- 小乾貨~ NFS在Linux系統中的應用NFSLinux
- 在Linux中,如何進行容器技術的應用?Linux
- Nmap在實戰中的高階用法(詳解)
- dbms_job包的應用:在Oracle中實現定時操作(轉)Oracle
- AWK高階應用
- Redis 高階應用Redis
- LINUX find的高階查詢Linux
- 高逼格又實用的Linux命令:持續更新中Linux
- 在Linux系統下實現Server Push(轉)LinuxServer
- 在Linux系統下實現ServerPush(轉)LinuxServer
- Linux作業系統的高階電源管理(轉)Linux作業系統
- 關於在linux下磁碟定額的實現(轉)Linux