1. 程式人生 > >從零開始之驅動發開、linux驅動(二十九、mmap原理)

從零開始之驅動發開、linux驅動(二十九、mmap原理)

一、概念

mmap是一種記憶體對映檔案的方法,即將一個檔案或者其它物件對映到程序的地址空間,實現檔案磁碟地址和程序虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程序就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。相反,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程序間的檔案共享。如下圖所示:

由上圖可以看出,程序的虛擬地址空間,由多個虛擬記憶體區域構成。虛擬記憶體區域是程序的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址範圍。上圖中所示的text資料段(程式碼段)、初始資料段、BSS資料段、堆、棧和記憶體對映,都是一個獨立的虛擬記憶體區域。而為記憶體對映服務的地址空間處在堆疊之間的空餘部分。

1.什麼是虛擬地址空間?

在32位系統中,每個程序都有4G的虛擬地址空間,其中3G使用者空間,1G核心空間(linux),每個程序共享核心空間,獨立的使用者空間,下圖形象地表達了這點

驅動程式執行在核心空間,所以驅動程式是面向所有程序的。

使用者空間切換到核心空間有兩種方法:

(1)系統呼叫,即軟中斷

(2)硬體中斷

 

2、虛擬地址空間裡面是什麼?

瞭解了什麼是虛擬地址空間,那麼虛擬地址空間裡面裝的是什麼?看下圖

虛擬空間裝的大概是上面那些資料了,記憶體對映大概就是把裝置地址對映到上圖的深色段了,暫且稱其為“記憶體對映段”,至於對映到哪個地址,是由作業系統分配的。

現在大概明白了“記憶體對映”是什麼了,那麼核心是怎麼管理這些地址空間的呢?任何複雜的理論最終也是通過各種資料結構體現出來的,而這裡這個資料結構就是程序描述符。從核心看,程序是分配系統資源(CPU、記憶體)的載體,為了管理程序,核心必須對每個程序所做的事情進行清楚的描述,這就是程序描述符,核心用task_struct結構體來表示程序,並且維護一個該結構體連結串列來管理所有程序。該結構體包含一些程序狀態、排程資訊等上千個成員,我們這裡主要關注程序描述符裡面的記憶體描述符(struct mm_struct mm)

linux核心使用vm_area_struct結構來表示一個獨立的虛擬記憶體區域,由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程序使用多個vm_area_struct結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct結構使用連結串列或者樹形結構連結,方便程序快速訪問,如下圖所示:

下面列出完整的vm_area_struct 這裡說明的現在核心採用雙向連結串列來管理程序中的所有記憶體區域。


/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree, or
	 * linkage of vma in the address_space->i_mmap_nonlinear list.
	 */
	union {
		struct {
			struct rb_node rb;
			unsigned long rb_subtree_last;
		} linear;
		struct list_head nonlinear;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units, *not* PAGE_CACHE_SIZE */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
};

現在已經知道了記憶體對映是把裝置地址對映到程序空間地址(注意:並不是所有記憶體對映都是對映到程序地址空間的,ioremap是對映到核心虛擬空間的,mmap是對映到程序虛擬地址的),實質上是分配了一個vm_area_struct結構體加入到程序的地址空間,也就是說,把裝置地址對映到這個結構體,對映過程就是驅動程式要做的事了。

 

二、mmap記憶體對映原理

mmap記憶體對映的實現過程,總的來說可以分為三個階段:

(一)程序啟動對映過程,並在虛擬地址空間中為對映建立虛擬對映區域

1、程序在使用者空間呼叫庫函式mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前程序的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址

3、為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化

4、將新建的虛擬區結構(vm_area_struct)插入程序的虛擬地址區域連結串列或樹中

 

(二)呼叫核心空間的系統呼叫函式mmap(不同於使用者空間函式),實現檔案實體地址和程序虛擬地址的一一對映關係

5、為對映分配了新的虛擬地址區域後,通過待對映的檔案指標,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,連結到核心“已開啟檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護著和這個已開啟檔案相關各項資訊。

6、通過該檔案的檔案結構體,連結到file_operations模組,呼叫核心函式mmap,其原型為:

int mmap(struct file *filp, struct vm_area_struct *vma),不同於使用者空間庫函式。

7、核心mmap函式通過虛擬檔案系統inode模組定位到檔案磁碟實體地址。

8、通過remap_pfn_range函式建立頁表,即實現了檔案地址和虛擬地址區域的對映關係。此時,這片虛擬地址並沒有任何資料關聯到主存中。

 

(三)程序發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝

注:前兩個階段僅在於建立虛擬區間並完成地址對映,但是並沒有將任何檔案資料的拷貝至主存。真正的檔案讀取是當程序發起讀或寫操作時。

9、程序的讀或寫操作訪問虛擬地址空間這一段對映地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址對映,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作後,核心發起請求調頁過程。

11、調頁過程先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫nopage函式把所缺的頁從磁碟裝入到主存中。

12、之後程序即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁碟地址,也即完成了寫入到檔案的過程。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以呼叫msync()來強制同步, 這樣所寫的內容就能立即儲存到檔案裡了。

這裡要說明的是對於普通檔案,是有第三個階段的;而對於裝置檔案,因為它的操作暫存器始終在可訪問的地址空間,故不會出現所謂的缺頁。

首先,“對映”這個詞,就和數學課上說的“一一對映”是一個意思,就是建立一種一一對應關係,在這裡主要是隻 硬碟上檔案 的位置與程序 邏輯地址空間 中一塊大小相同的區域之間的一一對應,如圖1中過程1所示。這種對應關係純屬是邏輯上的概念,物理上是不存在的,原因是程序的邏輯地址(虛擬)空間本身就是不存在的。在記憶體對映的過程中,並沒有實際的資料拷貝,檔案沒有被載入記憶體,只是邏輯上被放入了記憶體,具體到程式碼,就是建立並初始化了相關的資料結構(struct address_space),這個過程有系統呼叫mmap()實現,所以建立記憶體對映的效率很高。

 

既然建立記憶體對映沒有進行實際的資料拷貝,那麼程序又怎麼能最終直接通過記憶體操作訪問到硬碟上的檔案呢?那就要看記憶體對映之後的幾個相關的過程了。

mmap()會返回一個指標ptr,它指向程序邏輯地址空間中的一個地址,這樣以後,程序無需再呼叫read或write對檔案進行讀寫,而只需要通過ptr就能夠操作檔案。但是ptr所指向的是一個邏輯地址,要操作其中的資料,必須通過MMU將邏輯地址轉換成實體地址,如上圖中過程2所示。這個過程與記憶體對映無關。

前面講過,建立記憶體對映並沒有實際拷貝資料,這時,MMU在地址對映表中是無法找到與ptr相對應的實體地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函式會在swap中尋找相對應的頁面,如果找不到(也就是該檔案從來沒有被讀入記憶體的情況),則會通過mmap()建立的對映關係,從硬碟上將檔案讀取到實體記憶體中,上圖中過程3所示。這個過程與記憶體對映無關。

如果在拷貝資料時,發現物理記憶體不夠用,則會通過虛擬記憶體機制(swap)將暫時不用的物理頁面交換到硬碟上,如上圖中過程4所示。這個過程也與記憶體對映無關。

 

三、mmap和常規檔案操作的區別

 

對linux檔案系統不瞭解的朋友,請參閱我之前寫的博文《檔案系統》,我們首先簡單的回顧一下常規檔案系統操作(呼叫read/fread等類函式)中,函式的呼叫過程:

1、程序發起讀檔案請求。

2、核心通過查詢程序檔案符表,定位到核心已開啟檔案集上的檔案資訊,從而找到此檔案的inode。

3、inode在address_space上查詢要請求的檔案頁是否已經快取在頁快取中。如果存在,則直接返回這片檔案頁的內容。

4、如果不存在,則通過inode定位到檔案磁碟地址,將資料從磁碟複製到頁快取。之後再次發起讀頁面過程,進而將頁快取中的資料發給使用者程序。

總結來說,常規檔案操作為了提高讀寫效率和保護磁碟,使用了頁快取機制。這樣造成讀檔案時需要先將檔案頁從磁碟拷貝到頁快取中,由於頁快取處在核心空間,不能被使用者程序直接定址,所以還需要將頁快取中資料頁再次拷貝到記憶體對應的使用者空間中。這樣,通過了兩次資料拷貝過程,才能完成程序對檔案內容的獲取任務。寫操作也是一樣,待寫入的buffer在核心空間不能直接訪問,必須要先拷貝至核心空間對應的主存,再寫回磁碟中(延遲寫回),也是需要兩次資料拷貝。

而使用mmap操作檔案中,建立新的虛擬記憶體區域和建立檔案磁碟地址和虛擬記憶體區域對映這兩步,沒有任何檔案拷貝操作。而之後訪問資料時發現記憶體中並無資料而發起的缺頁異常過程,可以通過已經建立好的對映關係,只使用一次資料拷貝,就從磁碟中將資料傳入記憶體的使用者空間中,供程序使用。

即,常規檔案操作需要從磁碟到頁快取再到使用者主存的兩次資料拷貝。而mmap操控檔案,只需要從磁碟到使用者主存的一次資料拷貝過程。說白了,mmap的關鍵點是實現了使用者空間和核心空間的資料直接互動而省去了空間不同資料不通的繁瑣過程。因此mmap效率更高。

 

四、mmap優點總結

由上文討論可知,mmap優點共有一下幾點:

1、對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。

2、實現了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。

3、提供程序間共享記憶體及相互通訊的方式。不管是父子程序還是無親緣關係的程序,都可以將自身使用者空間對映到同一個檔案或匿名對映到同一片區域。從而通過各自對對映區域的改動,達到程序間通訊和程序間共享的目的。

     同時,如果程序A和程序B都映射了區域C,當A第一次讀取C時通過缺頁從磁碟複製檔案頁到記憶體中;但當B再讀C的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁碟中複製檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。

4、可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap對映很好的解決。換句話說,但凡是需要用磁碟空間代替記憶體的時候,mmap都可以發揮其功效。

 

五、mmap相關函式

函式原型

void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);

返回說明

成功執行時,mmap()返回被對映區的指標。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1], error被設為以下的某個值:

EACCES:訪問出錯
EAGAIN:檔案已被鎖定,或者太多的記憶體已被鎖定
EBADF:fd不是有效的檔案描述詞
EINVAL:一個或者多個引數無效
ENFILE:已達到系統對開啟檔案的限制
ENODEV:指定檔案所在的檔案系統不支援記憶體對映
ENOMEM:記憶體不足,或者程序已超出最大記憶體對映數量
EPERM:權能不足,操作不允許
ETXTBSY:已寫的方式開啟檔案,同時指定MAP_DENYWRITE標誌
SIGSEGV:試著向只讀區寫入
SIGBUS:試著訪問不屬於程序的記憶體區

引數

start:對映區的開始地址(設定為0時表示由系統決定對映區的起始地址。)
length:對映區的長度

prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突。是以下的某個值,可以通過or運算合理地組合在一起

PROT_EXEC :頁內容可以被執行
PROT_READ :頁內容可以被讀取
PROT_WRITE :頁可以被寫入
PROT_NONE :頁不可訪問

flags:指定對映物件的型別,對映選項和對映頁是否可以共享。它的值可以是一個或者多個以下位的組合體

MAP_FIXED :使用指定的對映起始地址,如果由start和len引數指定的記憶體區重疊於現存的對映空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。
MAP_SHARED :與其它所有對映這個物件的程序共享對映空間。對共享區的寫入,相當於輸出到檔案。直到msync()或者munmap()被呼叫,檔案實際上不會被更新。
MAP_PRIVATE :建立一個寫入時拷貝的私有對映。記憶體區域的寫入不會影響到原檔案。這個標誌和以上標誌是互斥的,只能使用其中一個。
MAP_DENYWRITE"這個標誌被忽略。
MAP_EXECUTABLE :同上
MAP_NORESERVE :不要為這個對映保留交換空間。當交換空間被保留,對對映區修改的可能會得到保證。當交換空間不被保留,同時記憶體不足,對對映區的修改會引起段違例訊號。
MAP_LOCKED :鎖定對映區的頁面,從而防止頁面被交換出記憶體。
MAP_GROWSDOWN :用於堆疊,告訴核心VM系統,對映區可以向下擴充套件。
MAP_ANONYMOUS :匿名對映,對映區不與任何檔案關聯。
MAP_ANON :MAP_ANONYMOUS的別稱,不再被使用。
MAP_FILE :相容標誌,被忽略。
MAP_32BIT :將對映區放在程序地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上得到支援。
MAP_POPULATE :為檔案對映通過預讀的方式準備好頁表。隨後對對映區的訪問不會被頁違例阻塞。
MAP_NONBLOCK :僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於記憶體中的頁面建立頁表入口。

fd:有效的檔案描述詞。如果MAP_ANONYMOUS被設定,為了相容問題,其值應為-1

offset:被對映物件內容的起點,通常都是用0。 該值應該為大小為PAGE_SIZE的整數倍 。

相關函式

int munmap( void * addr, size_t len ) ;

成功執行時,munmap()返回0。失敗時,munmap返回-1,error返回標誌和mmap一致;

該呼叫在程序地址空間中解除一個對映關係,addr是呼叫mmap()時返回的地址,len是對映區的大小;

當對映關係解除後,對原來對映地址的訪問將導致段錯誤發生。 

int msync( void *addr, size_t len, int flags )

一般說來,程序在對映空間的對共享內容的改變並不直接寫回到磁碟檔案中,往往在呼叫munmap()後才執行該操作。

可以通過呼叫msync()實現磁碟上檔案內容與共享記憶體區的內容一致。

 

五、mmap使用細節

1、使用mmap需要注意的一個關鍵點是,mmap對映區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k位元組)。原因是,記憶體的最小粒度是頁,而程序虛擬地址空間和記憶體的對映也是以頁為單位。為了匹配記憶體的操作,mmap從磁碟到虛擬地址空間的對映也必須是頁。

2、核心可以跟蹤被記憶體對映的底層物件(檔案)的大小,程序可以合法的訪問在當前檔案大小以內又在記憶體對映區以內的那些位元組。也就是說,如果檔案的大小一直在擴張,只要在對映區域範圍內的資料,程序都可以合法得到,這和對映建立時檔案的大小無關。具體情形參見“情形三”。

3、對映建立之後,即使檔案關閉,對映依然存在。因為對映的是磁碟的地址,不是檔案本身,和檔案控制代碼無關。同時可用於程序間通訊的有效地址空間不完全受限於被對映檔案的大小,因為是按頁對映。

 

在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:

情形一:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映5000位元組到虛擬記憶體中。

分析:因為單位物理頁面的大小是4096位元組,雖然被對映的檔案只有5000位元組,但是對應到程序虛擬地址區域的大小需要滿足整頁大小,因此mmap函式執行後,實際對映到虛擬記憶體區域8192個 位元組,5000~8191的位元組部分用零填充。

此時:

  • 讀/寫前5000個位元組(0~4999),會返回操作檔案內容。
  • 讀位元組5000~8191時,結果全為0。寫5000~8191時,程序不會報錯,但是所寫的內容不會寫入原檔案中 。
  • 讀/寫8192以外的磁碟部分,會返回一個SIGSECV錯誤。

 

情形二:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映15000位元組到虛擬記憶體中,即對映大小超過了原始檔案的大小。

分析:由於檔案的大小是5000位元組,和情形一一樣,其對應的兩個物理頁。那麼這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現在原檔案中。由於程式要求對映15000位元組,而檔案只佔兩個物理頁,因此8192位元組~15000位元組都不能讀寫,操作時會返回異常。

此時:

  • 程序可以正常讀/寫被對映的前5000位元組(0~4999),寫操作的改動會在一定時間後反映在原檔案中。
  • 對於5000~8191位元組,程序可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入後不會反映在檔案中。
  • 對於8192~14999位元組,程序不能對其進行讀寫,會報SIGBUS錯誤。
  • 對於15000以外的位元組,程序不能對其讀寫,會引發SIGSEGV錯誤。

 

情形三:一個檔案初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M位元組空間,mmap返回指標ptr。

分析:如果在對映建立之初,就對檔案進行讀寫操作,由於檔案大小為0,並沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。

但是如果,每次操作ptr讀寫前,先增加檔案的大小(通過檔案操作),那麼ptr在檔案大小內部的操作就是合法的。例如,檔案擴充4096位元組,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要檔案擴充的範圍在1000個物理頁(對映範圍)內,ptr都可以對應操作相同的大小。

這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費。

 

後面我大概會寫幾篇博文,採用例子的方式來舉例mmap的使用。

一篇是以兩個程序開啟同一個普通檔案,實現程序間的通訊和檔案共享。

另一篇會自己實現一個字元驅動,對映硬體暫存器,來讓使用者空間直接操縱暫存器。(這個只是功能性的例子,不建議工作使用)

分析framebuffer子系統中中的mmap的實現。

 

 

本文很大程度參考了下面這位博主的文章,這裡附上鍊接。如果有版權問題,隨時聯絡本人刪除。

http://www.cnblogs.com/huxiao-tee/p/4660352.html