在講述文件映射的概念時,不可避免的要牽涉到虛存(SVR 4的VM).實際上,文件映射是虛存的中心概念, 文件映射一方面給用戶提供了一組措施,好似用戶將文件映射到自己地址空間的某個部分,使用簡單的內存訪問指令讀寫文件;另一方面,它也可以用於內核的基本組織模式,在這種模式種,內核將整個地址空間視為諸如文件之類的一組不同對象的映射.中的傳統文件訪問方式是,首先用open系統調用打開文件,然後使用read, write以及lseek等調用進行順序或者隨即的I/O.這種方式是非常低效的,每一次I/O操作都需要一次系統調用.另外,如果若干個進程訪問同一個文件,每個進程都要在自己的地址空間維護一個副本,浪費了內存空間.而如果能夠通過一定的機制將頁面映射到進程的地址空間中,也就是說首先通過簡單的產生某些內存管理數據結構完成映射的創建.當進程訪問頁面時產生一個缺頁中斷,內核將頁面讀入內存並且更新頁表指向該頁面.而且這種方式非常方便於同一副本的共享.
VM是面向對象的方法設計的,這裡的對象是指內存對象:內存對象是一個軟件抽象的概念,它描述內存區與後備存儲之間的映射.系統可以使用多種類型的後備存儲,比如交換空間,本地或者遠程文件以及幀緩存等等. VM系統對它們統一處理,采用同一操作集操作,比如讀取頁面或者回寫頁面等.每種不同的後備存儲都可以用不同的方法實現這些操作.這樣,系統定義了一套統一的接口,每種後備存儲給出自己的實現方法.這樣,進程的地址空間就被視為一組映射到不同數據對象上的的映射組成.所有的有效地址就是那些映射到數據對象上的地址.這些對象為映射它的頁面提供了持久性的後備存儲.映射使得用戶可以直接尋址這些對象.
值得提出的是, VM體系結構獨立於Unix系統,所有的Unix系統語義,如正文,數據及堆棧區都可以建構在基本VM系統之上.同時, VM體系結構也是獨立於存儲管理的,存儲管理是由操作系統實施的,如:究竟采取什麼樣的對換和請求調頁算法,究竟是采取分段還是分頁機制進行存儲管理,究竟是如何將虛擬地址轉換成為物理地址等等(Linux中是一種叫Three Level Page Table的機制),這些都與內存對象的概念無關.
一、Linux中VM的實現.
一個進程應該包括一個mm_struct(memory manage struct),該結構是進程虛擬地址空間的抽象描述,裡面包括了進程虛擬空間的一些管理信息: start_code, end_code, start_data, end_data, start_brk, end_brk等等信息.另外,也有一個指向進程虛存區表(vm_area_struct: virtual memory area)的指針,該鏈是按照虛擬地址的增長順序排列的.在Linux進程的地址空間被分作許多區(vma),每個區(vma)都對應虛擬地址空間上一段連續的區域, vma是可以被共享和保護的獨立實體,這裡的vma就是前面提到的內存對象.
下面是vm_area_struct的結構,其中,前半部分是公共的,與類型無關的一些數據成員,如:指向mm_struct的指針,地址范圍等等,後半部分則是與類型相關的成員,其中最重要的是一個指向vm_operation_struct向量表的指針vm_ops, vm_pos向量表是一組虛函數,定義了與vma類型無關的接口.每一個特定的子類,即每種vma類型都必須在向量表中實現這些操作.這裡包括了: open, close, unmap, protect, sync, nopage, wppage, swapout這些操作.
struct vm_area_struct { /*公共的, 與vma類型無關的 */ struct mm_struct * vm_mm; unsigned long vm_start; unsigned long vm_end; struct vm_area_struct *vm_next; pgprot_t vm_page_prot; unsigned long vm_flags; short vm_avl_height; struct vm_area_struct * vm_avl_left; struct vm_area_struct * vm_avl_right; struct vm_area_struct *vm_next_share; struct vm_area_struct **vm_pprev_share; /* 與類型相關的 */ struct vm_operations_struct * vm_ops; unsigned long vm_pgoff; struct file * vm_file; unsigned long vm_raend; void * vm_private_data; };
vm_ops: open, close, no_page, swapin, swapout……
二、驅動中的mmap()函數解析
設備驅動的mmap實現主要是將一個物理設備的可操作區域(設備空間)映射到一個進程的虛擬地址空間。這樣就可以直接采用指針的方式像訪問內存的方式訪問設備。在驅動中的mmap實現主要是完成一件事,就是實際物理設備的操作區域到進程虛擬空間地址的映射過程。同時也需要保證這段映射的虛擬存儲器區域不會被進程當做一般的空間使用,因此需要添加一系列的保護方式。
/*主要是建立虛擬地址到物理地址的頁表關系,其他的過程又內核自己完成*/ static int mem_mmap(struct file* filp,struct vm_area_struct *vma) { /*間接的控制設備*/ struct mem_dev *dev = filp->private_data; /*標記這段虛擬內存映射為IO區域,並阻止系統將該區域包含在進程的存放轉存中*/ vma->vm_flags |= VM_IO; /*標記這段區域不能被換出*/ vma->vm_flags |= VM_RESERVED; /**/ if(remap_pfn_range(vma,/*虛擬內存區域*/ vma->vm_start, /*虛擬地址的起始地址*/ virt_to_phys(dev->data)>>PAGE_SHIFT, /*物理存儲區的物理頁號*/ dev->size, /*映射區域大小*/ vma->vm_page_prot /*虛擬區域保護屬性*/ )) return -EAGAIN; return 0; }
具體的實現分析如下:
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
上面的兩個保護機制就說明了被映射的這段區域具有映射IO的相似性,同時保證這段區域不能隨便的換出。就是建立一個物理頁與虛擬頁之間的關聯性。具體原理是虛擬頁和物理頁之間是以頁表的方式關聯起來,虛擬內存通常大於物理內存,在使用過程中虛擬頁通過頁表關聯一切對應的物理頁,當物理頁不夠時,會選擇性的犧牲一些頁,也就是將物理頁與虛擬頁之間切斷,重現關聯其他的虛擬頁,保證物理內存夠用。在設備驅動中應該具體的虛擬頁和物理頁之間的關系應該是長期的,應該保護起來,不能隨便被別的虛擬頁所替換。具體也可參看關於虛擬存儲器的文章。
接下來就是建立物理頁與虛擬頁之間的關系,即采用函數remap_pfn_range(),具體的參數如下:
int remap_pfn_range(structvm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot)
1、struct vm_area_struct是一個虛擬內存區域結構體,表示虛擬存儲器中的一個內存區域。其中的元素vm_start是指虛擬存儲器中的起始地址。
2、addr也就是虛擬存儲器中的起始地址,通常可以選擇addr = vma->vm_start。
3、pfn是指物理存儲器的具體頁號,通常通過物理地址得到對應的物理頁號,具體采用virt_to_phys(dev->data)>>PAGE_SHIFT.首先將虛擬內存轉換到物理內存,然後得到頁號。>>PAGE_SHIFT通常為12,這是因為每一頁的大小剛好是4K,這樣右移12相當於除以4096,得到頁號。
4、size區域大小
5、區域保護機制。
返回值,如果成功返回0,否則正數。
三、系統調用mmap函數解析
介紹完VM的基本概念後,我們可以講述mmap和munmap系統調用了.mmap調用實際上就是一個內存對象vma的創建過程,
1、mmap函數
Linux提供了內存映射函數mmap,它把文件內容映射到一段內存上(准確說是虛擬內存上),通過對這段內存的讀取和修改,實現對文件的讀取和修改。普通文件被映射到進程地址空間後,進程可以向訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。
先來看一下mmap的函數聲明:
頭文件:原型: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offsize); /* 返回值: 成功則返回映射區起始地址, 失敗則返回MAP_FAILED(-1). 參數: addr: 指定映射的起始地址, 通常設為NULL, 由系統指定. length: 將文件的多大長度映射到內存. prot: 映射區的保護方式, 可以是: PROT_EXEC: 映射區可被執行. PROT_READ: 映射區可被讀取. PROT_WRITE: 映射區可被寫入. PROT_NONE: 映射區不能存取. flags: 映射區的特性, 可以是: MAP_SHARED: 對映射區域的寫入數據會復制回文件, 且允許其他映射該文件的進程共享. MAP_PRIVATE: 對映射區域的寫入操作會產生一個映射的復制(copy-on-write), 對此區域所做的修改不會寫回原文件. 此外還有其他幾個flags不很常用, 具體查看linux C函數說明. fd: 由open返回的文件描述符, 代表要映射的文件. offset: 以文件開始處的偏移量, 必須是分頁大小的整數倍, 通常為0, 表示從文件頭開始映射. */
mmap的作用是映射文件描述符fd指定文件的 [off,off + len]區域至調用進程的[addr, addr + len]的內存區域, 如下圖所示:
mmap系統調用的實現過程是
1.先通過文件系統定位要映射的文件;
2.權限檢查,映射的權限不會超過文件打開的方式,也就是說如果文件是以只讀方式打開,那麼則不允許建立一個可寫映射;
3.創建一個vma對象,並對之進行初始化;
4.調用映射文件的mmap函數,其主要工作是給vm_ops向量表賦值;
5.把該vma鏈入該進程的vma鏈表中,如果可以和前後的vma合並則合並;
6.如果是要求VM_LOCKED(映射區不被換出)方式映射,則發出缺頁請求,把映射頁面讀入內存中.
2、munmap函數
munmap(void * start, size_t length):
該調用可以看作是mmap的一個逆過程.它將進程中從start開始length長度的一段區域的映射關閉,如果該區域不是恰好對應一個vma,則有可能會分割幾個或幾個vma.
msync(void * start, size_t length, int flags):
把映射區域的修改回寫到後備存儲中.因為munmap時並不保證頁面回寫,如果不調用msync,那麼有可能在munmap後丟失對映射區的修改.其中flags可以是MS_SYNC, MS_ASYNC, MS_INVALIDATE, MS_SYNC要求回寫完成後才返回, MS_ASYNC發出回寫請求後立即返回, MS_INVALIDATE使用回寫的內容更新該文件的其它映射.該系統調用是通過調用映射文件的sync函數來完成工作的.
brk(void * end_data_segement):
將進程的數據段擴展到end_data_segement指定的地址,該系統調用和mmap的實現方式十分相似,同樣是產生一個vma,然後指定其屬性.不過在此之前需要做一些合法性檢查,比如該地址是否大於mm->end_code, end_data_segement和mm->brk之間是否還存在其它vma等等.通過brk產生的vma映射的文件為空,這和匿名映射產生的vma相似,關於匿名映射不做進一步介紹.庫函數malloc就是通過brk實現的.
下面這個例子顯示了把文件映射到內存的方法,源代碼是:
/************關於本文 檔******************************************** *filename: mmap.c *purpose: 說明調用mmap把文件映射到內存的方法 *wrote by: zhoulifa([email protected]) 周立發(http://zhoulifa.bokee.com) Linux愛好者 Linux知識傳播者 SOHO族 開發者 最擅長C語言 *date time:2008-01-27 18:59 上海大雪天,據說是多年不遇 *Note: 任何人可以任意復制代碼並運用這些文檔,當然包括你的商業用途 * 但請遵循GPL *Thanks to: * Ubuntu 本程序在Ubuntu 7.10系統上測試完全正常 * Google.com 我通常通過google搜索發現許多有用的資料 *Hope:希望越來越多的人貢獻自己的力量,為科學技術發展出力 * 科技站在巨人的肩膀上進步更快!感謝有開源前輩的貢獻! *********************************************************************/ #include/* for mmap and munmap */ #include /* for open */ #include /* for open */ #include /* for open */ #include /* for lseek and write */ #include int main(int argc, char **argv) { int fd; char *mapped_mem, * p; int flength = 1024; void * start_addr = 0; fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); flength = lseek(fd, 1, SEEK_END); write(fd, "\0", 1); /* 在文件最後添加一個空字符,以便下面printf正常工作 */ lseek(fd, 0, SEEK_SET); mapped_mem = mmap(start_addr, flength, PROT_READ, //允許讀 MAP_PRIVATE, //不允許其它進程訪問此內存區域 fd, 0); /* 使用映射區域. */ printf("%s\n", mapped_mem); /* 為了保證這裡工作正常,參數傳遞的文件名最好是一個文本文件 */ close(fd); munmap(mapped_mem, flength); return 0; }
編譯運行此程序:
gcc -Wall mmap.c
./a.out text_filename
上面的方法因為用了PROT_READ,所以只能讀取文件裡的內容,不能修改,如果換成PROT_WRITE就可以修改文件的內容了。又由於 用了MAAP_PRIVATE所以只能此進程使用此內存區域,如果換成MAP_SHARED,則可以被其它進程訪問,比如下面的
#include/* for mmap and munmap */ #include /* for open */ #include /* for open */ #include /* for open */ #include /* for lseek and write */ #include #include /* for memcpy */ int main(int argc, char **argv) { int fd; char *mapped_mem, * p; int flength = 1024; void * start_addr = 0; fd = open(argv[1], O_RDWR | O_CREAT, S_IRUSR | S_IWUSR); flength = lseek(fd, 1, SEEK_END); write(fd, "\0", 1); /* 在文件最後添加一個空字符,以便下面printf正常工作 */ lseek(fd, 0, SEEK_SET); start_addr = 0x80000; mapped_mem = mmap(start_addr, flength, PROT_READ|PROT_WRITE, //允許寫入 MAP_SHARED, //允許其它進程訪問此內存區域 fd, 0); * 使用映射區域. */ printf("%s\n", mapped_mem); /* 為了保證這裡工作正常,參數傳遞的文件名最好是一個文本文 */ while((p = strstr(mapped_mem, "Hello"))) { /* 此處來修改文件 內容 */ memcpy(p, "Linux", 5); p += 5; } close(fd); munmap(mapped_mem, flength); return 0; }
五、mmap和共享內存對比
共享內存允許兩個或多個進程共享一給定的存儲區,因為數據不需要來回復制,所以是最快的一種進程間通信機制。共享內存可以通過mmap()映射普通文件(特殊情況下還可以采用匿名映射)機制實現,也可以通過系統V共享內存機制實現。應用接口和原理很簡單,內部機制復雜。為了實現更安全通信,往往還與信號燈等同步機制共同使用。
對比如下:
mmap機制:就是在磁盤上建立一個文件,每個進程存儲器裡面,單獨開辟一個空間來進行映射。如果多進程的話,那麼不會對實際的物理存儲器(主存)消耗太大。
shm機制:每個進程的共享內存都直接映射到實際物理存儲器裡面。
1、mmap保存到實際硬盤,實際存儲並沒有反映到主存上。優點:儲存量可以很大(多於主存);缺點:進程間讀取和寫入速度要比主存的要慢。
2、shm保存到物理存儲器(主存),實際的儲存量直接反映到主存上。優點,進程間訪問速度(讀寫)比磁盤要快;缺點,儲存量不能非常大(多於主存)
使用上看:如果分配的存儲量不大,那麼使用shm;如果存儲量大,那麼使用mmap。