➤背景
一般情況下,Linux系統中,進程的4GB內存空間被划分成為兩個部分------用戶空間和內核空間,大小分別為0~3G,3~4G。用戶進程通常情況下,只能訪問用戶空間的虛擬地址,不能訪問到內核空間。每個進程的用戶空間都是完全獨立、互不相干的,用戶進程各自有不同的頁表。而內核空間是由內核負責映射,它並不會跟着進程改變,是固定的。內核空間地址有自己對應的頁表,內核的虛擬空間獨立於其他程序。3~4G之間的內核空間中,從低地址到高地址依次為:系統物理內存映射區—隔離帶—vmalloc虛擬內存分配區—隔離帶—高端內存映射區—專用頁面映射區—保留區。
➤內核空間內存動態申請
主要包括三個函數:kmalloc(), __get_free_pages, vmalloc。
➣kmalloc(), __get_free_pages申請的內存位於物理地址映射區,而且在物理上也是連續的,返回的虛擬地址與真實的物理地址(物理地址是連續的,虛擬地址也是連續的)只有一個固定的偏移,因此存在較簡單的轉換關系。
➣而vmalloc申請的內存位於vmalloc虛擬內存分配區(這些區都是以線性地址為度量),它在虛擬內存空間給出一塊連續的內存區,實質上,這片連續的虛擬內存在物理內存中並不一定連續,而vmalloc申請的虛擬內存和物理內存之間也沒有簡單的換算關系。因為vmalloc申請的在虛擬內存空間連續的內存區在物理內存中並不一定連續,可以想象為了完成vmalloc,新的頁表需要被建立,因此,調用vmalloc來分配少量內存是不妥的。一般來講,kmalloc用來分配小於128K的內存,而更大的內存塊需要用vmalloc來實現。
➤虛擬地址與物理地址關系
對於內核物理內存映射區的虛擬內存(用kmalloc(), __get_free_pages申請的),使用virt_to_phys()和phys_to_virt()來實現物理地址和內核虛擬地址之間的互相轉換。它實際上,僅僅做了3G的地址移位。上述方法適用於常規內存(內核物理內存映射區),高端內存的虛擬地址與物理地址之間不存在如此簡單的換算關系。因為它涉及到了分離物理頁的頁表控制機制。
➤ioremap
在ARM中,設備的寄存器或者存儲塊的這部分空間屬於內存空間的一部分,我們稱之為IO內存。在內核中訪問IO內存之前,我們只有IO內存的物理地址,這樣是無法通過軟件直接訪問的,需要首先用ioremap()函數將設備所處的物理地址映射到內核虛擬地址空間(3GB~4GB)。然后,才能根據映射所得到的內核虛擬地址范圍,通過訪問指令訪問這些IO內存資源。在將I/O內存資源的物理地址映射成核心虛地址后,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O內存資源了。為了保證驅動程序的跨平台的可移植性,我們應該使用Linux中特定的函數來訪問I/O內存資源,而不應該通過指向核心虛地址的指針來訪問。
➤mmap
用mmap映射一個設備,意味着使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址范圍內進行讀取或者寫入,實際上就是對設備的訪問。這種數據傳輸是直接的,不需要用到內核空間作為數據轉移的中間站。remap_page_range()函數的功能是構造用於映射一段物理地址的新頁表,實現了內核空間與用戶空間的映射。在內核驅動程序的初始化階段,通過ioremap()將物理地址映射到內核虛擬空間;在驅動程序的mmap系統調用中,使用remap_page_range()將該塊ROM映射到用戶虛擬空間。這樣內核空間和用戶空間都能訪問這段被映射后的虛擬地址。
☢進程空間/內核空間/IO內存
其中,后面兩個指的是同一段物理內存區域,只是一個為虛擬地址(內核空間),一個為物理地址(IO內存)。進程空間和內核空間對應着不同的物理地址,它們之間的數據傳遞,是實際的數據的拷貝。
☢進程空間/IO內存
其中,進程空間mmap得到的那段虛擬地址跟IO內存對應着同一段物理地址。這個過程沒有額外的數據中轉,讀寫都直接針對硬件的物理地址進行。
一般來講,小數據量的傳輸用ioremap()就足夠了,
➤IO內存的一般訪問方法
➣首先是調用request_mem_region()申請資源,即告訴內核,本驅動正在使用這段物理內存,其他驅動不得訪問它們。在設備驅動模塊加載或open()函數中進行。
➣接着講寄存器地址通過ioremap()映射到內核空間虛擬地址,之后就可以通過Linux設備訪問編程接口訪問這些設備的寄存器了。在設備驅動初始化、write(),read(),ioctl()函數中進行。
➣ 訪問完成之后,應對ioremap()申請的虛擬地址進行釋放,並釋放release_mem_region()申請的IO內存資源。在設備驅動模塊卸載或release()函數中進行。
➣linux中的物理地址和虛擬地址 :
在支持MMU的32位處理器平台上,Linux系統中的物理存儲空間和虛擬存儲空間的地址范圍分別都是從0x00000000到0xFFFFFFFF,共4GB,但物理存儲空間與虛擬存儲空間布局完全不同。Linux運行在虛擬存儲空間,並負責把系統中實際存在的遠小於4GB的物理內存根據不同需求映射到整個4GB的虛擬存儲空間中。
➤物理存儲空間布局
Linux的物理存儲空間布局與處理器相關,詳細情況可以從處理器用戶手冊的存儲空間分布表(memory map)相關章節中查到,我們這里只列出嵌入式處理器平台Linux物理內存空間的一般布局。
說明:
➣最大node號n不能大於MAX_NUMNODES-1。
➣MAX_NUMNODES表示系統支持的最多node數。在ARM系統中,Sharp芯片最多支持16個nodes,其他芯片最多支持4個nodes。
➣numnodes是當前系統中實際的內存node數。
➣在不支持CONFIG_DISCONTIGMEM選項的系統中,只有一個內存node。
➣最大bank號m不能大於NR_BANKS-1。
➣NR_BANKS表示系統中支持的最大內存bank數,一般等於處理器的RAM片選數。在ARM系統中,Sharp芯片最多支持16個banks,其他芯片最多支持8個banks。
➣mem_init()函數會將所有節點的頁幀位碼表所占空間、孔洞頁描述符空間及空閑內存頁都釋放掉。
➤虛擬存儲空間布局
在支持MMU的系統中,當系統做完硬件初始化后就使能MMU功能,這樣整個系統就運行在虛擬存儲空間中,實現虛擬存儲空間到物理存儲空間映射功能的是處理器的MMU,而虛擬存儲空間與5路存儲空間的映射關系則是由Linux內核來管理的。32位系統中物理存儲空間占4GB空間,虛擬存儲空間同樣占4GB空間,Linux把物理空間中實際存在的遠遠小於4GB的內存空間映射到整個4GB虛擬存儲空間中除映射I/O空間之外的全部空間,所以虛擬內存空間遠遠大於物理內存空間,這就說同一塊物理內存可能映射到多處虛擬內存地址空間上,這正是Linux內存管理職責所在。
說明:
➣線性地址空間:是指Linux系統中從0x00000000到0xFFFFFFFF整個4GB虛擬存儲空間。
➣內核空間:內核空間表示運行在處理器最高級別的超級用戶模式(supervisor mode)下的代碼或數據,內核空間占用從0xC000000到0xFFFFFFFF的1GB線性地址空間,內核線性地址空間由所有進程共享,但只有運行在內核態的進程才能訪問,用戶進程可以通過系統調用切換到內核態訪問內核空間,進程運行在內核態時所產生的地址都屬於內核空間。
➣用戶空間:用戶空間占用從0x00000000到0xBFFFFFFF共3GB的線性地址空間,每個進程都有一個獨立的3GB用戶空間,所以用戶空間由每個進程獨有,但是內核線程沒有用戶空間,因為它不產生用戶空間地址。另外子進程共享(繼承)父進程的用戶空間只是使用與父進程相同的用戶線性地址到物理內存地址的映射關系,而不是共享父進程用戶空間。運行在用戶態和內核態的進程都可以訪問用戶空間。
➣內核邏輯地址空間:是指從PAGE_OFFSET到high_memory之間的線性地址空間,是系統物理內存映射區,它映射了全部或部分(如果系統包含高端內存)物理內存。內核邏輯地址空間與圖18-4中的系統RAM內存物理地址空間是一一對應的(包括內存孔洞也是一一對應的),內核邏輯地址空間中的地址與RAM內存物理地址空間中對應的地址只差一個固定偏移量,如果RAM內存物理地址空間從0x00000000地址編址,那么這個偏移量就是PAGE_OFFSET。
➣低端內存:內核邏輯地址空間所映射物理內存就是低端內存,低端內存在Linux線性地址空間中始終有永久的一一對應的內核邏輯地址,系統初始化過程中將低端內存永久映射到了內核邏輯地址空間,為低端內存建立了虛擬映射頁表。低端內存內物理內存的物理地址與線性地址之間的轉換可以通過__pa(x)和__va(x)兩個宏來進行,__pa(x)將內核邏輯地址空間的地址x轉換成對應的物理地址,相當於__virt_to_phys((unsigned long)(x)),__va(x)則相反,把低端物理內存空間的地址轉換成對應的內核邏輯地址,相當於((void *)__phys_to_virt((unsigned long)(x)))。
➣高端內存:低端內存地址之上的物理內存是高端內存,高端內存在Linux線性地址空間中沒有沒有固定的一一對應的內核邏輯地址,系統初始化過程中不會為這些內存建立映射頁表將其固定映射到Linux線性地址空間,而是需要使用高端內存的時候才為分配的高端物理內存建立映射頁表,使其能夠被內核使用,否則不能被使用。高端內存的物理地址於現行地址之間的轉換不能使用上面的__pa(x)和__va(x)宏。
➣高端內存概念的由來:如上所述,Linux將4GB的線性地址空間划分成兩部分,從0x00000000到0xBFFFFFFF共3GB空間作為用戶空間由用戶進程獨占,這部分線性地址空間並沒有固定映射到物理內存空間上;從0xC0000000到0xFFFFFFFF的第4GB線性地址空間作為內核空間,在嵌入式系統中,這部分線性地址空間除了映射物理內存空間之外還要映射處理器內部外設寄存器空間等I/O空間。0xC0000000~high_memory之間的內核邏輯地址空間專用來固定映射系統中的物理內存,也就是說0xC0000000~high_memory之間空間大小與系統的物理內存空間大小是相同的(當然在配置了CONFIG_DISCONTIGMEMD選項的非連續內存系統中,內核邏輯地址空間和物理內存空間一樣可能存在內存孔洞),如果系統中的物理內存容量遠小於1GB,那么內核現行地址空間中內核邏輯地址空間之上的high_memory~0xFFFFFFFF之間還有足夠的空間來固定映射一些I/O空間。可是,如果系統中的物理內存容量(包括內存孔洞)小於1GB,那么就沒有足夠的內核線性地址空間來固定映射系統全部物理內存以及一些I/O空間了,為了解決這個問題,在x86處理器平台設置了一個經驗值:896MB,就是說,如果系統中的物理內存(包括內存孔洞)大於896MB,那么將前896MB物理內存固定映射到內核邏輯地址空間0xC0000000~0xC0000000+896MB(=high_memory)上,而896MB之后的物理內存則不建立到內核線性地址空間的固定映射,這部分內存就叫高端物理內存。此時內核線性地址空間high_memory~0xFFFFFFFF之間的128MB空間就稱為高端內存線性地址空間,用來映射高端物理內存和I/O空間。896MB是x86處理器平台的經驗值,留了128MB線性地址空間來映射高端內存以及I/O地址空間,我們在嵌入式系統中可以根據具體情況修改這個閾值,比如,MIPS中將這個值設置為0x20000000B(512MB),那么只有當系統中的物理內存空間容量大於0x20000000B時,內核才需要配置CONFIG_HIGHMEM選項,使能內核對高端內存的分配和映射功能。什么情況需要划分出高端物理內存以及高端物理內存閾值的設置原則見上面的內存頁區(zone)概念說明。
➣高端線性地址空間:從high_memory到0xFFFFFFFF之間的線性地址空間屬於高端線性地址空間,其中VMALLOC_START~VMALLOC_END之間線性地址被vmalloc()函數用來分配物理上不連續但線性地址空間連續的高端物理內存,或者被vmap()函數用來映射高端或低端物理內存,或者由ioremap()函數來重新映射I/O物理空間。PKMAP_BASE開始的LAST_PKMAP(一般等於1024)頁線性地址空間被kmap()函數用來永久映射高端物理內存。FIXADDR_START開始的KM_TYPE_NR*NR_CPUS頁線性地址空間被kmap_atomic()函數用來臨時映射高端物理內存,其他未用高端線性地址空間可以用來在系統初始化期間永久映射I/O地址空間。
➤嵌入式系統中如何訪問I/O資源
幾乎每一種外設都是通過讀寫設備上的寄存器來進行的,通常包括控制寄存器、狀態寄存器和數據寄存器三大類,外設的寄存器通常被連續地編址。根據CPU體系結構的不同,CPU對IO端口的編址方式有兩種:
➣I/O映射方式(I/O-mapped)
典型地,如X86處理器為外設專門實現了一個單獨的地址空間,稱為"I/O地址空間"或者"I/O端口空間",CPU通過專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元。
➣內存映射方式(Memory-mapped)
RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個物理地址空間,外設I/O端口成為內存的一部分。此時,CPU可以象訪問一個內存單元那樣訪問外設I/O端口,而不需要設立專門的外設I/O指令。
但是,這兩者在硬件實現上的差異對於軟件來說是完全透明的,驅動程序開發人員可以將內存映射方式的I/O端口和外設內存統一看作是"I/O內存"資源。
一般來說,在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定。但是CPU通常並沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址范圍,驅動程序並不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內(通過頁表),然后才能根據映射所內指令訪問這些I/O得到的核心虛地址范圍,通過訪內存資源。Linux在io.h頭文件中聲明了函數ioremap(),用來將I/O內存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,原型如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
iounmap函數用於取消ioremap()所做的映射,原型如下:
void iounmap(void * addr);
這兩個函數都是實現在mm/ioremap.c文件中。
在將I/O內存資源的物理地址映射成核心虛地址后,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O內存資源了。為了保證驅動程序的跨平台的可移植性,我們應該使用Linux中特定的函數來訪問I/O內存資源,而不應該通過指向核心虛地址的指針來訪問。如在x86平台上,讀寫I/O的函數如下所示:
#define readb(addr) (*(volatile unsigned char *) __io_virt(addr)) #define readw(addr) (*(volatile unsigned short *) __io_virt(addr)) #define readl(addr) (*(volatile unsigned int *) __io_virt(addr)) #define writeb(b,addr) (*(volatile unsigned char *) __io_virt(addr) = (b)) #define writew(b,addr) (*(volatile unsigned short *) __io_virt(addr) = (b)) #define writel(b,addr) (*(volatile unsigned int *) __io_virt(addr) = (b)) #define memset_io(a,b,c) memset(__io_virt(a),(b),(c)) #define memcpy_fromio(a,b,c) memcpy((a),__io_virt(b),(c)) #define memcpy_toio(a,b,c) memcpy(__io_virt(a),(b),(c))
最后,我們要特別強調驅動程序中mmap函數的實現方法。用mmap映射一個設備,意味着使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址范圍內進行讀取或者寫入,實際上就是對設備的訪問。