1. 詳解內(nèi)存映射系統(tǒng)調(diào)用 mmap
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset); //內(nèi)核文件:/arch/x86/kernel/sys_x86_64.c SYSCALL_DEFINE6(mmap,unsignedlong,addr,unsignedlong,len, unsignedlong,prot,unsignedlong,flags, unsignedlong,fd,unsignedlong,off)
mmap 內(nèi)存映射里所謂的內(nèi)存其實(shí)指的是虛擬內(nèi)存,在調(diào)用 mmap 進(jìn)行匿名映射的時(shí)候(比如進(jìn)行堆內(nèi)存的分配),是將進(jìn)程虛擬內(nèi)存空間中的某一段虛擬內(nèi)存區(qū)域與物理內(nèi)存中的匿名內(nèi)存頁(yè)進(jìn)行映射,當(dāng)調(diào)用 mmap 進(jìn)行文件映射的時(shí)候,是將進(jìn)程虛擬內(nèi)存空間中的某一段虛擬內(nèi)存區(qū)域與磁盤中某個(gè)文件中的某段區(qū)域進(jìn)行映射。
而用于內(nèi)存映射所消耗的這些虛擬內(nèi)存位于進(jìn)程虛擬內(nèi)存空間的哪里呢 ?
筆者在之前的文章《一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理》 中曾為大家詳細(xì)介紹過(guò)進(jìn)程虛擬內(nèi)存空間的布局,在進(jìn)程虛擬內(nèi)存空間的布局中,有一段叫做文件映射與匿名映射區(qū)的虛擬內(nèi)存區(qū)域,當(dāng)我們?cè)谟脩魬B(tài)應(yīng)用程序中調(diào)用 mmap 進(jìn)行內(nèi)存映射的時(shí)候,所需要的虛擬內(nèi)存就是在這個(gè)區(qū)域中劃分出來(lái)的。
在文件映射與匿名映射這段虛擬內(nèi)存區(qū)域中,包含了一段一段的虛擬映射區(qū),每當(dāng)我們調(diào)用一次 mmap 進(jìn)行內(nèi)存映射的時(shí)候,內(nèi)核都會(huì)在文件映射與匿名映射區(qū)中劃分出一段虛擬映射區(qū)出來(lái),這段虛擬映射區(qū)就是我們申請(qǐng)到的虛擬內(nèi)存。
那么我們申請(qǐng)的這塊虛擬內(nèi)存到底有多大呢 ?這就用到了 mmap 系統(tǒng)調(diào)用的前兩個(gè)參數(shù):
addr : 表示我們要映射的這段虛擬內(nèi)存區(qū)域在進(jìn)程虛擬內(nèi)存空間中的起始地址(虛擬內(nèi)存地址),但是這個(gè)參數(shù)只是給內(nèi)核的一個(gè)暗示,內(nèi)核并非一定得從我們指定的 addr 虛擬內(nèi)存地址上劃分虛擬內(nèi)存區(qū)域,內(nèi)核只不過(guò)在劃分虛擬內(nèi)存區(qū)域的時(shí)候會(huì)優(yōu)先考慮我們指定的 addr,如果這個(gè)虛擬地址已經(jīng)被使用或者是一個(gè)無(wú)效的地址,那么內(nèi)核則會(huì)自動(dòng)選取一個(gè)合適的地址來(lái)劃分虛擬內(nèi)存區(qū)域。我們一般會(huì)將 addr 設(shè)置為 NULL,意思就是完全交由內(nèi)核來(lái)幫我們決定虛擬映射區(qū)的起始地址。
length :從進(jìn)程虛擬內(nèi)存空間中的什么位置開(kāi)始劃分虛擬內(nèi)存區(qū)域的問(wèn)題解決了,那么我們要申請(qǐng)的這段虛擬內(nèi)存有多大呢 ? 這個(gè)就是 length 參數(shù)的作用了,如果是匿名映射,length 參數(shù)決定了我們要映射的匿名物理內(nèi)存有多大,如果是文件映射,length 參數(shù)決定了我們要映射的文件區(qū)域有多大。
addr,length 必須要按照 PAGE_SIZE(4K) 對(duì)齊。
如果我們通過(guò) mmap 映射的是磁盤上的一個(gè)文件,那么就需要通過(guò)參數(shù) fd 來(lái)指定要映射文件的描述符(file descriptor),通過(guò)參數(shù) offset 來(lái)指定文件映射區(qū)域在文件中偏移。
在內(nèi)存管理系統(tǒng)中,物理內(nèi)存是按照內(nèi)存頁(yè)為單位組織的,在文件系統(tǒng)中,磁盤中的文件是按照磁盤塊為單位組織的,內(nèi)存頁(yè)和磁盤塊大小一般情況下都是 4K 大小,所以這里的 offset 也必須是按照 4K 對(duì)齊的。
而在文件映射與匿名映射區(qū)中的這一段一段的虛擬映射區(qū),其實(shí)本質(zhì)上也是虛擬內(nèi)存區(qū)域,它們和進(jìn)程虛擬內(nèi)存空間中的代碼段,數(shù)據(jù)段,BSS 段,堆,棧沒(méi)有任何區(qū)別,在內(nèi)核中都是 struct vm_area_struct 結(jié)構(gòu)來(lái)表示的,下面我們把進(jìn)程空間中的這些虛擬內(nèi)存區(qū)域統(tǒng)稱為 VMA。
進(jìn)程虛擬內(nèi)存空間中的所有 VMA 在內(nèi)核中有兩種組織形式:一種是雙向鏈表,用于高效的遍歷進(jìn)程 VMA,這個(gè) VMA 雙向鏈表是有順序的,所有 VMA 節(jié)點(diǎn)在雙向鏈表中的排列順序是按照虛擬內(nèi)存低地址到高地址進(jìn)行的。
另一種則是用紅黑樹(shù)進(jìn)行組織,用于在進(jìn)程空間中高效的查找 VMA,因?yàn)樵谶M(jìn)程虛擬內(nèi)存空間中不僅僅是只有代碼段,數(shù)據(jù)段,BSS 段,堆,棧這些虛擬內(nèi)存區(qū)域 VMA,尤其是在數(shù)據(jù)密集型應(yīng)用進(jìn)程中,文件映射與匿名映射區(qū)里也會(huì)包含有大量的 VMA,進(jìn)程的各種動(dòng)態(tài)鏈接庫(kù)所映射的虛擬內(nèi)存在這里,進(jìn)程運(yùn)行過(guò)程中進(jìn)行的匿名映射,文件映射所需要的虛擬內(nèi)存也在這里。而內(nèi)核需要頻繁地對(duì)進(jìn)程虛擬內(nèi)存空間中的這些眾多 VMA 進(jìn)行增,刪,改,查。所以需要這么一個(gè)紅黑樹(shù)結(jié)構(gòu),方便內(nèi)核進(jìn)行高效的查找。
//進(jìn)程虛擬內(nèi)存空間描述符 structmm_struct{ //串聯(lián)組織進(jìn)程空間中所有的VMA的雙向鏈表 structvm_area_struct*mmap;/*listofVMAs*/ //管理進(jìn)程空間中所有VMA的紅黑樹(shù) structrb_rootmm_rb; } //虛擬內(nèi)存區(qū)域描述符 structvm_area_struct{ //vma在mm_struct->mmap雙向鏈表中的前驅(qū)節(jié)點(diǎn)和后繼節(jié)點(diǎn) structvm_area_struct*vm_next,*vm_prev; //vma在mm_struct->mm_rb紅黑樹(shù)中的節(jié)點(diǎn) structrb_nodevm_rb; }

上圖中的文件映射與匿名映射區(qū)里邊其實(shí)包含了大量的 VMA,這里只是為了清晰的給大家展示虛擬內(nèi)存在內(nèi)核中的組織結(jié)構(gòu),所以只畫(huà)了一個(gè)大的 VMA 來(lái)表示文件映射與匿名映射區(qū),這一點(diǎn)大家需要知道。
mmap 系統(tǒng)調(diào)用的本質(zhì)是首先要在進(jìn)程虛擬內(nèi)存空間里的文件映射與匿名映射區(qū)中劃分出一段虛擬內(nèi)存區(qū)域 VMA 出來(lái) ,這段 VMA 區(qū)域的大小用 vm_start,vm_end 來(lái)表示,它們由 mmap 系統(tǒng)調(diào)用參數(shù) addr,length 決定。
structvm_area_struct{ unsignedlongvm_start;/*Ourstartaddresswithinvm_mm.*/ unsignedlongvm_end;/*Thefirstbyteafterourendaddress*/ }
隨后內(nèi)核會(huì)對(duì)這段 VMA 進(jìn)行相關(guān)的映射,如果是文件映射的話,內(nèi)核會(huì)將我們要映射的文件,以及要映射的文件區(qū)域在文件中的 offset,與 VMA 結(jié)構(gòu)中的 vm_file,vm_pgoff 關(guān)聯(lián)映射起來(lái),它們由 mmap 系統(tǒng)調(diào)用參數(shù) fd,offset 決定。
structvm_area_struct{ structfile*vm_file;/*Filewemapto(canbeNULL).*/ unsignedlongvm_pgoff;/*Offset(withinvm_file)inPAGE_SIZE*/ }
另外由 mmap 在文件映射與匿名映射區(qū)中映射出來(lái)的這一段虛擬內(nèi)存區(qū)域同進(jìn)程虛擬內(nèi)存空間中的其他虛擬內(nèi)存區(qū)域一樣,也都是有權(quán)限控制的。
比如上圖進(jìn)程虛擬內(nèi)存空間中的代碼段,它是與磁盤上 ELF 格式可執(zhí)行文件中的 .text section(磁盤文件中各個(gè)區(qū)域的單元組織結(jié)構(gòu))進(jìn)行映射的,存放的是程序執(zhí)行的機(jī)器碼,所以在可執(zhí)行文件與進(jìn)程虛擬內(nèi)存空間進(jìn)行文件映射的時(shí)候,需要指定代碼段這個(gè)虛擬內(nèi)存區(qū)域的權(quán)限為可讀(VM_READ),可執(zhí)行的(VM_EXEC)。
數(shù)據(jù)段也是通過(guò)文件映射進(jìn)來(lái)的,內(nèi)核會(huì)將磁盤上 ELF 格式可執(zhí)行文件中的 .data section 與數(shù)據(jù)段映射起來(lái),在映射的時(shí)候需要指定數(shù)據(jù)段這個(gè)虛擬內(nèi)存區(qū)域的權(quán)限為可讀(VM_READ),可寫(xiě)(VM_WRITE)。
與代碼段和數(shù)據(jù)段不同的是,BSS段,堆,棧這些虛擬內(nèi)存區(qū)域并不是從磁盤二進(jìn)制可執(zhí)行文件中加載的,它們是通過(guò)匿名映射的方式映射到進(jìn)程虛擬內(nèi)存空間的。
BSS 段中存放的是程序未初始化的全局變量,這段虛擬內(nèi)存區(qū)域的權(quán)限是可讀(VM_READ),可寫(xiě)(VM_WRITE)。
堆是用來(lái)描述進(jìn)程在運(yùn)行期間動(dòng)態(tài)申請(qǐng)的虛擬內(nèi)存區(qū)域的,所以堆也會(huì)具有可讀(VM_READ),可寫(xiě)(VM_WRITE)權(quán)限,在有些情況下,堆也具有可執(zhí)行(VM_EXEC)的權(quán)限,比如 Java 中的字節(jié)碼存儲(chǔ)在堆中,所以需要可執(zhí)行權(quán)限。
棧是用來(lái)保存進(jìn)程運(yùn)行時(shí)的命令行參,環(huán)境變量,以及函數(shù)調(diào)用過(guò)程中產(chǎn)生的棧幀的,棧一般擁有可讀(VM_READ),可寫(xiě)(VM_WRITE)的權(quán)限,但是也可以設(shè)置可執(zhí)行(VM_EXEC)權(quán)限,不過(guò)出于安全的考慮,很少這么設(shè)置。
而在文件映射與匿名映射區(qū)中的情況就變得更加復(fù)雜了,因?yàn)槲募成渑c匿名映射區(qū)里包含了數(shù)量眾多的 VMA,尤其是在數(shù)據(jù)密集型應(yīng)用進(jìn)程里更是如此,我們每調(diào)用一次 mmap ,無(wú)論是匿名映射也好還是文件映射也好,都會(huì)在文件映射與匿名映射區(qū)里產(chǎn)生一個(gè) VMA,而通過(guò) mmap 映射出的這段 VMA 中的相關(guān)權(quán)限和標(biāo)志位,是由 mmap 系統(tǒng)調(diào)用參數(shù)里的 prot,flags 決定的,最終會(huì)映射到虛擬內(nèi)存區(qū)域 VMA 結(jié)構(gòu)中的 vm_page_prot,vm_flags 屬性中,指定進(jìn)程對(duì)這塊虛擬內(nèi)存區(qū)域的訪問(wèn)權(quán)限和相關(guān)標(biāo)志位。
除此之外,進(jìn)程運(yùn)行過(guò)程中所依賴的動(dòng)態(tài)鏈接庫(kù) .so 文件,也是通過(guò)文件映射的方式將動(dòng)態(tài)鏈接庫(kù)中的代碼段,數(shù)據(jù)段映射進(jìn)文件映射與匿名映射區(qū)中。
structvm_area_struct{ /* *AccesspermissionsofthisVMA. */ pgprot_tvm_page_prot; unsignedlongvm_flags; }
我們可以通過(guò) mmap 系統(tǒng)調(diào)用中的參數(shù) prot 來(lái)指定其在進(jìn)程虛擬內(nèi)存空間中映射出的這段虛擬內(nèi)存區(qū)域 VMA 的訪問(wèn)權(quán)限,它的取值有如下四種:
#definePROT_READ0x1/*pagecanberead*/ #definePROT_WRITE0x2/*pagecanbewritten*/ #definePROT_EXEC0x4/*pagecanbeexecuted*/ #definePROT_NONE0x0/*pagecannotbeaccessed*/
PROT_READ 表示該虛擬內(nèi)存區(qū)域背后映射的物理內(nèi)存是可讀的。
PROT_WRITE 表示該虛擬內(nèi)存區(qū)域背后映射的物理內(nèi)存是可寫(xiě)的。
PROT_EXEC 表示該虛擬內(nèi)存區(qū)域背后映射的物理內(nèi)存所存儲(chǔ)的內(nèi)容是可以被執(zhí)行的,該內(nèi)存區(qū)域內(nèi)往往存儲(chǔ)的是執(zhí)行程序的機(jī)器碼,比如進(jìn)程虛擬內(nèi)存空間中的代碼段,以及動(dòng)態(tài)鏈接庫(kù)通過(guò)文件映射的方式加載進(jìn)文件映射與匿名映射區(qū)里的代碼段,這些 VMA 的權(quán)限就是 PROT_EXEC 。
PROT_NONE 表示這段虛擬內(nèi)存區(qū)域是不能被訪問(wèn)的,既不可讀寫(xiě),也不可執(zhí)行。用于實(shí)現(xiàn)防范攻擊的 guard page。如果攻擊者訪問(wèn)了某個(gè) guard page,就會(huì)觸發(fā) SIGSEV 段錯(cuò)誤。除此之外,指定 PROT_NONE 還可以為進(jìn)程預(yù)先保留這部分虛擬內(nèi)存區(qū)域,雖然不能被訪問(wèn),但是當(dāng)后面進(jìn)程需要的時(shí)候,可以通過(guò) mprotect 系統(tǒng)調(diào)用修改這部分虛擬內(nèi)存區(qū)域的權(quán)限。
mprotect 系統(tǒng)調(diào)用可以動(dòng)態(tài)修改進(jìn)程虛擬內(nèi)存空間中任意一段虛擬內(nèi)存區(qū)域的權(quán)限。
我們除了要為 mmap 映射出的這段虛擬內(nèi)存區(qū)域 VMA 指定訪問(wèn)權(quán)限之外,還需要為這段映射區(qū)域 VMA 指定映射方式,VMA 的映射方式由 mmap 系統(tǒng)調(diào)用參數(shù) flags 決定。內(nèi)核為 flags 定義了數(shù)量眾多的枚舉值,下面筆者將一些非常重要且核心的枚舉值為大家挑選出來(lái)并解釋下它們的含義:
#defineMAP_FIXED0x10/*Interpretaddrexactly*/ #defineMAP_ANONYMOUS0x20/*don'tuseafile*/ #defineMAP_SHARED0x01/*Sharechanges*/ #defineMAP_PRIVATE0x02/*Changesareprivate*/
前邊我們介紹了 mmap 系統(tǒng)調(diào)用的 addr 參數(shù),這個(gè)參數(shù)只是我們給內(nèi)核的一個(gè)暗示并非是強(qiáng)制性的,表示我們希望內(nèi)核可以根據(jù)我們指定的虛擬內(nèi)存地址 addr 處開(kāi)始創(chuàng)建虛擬內(nèi)存映射區(qū)域 VMA。
但如果我們指定的 addr 是一個(gè)非法地址,比如 [addr , addr + length] 這段虛擬內(nèi)存地址已經(jīng)存在映射關(guān)系了,那么內(nèi)核就會(huì)自動(dòng)幫我們選取一個(gè)合適的虛擬內(nèi)存地址開(kāi)始映射,但是當(dāng)我們?cè)?mmap 系統(tǒng)調(diào)用的參數(shù) flags 中指定了 MAP_FIXED, 這時(shí)參數(shù) addr 就變成強(qiáng)制要求了,如果 [addr , addr + length] 這段虛擬內(nèi)存地址已經(jīng)存在映射關(guān)系了,那么內(nèi)核就會(huì)將這段映射關(guān)系 unmmap 解除掉映射,然后重新根據(jù)我們的要求進(jìn)行映射,如果 addr 是一個(gè)非法地址,內(nèi)核就會(huì)報(bào)錯(cuò)停止映射。
操作系統(tǒng)對(duì)于物理內(nèi)存的管理是按照內(nèi)存頁(yè)為單位進(jìn)行的,而內(nèi)存頁(yè)的類型有兩種:一種是匿名頁(yè),另一種是文件頁(yè)。根據(jù)內(nèi)存頁(yè)類型的不同,內(nèi)存映射也自然分為兩種:一種是虛擬內(nèi)存對(duì)匿名物理內(nèi)存頁(yè)的映射,另一種是虛擬內(nèi)存對(duì)文件頁(yè)的也映射,也就是我們常提到的匿名映射和文件映射。
當(dāng)我們將 mmap 系統(tǒng)調(diào)用參數(shù) flags 指定為 MAP_ANONYMOUS 時(shí),表示我們需要進(jìn)行匿名映射,既然是匿名映射,fd 和 offset 這兩個(gè)參數(shù)也就沒(méi)有了意義,fd 參數(shù)需要被設(shè)置為 -1 。當(dāng)我們進(jìn)行文件映射的時(shí)候,只需要指定 fd 和 offset 參數(shù)就可以了。
而根據(jù) mmap 創(chuàng)建出的這片虛擬內(nèi)存區(qū)域背后所映射的物理內(nèi)存能否在多進(jìn)程之間共享,又分為了兩種內(nèi)存映射方式:
MAP_SHARED 表示共享映射,通過(guò) mmap 映射出的這片內(nèi)存區(qū)域在多進(jìn)程之間是共享的,一個(gè)進(jìn)程修改了共享映射的內(nèi)存區(qū)域,其他進(jìn)程是可以看到的,用于多進(jìn)程之間的通信。
MAP_PRIVATE 表示私有映射,通過(guò) mmap 映射出的這片內(nèi)存區(qū)域是進(jìn)程私有的,其他進(jìn)程是看不到的。如果是私有文件映射,那么多進(jìn)程針對(duì)同一映射文件的修改將不會(huì)回寫(xiě)到磁盤文件上
這里介紹的這些 flags 參數(shù)枚舉值是可以相互組合的,我們可以通過(guò)這些枚舉值組合出如下幾種內(nèi)存映射方式。
2. 私有匿名映射
MAP_PRIVATE | MAP_ANONYMOUS 表示私有匿名映射,我們常常利用這種映射方式來(lái)申請(qǐng)?zhí)摂M內(nèi)存,比如,我們使用 glibc 庫(kù)里封裝的 malloc 函數(shù)進(jìn)行虛擬內(nèi)存申請(qǐng)時(shí),當(dāng)申請(qǐng)的內(nèi)存大于 128K 的時(shí)候,malloc 就會(huì)調(diào)用 mmap 采用私有匿名映射的方式來(lái)申請(qǐng)堆內(nèi)存。因?yàn)樗撬接械?,所以申?qǐng)到的內(nèi)存是進(jìn)程獨(dú)占的,多進(jìn)程之間不能共享。
這里需要特別強(qiáng)調(diào)一下 mmap 私有匿名映射申請(qǐng)到的只是虛擬內(nèi)存,內(nèi)核只是在進(jìn)程虛擬內(nèi)存空間中劃分一段虛擬內(nèi)存區(qū)域 VMA 出來(lái),并將 VMA 該初始化的屬性初始化好,mmap 系統(tǒng)調(diào)用就結(jié)束了。這里和物理內(nèi)存還沒(méi)有發(fā)生任何關(guān)系。在后面的章節(jié)中大家將會(huì)看到這個(gè)過(guò)程。
當(dāng)進(jìn)程開(kāi)始訪問(wèn)這段虛擬內(nèi)存區(qū)域時(shí),發(fā)現(xiàn)這段虛擬內(nèi)存區(qū)域背后沒(méi)有任何物理內(nèi)存與其關(guān)聯(lián),體現(xiàn)在內(nèi)核中就是這段虛擬內(nèi)存地址在頁(yè)表中的 PTE 項(xiàng)是空的。
或者 PTE 中的 P 位為 0 ,這些都是表示虛擬內(nèi)存還未與物理內(nèi)存進(jìn)行映射。
關(guān)于頁(yè)表相關(guān)的知識(shí),不熟悉的讀者可以回顧下筆者之前的文章 《一步一圖帶你構(gòu)建 Linux 頁(yè)表體系》
這時(shí) MMU 就會(huì)觸發(fā)缺頁(yè)異常(page fault),這里的缺頁(yè)指的就是缺少物理內(nèi)存頁(yè),隨后進(jìn)程就會(huì)切換到內(nèi)核態(tài),在內(nèi)核缺頁(yè)中斷處理程序中,為這段虛擬內(nèi)存區(qū)域分配對(duì)應(yīng)大小的物理內(nèi)存頁(yè),隨后將物理內(nèi)存頁(yè)中的內(nèi)容全部初始化為 0 ,最后在頁(yè)表中建立虛擬內(nèi)存與物理內(nèi)存的映射關(guān)系,缺頁(yè)異常處理結(jié)束。
當(dāng)缺頁(yè)處理程序返回時(shí),CPU 會(huì)重新啟動(dòng)引起本次缺頁(yè)異常的訪存指令,這時(shí) MMU 就可以正常翻譯出物理內(nèi)存地址了。
mmap 的私有匿名映射除了用于為進(jìn)程申請(qǐng)?zhí)摂M內(nèi)存之外,還會(huì)應(yīng)用在 execve 系統(tǒng)調(diào)用中,execve 用于在當(dāng)前進(jìn)程中加載并執(zhí)行一個(gè)新的二進(jìn)制執(zhí)行文件:
#includeintexecve(constchar*filename,constchar*argv[],constchar*envp[])
參數(shù) filename 指定新的可執(zhí)行文件的文件名,argv 用于傳遞新程序的命令行參數(shù),envp 用來(lái)傳遞環(huán)境變量。
既然是在當(dāng)前進(jìn)程中重新執(zhí)行一個(gè)程序,那么當(dāng)前進(jìn)程的用戶態(tài)虛擬內(nèi)存空間就沒(méi)有用了,內(nèi)核需要根據(jù)這個(gè)可執(zhí)行文件重新映射進(jìn)程的虛擬內(nèi)存空間。
既然現(xiàn)在要重新映射進(jìn)程虛擬內(nèi)存空間,內(nèi)核首先要做的就是刪除釋放舊的虛擬內(nèi)存空間,并清空進(jìn)程頁(yè)表。然后根據(jù) filename 打開(kāi)可執(zhí)行文件,并解析文件頭,判斷可執(zhí)行文件的格式,不同的文件格式需要不同的函數(shù)進(jìn)行加載。
linux 中支持多種可執(zhí)行文件格式,比如,elf 格式,a.out 格式。內(nèi)核中使用 struct linux_binfmt 結(jié)構(gòu)來(lái)描述可執(zhí)行文件,里邊定義了用于加載可執(zhí)行文件的函數(shù)指針 load_binary,加載動(dòng)態(tài)鏈接庫(kù)的函數(shù)指針 load_shlib,不同文件格式指向不同的加載函數(shù):
staticstructlinux_binfmtelf_format={ .module=THIS_MODULE, .load_binary=load_elf_binary, .load_shlib=load_elf_library, .core_dump=elf_core_dump, .min_coredump=ELF_EXEC_PAGESIZE, };
staticstructlinux_binfmtaout_format={ .module=THIS_MODULE, .load_binary=load_aout_binary, .load_shlib=load_aout_library, };
在 load_binary 中會(huì)解析對(duì)應(yīng)格式的可執(zhí)行文件,并根據(jù)文件內(nèi)容重新映射進(jìn)程的虛擬內(nèi)存空間。比如,虛擬內(nèi)存空間中的 BSS 段,堆,棧這些內(nèi)存區(qū)域中的內(nèi)容不依賴于可執(zhí)行文件,所以在 load_binary 中采用私有匿名映射的方式來(lái)創(chuàng)建新的虛擬內(nèi)存空間中的 BSS 段,堆,棧。
BSS 段雖然定義在可執(zhí)行二進(jìn)制文件中,不過(guò)只是在文件中記錄了 BSS 段的長(zhǎng)度,并沒(méi)有相關(guān)內(nèi)容關(guān)聯(lián),所以 BSS 段也會(huì)采用私有匿名映射的方式加載到進(jìn)程虛擬內(nèi)存空間中。
3. 私有文件映射
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);
我們?cè)谡{(diào)用 mmap 進(jìn)行內(nèi)存文件映射的時(shí)候可以通過(guò)指定參數(shù) flags 為 MAP_PRIVATE,然后將參數(shù) fd 指定為要映射文件的文件描述符(file descriptor)來(lái)實(shí)現(xiàn)對(duì)文件的私有映射。
假設(shè)現(xiàn)在磁盤上有一個(gè)名叫 file-read-write.txt 的磁盤文件,現(xiàn)在多個(gè)進(jìn)程采用私有文件映射的方式,從文件 offset 偏移處開(kāi)始,映射 length 長(zhǎng)度的文件內(nèi)容到各個(gè)進(jìn)程的虛擬內(nèi)存空間中,調(diào)用完 mmap 之后,相關(guān)內(nèi)存映射內(nèi)核數(shù)據(jù)結(jié)構(gòu)關(guān)系如下圖所示:
為了方便描述,我們指定映射長(zhǎng)度 length 為 4K 大小,因?yàn)槲募到y(tǒng)中的磁盤塊大小為 4K ,映射到內(nèi)存中的內(nèi)存頁(yè)剛好也是 4K 。
當(dāng)進(jìn)程打開(kāi)一個(gè)文件的時(shí)候,內(nèi)核會(huì)為其創(chuàng)建一個(gè) struct file 結(jié)構(gòu)來(lái)描述被打開(kāi)的文件,并在進(jìn)程文件描述符列表 fd_array 數(shù)組中找到一個(gè)空閑位置分配給它,數(shù)組中對(duì)應(yīng)的下標(biāo),就是我們?cè)谟脩艨臻g用到的文件描述符。
而 struct file 結(jié)構(gòu)是和進(jìn)程相關(guān)的( fd 的作用域也是和進(jìn)程相關(guān)的),即使多個(gè)進(jìn)程打開(kāi)同一個(gè)文件,那么內(nèi)核會(huì)為每一個(gè)進(jìn)程創(chuàng)建一個(gè) struct file 結(jié)構(gòu),如上圖中所示,進(jìn)程 1 和 進(jìn)程 2 都打開(kāi)了同一個(gè) file-read-write.txt 文件,那么內(nèi)核會(huì)為進(jìn)程 1 創(chuàng)建一個(gè) struct file 結(jié)構(gòu),也會(huì)為進(jìn)程 2 創(chuàng)建一個(gè) struct file 結(jié)構(gòu)。
每一個(gè)磁盤上的文件在內(nèi)核中都會(huì)有一個(gè)唯一的 struct inode 結(jié)構(gòu),inode 結(jié)構(gòu)和進(jìn)程是沒(méi)有關(guān)系的,一個(gè)文件在內(nèi)核中只對(duì)應(yīng)一個(gè) inode,inode 結(jié)構(gòu)用于描述文件的元信息,比如,文件的權(quán)限,文件中包含多少個(gè)磁盤塊,每個(gè)磁盤塊位于磁盤中的什么位置等等。
//ext4文件系統(tǒng)中的inode結(jié)構(gòu) structext4_inode{ //文件權(quán)限 __le16i_mode;/*Filemode*/ //文件包含磁盤塊的個(gè)數(shù) __le32i_blocks_lo;/*Blockscount*/ //存放文件包含的磁盤塊 __le32i_block[EXT4_N_BLOCKS];/*Pointerstoblocks*/ };
那么什么是磁盤塊呢 ?我們可以類比內(nèi)存管理系統(tǒng),Linux 是按照內(nèi)存頁(yè)為單位來(lái)對(duì)物理內(nèi)存進(jìn)行管理和調(diào)度的,在文件系統(tǒng)中,Linux 是按照磁盤塊為單位對(duì)磁盤中的數(shù)據(jù)進(jìn)行管理的,它們的大小均是 4K 。
如下圖所示,磁盤盤面上一圈一圈的同心圓叫做磁道,磁盤上存儲(chǔ)的數(shù)據(jù)就是沿著磁道的軌跡存放著,隨著磁盤的旋轉(zhuǎn),磁頭在磁道上讀寫(xiě)硬盤中的數(shù)據(jù)。而在每個(gè)磁盤上,會(huì)進(jìn)一步被劃分成多個(gè)大小相等的圓弧,這個(gè)圓弧就叫做扇區(qū),磁盤會(huì)以扇區(qū)為單位進(jìn)行數(shù)據(jù)的讀寫(xiě)。每個(gè)扇區(qū)大小為 512 字節(jié)。
而在 Linux 的文件系統(tǒng)中是按照磁盤塊為單位對(duì)數(shù)據(jù)讀寫(xiě)的,因?yàn)槊總€(gè)扇區(qū)大小為 512 字節(jié),能夠存儲(chǔ)的數(shù)據(jù)比較小,而且扇區(qū)數(shù)量眾多,這樣在尋址的時(shí)候比較困難,Linux 文件系統(tǒng)將相鄰的扇區(qū)組合在一起,形成一個(gè)磁盤塊,后續(xù)針對(duì)磁盤塊整體進(jìn)行操作效率更高。
只要我們找到了文件中的磁盤塊,我們就可以尋址到文件在磁盤上的存儲(chǔ)內(nèi)容了,所以使用 mmap 進(jìn)行內(nèi)存文件映射的本質(zhì)就是建立起虛擬內(nèi)存區(qū)域 VMA 到文件磁盤塊之間的映射關(guān)系 。
調(diào)用 mmap 進(jìn)行內(nèi)存文件映射的時(shí)候,內(nèi)核首先會(huì)在進(jìn)程的虛擬內(nèi)存空間中創(chuàng)建一個(gè)新的虛擬內(nèi)存區(qū)域 VMA 用于映射文件,通過(guò) vm_area_struct->vm_file 將映射文件的 struct flle 結(jié)構(gòu)與虛擬內(nèi)存映射關(guān)聯(lián)起來(lái)。
structvm_area_struct{ structfile*vm_file;/*Filewemapto(canbeNULL).*/ unsignedlongvm_pgoff;/*Offset(withinvm_file)inPAGE_SIZE*/ }
根據(jù) vm_file->f_inode 我們可以關(guān)聯(lián)到映射文件的 struct inode,近而關(guān)聯(lián)到映射文件在磁盤中的磁盤塊 i_block,這個(gè)就是 mmap 內(nèi)存文件映射最本質(zhì)的東西。
站在文件系統(tǒng)的視角,映射文件中的數(shù)據(jù)是按照磁盤塊來(lái)存儲(chǔ)的,讀寫(xiě)文件數(shù)據(jù)也是按照磁盤塊為單位進(jìn)行的,磁盤塊大小為 4K,當(dāng)進(jìn)程讀取磁盤塊的內(nèi)容到內(nèi)存之后,站在內(nèi)存管理系統(tǒng)的視角,磁盤塊中的數(shù)據(jù)被 DMA 拷貝到了物理內(nèi)存頁(yè)中,這個(gè)物理內(nèi)存頁(yè)就是前面提到的文件頁(yè)。
根據(jù)程序的時(shí)間局部性原理我們知道,磁盤文件中的數(shù)據(jù)一旦被訪問(wèn),那么它很有可能在短期內(nèi)被再次訪問(wèn),所以為了加快進(jìn)程對(duì)文件數(shù)據(jù)的訪問(wèn),內(nèi)核會(huì)將已經(jīng)訪問(wèn)過(guò)的磁盤塊緩存在文件頁(yè)中。
一個(gè)文件包含多個(gè)磁盤塊,當(dāng)它們被讀取到內(nèi)存之后,一個(gè)文件也就對(duì)應(yīng)了多個(gè)文件頁(yè),這些文件頁(yè)在內(nèi)存中統(tǒng)一被一個(gè)叫做 page cache 的結(jié)構(gòu)所組織。
每一個(gè)文件在內(nèi)核中都會(huì)有一個(gè)唯一的 page cache 與之對(duì)應(yīng),用于緩存文件中的數(shù)據(jù),page cache 是和文件相關(guān)的,它和進(jìn)程是沒(méi)有關(guān)系的,多個(gè)進(jìn)程可以打開(kāi)同一個(gè)文件,每個(gè)進(jìn)程中都有有一個(gè) struct file 結(jié)構(gòu)來(lái)描述這個(gè)文件,但是一個(gè)文件在內(nèi)核中只會(huì)對(duì)應(yīng)一個(gè) page cache。
文件的 struct inode 結(jié)構(gòu)中除了有磁盤塊的信息之外,還有指向文件 page cache 的 i_mapping 指針。
structinode{ structaddress_space*i_mapping; }
page cache 在內(nèi)核中是使用 struct address_space 結(jié)構(gòu)來(lái)描述的:
structaddress_space{ //這里就是pagecache。里邊緩存了文件的所有緩存頁(yè)面 structradix_tree_rootpage_tree; }
關(guān)于 page cache 的詳細(xì)介紹,感興趣的讀者可以回看下 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》 一文中的 “5. 頁(yè)高速緩存 page cache” 小節(jié)。
當(dāng)我們理清了內(nèi)存系統(tǒng)和文件系統(tǒng)這些核心數(shù)據(jù)結(jié)構(gòu)之間的關(guān)聯(lián)關(guān)系之后,現(xiàn)在再來(lái)看,下面這幅 mmap 私有文件映射關(guān)系圖是不是清晰多了。
page cache 在內(nèi)核中是使用基樹(shù) radix_tree 結(jié)構(gòu)來(lái)表示的,這里我們只需要知道文件頁(yè)是掛在 radix_tree 的葉子結(jié)點(diǎn)上,radix_tree 中的 root 節(jié)點(diǎn)和 node 節(jié)點(diǎn)是文件頁(yè)(葉子節(jié)點(diǎn))的索引節(jié)點(diǎn)就可以了。
當(dāng)多個(gè)進(jìn)程調(diào)用 mmap 對(duì)磁盤上同一個(gè)文件進(jìn)行私有文件映射的時(shí)候,內(nèi)核只是在每個(gè)進(jìn)程的虛擬內(nèi)存空間中創(chuàng)建出一段虛擬內(nèi)存區(qū)域 VMA 出來(lái),注意,此時(shí)內(nèi)核只是為進(jìn)程申請(qǐng)了用于映射的虛擬內(nèi)存,并將虛擬內(nèi)存與文件映射起來(lái),mmap 系統(tǒng)調(diào)用就返回了,全程并沒(méi)有物理內(nèi)存的影子出現(xiàn)。文件的 page cache 也是空的,沒(méi)有包含任何的文件頁(yè)。
當(dāng)任意一個(gè)進(jìn)程,比如上圖中的進(jìn)程 1 開(kāi)始訪問(wèn)這段映射的虛擬內(nèi)存時(shí),CPU 會(huì)把虛擬內(nèi)存地址送到 MMU 中進(jìn)行地址翻譯,因?yàn)?mmap 只是為進(jìn)程分配了虛擬內(nèi)存,并沒(méi)有分配物理內(nèi)存,所以這段映射的虛擬內(nèi)存在頁(yè)表中是沒(méi)有頁(yè)表項(xiàng) PTE 的。
隨后 MMU 就會(huì)觸發(fā)缺頁(yè)異常(page fault),進(jìn)程切換到內(nèi)核態(tài),在內(nèi)核缺頁(yè)中斷處理程序中會(huì)發(fā)現(xiàn)引起缺頁(yè)的這段 VMA 是私有文件映射的,所以內(nèi)核會(huì)首先通過(guò) vm_area_struct->vm_pgoff 在文件 page cache 中查找是否有緩存相應(yīng)的文件頁(yè)(映射的磁盤塊對(duì)應(yīng)的文件頁(yè))。
structvm_area_struct{ unsignedlongvm_pgoff;/*Offset(withinvm_file)inPAGE_SIZE*/ } staticinlinestructpage*find_get_page(structaddress_space*mapping, pgoff_toffset) { returnpagecache_get_page(mapping,offset,0,0); }
如果文件頁(yè)不在 page cache 中,內(nèi)核則會(huì)在物理內(nèi)存中分配一個(gè)內(nèi)存頁(yè),然后將新分配的內(nèi)存頁(yè)加入到 page cache 中,并增加頁(yè)引用計(jì)數(shù)。
隨后會(huì)通過(guò) address_space_operations 重定義的 readpage 激活塊設(shè)備驅(qū)動(dòng)從磁盤中讀取映射的文件內(nèi)容,然后將讀取到的內(nèi)容填充新分配的內(nèi)存頁(yè)。
staticconststructaddress_space_operationsext4_aops={ .readpage=ext4_readpage }
現(xiàn)在文件中映射的內(nèi)容已經(jīng)加載進(jìn) page cache 了,此時(shí)物理內(nèi)存才正式登場(chǎng),在缺頁(yè)中斷處理程序的最后一步,內(nèi)核會(huì)為映射的這段虛擬內(nèi)存在頁(yè)表中創(chuàng)建 PTE,然后將虛擬內(nèi)存與 page cache 中的文件頁(yè)通過(guò) PTE 關(guān)聯(lián)起來(lái),缺頁(yè)處理就結(jié)束了,但是由于我們指定的私有文件映射,所以 PTE 中文件頁(yè)的權(quán)限是只讀的。
當(dāng)內(nèi)核處理完缺頁(yè)中斷之后,mmap 私有文件映射在內(nèi)核中的關(guān)系圖就變成下面這樣:
此時(shí)進(jìn)程 1 中的頁(yè)表已經(jīng)建立起了虛擬內(nèi)存與文件頁(yè)的映射關(guān)系,進(jìn)程 1 再次訪問(wèn)這段虛擬內(nèi)存的時(shí)候,其實(shí)就等于直接訪問(wèn)文件的 page cache。整個(gè)過(guò)程是在用戶態(tài)進(jìn)行的,不需要切態(tài)。
現(xiàn)在我們?cè)趯⒁暯乔袚Q到進(jìn)程 2 中,進(jìn)程 2 和進(jìn)程 1 一樣,都是采用 mmap 私有文件映射的方式映射到了同一個(gè)文件中,雖然現(xiàn)在已經(jīng)有了物理內(nèi)存了(通過(guò)進(jìn)程 1 的缺頁(yè)產(chǎn)生),但是目前還和進(jìn)程 2 沒(méi)有關(guān)系。
因?yàn)檫M(jìn)程 2 的虛擬內(nèi)存空間中這段映射的虛擬內(nèi)存區(qū)域 VMA,在進(jìn)程 2 的頁(yè)表中還沒(méi)有 PTE,所以當(dāng)進(jìn)程 2 訪問(wèn)這段映射虛擬內(nèi)存時(shí),同樣會(huì)產(chǎn)生缺頁(yè)中斷,隨后進(jìn)程 2 切換到內(nèi)核態(tài),進(jìn)行缺頁(yè)處理,這里和進(jìn)程 1 不同的是,此時(shí)被映射的文件內(nèi)容已經(jīng)加載到 page cache 中了,進(jìn)程 2 只需要?jiǎng)?chuàng)建 PTE ,并將 page cache 中的文件頁(yè)與進(jìn)程 2 映射的這段虛擬內(nèi)存通過(guò) PTE 關(guān)聯(lián)起來(lái)就可以了。同樣,因?yàn)椴捎盟接形募成涞脑?,進(jìn)程 2 的 PTE 也是只讀的。
現(xiàn)在進(jìn)程 1 和進(jìn)程 2 都可以根據(jù)各自虛擬內(nèi)存空間中映射的這段虛擬內(nèi)存對(duì)文件的 page cache 進(jìn)行讀取了,整個(gè)過(guò)程都發(fā)生在用戶態(tài),不需要切態(tài),更不需要拷貝,因?yàn)樘摂M內(nèi)存現(xiàn)在已經(jīng)直接映射到 page cache 了。
雖然我們采用的是私有文件映射的方式,但是進(jìn)程 1 和進(jìn)程 2 如果只是對(duì)文件映射部分進(jìn)行讀取的話,文件頁(yè)其實(shí)在多進(jìn)程之間是共享的,整個(gè)內(nèi)核中只有一份。
但是當(dāng)任意一個(gè)進(jìn)程通過(guò)虛擬映射區(qū)對(duì)文件進(jìn)行寫(xiě)入操作的時(shí)候,情況就發(fā)生了變化,雖然通過(guò) mmap 映射的時(shí)候指定的這段虛擬內(nèi)存是可寫(xiě)的,但是由于采用的是私有文件映射的方式,各個(gè)進(jìn)程頁(yè)表中對(duì)應(yīng) PTE 卻是只讀的,當(dāng)進(jìn)程對(duì)這段虛擬內(nèi)存進(jìn)行寫(xiě)入的時(shí)候,MMU 會(huì)發(fā)現(xiàn) PTE 是只讀的,所以會(huì)產(chǎn)生一個(gè)寫(xiě)保護(hù)類型的缺頁(yè)中斷,寫(xiě)入進(jìn)程,比如是進(jìn)程 1,此時(shí)又會(huì)陷入到內(nèi)核態(tài),在寫(xiě)保護(hù)缺頁(yè)處理中,內(nèi)核會(huì)重新申請(qǐng)一個(gè)內(nèi)存頁(yè),然后將 page cache 中的內(nèi)容拷貝到這個(gè)新的內(nèi)存頁(yè)中,進(jìn)程 1 頁(yè)表中對(duì)應(yīng)的 PTE 會(huì)重新關(guān)聯(lián)到這個(gè)新的內(nèi)存頁(yè)上,此時(shí) PTE 的權(quán)限變?yōu)榭蓪?xiě)。
從此以后,進(jìn)程 1 對(duì)這段虛擬內(nèi)存區(qū)域進(jìn)行讀寫(xiě)的時(shí)候就不會(huì)再發(fā)生缺頁(yè)了,讀寫(xiě)操作都會(huì)發(fā)生在這個(gè)新申請(qǐng)的內(nèi)存頁(yè)上,但是有一點(diǎn),進(jìn)程 1 對(duì)這個(gè)內(nèi)存頁(yè)的任何修改均不會(huì)回寫(xiě)到磁盤文件上,這也體現(xiàn)了私有文件映射的特點(diǎn),進(jìn)程對(duì)映射文件的修改,其他進(jìn)程是看不到的,并且修改不會(huì)同步回磁盤文件中。
進(jìn)程 2 對(duì)這段虛擬映射區(qū)進(jìn)行寫(xiě)入的時(shí)候,也是一樣的道理,同樣會(huì)觸發(fā)寫(xiě)保護(hù)類型的缺頁(yè)中斷,進(jìn)程 2 陷入內(nèi)核態(tài),內(nèi)核為進(jìn)程 2 新申請(qǐng)一個(gè)物理內(nèi)存頁(yè),并將 page cache 中的內(nèi)容拷貝到剛為進(jìn)程 2 申請(qǐng)的這個(gè)內(nèi)存頁(yè)中,進(jìn)程 2 頁(yè)表中對(duì)應(yīng)的 PTE 會(huì)重新關(guān)聯(lián)到新的內(nèi)存頁(yè)上, PTE 的權(quán)限變?yōu)榭蓪?xiě)。
這樣一來(lái),進(jìn)程 1 和進(jìn)程 2 各自的這段虛擬映射區(qū),就映射到了各自專屬的物理內(nèi)存頁(yè)上,而且這兩個(gè)內(nèi)存頁(yè)中的內(nèi)容均是文件中映射的部分,他們已經(jīng)和 page cache 脫離了。
進(jìn)程 1 和進(jìn)程 2 對(duì)各自虛擬內(nèi)存區(qū)的修改只能反應(yīng)到各自對(duì)應(yīng)的物理內(nèi)存頁(yè)上,而且各自的修改在進(jìn)程之間是互不可見(jiàn)的,最重要的一點(diǎn)是這些修改均不會(huì)回寫(xiě)到磁盤文件中,這就是私有文件映射的核心特點(diǎn)。
我們可以利用 mmap 私有文件映射這個(gè)特點(diǎn)來(lái)加載二進(jìn)制可執(zhí)行文件的 .text , .data section 到進(jìn)程虛擬內(nèi)存空間中的代碼段和數(shù)據(jù)段中。
因?yàn)橥环荽a,也就是同一份二進(jìn)制可執(zhí)行文件可以運(yùn)行多個(gè)進(jìn)程,而代碼段對(duì)于多進(jìn)程來(lái)說(shuō)是只讀的,沒(méi)有必要為每個(gè)進(jìn)程都保存一份,多進(jìn)程之間共享這一份代碼就可以了,正好私有文件映射的讀共享特點(diǎn)可以滿足我們的這個(gè)需求。
對(duì)于數(shù)據(jù)段來(lái)說(shuō),雖然它是可寫(xiě)的,但是我們需要的是多進(jìn)程之間對(duì)數(shù)據(jù)段的修改相互之間是不可見(jiàn)的,而且對(duì)數(shù)據(jù)段的修改不能回寫(xiě)到磁盤上的二進(jìn)制文件中,這樣當(dāng)我們利用這個(gè)可執(zhí)行文件在啟動(dòng)一個(gè)進(jìn)程的時(shí)候,進(jìn)程看到的就是數(shù)據(jù)段初始化未被修改的狀態(tài)。 mmap 私有文件映射的寫(xiě)時(shí)復(fù)制(copy on write)以及修改不會(huì)回寫(xiě)到映射文件中等特點(diǎn)正好也滿足我們的需求。
這一點(diǎn)我們可以在負(fù)責(zé)加載 elf 格式的二進(jìn)制可執(zhí)行文件并映射到進(jìn)程虛擬內(nèi)存空間的 load_elf_binary 函數(shù),以及負(fù)責(zé)加載 a.out 格式可執(zhí)行文件的 load_aout_binary 函數(shù)中可以看出。
staticintload_elf_binary(structlinux_binprm*bprm) { //將二進(jìn)制文件中的.text.datasection私有映射到虛擬內(nèi)存空間中代碼段和數(shù)據(jù)段中 error=elf_map(bprm->file,load_bias+vaddr,elf_ppnt, elf_prot,elf_flags,total_size); } staticintload_aout_binary(structlinux_binprm*bprm) { ............省略............. //將.text采用私有文件映射的方式映射到進(jìn)程虛擬內(nèi)存空間的代碼段 error=vm_mmap(bprm->file,N_TXTADDR(ex),ex.a_text, PROT_READ|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_DENYWRITE|MAP_EXECUTABLE, fd_offset); //將.data采用私有文件映射的方式映射到進(jìn)程虛擬內(nèi)存空間的數(shù)據(jù)段 error=vm_mmap(bprm->file,N_DATADDR(ex),ex.a_data, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_DENYWRITE|MAP_EXECUTABLE, fd_offset+ex.a_text); ............省略............. }
4. 共享文件映射
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);
我們通過(guò)將 mmap 系統(tǒng)調(diào)用中的 flags 參數(shù)指定為 MAP_SHARED , 參數(shù) fd 指定為要映射文件的文件描述符(file descriptor)來(lái)實(shí)現(xiàn)對(duì)文件的共享映射。
共享文件映射其實(shí)和私有文件映射前面的映射過(guò)程是一樣的,唯一不同的點(diǎn)在于私有文件映射是讀共享的,寫(xiě)的時(shí)候會(huì)發(fā)生寫(xiě)時(shí)復(fù)制(copy on write),并且多進(jìn)程針對(duì)同一映射文件的修改不會(huì)回寫(xiě)到磁盤文件上。
而共享文件映射因?yàn)槭枪蚕淼?,多個(gè)進(jìn)程中的虛擬內(nèi)存映射區(qū)最終會(huì)通過(guò)缺頁(yè)中斷的方式映射到文件的 page cache 中,后續(xù)多個(gè)進(jìn)程對(duì)各自的這段虛擬內(nèi)存區(qū)域的讀寫(xiě)都會(huì)直接發(fā)生在 page cache 上。
因?yàn)橛成湮募?page cache 在內(nèi)核中只有一份,所以對(duì)于共享文件映射來(lái)說(shuō),多進(jìn)程讀寫(xiě)都是共享的,由于多進(jìn)程直接讀寫(xiě)的是 page cache ,所以多進(jìn)程對(duì)共享映射區(qū)的任何修改,最終都會(huì)通過(guò)內(nèi)核回寫(xiě)線程 pdflush 刷新到磁盤文件中。
下面這幅是多進(jìn)程通過(guò) mmap 共享文件映射之后的內(nèi)核數(shù)據(jù)結(jié)構(gòu)關(guān)系圖:
同私有文件映射方式一樣,當(dāng)多個(gè)進(jìn)程調(diào)用 mmap 對(duì)磁盤上的同一個(gè)文件進(jìn)行共享文件映射的時(shí)候,內(nèi)核中的處理都是一樣的,也都只是在每個(gè)進(jìn)程的虛擬內(nèi)存空間中,創(chuàng)建出一段用于共享映射的虛擬內(nèi)存區(qū)域 VMA 出來(lái),隨后內(nèi)核會(huì)將各個(gè)進(jìn)程中的這段虛擬內(nèi)存映射區(qū)與映射文件關(guān)聯(lián)起來(lái),mmap 共享文件映射的邏輯就結(jié)束了。
唯一不同的是,共享文件映射會(huì)在這段用于映射文件的 VMA 中標(biāo)注是共享映射 —— MAP_SHARED
structvm_area_struct{ //MAP_SHARED共享映射 unsignedlongvm_flags; }
在 mmap 共享文件映射的過(guò)程中,內(nèi)核同樣不涉及任何的物理內(nèi)存分配,只是分配了一段虛擬內(nèi)存,在共享映射剛剛建立起來(lái)之后,文件對(duì)應(yīng)的 page cache 同樣是空的,沒(méi)有包含任何的文件頁(yè)。
由于 mmap 只是在各個(gè)進(jìn)程中分配了虛擬內(nèi)存,沒(méi)有分配物理內(nèi)存,所以在各個(gè)進(jìn)程的頁(yè)表中,這段用于文件映射的虛擬內(nèi)存區(qū)域?qū)?yīng)的頁(yè)表項(xiàng) PTE 是空的,當(dāng)任意進(jìn)程對(duì)這段虛擬內(nèi)存進(jìn)行訪問(wèn)的時(shí)候(讀或者寫(xiě)),MMU 就會(huì)產(chǎn)生缺頁(yè)中斷,這里我們以上圖中的進(jìn)程 1 為例,隨后進(jìn)程 1 切換到內(nèi)核態(tài),執(zhí)行內(nèi)核缺頁(yè)中斷處理程序。
同私有文件映射的缺頁(yè)處理一樣,內(nèi)核會(huì)首先通過(guò) vm_area_struct->vm_pgoff 在文件 page cache 中查找是否有緩存相應(yīng)的文件頁(yè)(映射的磁盤塊對(duì)應(yīng)的文件頁(yè))。如果文件頁(yè)不在 page cache 中,內(nèi)核則會(huì)在物理內(nèi)存中分配一個(gè)內(nèi)存頁(yè),然后將新分配的內(nèi)存頁(yè)加入到 page cache 中。
然后調(diào)用 readpage 激活塊設(shè)備驅(qū)動(dòng)從磁盤中讀取映射的文件內(nèi)容,用讀取到的內(nèi)容填充新分配的內(nèi)存頁(yè),現(xiàn)在物理內(nèi)存有了,最后一步就是在進(jìn)程 1 的頁(yè)表中建立共享映射的這段虛擬內(nèi)存與 page cache 中緩存的文件頁(yè)之間的關(guān)聯(lián)。
這里和私有文件映射不同的地方是,私有文件映射由于是私有的,所以在內(nèi)核創(chuàng)建 PTE 的時(shí)候會(huì)將 PTE 設(shè)置為只讀,目的是當(dāng)進(jìn)程寫(xiě)入的時(shí)候觸發(fā)寫(xiě)保護(hù)類型的缺頁(yè)中斷進(jìn)行寫(xiě)時(shí)復(fù)制 (copy on write)。
共享文件映射由于是共享的,PTE 被創(chuàng)建出來(lái)的時(shí)候就是可寫(xiě)的,所以后續(xù)進(jìn)程 1 在對(duì)這段虛擬內(nèi)存區(qū)域?qū)懭氲臅r(shí)候不會(huì)觸發(fā)缺頁(yè)中斷,而是直接寫(xiě)入 page cache 中,整個(gè)過(guò)程沒(méi)有切態(tài),沒(méi)有數(shù)據(jù)拷貝。
現(xiàn)在我們?cè)谇袚Q到進(jìn)程 2 的視角中,雖然現(xiàn)在文件中被映射的這部分內(nèi)容已經(jīng)加載進(jìn)物理內(nèi)存頁(yè),并被緩存在文件的 page cache 中了。但是現(xiàn)在進(jìn)程 2 中這段虛擬映射區(qū)在進(jìn)程 2 頁(yè)表中對(duì)應(yīng)的 PTE 仍然是空的,當(dāng)進(jìn)程 2 訪問(wèn)這段虛擬映射區(qū)的時(shí)候依然會(huì)產(chǎn)生缺頁(yè)中斷。
當(dāng)進(jìn)程 2 切換到內(nèi)核態(tài),處理缺頁(yè)中斷的時(shí)候,此時(shí)進(jìn)程 2 通過(guò) vm_area_struct->vm_pgoff 在 page cache 查找文件頁(yè)的時(shí)候,文件頁(yè)已經(jīng)被進(jìn)程 1 加載進(jìn) page cache 了,進(jìn)程 2 一下就找到了,就不需要再去磁盤中讀取映射內(nèi)容了,內(nèi)核會(huì)直接為進(jìn)程 2 創(chuàng)建 PTE (由于是共享文件映射,所以這里的 PTE 也是可寫(xiě)的),并插入到進(jìn)程 2 頁(yè)表中,隨后將進(jìn)程 2 中的虛擬映射區(qū)通過(guò) PTE 與 page cache 中緩存的文件頁(yè)映射關(guān)聯(lián)起來(lái)。
現(xiàn)在進(jìn)程 1 和進(jìn)程 2 各自虛擬內(nèi)存空間中的這段虛擬內(nèi)存區(qū)域 VMA,已經(jīng)共同映射到了文件的 page cache 中,由于文件的 page cache 在內(nèi)核中只有一份,它是和進(jìn)程無(wú)關(guān)的,page cache 中的內(nèi)容發(fā)生的任何變化,進(jìn)程 1 和進(jìn)程 2 都是可以看到的。
重要的一點(diǎn)是,多進(jìn)程對(duì)各自虛擬內(nèi)存映射區(qū) VMA 的寫(xiě)入操作,內(nèi)核會(huì)根據(jù)自己的臟頁(yè)回寫(xiě)策略將修改內(nèi)容回寫(xiě)到磁盤文件中。
內(nèi)核提供了以下六個(gè)系統(tǒng)參數(shù),來(lái)供我們配置調(diào)整內(nèi)核臟頁(yè)回寫(xiě)的行為,這些參數(shù)的配置文件存在于 proc/sys/vm 目錄下:
dirty_writeback_centisecs 內(nèi)核參數(shù)的默認(rèn)值為 500。單位為 0.01 s。也就是說(shuō)內(nèi)核默認(rèn)會(huì)每隔 5s 喚醒一次 flusher 線程來(lái)執(zhí)行相關(guān)臟頁(yè)的回寫(xiě)。
drity_background_ratio :當(dāng)臟頁(yè)數(shù)量在系統(tǒng)的可用內(nèi)存 available 中占用的比例達(dá)到 drity_background_ratio 的配置值時(shí),內(nèi)核就會(huì)喚醒 flusher 線程異步回寫(xiě)臟頁(yè)。默認(rèn)值為:10。表示如果 page cache 中的臟頁(yè)數(shù)量達(dá)到系統(tǒng)可用內(nèi)存的 10% 的話,就主動(dòng)喚醒 flusher 線程去回寫(xiě)臟頁(yè)到磁盤。
dirty_background_bytes :如果 page cache 中臟頁(yè)占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_background_bytes。內(nèi)核就會(huì)喚醒 flusher 線程異步回寫(xiě)臟頁(yè)。默認(rèn)為:0。
dirty_ratio : dirty_background_* 相關(guān)的內(nèi)核配置參數(shù)均是內(nèi)核通過(guò)喚醒 flusher 線程來(lái)異步回寫(xiě)臟頁(yè)。下面要介紹的 dirty_* 配置參數(shù),均是由用戶進(jìn)程同步回寫(xiě)臟頁(yè)。表示內(nèi)存中的臟頁(yè)太多了,用戶進(jìn)程自己都看不下去了,不用等內(nèi)核 flusher 線程喚醒,用戶進(jìn)程自己主動(dòng)去回寫(xiě)臟頁(yè)到磁盤中。當(dāng)臟頁(yè)占用系統(tǒng)可用內(nèi)存的比例達(dá)到 dirty_ratio 配置的值時(shí),用戶進(jìn)程同步回寫(xiě)臟頁(yè)。默認(rèn)值為:20 。
dirty_bytes :如果 page cache 中臟頁(yè)占用的內(nèi)存用量絕對(duì)值達(dá)到指定的 dirty_bytes。用戶進(jìn)程同步回寫(xiě)臟頁(yè)。默認(rèn)值為:0。
內(nèi)核為了避免 page cache 中的臟頁(yè)在內(nèi)存中長(zhǎng)久的停留,所以會(huì)給臟頁(yè)在內(nèi)存中的駐留時(shí)間設(shè)置一定的期限,這個(gè)期限可由前邊提到的 dirty_expire_centisecs 內(nèi)核參數(shù)配置。默認(rèn)為:3000。單位為:0.01 s。也就是說(shuō)在默認(rèn)配置下,臟頁(yè)在內(nèi)存中的駐留時(shí)間為 30 s。超過(guò) 30 s 之后,flusher 線程將會(huì)在下次被喚醒的時(shí)候?qū)⑦@些臟頁(yè)回寫(xiě)到磁盤中。
關(guān)于臟頁(yè)回寫(xiě)詳細(xì)的內(nèi)容介紹,感興趣的讀者可以回看下 《從 Linux 內(nèi)核角度探秘 JDK NIO 文件讀寫(xiě)本質(zhì)》 一文中的 “13. 內(nèi)核回寫(xiě)臟頁(yè)的觸發(fā)時(shí)機(jī)” 小節(jié)。
根據(jù) mmap 共享文件映射多進(jìn)程之間讀寫(xiě)共享(不會(huì)發(fā)生寫(xiě)時(shí)復(fù)制)的特點(diǎn),常用于多進(jìn)程之間共享內(nèi)存(page cache),多進(jìn)程之間的通訊。
5. 共享匿名映射
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);
我們通過(guò)將 mmap 系統(tǒng)調(diào)用中的 flags 參數(shù)指定為 MAP_SHARED | MAP_ANONYMOUS ,并將 fd 參數(shù)指定為 -1 來(lái)實(shí)現(xiàn)共享匿名映射,這種映射方式常用于父子進(jìn)程之間共享內(nèi)存,父子進(jìn)程之間的通訊。注意,這里需要和大家強(qiáng)調(diào)一下是父子進(jìn)程,為什么只能是父子進(jìn)程,筆者后面再給大家解答。
在筆者介紹完 mmap 的私有匿名映射,私有文件映射,以及共享文件映射之后,共享匿名映射看似就非常簡(jiǎn)單了,由于不對(duì)文件進(jìn)行映射,所以它不涉及到文件系統(tǒng)相關(guān)的知識(shí),而且又是共享的,多個(gè)進(jìn)程通過(guò)將自己的頁(yè)表指向同一個(gè)物理內(nèi)存頁(yè)面不就實(shí)現(xiàn)共享匿名映射了嗎?
看起來(lái)簡(jiǎn)單,實(shí)際上并沒(méi)有那么簡(jiǎn)單,甚至可以說(shuō)共享匿名映射是 mmap 這四種映射方式中最為復(fù)雜的,為什么這么說(shuō)的 ?我們一起來(lái)看下共享匿名映射的映射過(guò)程。
首先和其他幾種映射方式一樣,mmap 只是負(fù)責(zé)在各個(gè)進(jìn)程的虛擬內(nèi)存空間中劃分一段用于共享匿名映射的虛擬內(nèi)存區(qū)域而已,這點(diǎn)筆者已經(jīng)強(qiáng)調(diào)過(guò)很多遍了,整個(gè)映射過(guò)程并不涉及到物理內(nèi)存的分配。
當(dāng)多個(gè)進(jìn)程調(diào)用 mmap 進(jìn)行共享匿名映射之后,內(nèi)核只不過(guò)是為每個(gè)進(jìn)程在各自的虛擬內(nèi)存空間中分配了一段虛擬內(nèi)存而已,由于并不涉及物理內(nèi)存的分配,所以這段用于映射的虛擬內(nèi)存在各個(gè)進(jìn)程的頁(yè)表中對(duì)應(yīng)的頁(yè)表項(xiàng) PTE 都還是空的,如下圖所示:
當(dāng)任一進(jìn)程,比如上圖中的進(jìn)程 1 開(kāi)始訪問(wèn)這段虛擬映射區(qū)的時(shí)候,MMU 會(huì)產(chǎn)生缺頁(yè)中斷,進(jìn)程 1 切換到內(nèi)核態(tài),開(kāi)始處理缺頁(yè)中斷邏輯,在缺頁(yè)中斷處理程序中,內(nèi)核為進(jìn)程 1 分配一個(gè)物理內(nèi)存頁(yè),并創(chuàng)建對(duì)應(yīng)的 PTE 插入到進(jìn)程 1 的頁(yè)表中,隨后用 PTE 將進(jìn)程 1 的這段虛擬映射區(qū)與物理內(nèi)存映射關(guān)聯(lián)起來(lái)。進(jìn)程 1 的缺頁(yè)處理結(jié)束,從此以后,進(jìn)程 1 就可以讀寫(xiě)這段共享映射的物理內(nèi)存了。
g
現(xiàn)在我們把視角切換到進(jìn)程 2 中,當(dāng)進(jìn)程 2 訪問(wèn)它自己的這段虛擬映射區(qū)的時(shí)候,由于進(jìn)程 2 頁(yè)表中對(duì)應(yīng)的 PTE 為空,所以進(jìn)程 2 也會(huì)發(fā)生缺頁(yè)中斷,隨后切換到內(nèi)核態(tài)處理缺頁(yè)邏輯。
當(dāng)進(jìn)程 2 開(kāi)始處理缺頁(yè)邏輯的時(shí)候,進(jìn)程 2 就懵了,為什么呢 ?原因是進(jìn)程 2 和進(jìn)程 1 進(jìn)行的是共享映射,所以進(jìn)程 2 不能隨便找一個(gè)物理內(nèi)存頁(yè)進(jìn)行映射,進(jìn)程 2 必須和 進(jìn)程 1 映射到同一個(gè)物理內(nèi)存頁(yè)面,這樣才能共享內(nèi)存。那現(xiàn)在的問(wèn)題是,進(jìn)程 2 面對(duì)著茫茫多的物理內(nèi)存頁(yè),進(jìn)程 2 怎么知道進(jìn)程 1 已經(jīng)映射了哪個(gè)物理內(nèi)存頁(yè) ?
內(nèi)核在缺頁(yè)中斷處理中只能知道當(dāng)前正在缺頁(yè)的進(jìn)程是誰(shuí),以及發(fā)生缺頁(yè)的虛擬內(nèi)存地址是什么,內(nèi)核根據(jù)這些信息,根本無(wú)法知道,此時(shí)是否已經(jīng)有其他進(jìn)程把共享的物理內(nèi)存頁(yè)準(zhǔn)備好了。
這一點(diǎn)對(duì)于共享文件映射來(lái)說(shuō)特別簡(jiǎn)單,因?yàn)橛形募?page cache 存在,進(jìn)程 2 可以根據(jù)映射的文件內(nèi)容在文件中的偏移 offset,從 page cache 中查找是否已經(jīng)有其他進(jìn)程把映射的文件內(nèi)容加載到文件頁(yè)中。如果文件頁(yè)已經(jīng)存在 page cache 中了,進(jìn)程 2 直接映射這個(gè)文件頁(yè)就可以了。
structvm_area_struct{ unsignedlongvm_pgoff;/*Offset(withinvm_file)inPAGE_SIZE*/ } staticinlinestructpage*find_get_page(structaddress_space*mapping, pgoff_toffset) { returnpagecache_get_page(mapping,offset,0,0); }
由于共享匿名映射并沒(méi)有對(duì)文件映射,所以其他進(jìn)程想要在內(nèi)存中查找要進(jìn)行共享的內(nèi)存頁(yè)就非常困難了,那怎么解決這個(gè)問(wèn)題呢 ?
既然共享文件映射可以輕松解決這個(gè)問(wèn)題,那我們何不借鑒一下文件映射的方式 ?
共享匿名映射在內(nèi)核中是通過(guò)一個(gè)叫做 tmpfs 的虛擬文件系統(tǒng)來(lái)實(shí)現(xiàn)的,tmpfs 不是傳統(tǒng)意義上的文件系統(tǒng),它是基于內(nèi)存實(shí)現(xiàn)的,掛載在 dev/zero 目錄下。
當(dāng)多個(gè)進(jìn)程通過(guò) mmap 進(jìn)行共享匿名映射的時(shí)候,內(nèi)核會(huì)在 tmpfs 文件系統(tǒng)中創(chuàng)建一個(gè)匿名文件,這個(gè)匿名文件并不是真實(shí)存在于磁盤上的,它是內(nèi)核為了共享匿名映射而模擬出來(lái)的,匿名文件也有自己的 inode 結(jié)構(gòu)以及 page cache。
在 mmap 進(jìn)行共享匿名映射的時(shí)候,內(nèi)核會(huì)把這個(gè)匿名文件關(guān)聯(lián)到進(jìn)程的虛擬映射區(qū) VMA 中。這樣一來(lái),當(dāng)進(jìn)程虛擬映射區(qū)域與 tmpfs 文件系統(tǒng)中的這個(gè)匿名文件映射起來(lái)之后,后面的流程就和共享文件映射一模一樣了。
structvm_area_struct{ structfile*vm_file;/*Filewemapto(canbeNULL).*/ }
最后,筆者來(lái)回答下在本小節(jié)開(kāi)始處拋出的一個(gè)問(wèn)題,就是共享匿名映射只適用于父子進(jìn)程之間的通訊,為什么只能是父子進(jìn)程呢 ?
因?yàn)楫?dāng)父進(jìn)程進(jìn)行 mmap 共享匿名映射的時(shí)候,內(nèi)核會(huì)為其創(chuàng)建一個(gè)匿名文件,并關(guān)聯(lián)到父進(jìn)程的虛擬內(nèi)存空間中 vm_area_struct->vm_file 中。但是這時(shí)候其他進(jìn)程并不知道父進(jìn)程虛擬內(nèi)存空間中關(guān)聯(lián)的這個(gè)匿名文件,因?yàn)檫M(jìn)程之間的虛擬內(nèi)存空間都是隔離的。
子進(jìn)程就不一樣了,在父進(jìn)程調(diào)用完 mmap 之后,父進(jìn)程的虛擬內(nèi)存空間中已經(jīng)有了一段虛擬映射區(qū) VMA 并關(guān)聯(lián)到匿名文件了。這時(shí)父進(jìn)程進(jìn)行 fork() 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程,子進(jìn)程會(huì)拷貝父進(jìn)程的所有資源,當(dāng)然也包括父進(jìn)程的虛擬內(nèi)存空間以及父進(jìn)程的頁(yè)表。
long_do_fork(unsignedlongclone_flags, unsignedlongstack_start, unsignedlongstack_size, int__user*parent_tidptr, int__user*child_tidptr, unsignedlongtls) { .........省略.......... structpid*pid; structtask_struct*p; .........省略.......... //拷貝父進(jìn)程的所有資源 p=copy_process(clone_flags,stack_start,stack_size, child_tidptr,NULL,trace,tls,NUMA_NO_NODE); .........省略.......... }
當(dāng) fork 出子進(jìn)程的時(shí)候,這時(shí)子進(jìn)程的虛擬內(nèi)存空間和父進(jìn)程的虛擬內(nèi)存空間完全是一模一樣的,在子進(jìn)程的虛擬內(nèi)存空間中自然也有一段虛擬映射區(qū) VMA 并且已經(jīng)關(guān)聯(lián)到匿名文件中了(繼承自父進(jìn)程)。
現(xiàn)在父子進(jìn)程的頁(yè)表也是一模一樣的,各自的這段虛擬映射區(qū)對(duì)應(yīng)的 PTE 都是空的,一旦發(fā)生缺頁(yè),后面的流程就和共享文件映射一樣了。我們可以把共享匿名映射看作成一種特殊的共享文件映射方式。
6. 參數(shù) flags 的其他枚舉值
#includevoid*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);
在前邊的幾個(gè)小節(jié)中,筆者為大家介紹了 mmap 系統(tǒng)調(diào)用參數(shù) flags 最為核心的三個(gè)枚舉值:MAP_ANONYMOUS,MAP_SHARED,MAP_PRIVATE。隨后我們通過(guò)這三個(gè)枚舉值組合出了四種內(nèi)存映射方式:私有匿名映射,私有文件映射,共享文件映射,共享匿名映射。
到現(xiàn)在為止,筆者算是把 mmap 內(nèi)存映射的核心原理及其在內(nèi)核中的映射過(guò)程給大家詳細(xì)剖析完了,不過(guò)參數(shù) flags 的枚舉值在內(nèi)核中并不只是上述三個(gè),除此之外,內(nèi)核還定義了很多。在本小節(jié)的最后,筆者為大家挑了幾個(gè)相對(duì)重要的枚舉值給大家做一些額外的補(bǔ)充,這樣能夠讓大家對(duì) mmap 內(nèi)存映射有一個(gè)更加全面的認(rèn)識(shí)。
#defineMAP_LOCKED0x2000/*pagesarelocked*/ #defineMAP_POPULATE0x008000/*populate(prefault)pagetables*/ #defineMAP_HUGETLB0x040000/*createahugepagemapping*/
經(jīng)過(guò)前面的介紹我們知道,mmap 僅僅只是在進(jìn)程虛擬內(nèi)存空間中劃分出一段用于映射的虛擬內(nèi)存區(qū)域 VMA ,并將這段 VMA 與磁盤上的文件映射起來(lái)而已。整個(gè)映射過(guò)程并不涉及物理內(nèi)存的分配,更別說(shuō)虛擬內(nèi)存與物理內(nèi)存的映射了,這些都是在進(jìn)程訪問(wèn)這段 VMA 的時(shí)候,通過(guò)缺頁(yè)中斷來(lái)補(bǔ)齊的。
如果我們?cè)谑褂?mmap 系統(tǒng)調(diào)用的時(shí)候設(shè)置了 MAP_POPULATE ,內(nèi)核在分配完虛擬內(nèi)存之后,就會(huì)馬上分配物理內(nèi)存,并在進(jìn)程頁(yè)表中建立起虛擬內(nèi)存與物理內(nèi)存的映射關(guān)系,這樣進(jìn)程在調(diào)用 mmap 之后就可以直接訪問(wèn)這段映射的虛擬內(nèi)存地址了,不會(huì)發(fā)生缺頁(yè)中斷。
但是當(dāng)系統(tǒng)內(nèi)存資源緊張的時(shí)候,內(nèi)核依然會(huì)將 mmap 背后映射的這塊物理內(nèi)存 swap out 到磁盤中,這樣進(jìn)程在訪問(wèn)的時(shí)候仍然會(huì)發(fā)生缺頁(yè)中斷,為了防止這種現(xiàn)象,我們可以在調(diào)用 mmap 的時(shí)候設(shè)置 MAP_LOCKED。
在設(shè)置了 MAP_LOCKED 之后,mmap 系統(tǒng)調(diào)用在為進(jìn)程分配完虛擬內(nèi)存之后,內(nèi)核也會(huì)馬上為其分配物理內(nèi)存并在進(jìn)程頁(yè)表中建立虛擬內(nèi)存與物理內(nèi)存的映射關(guān)系,這里內(nèi)核還會(huì)額外做一個(gè)動(dòng)作,就是將映射的這塊物理內(nèi)存鎖定在內(nèi)存中,不允許它 swap,這樣一來(lái)映射的物理內(nèi)存將會(huì)一直停留在內(nèi)存中,進(jìn)程無(wú)論何時(shí)訪問(wèn)這段映射內(nèi)存都不會(huì)發(fā)生缺頁(yè)中斷。
MAP_HUGETLB 則是用于大頁(yè)內(nèi)存映射的,在內(nèi)核中關(guān)于物理內(nèi)存的調(diào)度是按照物理內(nèi)存頁(yè)為單位進(jìn)行的,普通物理內(nèi)存頁(yè)大小為 4K。但在一些對(duì)于內(nèi)存敏感的使用場(chǎng)景中,我們往往期望使用一些比普通 4K 更大的頁(yè)。
因?yàn)檫@些巨型頁(yè)要比普通的 4K 內(nèi)存頁(yè)要大很多,而且這些巨型頁(yè)不允許被 swap,所以遇到缺頁(yè)中斷的情況就會(huì)相對(duì)減少,由于減少了缺頁(yè)中斷所以性能會(huì)更高。
另外,由于巨型頁(yè)比普通頁(yè)要大,所以巨型頁(yè)需要的頁(yè)表項(xiàng)要比普通頁(yè)要少,頁(yè)表項(xiàng)里保存了虛擬內(nèi)存地址與物理內(nèi)存地址的映射關(guān)系,當(dāng) CPU 訪問(wèn)內(nèi)存的時(shí)候需要頻繁通過(guò) MMU 訪問(wèn)頁(yè)表項(xiàng)獲取物理內(nèi)存地址,由于要頻繁訪問(wèn),所以頁(yè)表項(xiàng)一般會(huì)緩存在 TLB 中,因?yàn)榫扌晚?yè)需要的頁(yè)表項(xiàng)較少,所以節(jié)約了 TLB 的空間同時(shí)降低了 TLB 緩存 MISS 的概率,從而加速了內(nèi)存訪問(wèn)。
7. 大頁(yè)內(nèi)存映射
在 64 位 x86 CPU 架構(gòu) Linux 的四級(jí)頁(yè)表體系下,系統(tǒng)支持的大頁(yè)尺寸有 2M,1G。我們可以在 /sys/kernel/mm/hugepages 路徑下查看當(dāng)前系統(tǒng)所支持的大頁(yè)尺寸:
要想在應(yīng)用程序中使用 HugePage,我們需要在內(nèi)核編譯的時(shí)候通過(guò)設(shè)置 CONFIG_HUGETLBFS 和 CONFIG_HUGETLB_PAGE 這兩個(gè)編譯選項(xiàng)來(lái)讓內(nèi)核支持 HugePage。我們可以通過(guò) cat /proc/filesystems 命令來(lái)查看當(dāng)前內(nèi)核中是否支持 hugetlbfs 文件系統(tǒng),這是我們使用 HugePage 的基礎(chǔ)。
因?yàn)?HugePage 要求的是一大片連續(xù)的物理內(nèi)存,和普通內(nèi)存頁(yè)一樣,巨型大頁(yè)里的內(nèi)存必須是連續(xù)的,但是隨著系統(tǒng)的長(zhǎng)時(shí)間運(yùn)行,內(nèi)存頁(yè)被頻繁無(wú)規(guī)則的分配與回收,系統(tǒng)中會(huì)產(chǎn)生大量的內(nèi)存碎片,由于內(nèi)存碎片的影響,內(nèi)核很難尋找到大片連續(xù)的物理內(nèi)存,這樣一來(lái)就很難分配到巨型大頁(yè)。
所以這就要求內(nèi)核在系統(tǒng)啟動(dòng)的時(shí)候預(yù)先為我們分配好足夠多的大頁(yè)內(nèi)存,這些大頁(yè)內(nèi)存被內(nèi)核管理在一個(gè)大頁(yè)內(nèi)存池中,大頁(yè)內(nèi)存池中的內(nèi)存全部是專用的,專門用于巨型大頁(yè)的分配,不能用于其他目的,即使系統(tǒng)中沒(méi)有使用巨型大頁(yè),這些大頁(yè)內(nèi)存就只能空閑在那里,另外這些大頁(yè)內(nèi)存都是被內(nèi)核鎖定在內(nèi)存中的,即使系統(tǒng)內(nèi)存資源緊張,大頁(yè)內(nèi)存也不允許被 swap。而且內(nèi)核大頁(yè)池中的這些大頁(yè)內(nèi)存使用完了就完了,大頁(yè)池耗盡之后,應(yīng)用程序?qū)o(wú)法再使用大頁(yè)。
既然大頁(yè)內(nèi)存池在內(nèi)核啟動(dòng)的時(shí)候就需要被預(yù)先創(chuàng)建好,而創(chuàng)建大頁(yè)內(nèi)存池,內(nèi)核需要首先知道內(nèi)存池中究竟包含多少個(gè) HugePage,每個(gè) HugePage 的尺寸是多少 。我們可以將這些參數(shù)在內(nèi)核啟動(dòng)的時(shí)候添加到 kernel command line 中,隨后內(nèi)核在啟動(dòng)的過(guò)程中就可以根據(jù) kernel command line 中 HugePage 相關(guān)的參數(shù)進(jìn)行大頁(yè)內(nèi)存池的創(chuàng)建。下面是一些 HugePage 相關(guān)的核心 command line 參數(shù)含義:
hugepagesz : 用于指定大頁(yè)內(nèi)存池中 HugePage 的 size,我們這里可以指定 hugepagesz=2M 或者 hugepagesz=1G,具體支持多少種大頁(yè)尺寸由 CPU 架構(gòu)決定。
hugepages:用于指定內(nèi)核需要預(yù)先創(chuàng)建多少個(gè) HugePage 在大頁(yè)內(nèi)存池中,我們可以通過(guò)指定 hugepages=256 ,來(lái)表示內(nèi)核需要預(yù)先創(chuàng)建 256 個(gè) HugePage 出來(lái)。除此之外 hugepages 參數(shù)還可以有 NUMA 格式,用于告訴內(nèi)核需要在每個(gè) NUMA node 上創(chuàng)建多少個(gè) HugePage。我們可以通過(guò)設(shè)置 hugepages=0:1,1:2 ... 來(lái)指定 NUMA node 0 上分配 1 個(gè) HugePage,在 NUMA node 1 上分配 2 個(gè) HugePage。
default_hugepagesz:用于指定 HugePage 默認(rèn)大小。各種不同類型的 CPU 架構(gòu)一般都支持多種 size 的 HugePage,比如 x86 CPU 支持 2M,1G 的 HugePage。arm64 支持 64K,2M,32M,1G 的 HugePage。這么多尺寸的 HugePage 我們到底該使用哪種尺寸呢 ? 這時(shí)就需要通過(guò) default_hugepagesz 來(lái)指定默認(rèn)使用的 HugePage 尺寸。
以上為大家介紹的是在內(nèi)核啟動(dòng)的時(shí)候(boot time)通過(guò)向 kernel command line 指定 HugePage 相關(guān)的命令行參數(shù)來(lái)配置大頁(yè),除此之外,我們還可以在系統(tǒng)剛剛啟動(dòng)之后(run time)來(lái)配置大頁(yè),因?yàn)橄到y(tǒng)剛剛啟動(dòng),所以系統(tǒng)內(nèi)存碎片化程度最小,也是一個(gè)配置大頁(yè)的時(shí)機(jī):
在 /proc/sys/vm 路徑下有兩個(gè)系統(tǒng)參數(shù)可以讓我們?cè)谙到y(tǒng) run time 的時(shí)候動(dòng)態(tài)調(diào)整當(dāng)前系統(tǒng)中 default size (由 default_hugepagesz 指定)大小的 HugePage 個(gè)數(shù)。
nr_hugepages 表示當(dāng)前系統(tǒng)中 default size 大小的 HugePage 個(gè)數(shù),我們可以通過(guò) echo HugePageNum > /proc/sys/vm/nr_hugepages 命令來(lái)動(dòng)態(tài)增大或者縮小 HugePage (default size )個(gè)數(shù)。
nr_overcommit_hugepages 表示當(dāng)系統(tǒng)中的應(yīng)用程序申請(qǐng)的大頁(yè)個(gè)數(shù)超過(guò) nr_hugepages 時(shí),內(nèi)核允許在額外申請(qǐng)多少個(gè)大頁(yè)。當(dāng)大頁(yè)內(nèi)存池中的大頁(yè)個(gè)數(shù)被耗盡時(shí),如果此時(shí)繼續(xù)有進(jìn)程來(lái)申請(qǐng)大頁(yè),那么內(nèi)核則會(huì)從當(dāng)前系統(tǒng)中選取多個(gè)連續(xù)的普通 4K 大小的內(nèi)存頁(yè),湊出若干個(gè)大頁(yè)來(lái)供進(jìn)程使用,這些被湊出來(lái)的大頁(yè)叫做 surplus_hugepage,surplus_hugepage 的個(gè)數(shù)不能超過(guò) nr_overcommit_hugepages。當(dāng)這些 surplus_hugepage 不在被使用時(shí),就會(huì)被釋放回內(nèi)核中。nr_hugepages 個(gè)數(shù)的大頁(yè)則會(huì)一直停留在大頁(yè)內(nèi)存池中,不會(huì)被釋放,也不會(huì)被 swap。
nr_hugepages 有點(diǎn)像 JDK 線程池中的 corePoolSize 參數(shù),(nr_hugepages + nr_overcommit_hugepages) 有點(diǎn)像線程池中的 maximumPoolSize 參數(shù)。
以上介紹的是修改默認(rèn)尺寸大小的 HugePage,另外,我們還可以在系統(tǒng) run time 的時(shí)候動(dòng)態(tài)修改指定尺寸的 HugePage,不同大頁(yè)尺寸的相關(guān)配置文件存放在 /sys/kernel/mm/hugepages 路徑下的對(duì)應(yīng)目錄中:
如上圖所示,當(dāng)前系統(tǒng)中所支持的大頁(yè)尺寸相關(guān)的配置文件,均存放在對(duì)應(yīng) hugepages-hugepagesize 格式的目錄中,下面我們以 2M 大頁(yè)為例,進(jìn)入到 hugepages-2048kB 目錄下,發(fā)現(xiàn)同樣也有 nr_hugepages 和 nr_overcommit_hugepages 這兩個(gè)配置文件,它們的含義和上邊介紹的一樣,只不過(guò)這里的是具體尺寸的 HugePage 相關(guān)配置。
我們可以通過(guò)如下命令來(lái)動(dòng)態(tài)調(diào)整系統(tǒng)中 2M 大頁(yè)的個(gè)數(shù):
echoHugePageNum>/sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
同理在 NUMA 架構(gòu)的系統(tǒng)下,我們可以在 /sys/devices/system/node/node_id 路徑下修改對(duì)應(yīng) numa node 節(jié)點(diǎn)中的相應(yīng)尺寸 的大頁(yè)個(gè)數(shù):
echoHugePageNum>/sys/devices/system/node/node_id/hugepages/hugepages-2048kB/nr_hugepages
現(xiàn)在內(nèi)核已經(jīng)支持了大頁(yè),并且我們從內(nèi)核的 boot time 或者 run time 配置好了大頁(yè)內(nèi)存池,我們終于可以在應(yīng)用程序中來(lái)使用大頁(yè)內(nèi)存了,內(nèi)核給我們提供了兩種方式來(lái)使用 HugePage:
一種是本文介紹的 mmap 系統(tǒng)調(diào)用,需要在 flags 參數(shù)中設(shè)置 MAP_HUGETLB。另外內(nèi)核提供了額外的兩個(gè)枚舉值來(lái)配合 MAP_HUGETLB 一起使用,它們分別是 MAP_HUGE_2MB 和 MAP_HUGE_1GB。
MAP_HUGETLB | MAP_HUGE_2MB 用于指定我們需要映射的是 2M 的大頁(yè)。
MAP_HUGETLB | MAP_HUGE_1GB 用于指定我們需要映射的是 1G 的大頁(yè)。
MAP_HUGETLB 表示按照 default_hugepagesz 指定的默認(rèn)尺寸來(lái)映射大頁(yè)。
另一種是 SYSV 標(biāo)準(zhǔn)的系統(tǒng)調(diào)用 shmget 和 shmat。
本小節(jié)我們主要介紹 mmap 系統(tǒng)調(diào)用使用大頁(yè)的方式:
intmain(void) { addr=mmap(addr,length,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB,-1,0); return0; }
MAP_HUGETLB 只能支持 MAP_ANONYMOUS 匿名映射的方式使用 HugePage
當(dāng)我們通過(guò) mmap 設(shè)置了 MAP_HUGETLB 進(jìn)行大頁(yè)內(nèi)存映射的時(shí)候,這個(gè)映射過(guò)程和普通的匿名映射一樣,同樣也是首先在進(jìn)程的虛擬內(nèi)存空間中劃分出一段虛擬映射區(qū) VMA 出來(lái),同樣不涉及物理內(nèi)存的分配,不一樣的地方是,內(nèi)核在分配完虛擬內(nèi)存之后,會(huì)在大頁(yè)內(nèi)存池中為映射的這段虛擬內(nèi)存預(yù)留好大頁(yè)內(nèi)存,相當(dāng)于是把即將要使用的大頁(yè)內(nèi)存先鎖定住,不允許其他進(jìn)程使用。這些被預(yù)留好的 HugePage 個(gè)數(shù)被記錄在上圖中的 resv_hugepages 文件中。
當(dāng)進(jìn)程在訪問(wèn)這段虛擬內(nèi)存的時(shí)候,同樣會(huì)發(fā)生缺頁(yè)中斷,隨后內(nèi)核會(huì)從大頁(yè)內(nèi)存池中將這部分已經(jīng)預(yù)留好的 resv_hugepages 分配給進(jìn)程,并在進(jìn)程頁(yè)表中建立好虛擬內(nèi)存與 HugePage 的映射。關(guān)于進(jìn)程頁(yè)表如何映射內(nèi)存大頁(yè)的詳細(xì)內(nèi)容,感興趣的同學(xué)可以回看下之前的文章 《一步一圖帶你構(gòu)建 Linux 頁(yè)表體系》。
由于這里我們調(diào)用 mmap 映射的是 HugePage ,所以系統(tǒng)調(diào)用參數(shù)中的 addr,length 需要和大頁(yè)尺寸進(jìn)行對(duì)齊,在本例中需要和 2M 進(jìn)行對(duì)齊。
前邊也提到了 MAP_HUGETLB 需要和 MAP_ANONYMOUS 配合一起使用,只能支持匿名映射的方式來(lái)使用 HugePage。那如果我們想使用 mmap 對(duì)文件進(jìn)行大頁(yè)映射該怎么辦呢 ?
這就用到了前面提到的 hugetlbfs 文件系統(tǒng):
hugetlbfs 是一個(gè)基于內(nèi)存的文件系統(tǒng),類似前邊介紹的 tmpfs 文件系統(tǒng),位于 hugetlbfs 文件系統(tǒng)下的所有文件都是被大頁(yè)支持的,也就說(shuō)通過(guò) mmap 對(duì) hugetlbfs 文件系統(tǒng)下的文件進(jìn)行文件映射,默認(rèn)都是用 HugePage 進(jìn)行映射。
hugetlbfs 下的文件支持大多數(shù)的文件系統(tǒng)操作,比如:open , close , chmod , read 等等,但是不支持 write 系統(tǒng)調(diào)用,如果想要對(duì) hugetlbfs 下的文件進(jìn)行寫(xiě)入操作,那么必須通過(guò)文件映射的方式將 hugetlbfs 中的文件通過(guò)大頁(yè)映射進(jìn)內(nèi)存,然后在映射內(nèi)存中進(jìn)行寫(xiě)入操作。
所以在我們使用 mmap 系統(tǒng)調(diào)用對(duì) hugetlbfs 下的文件進(jìn)行大頁(yè)映射之前,首先需要做的事情就是在系統(tǒng)中掛載 hugetlbfs 文件系統(tǒng)到指定的路徑下。
mount-thugetlbfs-ouid=,gid=,mode=,pagesize=,size=,min_size=,nr_inodes=none/mnt/huge
上面的這條命令用于將 hugetlbfs 掛載到 /mnt/huge 目錄下,從此以后只要是在 /mnt/huge 目錄下創(chuàng)建的文件,背后都是由大頁(yè)支持的,也就是說(shuō)如果我們通過(guò) mmap 系統(tǒng)調(diào)用對(duì) /mnt/huge 目錄下的文件進(jìn)行文件映射,缺頁(yè)的時(shí)候,內(nèi)核分配的就是內(nèi)存大頁(yè)。
只有在 hugetlbfs 下的文件進(jìn)行 mmap 文件映射的時(shí)候才能使用大頁(yè),其他普通文件系統(tǒng)下的文件依然只能映射普通 4K 內(nèi)存頁(yè)。
mount 命令中的 uid 和 gid 用于指定 hugetlbfs 根目錄的 owner 和 group。
pagesize 用于指定 hugetlbfs 支持的大頁(yè)尺寸,默認(rèn)單位是字節(jié),我們可以通過(guò)設(shè)置 pagesize=2M 或者 pagesize=1G 來(lái)指定 hugetlbfs 中的大頁(yè)尺寸為 2M 或者 1G。
size 用于指定 hugetlbfs 文件系統(tǒng)可以使用的最大內(nèi)存容量是多少,單位同 pagesize 一樣。
min_size 用于指定 hugetlbfs 文件系統(tǒng)可以使用的最小內(nèi)存容量是多少。
nr_inodes 用于指定 hugetlbfs 文件系統(tǒng)中 inode 的最大個(gè)數(shù),決定該文件系統(tǒng)中最大可以創(chuàng)建多少個(gè)文件。
當(dāng) hugetlbfs 被我們掛載好之后,接下來(lái)我們就可以直接通過(guò) mmap 系統(tǒng)調(diào)用對(duì)掛載目錄 /mnt/huge 下的文件進(jìn)行內(nèi)存映射了,當(dāng)缺頁(yè)的時(shí)候,內(nèi)核會(huì)直接分配大頁(yè),大頁(yè)尺寸是 pagesize。
intmain(void) { fd=open(“/mnt/huge/test.txt”,O_CREAT|O_RDWR); addr=mmap(0,MAP_LENGTH,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); return0; }
這里需要注意是,通過(guò) mmap 映射 hugetlbfs 中的文件的時(shí)候,并不需要指定 MAP_HUGETLB 。而我們通過(guò) SYSV 標(biāo)準(zhǔn)的系統(tǒng)調(diào)用 shmget 和 shmat 以及前邊介紹的 mmap ( flags 參數(shù)設(shè)置 MAP_HUGETLB)進(jìn)行大頁(yè)申請(qǐng)的時(shí)候,并不需要掛載 hugetlbfs。
在內(nèi)核中一共支持兩種類型的內(nèi)存大頁(yè),一種是標(biāo)準(zhǔn)大頁(yè)(hugetlb pages),也就是上面內(nèi)容所介紹的使用大頁(yè)的方式,我們可以通過(guò)命令 grep Huge /proc/meminfo 來(lái)查看標(biāo)準(zhǔn)大頁(yè)在系統(tǒng)中的使用情況:
和標(biāo)準(zhǔn)大頁(yè)相關(guān)的統(tǒng)計(jì)參數(shù)含義如下:
HugePages_Total 表示標(biāo)準(zhǔn)大頁(yè)池中大頁(yè)的個(gè)數(shù)。HugePages_Free 表示大頁(yè)池中還未被使用的大頁(yè)個(gè)數(shù)(未被分配)。
HugePages_Rsvd 表示大頁(yè)池中已經(jīng)被預(yù)留出來(lái)的大頁(yè),這個(gè)預(yù)留大頁(yè)是什么意思呢 ?我們知道 mmap 系統(tǒng)調(diào)用只是為進(jìn)程分配一段虛擬內(nèi)存而已,并不會(huì)分配物理內(nèi)存,當(dāng) mmap 進(jìn)行大頁(yè)映射的時(shí)候也是一樣。不同之處在于,內(nèi)核為進(jìn)程分配完虛擬內(nèi)存之后,還需要為進(jìn)程在大頁(yè)池中預(yù)留好本次映射所需要的大頁(yè)個(gè)數(shù),注意此時(shí)只是預(yù)留,還并未分配給進(jìn)程,大頁(yè)池中被預(yù)留好的大頁(yè)不能被其他進(jìn)程使用。這時(shí) HugePages_Rsvd 的個(gè)數(shù)會(huì)相應(yīng)增加,當(dāng)進(jìn)程發(fā)生缺頁(yè)的時(shí)候,內(nèi)核會(huì)直接從大頁(yè)池中把這些提前預(yù)留好的大頁(yè)內(nèi)存映射到進(jìn)程的虛擬內(nèi)存空間中。這時(shí) HugePages_Rsvd 的個(gè)數(shù)會(huì)相應(yīng)減少。系統(tǒng)中真正剩余可用的個(gè)數(shù)其實(shí)是 HugePages_Free - HugePages_Rsvd。
HugePages_Surp 表示大頁(yè)池中超額分配的大頁(yè)個(gè)數(shù),這個(gè)概念其實(shí)筆者前面在介紹 nr_overcommit_hugepages 參數(shù)的時(shí)候也提到過(guò),nr_overcommit_hugepages 參數(shù)表示最多能超額分配多少個(gè)大頁(yè)。當(dāng)大頁(yè)池中的大頁(yè)全部被耗盡的時(shí)候,也就是 /proc/sys/vm/nr_hugepages 指定的大頁(yè)個(gè)數(shù)全部被分配完了,內(nèi)核還可以超額為進(jìn)程分配大頁(yè),超額分配出的大頁(yè)個(gè)數(shù)就統(tǒng)計(jì)在 HugePages_Surp 中。
Hugepagesize 表示系統(tǒng)中大頁(yè)的默認(rèn) size 大小,單位為 KB。
Hugetlb 表示系統(tǒng)中所有尺寸的大頁(yè)所占用的物理內(nèi)存總量。單位為 KB。
內(nèi)核中另外一種類型的大頁(yè)是透明大頁(yè) THP (Transparent Huge Pages),這里的透明指的是應(yīng)用進(jìn)程在使用 THP 的時(shí)候完全是透明的,不需要像使用標(biāo)準(zhǔn)大頁(yè)那樣需要系統(tǒng)管理員對(duì)系統(tǒng)進(jìn)行顯示的大頁(yè)配置,在應(yīng)用程序中也不需要向標(biāo)準(zhǔn)大頁(yè)那樣需要顯示指定 MAP_HUGETLB , 或者顯示映射到 hugetlbfs 里的文件中。
透明大頁(yè)的使用對(duì)用戶完全是透明的,內(nèi)核會(huì)在背后為我們自動(dòng)做大頁(yè)的映射,透明大頁(yè)不需要像標(biāo)準(zhǔn)大頁(yè)那樣需要提前預(yù)先分配好大頁(yè)內(nèi)存池,透明大頁(yè)的分配是動(dòng)態(tài)的,由內(nèi)核線程 khugepaged 負(fù)責(zé)在背后默默地將普通 4K 內(nèi)存頁(yè)整理成內(nèi)存大頁(yè)給進(jìn)程使用。但是如果由于內(nèi)存碎片的因素,內(nèi)核無(wú)法整理出內(nèi)存大頁(yè),那么就會(huì)降級(jí)為使用普通 4K 內(nèi)存頁(yè)。但是透明大頁(yè)這里會(huì)有一個(gè)問(wèn)題,當(dāng)碎片化嚴(yán)重的時(shí)候,內(nèi)核會(huì)啟動(dòng) kcompactd 線程去整理碎片,期望獲得連續(xù)的內(nèi)存用于大頁(yè)分配,但是 compact 的過(guò)程可能會(huì)引起 sys cpu 飆高,應(yīng)用程序卡頓。
透明大頁(yè)是允許 swap 的,這一點(diǎn)和標(biāo)準(zhǔn)大頁(yè)不同,在內(nèi)存緊張需要 swap 的時(shí)候,透明大頁(yè)會(huì)被內(nèi)核默默拆分成普通 4K 內(nèi)存頁(yè),然后 swap out 到磁盤。
透明大頁(yè)只支持 2M 的大頁(yè),標(biāo)準(zhǔn)大頁(yè)可以支持 1G 的大頁(yè),透明大頁(yè)主要應(yīng)用于匿名內(nèi)存中,可以在 tmpfs 文件系統(tǒng)中使用。
在我們對(duì)比完了透明大頁(yè)與標(biāo)準(zhǔn)大頁(yè)之間的區(qū)別之后,我們現(xiàn)在來(lái)看一下如何使用透明大頁(yè),其實(shí)非常簡(jiǎn)單,我們可以通過(guò)修改 /sys/kernel/mm/transparent_hugepage/enabled 配置文件來(lái)選擇開(kāi)啟或者禁用透明大頁(yè):
always 表示系統(tǒng)全局開(kāi)啟透明大頁(yè) THP 功能。這意味著每個(gè)進(jìn)程都會(huì)去嘗試使用透明大頁(yè)。
never 表示系統(tǒng)全局關(guān)閉透明大頁(yè) THP 功能。進(jìn)程將永遠(yuǎn)不會(huì)使用透明大頁(yè)。
madvise 表示進(jìn)程如果想要使用透明大頁(yè),需要通過(guò) madvise 系統(tǒng)調(diào)用并設(shè)置參數(shù) advice 為 MADV_HUGEPAGE 來(lái)建議內(nèi)核,在 addr 到 addr+length 這片虛擬內(nèi)存區(qū)域中,需要使用透明大頁(yè)來(lái)映射。
#includeintmadvise(voidaddr,size_tlength,intadvice);
一般我們會(huì)首先使用 mmap 先映射一段虛擬內(nèi)存區(qū)域,然后通過(guò) madvise 建議內(nèi)核,將來(lái)在缺頁(yè)的時(shí)候,需要為這段虛擬內(nèi)存映射透明大頁(yè)。由于背后需要通過(guò)內(nèi)核線程 khugepaged 來(lái)不斷的掃描整理系統(tǒng)中的普通 4K 內(nèi)存頁(yè),然后將他們拼接成一個(gè)大頁(yè)來(lái)給進(jìn)程使用,其中涉及內(nèi)存整理和回收等耗時(shí)的操作,且這些操作會(huì)在內(nèi)存路徑中加鎖,而 khugepaged 內(nèi)核線程可能會(huì)在錯(cuò)誤的時(shí)間啟動(dòng)掃描和轉(zhuǎn)換大頁(yè)的操作,造成隨機(jī)不可控的性能下降。
另外一點(diǎn),透明大頁(yè)不像標(biāo)準(zhǔn)大頁(yè)那樣是提前預(yù)分配好的,透明大頁(yè)是在系統(tǒng)運(yùn)行時(shí)動(dòng)態(tài)分配的,在內(nèi)存緊張的時(shí)候,透明大頁(yè)和普通 4K 內(nèi)存頁(yè)的分配過(guò)程一樣,有可能會(huì)遇到直接內(nèi)存回收(direct reclaim)以及直接內(nèi)存整理(direct compaction),這些操作都是同步的并且非常耗時(shí),會(huì)對(duì)性能造成非常大的影響。
前面在 cat /proc/meminfo 命令中顯示的 AnonHugePages 就表示透明大頁(yè)在系統(tǒng)中的使用情況。另外我們可以通過(guò) cat /proc/pid/smaps | grep AnonHugePages 命令來(lái)查看某個(gè)進(jìn)程對(duì)透明大頁(yè)的使用情況。
總結(jié)
本文筆者從五個(gè)角度為大家詳細(xì)介紹了 mmap 的使用方法及其在內(nèi)核中的實(shí)現(xiàn)原理,這五個(gè)角度分別是:
私有匿名映射,其主要用于進(jìn)程申請(qǐng)?zhí)摂M內(nèi)存,以及初始化進(jìn)程虛擬內(nèi)存空間中的 BSS 段,堆,棧這些虛擬內(nèi)存區(qū)域。
私有文件映射,其核心特點(diǎn)是背后映射的文件頁(yè)在多進(jìn)程之間是讀共享的,多個(gè)進(jìn)程對(duì)各自虛擬內(nèi)存區(qū)的修改只能反應(yīng)到各自對(duì)應(yīng)的文件頁(yè)上,而且各自的修改在進(jìn)程之間是互不可見(jiàn)的,最重要的一點(diǎn)是這些修改均不會(huì)回寫(xiě)到磁盤文件中。我們可以利用這些特點(diǎn)來(lái)加載二進(jìn)制可執(zhí)行文件的 .text , .data section 到進(jìn)程虛擬內(nèi)存空間中的代碼段和數(shù)據(jù)段中。
共享文件映射,多進(jìn)程之間讀寫(xiě)共享(不會(huì)發(fā)生寫(xiě)時(shí)復(fù)制),常用于多進(jìn)程之間共享內(nèi)存(page cache),多進(jìn)程之間的通訊。
共享匿名映射,用于父子進(jìn)程之間共享內(nèi)存,父子進(jìn)程之間的通訊。父子進(jìn)程之間需要依賴 tmpfs 中的匿名文件來(lái)實(shí)現(xiàn)共享內(nèi)存。是一種特殊的共享文件映射。
大頁(yè)內(nèi)存映射,這里我們介紹了標(biāo)準(zhǔn)大頁(yè)與透明大頁(yè)兩種大頁(yè)類型的區(qū)別與聯(lián)系,以及他們各自的實(shí)現(xiàn)原理和使用方法。
審核編輯:劉清
-
JAVA
+關(guān)注
關(guān)注
20文章
2989瀏覽量
109726 -
BSS
+關(guān)注
關(guān)注
0文章
19瀏覽量
12407 -
Linux開(kāi)發(fā)
+關(guān)注
關(guān)注
0文章
39瀏覽量
7333 -
虛擬內(nèi)存
+關(guān)注
關(guān)注
0文章
78瀏覽量
8260
原文標(biāo)題:3 萬(wàn)字 + 40 張圖 | 拆解 mmap 內(nèi)存映射的本質(zhì)!
文章出處:【微信號(hào):小林coding,微信公眾號(hào):小林coding】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
編譯例程partition_mmap,報(bào)錯(cuò)no such vaddr range怎么解決?
Linux的mmap文件內(nèi)存映射機(jī)制
dma_alloc_coherent申請(qǐng)內(nèi)存的訪問(wèn)速度,請(qǐng)問(wèn)有什么辦法能加快訪問(wèn)mmap的DMA內(nèi)存?
mmap()函數(shù)映射到內(nèi)存中出現(xiàn)bus error的錯(cuò)誤
使用UARTLite IP如何找到內(nèi)存映射IO方法
在arm里怎樣實(shí)現(xiàn)mmap編寫(xiě)驅(qū)動(dòng)和應(yīng)用共享內(nèi)存呢
linux_mmap_access_performance
mmap系統(tǒng)調(diào)用和vmalloc獲取地址空間
mmap作為L(zhǎng)inux內(nèi)存管理的關(guān)鍵之一

linux drivers中的mmap實(shí)現(xiàn)
Linux的mmap文件內(nèi)存映射機(jī)制
淺析linux內(nèi)存映射原理

一文詳細(xì)了解mmap內(nèi)存映射
Linux應(yīng)用開(kāi)發(fā)之共享內(nèi)存
mmap原理詳解

評(píng)論