国产精品电影_久久视频免费_欧美日韩国产激情_成年人视频免费在线播放_日本久久亚洲电影_久久都是精品_66av99_九色精品美女在线_蜜臀a∨国产成人精品_冲田杏梨av在线_欧美精品在线一区二区三区_麻豆mv在线看

騰訊三面折戟,被mmap虐哭了

存儲(chǔ) 數(shù)據(jù)管理
正是因?yàn)槲覍?duì) mmap 的理解存在如此多的不足,才在面試中被這個(gè)問(wèn)題難住,最終與騰訊的 offer 失之交臂。這次的經(jīng)歷讓我深刻認(rèn)識(shí)到,在技術(shù)學(xué)習(xí)的道路上,容不得半點(diǎn)馬虎和一知半解,每一個(gè)知識(shí)點(diǎn)都可能成為決定成敗的關(guān)鍵。

在面試失敗后的日子里,我開(kāi)始瘋狂地查閱資料,惡補(bǔ) mmap 的相關(guān)知識(shí)。我這才發(fā)現(xiàn),mmap 原來(lái)是一種內(nèi)存映射文件的方法,它能將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤(pán)地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)應(yīng)關(guān)系 。

簡(jiǎn)單來(lái)說(shuō),通過(guò) mmap,進(jìn)程可以采用指針的方式讀寫(xiě)操作這一段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)回寫(xiě)臟頁(yè)面到對(duì)應(yīng)的文件磁盤(pán)上,這樣就完成了對(duì)文件的操作,而不必再頻繁調(diào)用 read、write 等系統(tǒng)調(diào)用函數(shù)。而且,內(nèi)核空間對(duì)這段區(qū)域的修改也能直接反映到用戶(hù)空間,從而實(shí)現(xiàn)不同進(jìn)程間的文件共享。這就好比是在進(jìn)程和文件之間搭建了一座直接溝通的橋梁,大大提高了數(shù)據(jù)交互的效率。接下來(lái),讓我們層層剖析,探尋mmap映射文件的真實(shí)情況 。

Part1.Mmap是什么?

在日常開(kāi)發(fā)中,磁盤(pán) I/O 性能常常成為系統(tǒng)性能的瓶頸。傳統(tǒng)的文件讀寫(xiě)方式,如使用 read 和 write 系統(tǒng)調(diào)用,存在著數(shù)據(jù)拷貝次數(shù)多、系統(tǒng)調(diào)用頻繁等問(wèn)題,這在處理大文件或高并發(fā) I/O 場(chǎng)景時(shí),會(huì)嚴(yán)重影響系統(tǒng)的性能和效率。而 mmap(Memory - Mapped Files),即內(nèi)存映射文件,正是為解決這些問(wèn)題而出現(xiàn)的一種高效的文件訪問(wèn)機(jī)制。

mmap 是一種內(nèi)存映射文件的方法,它將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤(pán)地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一映射關(guān)系。簡(jiǎn)單來(lái)說(shuō),就是讓進(jìn)程可以像訪問(wèn)內(nèi)存一樣去訪問(wèn)文件,而不需要頻繁地進(jìn)行系統(tǒng)調(diào)用和數(shù)據(jù)拷貝。通過(guò) mmap,文件被映射到進(jìn)程的虛擬地址空間,進(jìn)程可以直接通過(guò)指針操作這段內(nèi)存,而系統(tǒng)會(huì)自動(dòng)將修改后的內(nèi)容回寫(xiě)到文件磁盤(pán)上。這不僅簡(jiǎn)化了文件操作的編程模型,還大大提高了 I/O 操作的效率。

圖片圖片

其函數(shù)原型為:void *mmap (void start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void start, size_t length);。下面介紹一下內(nèi)存映射的步驟:

  • 用 open 系統(tǒng)調(diào)用打開(kāi)文件,并返回描述符 fd。
  • 用 mmap 建立內(nèi)存映射,并返回映射首地址指針 start。
  • 對(duì)映射(文件)進(jìn)行各種操作,如顯示(printf)、修改(sprintf)等。
  • 用 munmap (void *start, size_t length) 關(guān)閉內(nèi)存映射。
  • 用 close 系統(tǒng)調(diào)用關(guān)閉文件 fd。

Part2.mmap 的工作原理

首先創(chuàng)建虛擬區(qū)間并完成地址映射,此時(shí)還沒(méi)有將任何文件數(shù)據(jù)拷貝至主存。當(dāng)進(jìn)程發(fā)起讀寫(xiě)操作時(shí),會(huì)訪問(wèn)虛擬地址空間,通過(guò)查詢(xún)頁(yè)表,發(fā)現(xiàn)這段地址不在物理頁(yè)上,因?yàn)橹唤⒘说刂酚成洌嬲臄?shù)據(jù)還沒(méi)有拷貝到內(nèi)存,因此引發(fā)缺頁(yè)異常。缺頁(yè)異常經(jīng)過(guò)一系列判斷,確定無(wú)非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過(guò)程。

最終會(huì)調(diào)用nopage函數(shù)把所缺的頁(yè)從文件在磁盤(pán)里的地址拷貝到物理內(nèi)存。之后進(jìn)程便可以對(duì)這片主存進(jìn)行讀寫(xiě),如果寫(xiě)操作修改了內(nèi)容,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫(xiě)臟頁(yè)面到對(duì)應(yīng)的磁盤(pán)地址,完成了寫(xiě)入到文件的過(guò)程。另外,也可以調(diào)用msync()來(lái)強(qiáng)制同步,這樣所寫(xiě)的內(nèi)存就能立刻保存到文件中。

2.1虛擬地址與物理地址的映射機(jī)制

在深入了解 mmap 之前,我們需要先理解虛擬地址與物理地址的映射機(jī)制。以 64 位 CPU 為例,它采用的是 4 級(jí)頁(yè)表來(lái)實(shí)現(xiàn)虛擬地址到物理地址的映射 。雖然 64 位 CPU 虛擬地址長(zhǎng)度理論上為 64 位,但在實(shí)際應(yīng)用中,48 位就足以滿足虛擬地址映射物理內(nèi)存的需求。

這 48 位虛擬地址被細(xì)分為五個(gè)部分,分別是 pgd 表偏移(9 位,對(duì)應(yīng)四級(jí)表)、pud 表偏移(9 位,對(duì)應(yīng)三級(jí)表)、pmd 表偏移(9 位,對(duì)應(yīng)二級(jí)表)、ptl 表偏移(9 位,對(duì)應(yīng)一級(jí)表)以及物理頁(yè)偏移(12 位)。這里可能大家會(huì)有疑問(wèn),為什么 pgd、pud、pmd、ptl 表偏移是 9 位,而物理頁(yè)偏移是 12 位呢?

先來(lái)說(shuō)說(shuō) pgd、pud、pmd、ptl 表偏移。以 pgd 表為例,一張 pgd 表對(duì)應(yīng)一個(gè)物理頁(yè),而一個(gè)物理頁(yè)的大小通常為 4KB。每個(gè) pgd_t 表項(xiàng)占用 8 個(gè)字節(jié),通過(guò)計(jì)算可得一張 pgd 表能存儲(chǔ) 4*1024/8 = 512 個(gè)表項(xiàng)。因?yàn)?2 的 9 次方正好等于 512,所以采用 9 位的表偏移就能夠索引整張表的所有表項(xiàng) 。同理,pud、pmd、ptl 表也是基于相同的原理。

再看物理頁(yè)偏移,由于一個(gè)物理頁(yè)大小是 4KB,而物理頁(yè)訪問(wèn)是以單字節(jié)為單位的,2 的 12 次方恰好是 4KB,所以物理頁(yè)偏移設(shè)置為 12 位,這樣就能準(zhǔn)確地定位到物理頁(yè)內(nèi)的每一個(gè)字節(jié) 。

當(dāng)進(jìn)行虛擬地址到物理地址的映射時(shí),需要依次索引 pgd、pud、pmd、ptl 表。首先,查詢(xún) pgd 表,通過(guò) task_struct-> mm_struct-> pgd 成員找到 pgd 表物理頁(yè)首地址,再加上虛擬地址中的 pgd 表偏移,從而索引到 pgd_t 表項(xiàng),完成 pgd 表查詢(xún)。接著,pgd_t 表項(xiàng)中存儲(chǔ)的是 pud 表物理頁(yè)首地址,依此類(lèi)推,通過(guò)類(lèi)似的方式完成 pud 表、pmd 表和 ptl 表的查詢(xún)。最后,ptl 表項(xiàng)存儲(chǔ)的是物理頁(yè)首地址,將其與虛擬地址中的物理頁(yè)偏移相加,就能成功定位到物理地址 。

2.2mmap 的實(shí)現(xiàn)步驟

了解了虛擬地址與物理地址的映射機(jī)制后,我們?cè)賮?lái)看看 mmap 具體是如何實(shí)現(xiàn)的,它主要包含以下三個(gè)關(guān)鍵步驟:

⑴進(jìn)程啟動(dòng)映射過(guò)程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域

進(jìn)程在用戶(hù)空間調(diào)用庫(kù)函數(shù) mmap,其函數(shù)原型為void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset)。這里的 start 參數(shù)指向欲映射的內(nèi)存起始地址,通常設(shè)為 NULL,讓系統(tǒng)自動(dòng)選定地址;length 代表將文件中多大的部分映射到內(nèi)存;prot 指定映射區(qū)域的保護(hù)方式,如 PROT_READ 表示映射區(qū)域可被讀取,PROT_WRITE 表示可被寫(xiě)入等;flags 影響映射區(qū)域的各種特性,調(diào)用時(shí)必須指定 MAP_SHARED 或 MAP_PRIVATE 等;fd 是要映射到內(nèi)存中的文件描述符;offset 為文件映射的偏移量,通常設(shè)為 0 。

調(diào)用 mmap 后,系統(tǒng)會(huì)在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的、滿足要求的連續(xù)虛擬地址。然后,為這片虛擬地址分配一個(gè)vm_area_struct結(jié)構(gòu),這個(gè)結(jié)構(gòu)就像是一個(gè) “管家”,用于描述虛擬內(nèi)存區(qū)域的各種屬性,包括起始地址(vm_start)、結(jié)束地址(vm_end,vm_end 減去 vm_start 即為映射區(qū)域長(zhǎng)度)、虛擬內(nèi)存訪問(wèn)權(quán)限(vm_page_prot ,如 PROT_READ、PROT_WRITE 等)、內(nèi)存映射標(biāo)志(vm_page_flags ,如 MAP_SHARED 共享映射、MAP_PRIVATE 私有映射)等。完成初始化后,將這個(gè)新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹(shù)中,方便后續(xù)的管理和訪問(wèn) 。

⑵調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶(hù)空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系

完成虛擬映射區(qū)域的創(chuàng)建后,接下來(lái)就要建立文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系。此時(shí),會(huì)調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù) mmap(注意,此 mmap 函數(shù)不同于用戶(hù)空間函數(shù))。通過(guò)待映射的文件指針,在文件描述符表中找到對(duì)應(yīng)的文件描述符,進(jìn)而鏈接到內(nèi)核 “已打開(kāi)文件集” 中該文件的文件結(jié)構(gòu)體(struct file),這個(gè)文件結(jié)構(gòu)體維護(hù)著與該已打開(kāi)文件相關(guān)的各項(xiàng)信息 。

接著,通過(guò)該文件的文件結(jié)構(gòu)體,鏈接到 file_operations 模塊,調(diào)用內(nèi)核函數(shù) mmap,其原型為int mmap(struct file *filp, struct vm_area_struct *vma) 。內(nèi)核 mmap 函數(shù)會(huì)通過(guò)虛擬文件系統(tǒng) inode 模塊定位到文件磁盤(pán)物理地址,再通過(guò) remap_pfn_range 函數(shù)建立頁(yè)表,最終實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的映射關(guān)系 。不過(guò),此時(shí)僅僅是建立了地址映射,真正的數(shù)據(jù)還沒(méi)有拷貝到內(nèi)存中 。

⑶進(jìn)程發(fā)起對(duì)這片映射空間的訪問(wèn),引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

注:前兩個(gè)階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒(méi)有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r(shí)。

當(dāng)進(jìn)程發(fā)起對(duì)這片映射空間的訪問(wèn)時(shí),如果訪問(wèn)的虛擬地址對(duì)應(yīng)的物理頁(yè)面還未加載到內(nèi)存中,就會(huì)引發(fā)缺頁(yè)異常 。缺頁(yè)異常會(huì)進(jìn)行一系列判斷,確定無(wú)非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過(guò)程。調(diào)頁(yè)過(guò)程會(huì)先在交換緩存空間(swap cache中尋找需要訪問(wèn)的內(nèi)存頁(yè),如果沒(méi)有找到,則調(diào)用nopage函數(shù)把所缺的頁(yè)從磁盤(pán)裝入到主存中 。

一旦數(shù)據(jù)被裝入主存,進(jìn)程就可以對(duì)這片主存進(jìn)行正常的讀或?qū)懖僮鳌H绻麑?xiě)操作改變了數(shù)據(jù)內(nèi)容,這些被修改的頁(yè)面會(huì)被標(biāo)記為 “臟頁(yè)面”。系統(tǒng)并不會(huì)立即將臟頁(yè)面回寫(xiě)到磁盤(pán),而是會(huì)在一定時(shí)間后自動(dòng)回寫(xiě),當(dāng)然,也可以調(diào)用 msync 函數(shù)來(lái)強(qiáng)制同步,讓數(shù)據(jù)立即保存到文件里 。

Part3.mmap的 I/O模型

3.1基礎(chǔ)概念

mmap 也是一種零拷貝技術(shù)。傳統(tǒng)的 I/O 操作,數(shù)據(jù)往往需要在用戶(hù)空間和內(nèi)核空間多次拷貝,比如從磁盤(pán)讀取數(shù)據(jù)到內(nèi)核緩沖區(qū),再?gòu)膬?nèi)核緩沖區(qū)復(fù)制到用戶(hù)緩沖區(qū),之后若要通過(guò)網(wǎng)絡(luò)發(fā)送,又得從用戶(hù)緩沖區(qū)拷貝到內(nèi)核的套接字緩沖區(qū),最后經(jīng)網(wǎng)卡發(fā)送出去,這一過(guò)程伴隨著多次上下文切換和數(shù)據(jù)拷貝,耗費(fèi)大量 CPU 資源和時(shí)間。,其 I/O 模型如下圖所示:

圖片圖片

#include <sys/mman.h>
void *mmap(
void *start,
size_t length,
int prot,
int flags,
int fd, off_t offset
)

mmap 技術(shù)有如下特點(diǎn):

  • 利用 DMA 技術(shù)來(lái)取代 CPU 來(lái)在內(nèi)存與其他組件之間的數(shù)據(jù)拷貝,例如從磁盤(pán)到內(nèi)存,從內(nèi)存到網(wǎng)卡;
  • 用戶(hù)空間的 mmap file 使用虛擬內(nèi)存,實(shí)際上并不占據(jù)物理內(nèi)存,只有在內(nèi)核空間的 kernel buffer cache 才占據(jù)實(shí)際的物理內(nèi)存;
  • mmap() 函數(shù)需要配合 write() 系統(tǒng)調(diào)動(dòng)進(jìn)行配合操作,這與 sendfile() 函數(shù)有所不同,后者一次性代替了 read() 以及 write();因此 mmap 也至少需要 4 次上下文切換;
  • mmap 僅僅能夠避免內(nèi)核空間到用戶(hù)空間的全程 CPU 負(fù)責(zé)的數(shù)據(jù)拷貝,但是內(nèi)核空間內(nèi)部還是需要全程 CPU 負(fù)責(zé)的數(shù)據(jù)拷貝;

利用 mmap() 替換 read(),配合 write() 調(diào)用的整個(gè)流程如下:

  • 用戶(hù)進(jìn)程調(diào)用 mmap(),從用戶(hù)態(tài)陷入內(nèi)核態(tài),將內(nèi)核緩沖區(qū)映射到用戶(hù)緩存區(qū);
  • DMA 控制器將數(shù)據(jù)從硬盤(pán)拷貝到內(nèi)核緩沖區(qū)(可見(jiàn)其使用了 Page Cache 機(jī)制);
  • mmap() 返回,上下文從內(nèi)核態(tài)切換回用戶(hù)態(tài);
  • 用戶(hù)進(jìn)程調(diào)用 write(),嘗試把文件數(shù)據(jù)寫(xiě)到內(nèi)核里的套接字緩沖區(qū),再次陷入內(nèi)核態(tài);
  • CPU 將內(nèi)核緩沖區(qū)中的數(shù)據(jù)拷貝到的套接字緩沖區(qū);
  • DMA 控制器將數(shù)據(jù)從套接字緩沖區(qū)拷貝到網(wǎng)卡完成數(shù)據(jù)傳輸;
  • write() 返回,上下文從內(nèi)核態(tài)切換回用戶(hù)態(tài)。

通過(guò)mmap實(shí)現(xiàn)的零拷貝I/O進(jìn)行了4次用戶(hù)空間與內(nèi)核空間的上下文切換,以及3次數(shù)據(jù)拷貝;其中3次數(shù)據(jù)拷貝中包括了2次DMA拷貝和1次CPU拷貝。

3.2 mmap與常規(guī)文件操作的差異對(duì)比

當(dāng)我們使用常規(guī)文件操作,調(diào)用 read/fread 等函數(shù)來(lái)讀取文件時(shí),數(shù)據(jù)需要經(jīng)歷一個(gè)較為復(fù)雜的拷貝過(guò)程。首先,數(shù)據(jù)會(huì)從磁盤(pán)被拷貝到內(nèi)核的頁(yè)緩存中,這是為了提高讀寫(xiě)效率和保護(hù)磁盤(pán)而采用的頁(yè)緩存機(jī)制 。然而,由于頁(yè)緩存處于內(nèi)核空間,用戶(hù)進(jìn)程無(wú)法直接尋址,所以還需要將頁(yè)緩存中的數(shù)據(jù)頁(yè)再次拷貝到內(nèi)存對(duì)應(yīng)的用戶(hù)空間中。也就是說(shuō),僅僅是讀取文件內(nèi)容,就需要進(jìn)行兩次數(shù)據(jù)拷貝,才能完成進(jìn)程對(duì)文件內(nèi)容的獲取任務(wù) 。

而寫(xiě)操作也是類(lèi)似的情況,待寫(xiě)入的 buffer 在內(nèi)核空間無(wú)法直接訪問(wèn),必須先從用戶(hù)空間拷貝至內(nèi)核空間對(duì)應(yīng)的主存,之后再寫(xiě)回磁盤(pán),同樣需要兩次數(shù)據(jù)拷貝 。并且,在這個(gè)過(guò)程中,每次進(jìn)行 read 或 write 操作,都需要進(jìn)行系統(tǒng)調(diào)用,這會(huì)導(dǎo)致用戶(hù)態(tài)和內(nèi)核態(tài)的上下文切換,頻繁的上下文切換也會(huì)消耗一定的系統(tǒng)資源 。

再看 mmap,它在操作文件時(shí),創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤(pán)地址和虛擬內(nèi)存區(qū)域映射這兩步,并沒(méi)有任何文件拷貝操作。當(dāng)進(jìn)程后續(xù)訪問(wèn)數(shù)據(jù)時(shí),如果發(fā)現(xiàn)內(nèi)存中并無(wú)數(shù)據(jù)而發(fā)起缺頁(yè)異常過(guò)程,由于已經(jīng)建立好的映射關(guān)系,此時(shí)只需要一次數(shù)據(jù)拷貝,就能從磁盤(pán)中將數(shù)據(jù)傳入內(nèi)存的用戶(hù)空間中,供進(jìn)程使用 。而且,mmap 只需要一次映射操作,之后進(jìn)程就可以像操作內(nèi)存一樣讀寫(xiě)數(shù)據(jù),大大減少了系統(tǒng)調(diào)用的次數(shù) 。

通過(guò)下面這張圖,我們可以更加直觀地看到兩者的差異:

操作類(lèi)型

常規(guī)文件操作

mmap 操作

數(shù)據(jù)拷貝次數(shù)

讀:磁盤(pán)到頁(yè)緩存,頁(yè)緩存到用戶(hù)空間,共 2 次;寫(xiě):用戶(hù)空間到內(nèi)核空間,內(nèi)核空間到磁盤(pán),共 2 次

讀:磁盤(pán)到用戶(hù)空間,1 次;寫(xiě):用戶(hù)空間修改,系統(tǒng)自動(dòng)回寫(xiě),無(wú)需額外拷貝(可調(diào)用 msync 強(qiáng)制同步)

系統(tǒng)調(diào)用次數(shù)

每次讀寫(xiě)都需調(diào)用 read/write 等函數(shù),系統(tǒng)調(diào)用頻繁

僅在映射時(shí)調(diào)用一次 mmap 函數(shù),后續(xù)像操作內(nèi)存一樣讀寫(xiě),系統(tǒng)調(diào)用少

上下文切換次數(shù)

每次系統(tǒng)調(diào)用伴隨用戶(hù)態(tài)與內(nèi)核態(tài)上下文切換,次數(shù)多

映射時(shí)一次上下文切換,后續(xù)操作少,上下文切換次數(shù)少

由此可見(jiàn),mmap 通過(guò)減少數(shù)據(jù)拷貝次數(shù)和上下文切換次數(shù),在性能上具有明顯的優(yōu)勢(shì),尤其是在處理大文件或者需要頻繁進(jìn)行文件 I/O 操作的場(chǎng)景下,這種優(yōu)勢(shì)更加突出 。

3.3mmap不是銀彈

mmap 不是銀彈,這意味著 mmap 也有其缺陷,在相關(guān)場(chǎng)景下的性能存在缺陷:

  • 由于 MMAP 使用時(shí)必須實(shí)現(xiàn)指定好內(nèi)存映射的大小,因此 mmap 并不適合變長(zhǎng)文件;
  • 如果更新文件的操作很多,mmap 避免兩態(tài)拷貝的優(yōu)勢(shì)就被攤還,最終還是落在了大量的臟頁(yè)回寫(xiě)及由此引發(fā)的隨機(jī) I/O 上,所以在隨機(jī)寫(xiě)很多的情況下,mmap 方式在效率上不一定會(huì)比帶緩沖區(qū)的一般寫(xiě)快;
  • 讀/寫(xiě)小文件(例如 16K 以下的文件),mmap 與通過(guò) read 系統(tǒng)調(diào)用相比有著更高的開(kāi)銷(xiāo)與延遲;同時(shí) mmap 的刷盤(pán)由系統(tǒng)全權(quán)控制,但是在小數(shù)據(jù)量的情況下由應(yīng)用本身手動(dòng)控制更好;
  • mmap 受限于操作系統(tǒng)內(nèi)存大小:例如在 32-bits 的操作系統(tǒng)上,虛擬內(nèi)存總大小也就 2GB,但由于 mmap 必須要在內(nèi)存中找到一塊連續(xù)的地址塊,此時(shí)你就無(wú)法對(duì) 4GB 大小的文件完全進(jìn)行 mmap,在這種情況下你必須分多塊分別進(jìn)行 mmap,但是此時(shí)地址內(nèi)存地址已經(jīng)不再連續(xù),使用 mmap 的意義大打折扣,而且引入了額外的復(fù)雜性;

Part4.mmap技術(shù)的優(yōu)勢(shì)

4.1簡(jiǎn)化用戶(hù)進(jìn)程編程

在用戶(hù)空間看來(lái),通過(guò) mmap 機(jī)制以后,磁盤(pán)上的文件仿佛直接就在內(nèi)存中,把訪問(wèn)磁盤(pán)文件簡(jiǎn)化為按地址訪問(wèn)內(nèi)存。這樣一來(lái),應(yīng)用程序自然不需要使用文件系統(tǒng)的 write(寫(xiě)入)、read(讀取)、fsync(同步)等系統(tǒng)調(diào)用,因?yàn)楝F(xiàn)在只要面向內(nèi)存的虛擬空間進(jìn)行開(kāi)發(fā)。但是,這并不意味著我們不再需要進(jìn)行這些系統(tǒng)調(diào)用,而是說(shuō)這些系統(tǒng)調(diào)用由操作系統(tǒng)在 mmap 機(jī)制的內(nèi)部封裝好了。

①基于缺頁(yè)異常的懶加載

出于節(jié)約物理內(nèi)存以及 mmap 方法快速返回的目的,mmap 映射采用懶加載機(jī)制。具體來(lái)說(shuō),通過(guò) mmap 申請(qǐng) 1000G 內(nèi)存可能僅僅占用了 100MB 的虛擬內(nèi)存空間,甚至沒(méi)有分配實(shí)際的物理內(nèi)存空間。當(dāng)你訪問(wèn)相關(guān)內(nèi)存地址時(shí),才會(huì)進(jìn)行真正的 write、read 等系統(tǒng)調(diào)用。CPU 會(huì)通過(guò)陷入缺頁(yè)異常的方式來(lái)將磁盤(pán)上的數(shù)據(jù)加載到物理內(nèi)存中,此時(shí)才會(huì)發(fā)生真正的物理內(nèi)存分配。

②數(shù)據(jù)一致性由 OS 確保

當(dāng)發(fā)生數(shù)據(jù)修改時(shí),內(nèi)存出現(xiàn)臟頁(yè),與磁盤(pán)文件出現(xiàn)不一致。mmap 機(jī)制下由操作系統(tǒng)自動(dòng)完成內(nèi)存數(shù)據(jù)落盤(pán)(臟頁(yè)回刷),用戶(hù)進(jìn)程通常并不需要手動(dòng)管理數(shù)據(jù)落盤(pán)。

4.2避免只讀操作時(shí)的 swap 操作

虛擬內(nèi)存帶來(lái)了種種好處,但是一個(gè)最大的問(wèn)題在于所有進(jìn)程的虛擬內(nèi)存大小總和可能大于物理內(nèi)存總大小,因此當(dāng)操作系統(tǒng)物理內(nèi)存不夠用時(shí),就會(huì)把一部分內(nèi)存 swap 到磁盤(pán)上。

在 mmap 下,如果虛擬空間沒(méi)有發(fā)生寫(xiě)操作,那么由于通過(guò) mmap 操作得到的內(nèi)存數(shù)據(jù)完全可以通過(guò)再次調(diào)用 mmap 操作映射文件得到。但是,通過(guò)其他方式分配的內(nèi)存,在沒(méi)有發(fā)生寫(xiě)操作的情況下,操作系統(tǒng)并不知道如何簡(jiǎn)單地從現(xiàn)有文件中(除非其重新執(zhí)行一遍應(yīng)用程序,但是代價(jià)很大)恢復(fù)內(nèi)存數(shù)據(jù),因此必須將內(nèi)存 swap 到磁盤(pán)上。

(1)高效的 I/O 操作方式,尤其在處理大文件或頻繁訪問(wèn)文件內(nèi)容時(shí)性能優(yōu)勢(shì)明顯。

在 Linux 系統(tǒng)中,mmap 是一種非常高效的 I/O 操作方式。當(dāng)處理大文件或需要頻繁訪問(wèn)文件內(nèi)容時(shí),能夠帶來(lái)很大的性能優(yōu)勢(shì)。例如,當(dāng)一個(gè)進(jìn)程通過(guò) mmap 映射一個(gè)文件時(shí),操作系統(tǒng)會(huì)在進(jìn)程的地址空間中創(chuàng)建一個(gè)映射區(qū)域,使得進(jìn)程可以直接訪問(wèn)這個(gè)文件而不需要進(jìn)行 read 或 write 系統(tǒng)調(diào)用。這種直接內(nèi)存訪問(wèn)的方式,避免了傳統(tǒng)文件訪問(wèn)中多次系統(tǒng)調(diào)用和數(shù)據(jù)復(fù)制的開(kāi)銷(xiāo),提高了文件訪問(wèn)的效率。

(2)減少 CPU 和內(nèi)存開(kāi)銷(xiāo),具有更好的內(nèi)核態(tài)數(shù)據(jù)傳輸效率。

mmap 技術(shù)可以減少 CPU 和內(nèi)存的開(kāi)銷(xiāo)。它通過(guò)將文件或設(shè)備映射到進(jìn)程的地址空間中,實(shí)現(xiàn)了直接內(nèi)存訪問(wèn),避免了內(nèi)核緩沖區(qū)和用戶(hù)空間緩沖區(qū)之間的數(shù)據(jù)復(fù)制。此外,mmap 還具有更好的內(nèi)核態(tài)數(shù)據(jù)傳輸效率,有助于減少數(shù)據(jù)傳輸時(shí)的內(nèi)存拷貝。例如,在 Kafka 中,Consumer 端對(duì)稀疏索引的操作使用了 mmap,將稀疏索引文件進(jìn)行內(nèi)存映射,不會(huì)招致系統(tǒng)調(diào)用以及額外的內(nèi)存復(fù)制開(kāi)銷(xiāo),從而提高了文件讀取效率。

(3)提升系統(tǒng)整體性能,改善用戶(hù)體驗(yàn)。

合理地利用 mmap 技術(shù),能夠提升系統(tǒng)的整體性能,改善用戶(hù)體驗(yàn)。在開(kāi)發(fā)應(yīng)用程序時(shí),可以考慮使用 mmap 技術(shù)來(lái)加速文件訪問(wèn)、減少內(nèi)存拷貝、提高數(shù)據(jù)傳輸效率等方面。例如,在處理大文件時(shí),mmap 可以不用把全部數(shù)據(jù)都加載到內(nèi)存,可以通過(guò) MappedByteBuffer 的 position 來(lái)設(shè)置獲取數(shù)據(jù)的位置,還可以使用虛擬內(nèi)存來(lái)映射超過(guò)物理內(nèi)存大小的大文件。同時(shí),mmap 也支持多進(jìn)程訪問(wèn)和文件的共享,多個(gè)進(jìn)程可以共享同一個(gè)文件的內(nèi)容,從而減少內(nèi)存的使用,提高系統(tǒng)的性能。

Part5.mmap技術(shù)的應(yīng)用場(chǎng)景

5.1內(nèi)存映射 I/O,加速文件讀寫(xiě)操作,適合處理大文件。

mmap 可以將文件直接映射到進(jìn)程的虛擬地址空間,避免了傳統(tǒng)文件讀寫(xiě)中的多次系統(tǒng)調(diào)用和數(shù)據(jù)拷貝。在處理大文件時(shí),這種方式尤其有效。例如,當(dāng)需要對(duì)一個(gè)大型數(shù)據(jù)文件進(jìn)行頻繁的讀寫(xiě)操作時(shí),使用 mmap 可以大大提高效率。通過(guò)內(nèi)存映射,進(jìn)程可以像訪問(wèn)內(nèi)存一樣訪問(wèn)文件數(shù)據(jù),減少了磁盤(pán) I/O 的開(kāi)銷(xiāo)。

參考資料中提到,進(jìn)程讀寫(xiě)數(shù)據(jù)時(shí),使用 mmap 進(jìn)行文件映射可以減少一次拷貝操作。磁盤(pán)文件直接加載到用戶(hù)空間,進(jìn)程可以通過(guò)指針直接操作文件,理論上比傳統(tǒng)的 read 和 write 操作要快。雖然在讀寫(xiě)過(guò)程中可能會(huì)觸發(fā)大量中斷,但對(duì)于大文件的處理,mmap 仍然具有很大的優(yōu)勢(shì)。

5.2進(jìn)程間通信,多個(gè)進(jìn)程可通過(guò)共享內(nèi)存實(shí)現(xiàn)快速通信。

多個(gè)進(jìn)程可以通過(guò)共享內(nèi)存的方式,使用 mmap 來(lái)共享內(nèi)存段,實(shí)現(xiàn)進(jìn)程間快速通信。例如,在父子進(jìn)程或無(wú)親緣關(guān)系的進(jìn)程中,都可以將自身用戶(hù)空間映射到同一個(gè)文件或匿名映射到同一片區(qū)域,從而實(shí)現(xiàn)進(jìn)程間通信。

參考資料中提到,在進(jìn)程間通信的場(chǎng)景下,可以使用 mmap 將文件映射到內(nèi)存,多個(gè)進(jìn)程通過(guò)對(duì)同一文件的讀寫(xiě)達(dá)到進(jìn)程間通信的目的。同時(shí),共享匿名內(nèi)存也可以讓相關(guān)進(jìn)程共享一塊內(nèi)存區(qū)域,通常用于父子進(jìn)程。

5.3內(nèi)存分配,匿名映射可提供比 malloc 更靈活的內(nèi)存管理機(jī)制。

當(dāng)需要大塊的內(nèi)存,或者特定對(duì)齊要求的內(nèi)存時(shí),mmap 的匿名映射可以提供比 malloc 更靈活的內(nèi)存管理機(jī)制。例如,當(dāng)需要分配的內(nèi)存大于一定閾值(如 128KB)時(shí),glibc 會(huì)默認(rèn)使用 mmap 代替 brk 來(lái)分配內(nèi)存。

私有匿名映射最常見(jiàn)的用途是在 glibc 分配大塊的內(nèi)存中。同時(shí),共享匿名映射也可以讓相關(guān)進(jìn)程共享一塊內(nèi)存區(qū)域,為內(nèi)存分配提供了更多的靈活性。

Part6如何使用mmap技術(shù)

6.1mmap使用細(xì)節(jié)

使用mmap需要注意的一個(gè)關(guān)鍵點(diǎn)是,mmap映射區(qū)域大小必須是物理頁(yè)大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))。原因是,內(nèi)存的最小粒度是頁(yè),而進(jìn)程虛擬地址空間和內(nèi)存的映射也是以頁(yè)為單位。為了匹配內(nèi)存的操作,mmap從磁盤(pán)到虛擬地址空間的映射也必須是頁(yè)。

內(nèi)核可以跟蹤被內(nèi)存映射的底層對(duì)象(文件)的大小,進(jìn)程可以合法的訪問(wèn)在當(dāng)前文件大小以?xún)?nèi)又在內(nèi)存映射區(qū)以?xún)?nèi)的那些字節(jié)。也就是說(shuō),如果文件的大小一直在擴(kuò)張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù),進(jìn)程都可以合法得到,這和映射建立時(shí)文件的大小無(wú)關(guān)。

映射建立之后,即使文件關(guān)閉,映射依然存在。因?yàn)橛成涞氖谴疟P(pán)的地址,不是文件本身,和文件句柄無(wú)關(guān)。同時(shí)可用于進(jìn)程間通信的有效地址空間不完全受限于被映射文件的大小,因?yàn)槭前错?yè)映射。在上面的知識(shí)前提下,我們下面看看如果大小不是頁(yè)的整倍數(shù)的具體情況:

情形一:一個(gè)文件的大小是5000字節(jié),mmap函數(shù)從一個(gè)文件的起始位置開(kāi)始,映射5000字節(jié)到虛擬內(nèi)存中。

分析:因?yàn)閱挝晃锢眄?yè)面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié),但是對(duì)應(yīng)到進(jìn)程虛擬地址區(qū)域的大小需要滿足整頁(yè)大小,因此mmap函數(shù)執(zhí)行后,實(shí)際映射到虛擬內(nèi)存區(qū)域8192個(gè) 字節(jié),5000~8191的字節(jié)部分用零填充。映射后的對(duì)應(yīng)關(guān)系如下圖所示:

圖片圖片

此時(shí):(1)讀/寫(xiě)前5000個(gè)字節(jié)(0~4999),會(huì)返回操作文件內(nèi)容。(2)讀字節(jié)50008191時(shí),結(jié)果全為0。寫(xiě)50008191時(shí),進(jìn)程不會(huì)報(bào)錯(cuò),但是所寫(xiě)的內(nèi)容不會(huì)寫(xiě)入原文件中 。(3)讀/寫(xiě)8192以外的磁盤(pán)部分,會(huì)返回一個(gè)SIGSECV錯(cuò)誤。

情形二:一個(gè)文件的大小是5000字節(jié),mmap函數(shù)從一個(gè)文件的起始位置開(kāi)始,映射15000字節(jié)到虛擬內(nèi)存中,即映射大小超過(guò)了原始文件的大小。

分析:由于文件的大小是5000字節(jié),和情形一一樣,其對(duì)應(yīng)的兩個(gè)物理頁(yè)。那么這兩個(gè)物理頁(yè)都是合法可以讀寫(xiě)的,只是超出5000的部分不會(huì)體現(xiàn)在原文件中。由于程序要求映射15000字節(jié),而文件只占兩個(gè)物理頁(yè),因此8192字節(jié)~15000字節(jié)都不能讀寫(xiě),操作時(shí)會(huì)返回異常。如下圖所示:

圖片圖片

此時(shí):(1)進(jìn)程可以正常讀/寫(xiě)被映射的前5000字節(jié)(0~4999),寫(xiě)操作的改動(dòng)會(huì)在一定時(shí)間后反映在原文件中。(2)對(duì)于5000~8191字節(jié),進(jìn)程可以進(jìn)行讀寫(xiě)過(guò)程,不會(huì)報(bào)錯(cuò)。但是內(nèi)容在寫(xiě)入前均為0,另外,寫(xiě)入后不會(huì)反映在文件中。(3)對(duì)于8192~14999字節(jié),進(jìn)程不能對(duì)其進(jìn)行讀寫(xiě),會(huì)報(bào)SIGBUS錯(cuò)誤。(4)對(duì)于15000以外的字節(jié),進(jìn)程不能對(duì)其讀寫(xiě),會(huì)引發(fā)SIGSEGV錯(cuò)誤。

情形三:一個(gè)文件初始大小為0,使用mmap操作映射了10004K的大小,即1000個(gè)物理頁(yè)大約4M字節(jié)空間,mmap返回指針ptr。

分析:如果在映射建立之初,就對(duì)文件進(jìn)行讀寫(xiě)操作,由于文件大小為0,并沒(méi)有合法的物理頁(yè)對(duì)應(yīng),如同情形二一樣,會(huì)返回SIGBUS錯(cuò)誤。但是如果,每次操作ptr讀寫(xiě)前,先增加文件的大小,那么ptr在文件大小內(nèi)部的操作就是合法的。例如,文件擴(kuò)充4096字節(jié),ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴(kuò)充的范圍在1000個(gè)物理頁(yè)(映射范圍)內(nèi),ptr都可以對(duì)應(yīng)操作相同的大小。這樣,方便隨時(shí)擴(kuò)充文件空間,隨時(shí)寫(xiě)入文件,不造成空間浪費(fèi)。

6.2函數(shù)定義及參數(shù)解釋

在 Linux 中,mmap 函數(shù)定義如下:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);。參數(shù)解釋如下:

  • addr:希望映射的起始地址,通常為 NULL,表示由內(nèi)核決定映射的地址。
  • length:映射區(qū)域的大小(以字節(jié)為單位)。
  • prot:映射區(qū)域的保護(hù)權(quán)限,決定映射的頁(yè)面是否可讀、可寫(xiě)等。常見(jiàn)的權(quán)限選項(xiàng)包括:PROT_READ(可讀)、PROT_WRITE(可寫(xiě))、PROT_EXEC(可執(zhí)行)、PROT_NONE(無(wú)權(quán)限)。
  • flags:映射的類(lèi)型和行為控制。常見(jiàn)的標(biāo)志包括:MAP_SHARED(共享映射,對(duì)該內(nèi)存的修改會(huì)同步到文件)、MAP_PRIVATE(私有映射,對(duì)該內(nèi)存的修改不會(huì)影響原文件,寫(xiě)時(shí)拷貝)、MAP_ANONYMOUS(匿名映射,不涉及文件,通常用于分配未初始化的內(nèi)存)。
  • fd:文件描述符,指向要映射的文件。如果使用匿名映射,應(yīng)將 fd 設(shè)置為 -1,并且需要設(shè)置 MAP_ANONYMOUS 標(biāo)志。
  • offset:文件映射的偏移量,必須是頁(yè)面大小的整數(shù)倍(通常為 4096 字節(jié))。

返回值:返回映射區(qū)域的起始地址,如果映射失敗,則返回 MAP_FAILED。

6.3mmap映射

在內(nèi)存映射的過(guò)程中,并沒(méi)有實(shí)際的數(shù)據(jù)拷貝,文件沒(méi)有被載入內(nèi)存,只是邏輯上被放入了內(nèi)存,具體到代碼,就是建立并初始化了相關(guān)的數(shù)據(jù)結(jié)構(gòu)(struct address_space),這個(gè)過(guò)程有系統(tǒng)調(diào)用mmap()實(shí)現(xiàn),所以建立內(nèi)存映射的效率很高。既然建立內(nèi)存映射沒(méi)有進(jìn)行實(shí)際的數(shù)據(jù)拷貝,那么進(jìn)程又怎么能最終直接通過(guò)內(nèi)存操作訪問(wèn)到硬盤(pán)上的文件呢?

那就要看內(nèi)存映射之后的幾個(gè)相關(guān)的過(guò)程了。mmap()會(huì)返回一個(gè)指針ptr,它指向進(jìn)程邏輯地址空間中的一個(gè)地址,這樣以后,進(jìn)程無(wú)需再調(diào)用read或write對(duì)文件進(jìn)行讀寫(xiě),而只需要通過(guò)ptr就能夠操作文件。但是ptr所指向的是一個(gè)邏輯地址,要操作其中的數(shù)據(jù),必須通過(guò)MMU將邏輯地址轉(zhuǎn)換成物理地址,這個(gè)過(guò)程與內(nèi)存映射無(wú)關(guān)。

前面講過(guò),建立內(nèi)存映射并沒(méi)有實(shí)際拷貝數(shù)據(jù),這時(shí),MMU在地址映射表中是無(wú)法找到與ptr相對(duì)應(yīng)的物理地址的,也就是MMU失敗,將產(chǎn)生一個(gè)缺頁(yè)中斷,缺頁(yè)中斷的中斷響應(yīng)函數(shù)會(huì)在swap中尋找相對(duì)應(yīng)的頁(yè)面,如果找不到(也就是該文件從來(lái)沒(méi)有被讀入內(nèi)存的情況),則會(huì)通過(guò)mmap()建立的映射關(guān)系,從硬盤(pán)上將文件讀取到物理內(nèi)存中,如圖1中過(guò)程3所示。這個(gè)過(guò)程與內(nèi)存映射無(wú)關(guān)。如果在拷貝數(shù)據(jù)時(shí),發(fā)現(xiàn)物理內(nèi)存不夠用,則會(huì)通過(guò)虛擬內(nèi)存機(jī)制(swap)將暫時(shí)不用的物理頁(yè)面交換到硬盤(pán)上,這個(gè)過(guò)程也與內(nèi)存映射無(wú)關(guān)。

mmap內(nèi)存映射的實(shí)現(xiàn)過(guò)程:

  • 進(jìn)程啟動(dòng)映射過(guò)程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
  • 調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶(hù)空間函數(shù)),實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
  • 進(jìn)程發(fā)起對(duì)這片映射空間的訪問(wèn),引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

適合的場(chǎng)景

  • 您有一個(gè)很大的文件,其內(nèi)容您想要隨機(jī)訪問(wèn)一個(gè)或多個(gè)時(shí)間
  • 您有一個(gè)小文件,它的內(nèi)容您想要立即讀入內(nèi)存并經(jīng)常訪問(wèn)。這種技術(shù)最適合那些大小不超過(guò)幾個(gè)虛擬內(nèi)存頁(yè)的文件。(頁(yè)是地址空間的最小單位,虛擬頁(yè)和物理頁(yè)的大小是一樣的,通常為4KB。)
  • 您需要在內(nèi)存中緩存文件的特定部分。文件映射消除了緩存數(shù)據(jù)的需要,這使得系統(tǒng)磁盤(pán)緩存中的其他數(shù)據(jù)空間更大 當(dāng)隨機(jī)訪問(wèn)一個(gè)非常大的文件時(shí),通常最好只映射文件的一小部分。映射大文件的問(wèn)題是文件會(huì)消耗活動(dòng)內(nèi)存。如果文件足夠大,系統(tǒng)可能會(huì)被迫將其他部分的內(nèi)存分頁(yè)以加載文件。將多個(gè)文件映射到內(nèi)存中會(huì)使這個(gè)問(wèn)題更加復(fù)雜。

不適合的場(chǎng)景

  • 您希望從開(kāi)始到結(jié)束的順序從頭到尾讀取一個(gè)文件
  • 這個(gè)文件有幾百兆字節(jié)或者更大。將大文件映射到內(nèi)存中會(huì)快速地填充內(nèi)存,并可能導(dǎo)致分頁(yè),這將抵消首先映射文件的好處。對(duì)于大型順序讀取操作,禁用磁盤(pán)緩存并將文件讀入一個(gè)小內(nèi)存緩沖區(qū)
  • 該文件大于可用的連續(xù)虛擬內(nèi)存地址空間。對(duì)于64位應(yīng)用程序來(lái)說(shuō),這不是什么問(wèn)題,但是對(duì)于32位應(yīng)用程序來(lái)說(shuō),這是一個(gè)問(wèn)題
  • 該文件位于可移動(dòng)驅(qū)動(dòng)器上
  • 該文件位于網(wǎng)絡(luò)驅(qū)動(dòng)器上

示例代碼

//
//  ViewController.m
//  TestCode
//
//  Created by zhangdasen on 2020/5/24.
//  Copyright ? 2020 zhangdasen. All rights reserved.
//

#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
    NSLog(@"path: %@", path);
    NSString *str = @"test str2";
    [str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];

    ProcessFile(path.UTF8String);
    NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"result:%@", result);
}


int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize)
{
    int outError;
    int fileDescriptor;
    struct stat statInfo;

    // Return safe values on error.
    outError = 0;
    *outDataPtr = NULL;
    *outDataLength = 0;

    // Open the file.
    fileDescriptor = open( inPathName, O_RDWR, 0 );
    if( fileDescriptor < 0 )
    {
        outError = errno;
    }
    else
    {
        // We now know the file exists. Retrieve the file size.
        if( fstat( fileDescriptor, &statInfo ) != 0 )
        {
            outError = errno;
        }
        else
        {
            ftruncate(fileDescriptor, statInfo.st_size + appendSize);
            fsync(fileDescriptor);
            *outDataPtr = mmap(NULL,
                               statInfo.st_size + appendSize,
                               PROT_READ|PROT_WRITE,
                               MAP_FILE|MAP_SHARED,
                               fileDescriptor,
                               0);
            if( *outDataPtr == MAP_FAILED )
            {
                outError = errno;
            }
            else
            {
                // On success, return the size of the mapped file.
                *outDataLength = statInfo.st_size;
            }
        }

        // Now close the file. The kernel doesn’t use our file descriptor.
        close( fileDescriptor );
    }

    return outError;
}


void ProcessFile(const char * inPathName)
{
    size_t dataLength;
    void * dataPtr;
    char *appendStr = " append_key2";
    int appendSize = (int)strlen(appendStr);
    if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
        dataPtr = dataPtr + dataLength;
        memcpy(dataPtr, appendStr, appendSize);
        // Unmap files
        munmap(dataPtr, appendSize + dataLength);
    }
}
@end

6.5解除映射的方法

使用 mmap 后,必須調(diào)用 munmap 來(lái)解除映射,釋放分配的虛擬內(nèi)存。其函數(shù)定義如下:int munmap(void *addr, size_t length);。

  • addr:要解除映射的內(nèi)存區(qū)域的起始地址。
  • length:要解除映射的大小。

返回值:成功返回 0,失敗返回 -1。

⑴利用 mmap 訪問(wèn)硬件,減少數(shù)據(jù)拷貝次數(shù)

mmap 可以將文件、設(shè)備等外部資源映射到內(nèi)存地址空間,進(jìn)程可以像訪問(wèn)內(nèi)存一樣訪問(wèn)文件數(shù)據(jù)或硬件資源。當(dāng)使用 mmap 訪問(wèn)硬件時(shí),數(shù)據(jù)可以直接從硬件設(shè)備通過(guò) DMA 拷貝到內(nèi)核緩沖區(qū),然后進(jìn)程可以直接訪問(wèn)這個(gè)緩沖區(qū),減少了數(shù)據(jù)拷貝的次數(shù)。例如,在嵌入式系統(tǒng)中,可以使用 mmap 將物理地址映射到用戶(hù)虛擬地址空間,實(shí)現(xiàn)對(duì)硬件設(shè)備的直接訪問(wèn)。在進(jìn)行數(shù)據(jù)傳輸時(shí),避免了傳統(tǒng)方式中從內(nèi)核空間到用戶(hù)空間的多次數(shù)據(jù)拷貝,提高了數(shù)據(jù)傳輸?shù)男省?/span>

⑵通過(guò) mmap 實(shí)現(xiàn)將物理地址映射到用戶(hù)虛擬地址空間

  • 打開(kāi) /dev/mem 文件獲得文件描述符 dev_mem_fd。
  • 使用 mmap 函數(shù)進(jìn)行映射,將物理地址映射到用戶(hù)虛擬地址空間。例如,定義一個(gè)函數(shù) dma_mmap 來(lái)實(shí)現(xiàn)這個(gè)功能,函數(shù)原型為 int dma_mmap(unsigned long addr_p, unsigned int len, unsigned char** addr_v)。在這個(gè)函數(shù)中,首先打開(kāi) /dev/mem 文件,然后使用 mmap 函數(shù)進(jìn)行映射,最后返回虛擬地址。
  • 使用映射后的虛擬地址進(jìn)行操作,例如讀寫(xiě)硬件設(shè)備。
  • 在使用完后,調(diào)用 dma_munmap 函數(shù)解除映射,釋放資源。函數(shù)原型為 unsigned int dma_munmap(unsigned char* addr_v, unsigned long addr_p, unsigned int len)。

⑶在嵌入式系統(tǒng)中,還可以通過(guò)以下方式實(shí)現(xiàn)物理地址到用戶(hù)虛擬地址空間的映射:

  • 在驅(qū)動(dòng)程序中,實(shí)現(xiàn) mmap 方法,建立虛擬地址到物理地址的頁(yè)表。例如,可以使用 remap_pfn_range 函數(shù)一次建立所有頁(yè)表,或者使用 nopage VMA 方法每次建立一個(gè)頁(yè)表。
  • 在用戶(hù)空間程序中,使用 mmap 函數(shù)進(jìn)行映射,將文件描述符、映射大小、保護(hù)權(quán)限等參數(shù)傳入,獲得映射后的虛擬地址。然后可以通過(guò)這個(gè)虛擬地址對(duì)硬件設(shè)備進(jìn)行操作。

Part7.mmap使用技巧與注意事項(xiàng)

7.1 mmap函數(shù)參數(shù)解析

在使用 mmap 時(shí),正確設(shè)置其函數(shù)參數(shù)至關(guān)重要。mmap 函數(shù)原型為void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset) ,下面我們來(lái)詳細(xì)解析每個(gè)參數(shù):

start:指向欲映射的內(nèi)存起始地址,通常設(shè)為 NULL ,讓系統(tǒng)自動(dòng)選定合適的地址。若指定非 NULL 值,系統(tǒng)會(huì)嘗試從該地址開(kāi)始映射,但可能會(huì)因?yàn)榈刂凡豢捎没蚱渌驅(qū)е掠成涫 @纾谀承┫到y(tǒng)中,指定的地址必須是頁(yè)面大小的整數(shù)倍,否則映射將無(wú)法成功 。

length:代表將文件中多大的部分映射到內(nèi)存,這個(gè)值必須大于 0 。它會(huì)自動(dòng)調(diào)整為系統(tǒng)頁(yè)面大小(通常為 4KB)的整數(shù)倍。如果設(shè)置為 0,mmap 調(diào)用將失敗 。在處理大文件時(shí),需要根據(jù)實(shí)際需求合理設(shè)置 length,避免設(shè)置過(guò)小導(dǎo)致多次映射,影響效率;也不能設(shè)置過(guò)大,以免占用過(guò)多內(nèi)存資源 。

prot:指定映射區(qū)域的保護(hù)方式,常見(jiàn)的取值有:

  • PROT_READ:表示映射區(qū)域可被讀取,若文件以只讀方式打開(kāi),該權(quán)限必須設(shè)置 。
  • PROT_WRITE:意味著映射區(qū)域可被寫(xiě)入,如果需要對(duì)映射的文件進(jìn)行修改,需設(shè)置此權(quán)限 ,但要注意文件打開(kāi)模式需允許寫(xiě)入,否則 mmap 調(diào)用會(huì)失敗 。
  • PROT_EXEC:說(shuō)明映射區(qū)域可被執(zhí)行,常用于映射可執(zhí)行文件或共享庫(kù) 。
  • PROT_NONE:表示映射區(qū)域不能被存取,一般較少單獨(dú)使用,常與其他權(quán)限組合來(lái)限制特定區(qū)域的訪問(wèn) 。
  • 這些權(quán)限可以通過(guò)邏輯或(|)操作進(jìn)行組合,如PROT_READ | PROT_WRITE表示映射區(qū)域可讀可寫(xiě) 。

flags:影響映射區(qū)域的各種特性,調(diào)用時(shí)必須指定MAP_SHARED或MAP_PRIVATE :

  • MAP_SHARED:對(duì)映射區(qū)域的寫(xiě)入數(shù)據(jù)會(huì)復(fù)制回文件內(nèi),并且允許其他映射該文件的進(jìn)程共享。適用于進(jìn)程間通信、多進(jìn)程共享數(shù)據(jù)等場(chǎng)景 。例如,在數(shù)據(jù)庫(kù)緩存模塊中,多個(gè)進(jìn)程通過(guò)MAP_SHARED映射同一緩存文件,實(shí)現(xiàn)數(shù)據(jù)共享和實(shí)時(shí)更新 。
  • MAP_PRIVATE:對(duì)映射區(qū)域的寫(xiě)入操作會(huì)產(chǎn)生一個(gè)映射文件的復(fù)制,即私人的 “寫(xiě)入時(shí)復(fù)制”(copy on write),對(duì)此區(qū)域作的任何修改都不會(huì)寫(xiě)回原來(lái)的文件內(nèi)容。常用于需要對(duì)文件進(jìn)行臨時(shí)修改,而不希望影響原文件的場(chǎng)景 。
  • 此外,還有其他一些可選標(biāo)志,如MAP_ANONYMOUS用于建立匿名映射,此時(shí)會(huì)忽略參數(shù) fd,不涉及文件,且映射區(qū)域無(wú)法和其他進(jìn)程共享;MAP_LOCKED將映射區(qū)域鎖定住,防止該區(qū)域被置換(swap),適用于對(duì)性能要求極高,不允許內(nèi)存被交換出去的場(chǎng)景 。

fd:要映射到內(nèi)存中的文件描述符。如果使用匿名內(nèi)存映射,即 flags 中設(shè)置了MAP_ANONYMOUS,fd 需設(shè)為 - 1 。有些系統(tǒng)不支持匿名內(nèi)存映射時(shí),可以打開(kāi)/dev/zero文件,然后對(duì)該文件進(jìn)行映射,達(dá)到類(lèi)似匿名內(nèi)存映射的效果 。

offset:文件映射的偏移量,通常設(shè)置為 0,代表從文件最前方開(kāi)始對(duì)應(yīng)。offset 必須是分頁(yè)大小(一般為 4KB)的整數(shù)倍,否則 mmap 調(diào)用會(huì)失敗 。在某些情況下,可能需要從文件的特定位置開(kāi)始映射,此時(shí)就需要正確計(jì)算并設(shè)置 offset 。

7.2錯(cuò)誤處理與調(diào)試策略

在使用 mmap 過(guò)程中,難免會(huì)遇到各種錯(cuò)誤,掌握正確的錯(cuò)誤處理和調(diào)試方法是確保程序穩(wěn)定運(yùn)行的關(guān)鍵。

常見(jiàn)的 mmap 錯(cuò)誤及對(duì)應(yīng)的 errno 值如下:

  • EACCES:表示存取權(quán)限有誤。如果是MAP_PRIVATE情況下,文件必須可讀;使用MAP_SHARED則要有PROT_WRITE權(quán)限,并且該文件要能寫(xiě)入 。例如,當(dāng)以只讀方式打開(kāi)文件,卻在 mmap 時(shí)設(shè)置了PROT_WRITE權(quán)限,就會(huì)返回此錯(cuò)誤 。
  • EBADF:參數(shù) fd 不是有效的文件描述詞。可能是文件描述符已關(guān)閉,或者從未正確打開(kāi)過(guò)文件就使用其描述符進(jìn)行 mmap 操作 。
  • EINVAL:一個(gè)或者多個(gè)參數(shù)無(wú)效。比如 start、length 或 offset 有一個(gè)不合法,如 length 為 0,offset 不是分頁(yè)大小的整數(shù)倍等 。
  • EAGAIN:文件被鎖住,或是有太多內(nèi)存被鎖住。在系統(tǒng)資源緊張,內(nèi)存被大量占用或文件被其他進(jìn)程鎖定時(shí),可能會(huì)出現(xiàn)此錯(cuò)誤 。
  • ENOMEM:內(nèi)存不足,或者進(jìn)程已超出最大內(nèi)存映射數(shù)量。當(dāng)系統(tǒng)內(nèi)存不足,無(wú)法為 mmap 分配足夠的內(nèi)存,或者進(jìn)程已經(jīng)達(dá)到系統(tǒng)允許的最大內(nèi)存映射數(shù)量限制時(shí),會(huì)返回該錯(cuò)誤 。

為了正確處理這些錯(cuò)誤,我們可以在調(diào)用 mmap 后檢查其返回值。如果返回MAP_FAILED(其值為 (void *)-1),則說(shuō)明調(diào)用失敗,此時(shí)應(yīng)通過(guò)errno獲取具體的錯(cuò)誤碼,并根據(jù)錯(cuò)誤碼進(jìn)行相應(yīng)的處理 。例如:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main() {
    int fd = open("test.txt", O_RDWR);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    void *ptr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (ptr == MAP_FAILED) {
        perror("mmap");
        printf("Error number: %d\n", errno);
        close(fd);
        return 1;
    }

    // 后續(xù)操作
    if (munmap(ptr, 1024) == -1) {
        perror("munmap");
        close(fd);
        return 1;
    }

    close(fd);
    return 0;
}

當(dāng)遇到問(wèn)題時(shí),除了通過(guò)檢查返回值和errno來(lái)定位錯(cuò)誤,還可以使用調(diào)試工具進(jìn)行深入分析,gdb 就是一個(gè)強(qiáng)大的調(diào)試工具 。假設(shè)我們有一個(gè)使用 mmap 的程序mmap_test,可以通過(guò)以下步驟使用 gdb 進(jìn)行調(diào)試:

  1. 編譯程序:在編譯時(shí)加上-g選項(xiàng),以便生成調(diào)試信息,如gcc -g -o mmap_test mmap_test.c 。
  2. 啟動(dòng) gdb:運(yùn)行g(shù)db mmap_test進(jìn)入 gdb 調(diào)試環(huán)境 。
  3. 設(shè)置斷點(diǎn):使用break命令設(shè)置斷點(diǎn),例如break main在main函數(shù)入口處設(shè)置斷點(diǎn),或者break mmap在調(diào)用 mmap 函數(shù)的地方設(shè)置斷點(diǎn) 。
  4. 運(yùn)行程序:輸入run命令運(yùn)行程序,程序會(huì)在斷點(diǎn)處暫停 。
  5. 查看變量和堆棧信息:使用print命令查看變量的值,如print errno查看errno的值;使用backtrace(縮寫(xiě)bt)命令查看函數(shù)調(diào)用堆棧,幫助定位錯(cuò)誤發(fā)生的位置 。
  6. 單步執(zhí)行:通過(guò)next(縮寫(xiě)n)命令單步執(zhí)行代碼,觀察程序的執(zhí)行流程和變量變化,逐步排查問(wèn)題 。

7.3性能優(yōu)化建議

為了充分發(fā)揮 mmap 的性能優(yōu)勢(shì),在使用過(guò)程中可以采取以下性能優(yōu)化建議:

合理設(shè)置映射區(qū)域大小:根據(jù)實(shí)際需求,準(zhǔn)確設(shè)置映射區(qū)域的大小(length 參數(shù))。如果映射區(qū)域過(guò)小,可能導(dǎo)致頻繁的映射和取消映射操作,增加系統(tǒng)開(kāi)銷(xiāo);而映射區(qū)域過(guò)大,會(huì)占用過(guò)多內(nèi)存資源,影響系統(tǒng)整體性能 。例如,在處理大文件時(shí),可以根據(jù)文件的邏輯結(jié)構(gòu)和訪問(wèn)模式,將文件劃分為合適大小的塊進(jìn)行映射,避免一次性映射整個(gè)大文件 。

避免頻繁映射和取消映射:每次映射和取消映射操作都需要系統(tǒng)進(jìn)行資源分配和回收,頻繁進(jìn)行這些操作會(huì)消耗大量的系統(tǒng)時(shí)間 。在程序設(shè)計(jì)中,應(yīng)盡量減少不必要的映射和取消映射操作 。例如,可以在程序初始化階段一次性完成所需的映射,在程序結(jié)束時(shí)再統(tǒng)一取消映射 。如果需要?jiǎng)討B(tài)調(diào)整映射區(qū)域,可以考慮使用mremap函數(shù),它可以在不取消映射的情況下調(diào)整映射區(qū)域的大小 。

結(jié)合緩存機(jī)制:雖然 mmap 已經(jīng)減少了數(shù)據(jù)拷貝和系統(tǒng)調(diào)用次數(shù),但在某些場(chǎng)景下,結(jié)合緩存機(jī)制可以進(jìn)一步提升性能 。對(duì)于頻繁訪問(wèn)的數(shù)據(jù),可以在用戶(hù)空間設(shè)置緩存,當(dāng)訪問(wèn)數(shù)據(jù)時(shí),先檢查緩存中是否存在,如果存在則直接從緩存中讀取,避免每次都通過(guò) mmap 訪問(wèn)文件 。這樣可以減少磁盤(pán) I/O 操作,提高數(shù)據(jù)訪問(wèn)速度 。同時(shí),要注意緩存的一致性問(wèn)題,當(dāng)數(shù)據(jù)發(fā)生變化時(shí),及時(shí)更新緩存和文件 。

使用預(yù)讀和異步 I/O:在某些應(yīng)用場(chǎng)景中,如順序讀取大文件,可以使用madvise函數(shù)的MADV_SEQUENTIAL標(biāo)志來(lái)提示內(nèi)核進(jìn)行預(yù)讀優(yōu)化,讓內(nèi)核提前將數(shù)據(jù)讀入內(nèi)存,提高讀取效率 。此外,結(jié)合異步 I/O(如io_uring),可以在進(jìn)行 I/O 操作時(shí)不阻塞主線程,使程序能夠更高效地利用系統(tǒng)資源 。

注意內(nèi)存對(duì)齊:在訪問(wèn) mmap 映射的內(nèi)存時(shí),要注意內(nèi)存對(duì)齊問(wèn)題。不同的硬件架構(gòu)對(duì)內(nèi)存對(duì)齊有不同的要求,如果內(nèi)存訪問(wèn)未對(duì)齊,可能會(huì)導(dǎo)致性能下降甚至硬件異常 。確保數(shù)據(jù)結(jié)構(gòu)和內(nèi)存訪問(wèn)操作符合內(nèi)存對(duì)齊規(guī)則,可以提高程序的性能和穩(wěn)定性 。例如,在定義數(shù)據(jù)結(jié)構(gòu)時(shí),可以使用特定的編譯指令(如#pragma pack)來(lái)控制數(shù)據(jù)結(jié)構(gòu)的對(duì)齊方式 。

Part8.mmap面試總結(jié)

mmap 的實(shí)現(xiàn)過(guò)程相當(dāng)復(fù)雜,主要分為三個(gè)階段。在第一個(gè)階段,進(jìn)程啟動(dòng)映射過(guò)程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域。這就像是在一片空地上規(guī)劃出一塊特定的區(qū)域,準(zhǔn)備用來(lái)建造房屋(映射)。進(jìn)程在用戶(hù)空間調(diào)用庫(kù)函數(shù) mmap,然后在當(dāng)前進(jìn)程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址。找到合適的地址后,會(huì)為此虛擬區(qū)分配一個(gè) vm_area_struct 結(jié)構(gòu),接著對(duì)這個(gè)結(jié)構(gòu)的各個(gè)域進(jìn)行初始化,就像為即將建造的房屋準(zhǔn)備好各種建筑材料和設(shè)計(jì)藍(lán)圖。最后,將新建的虛擬區(qū)結(jié)構(gòu)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹(shù)中,方便后續(xù)快速訪問(wèn)。

第二個(gè)階段是調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù) mmap,實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系。這一步就像是按照設(shè)計(jì)藍(lán)圖開(kāi)始建造房屋,將規(guī)劃好的虛擬區(qū)域與實(shí)際的文件物理地址連接起來(lái)。通過(guò)待映射的文件指針,在文件描述符表中找到對(duì)應(yīng)的文件描述符,再通過(guò)文件描述符,鏈接到內(nèi)核 “已打開(kāi)文件集” 中該文件的文件結(jié)構(gòu)體。每個(gè)文件結(jié)構(gòu)體維護(hù)著和這個(gè)已打開(kāi)文件相關(guān)的各項(xiàng)信息,就像房屋的建筑圖紙上標(biāo)注著各種細(xì)節(jié)信息。

然后,通過(guò)該文件的文件結(jié)構(gòu)體,鏈接到 file_operations 模塊,調(diào)用內(nèi)核函數(shù) mmap。內(nèi)核 mmap 函數(shù)通過(guò)虛擬文件系統(tǒng) inode 模塊定位到文件磁盤(pán)物理地址,最后通過(guò) remap_pfn_range 函數(shù)建立頁(yè)表,實(shí)現(xiàn)文件地址和虛擬地址區(qū)域的映射關(guān)系。此時(shí),這片虛擬地址雖然已經(jīng)和文件物理地址建立了聯(lián)系,但還沒(méi)有將任何文件數(shù)據(jù)拷貝至主存,就像房屋雖然建好了框架,但里面還沒(méi)有擺放任何家具。

直到第三個(gè)階段,進(jìn)程發(fā)起對(duì)這片映射空間的訪問(wèn),引發(fā)缺頁(yè)異常,才真正實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝。當(dāng)進(jìn)程的讀或?qū)懖僮髟L問(wèn)虛擬地址空間這一段映射地址時(shí),通過(guò)查詢(xún)頁(yè)表,發(fā)現(xiàn)這一段地址并不在物理頁(yè)面上,因?yàn)槟壳爸唤⒘说刂酚成洌嬲挠脖P(pán)數(shù)據(jù)還沒(méi)有拷貝到內(nèi)存中,所以引發(fā)缺頁(yè)異常。缺頁(yè)異常會(huì)進(jìn)行一系列判斷,確定無(wú)非法操作后,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過(guò)程。

調(diào)頁(yè)過(guò)程先在交換緩存空間中尋找需要訪問(wèn)的內(nèi)存頁(yè),如果沒(méi)有則調(diào)用 nopage 函數(shù)把所缺的頁(yè)從磁盤(pán)裝入到主存中。之后進(jìn)程即可對(duì)這片主存進(jìn)行讀或者寫(xiě)的操作,如果寫(xiě)操作改變了其內(nèi)容,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫(xiě)臟頁(yè)面到對(duì)應(yīng)磁盤(pán)地址,也即完成了寫(xiě)入到文件的過(guò)程 。

回想起面試時(shí),我對(duì) mmap 的原理只是一知半解,回答得漏洞百出。對(duì)于它與普通文件讀寫(xiě)操作的區(qū)別,我也沒(méi)有分析到位。普通文件讀寫(xiě)操作通常會(huì)涉及多次數(shù)據(jù)拷貝,先將文件頁(yè)從磁盤(pán)拷貝到頁(yè)緩存中,由于頁(yè)緩存處在內(nèi)核空間,不能被用戶(hù)進(jìn)程直接尋址,所以還需要將頁(yè)緩存中數(shù)據(jù)頁(yè)再次拷貝到內(nèi)存對(duì)應(yīng)的用戶(hù)空間中,這樣就增加了數(shù)據(jù)傳輸?shù)臅r(shí)間和系統(tǒng)開(kāi)銷(xiāo)。

而 mmap 則減少了這種數(shù)據(jù)拷貝的次數(shù),提高了讀寫(xiě)效率 。在實(shí)際應(yīng)用場(chǎng)景方面,我更是考慮得不夠全面。mmap 在需要頻繁讀寫(xiě)大文件、實(shí)現(xiàn)進(jìn)程間通信以及高效的內(nèi)存管理等場(chǎng)景中都有著廣泛的應(yīng)用。比如在一些大數(shù)據(jù)處理場(chǎng)景中,使用 mmap 可以大大提高數(shù)據(jù)的讀取速度,減少處理時(shí)間。

正是因?yàn)槲覍?duì) mmap 的理解存在如此多的不足,才在面試中被這個(gè)問(wèn)題難住,最終與騰訊的 offer 失之交臂。這次的經(jīng)歷讓我深刻認(rèn)識(shí)到,在技術(shù)學(xué)習(xí)的道路上,容不得半點(diǎn)馬虎和一知半解,每一個(gè)知識(shí)點(diǎn)都可能成為決定成敗的關(guān)鍵。

責(zé)任編輯:武曉燕 來(lái)源: 深度Linux
相關(guān)推薦

2021-12-08 09:53:50

騰訊QQ號(hào)碼重復(fù)

2024-07-30 14:01:51

Java字節(jié)碼JVM?

2020-09-25 08:58:43

推薦系統(tǒng)業(yè)務(wù)

2020-11-24 08:15:09

Elasticsear面試分布式

2015-01-19 09:53:05

H3CAP智慧教育

2010-09-17 09:00:06

HTML 5Web SQL數(shù)據(jù)庫(kù)

2020-03-20 08:00:32

代碼程序員追求

2020-06-17 08:53:19

Redis集群SSH

2020-09-08 06:43:53

B+樹(shù)面試索引

2016-07-25 17:17:42

新華三

2024-01-09 15:51:56

Rust開(kāi)發(fā)Trait

2022-09-21 09:00:10

MySQL幻讀隔離級(jí)別

2012-07-09 11:17:19

2015-07-30 09:31:26

阿里巴巴前端面試

2021-05-12 13:40:16

JVM調(diào)優(yōu)經(jīng)驗(yàn)

2017-12-21 07:47:41

2015-12-30 17:52:26

隨身云Ucloud

2010-07-27 20:37:59

2009-07-08 19:14:19

2021-12-29 06:07:59

微信安卓騰訊
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

日韩欧美亚洲综合| xvideos国产精品| 亚洲一区站长工具| 国产精品999| 久久人人爽爽爽人久久久| 亚洲乱码国产一区三区| 天天av天天翘天天综合网色鬼国产| 免费成人黄色| 亚洲va国产va天堂va久久| 国产欧美一区二区三区在线老狼| 韩国日本一区| 色姑娘综合网| 成+人+亚洲+综合天堂| 国产免费a∨片在线观看不卡| 成人一区二区电影| 国产精品香蕉一区二区三区| 欧美激情网站| 久久av高潮av| 久久综合色88| 国产精品女同一区二区三区| 涩涩屋成人免费视频软件| 成人在线观看黄| 欧美xfplay| av男人天堂一区| 瑟瑟在线观看| 欧美一a一片一级一片| 欧美黑人猛交| 亚洲精品不卡在线| 极品美乳网红视频免费在线观看| 国产精品系列在线| 簧片在线观看| 国产女人水真多18毛片18精品视频| 99久久精品无码一区二区毛片| 国产精品久久久久久麻豆一区软件| 国产69精品久久久久久| 亚洲欧美福利一区二区| √8天堂资源地址中文在线| 懂色av粉嫩av蜜臀av| 九九热这里只有精品6| 丁香五六月婷婷久久激情| 亚洲日本免费| 亚洲人体在线| 福利视频在线导航| 在线视频欧美一区| 欧美在线视频网站| 欧洲日韩一区二区三区| 开心激情综合| 日韩在线免费播放| 男人透女人免费视频| 国产日韩在线免费| 亚洲视频axxx| 草草在线观看| 精品久久久久久亚洲精品| 日韩a一区二区| 国产一区久久精品| 91插插插插插插插插| 亚洲一区美女| 91传媒视频免费| 色婷婷av一区二区三区软件 | 亚洲一级免费观看| 久久精品丝袜高跟鞋| 欧美日韩黄色一区二区| 久久免费国产精品| 亚洲第一毛片| 一区二区三区四区电影| 在线观看免费版| 91精品国产一区二区三密臀| 欧美日韩国产精品激情在线播放| 久久综合精品一区| 91精品视频免费| 国产精品在线看| 国产精品免费小视频| 欧美亚洲激情视频| 久久高清视频免费| 中文字幕精品av| 在线影院国内精品| 欧美性猛交xxxx免费看漫画 | 成人手机在线视频| 91精品国产91久久久久久密臀| 久久久久久久久久久久久国产| 亚洲曰韩产成在线| 国产精品毛片久久久久久| 久久精品国产亚洲高清剧情介绍| 午夜亚洲性色福利视频| 国产精品毛片在线| 首页国产欧美久久| 99久久激情| 欧美日韩精品免费观看视频完整| 在线观看黄色| 成人免费xx| 在线免费激情视频| 无人视频在线观看免费| 福利视频在线播放| 羞羞视频在线观看不卡| 欧美精品人人做人人爱视频| 亚洲一区电影777| 欧美涩涩视频| 欧美私人啪啪vps| 久久亚洲国产| 亚洲精品影院在线观看| 久久午夜精品| 91麻豆产精品久久久久久| 亚洲乱码国产乱码精品精的特点 | av软件在线观看| av老司机在线观看| 日韩三区四区| 国产99久久精品一区二区300| 欧美日韩国产在线观看网站| 成人羞羞在线观看网站| 天堂va蜜桃一区二区三区漫画版| 高清av一区二区| 国产欧美日韩激情| 狠狠干狠狠久久| 亚洲午夜小视频| 97超级碰在线看视频免费在线看| 国产一区二区三区黄| 国产精品日韩精品| www日韩av| 白白操在线视频| 色视频在线播放| 日韩电影毛片| 欧美在线观看视频一区| 成人免费观看视频| 欧美午夜女人视频在线| 亚洲v精品v日韩v欧美v专区| 在线一区免费| 97超碰人人在线| 日本在线视频一区二区| 日韩电影一区| av一区二区三区| 欧美性受xxxx黑人xyx性爽| 亚洲国产日韩欧美综合久久| 亚洲免费人成在线视频观看| 5252色成人免费视频| 在线不卡日本| 如如影视在线观看经典| 国产精品亚洲综合在线观看 | 国产成人精品视频ⅴa片软件竹菊| 校园春色 亚洲色图| 黄色动漫在线观看| 欧美日韩一区二区三区四区在线观看 | 97视频com| 欧美一区二区三区在线免费观看| 欧美色欧美亚洲另类七区| 一区二区三区免费看| 91gao视频| 3d精品h动漫啪啪一区二区| 欧美成人一区二区在线| 亚洲福利精品视频| www黄色av| 国产69精品久久久久9999apgf| 久久久久久高潮国产精品视| 另类色图亚洲色图| 日韩在线观看免费av| 亚洲国产成人av在线| 欧美日韩一级片网站| 亚洲一级二级在线| 动漫精品一区二区| 国产精品igao视频| 欧美变态视频| 欧美三级午夜理伦三级在线观看| 成人av片在线观看| 亚洲国产99精品国自产| 精品国产一区二区三区久久久久久 | 91丝袜呻吟高潮美腿白嫩在线观看| 精品电影一区二区三区| 麻豆视频传媒入口| 四虎av网址| 久久国产精品久久| 久久久视频在线| 国产免费人做人爱午夜视频| 亚洲精品推荐| 欧美日韩精品福利| 不卡中文字幕在线| 亚洲羞羞网站| 国内毛片久久| 亚洲欧美偷拍三级| 91久久久亚洲精品| 国产亚av手机在线观看| 成a人片国产精品| 欧美在线免费看| 日本免费在线视频| 国产成人精品三级| 久久久精品一区二区三区| 久久国产精品-国产精品| 日韩欧美国产综合在线| 最近中文字幕mv2018在线高清| 亚洲激情播播| 久久久精品日韩欧美| 久久精品一偷一偷国产| 国产在线日韩在线| 香蕉视频禁止18| 牛牛精品在线视频| 欧美激情1区| 国产一区二区精品久久| 日韩午夜在线影院| 亚洲精品国产精品久久| 中文字幕在线免费| 国产精品一在线观看| 国产欧美日韩激情|