由於Linux系統提供了複雜的記憶體管理功能,本節將講解的是記憶體和I/O的訪問程式設計。
在X86中,I/O空間是相對於記憶體空間而言的,通過特定的in、out來訪問,in、out指令格式如下:
1 2 |
IN 累加器,{埠號|DX} OUT {埠號|DX},累加器 |
下面說說MMU(記憶體管理單元),作業系統藉助MMU可以讓使用者感覺到好像程式可以使用非常大的核心空間,實際上就是我們平時瞭解的虛擬地址一樣的。為了好好了解一下MMU,先看兩個概念
TLB:MMU的核心部件,快取少量的虛擬地址與實體地址的轉換關係,是轉換表的Cache
TTW:當TLB中沒有緩衝對應的地址轉換關係時候,需要通過對記憶體中轉換表的訪問來獲取虛擬地址和實體地址的對應關係,TTW成功後,結果應該寫入TLB。
為了說明MMU在記憶體中使用的關係,下圖可以說明如下關係
對於提供了 MMU(儲存管理器,輔助作業系統進行記憶體管理,提供虛實地址轉換等硬體支援)的處理器而言,Linux 提供了複雜的儲存管理系統,使得程式所能訪問的記憶體達到4GB。程式的 4GB 記憶體空間被人為的分為兩個部分——使用者空間與核心空間。使用者空間地址分佈從0 到3GB(PAGE_OFFSET,在0x86 中它等於0xC0000000),3GB 到4GB 為核心空間,
核心空間中,從3G 到vmalloc_start 這段地址是實體記憶體對映區域(該區域中包含了核心映象、物理頁框表mem_map 等等)kmalloc 和get_free_page 申請的記憶體位於實體記憶體對映區域,而且在物理上也是連續的,它們與真實的實體地址只有一個固定的偏移,因此存在較簡單的轉換關係,virt_to_phys()可以實現核心虛擬地址轉化為實體地址:
1 2 3 4 5 |
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) extern inline unsigned long virt_to_phys(volatile void * address) { return __pa(address); } |
上面轉換過程是將虛擬地址減去3G(PAGE_OFFSET=0XC000000)。與之對應的函式為phys_to_virt(),將核心實體地址轉化為虛擬地址:
1 2 3 4 5 |
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) extern inline void * phys_to_virt(unsigned long address) { return __va(address); } |
virt_to_phys()和phys_to_virt()都定義在includeasm-i386io.h中。而vmalloc申請的記憶體則位於vmalloc_start~vmalloc_end之間,與實體地址沒有簡單的轉換關係,雖然在邏輯上它們也是連續的,但是在物理上它們不要求連續。我們用下面的程式來演示kmalloc、get_free_page和vmalloc的區別:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#include <linux/module.h> #include <linux/slab.h> #include <linux/vmalloc.h> MODULE_LICENSE("GPL"); unsigned char *pagemem; unsigned char *kmallocmem; unsigned char *vmallocmem; int __init mem_module_init(void) { //最好每次記憶體申請都檢查申請是否成功 //下面這段僅僅作為演示的程式碼沒有檢查 pagemem = (unsigned char*)get_free_page(0); printk("<1>pagemem addr=%x", pagemem); kmallocmem = (unsigned char*)kmalloc(100, 0); printk("<1>kmallocmem addr=%x", kmallocmem); vmallocmem = (unsigned char*)vmalloc(1000000); printk("<1>vmallocmem addr=%x", vmallocmem); return 0; } void __exit mem_module_exit(void) { free_page(pagemem); kfree(kmallocmem); vfree(vmallocmem); } module_init(mem_module_init); module_exit(mem_module_exit); |
裝置通常會提供一組暫存器來用於控制裝置、讀寫裝置、獲取裝置狀態,那麼Linux裝置驅動究竟怎樣訪問裝置的I/O埠(暫存器)和I/O記憶體的訪問的呢
1.操作I/O口
(1)申請I/O 埠:
在驅動還沒獨佔裝置之前,不應對埠進行操作。核心提供了一個註冊介面,以允許驅動宣告其需要的埠:
1 2 3 4 5 |
/* request_region告訴核心:要使用first開始的n個埠。引數name為裝置名。 如果分配成功返回值是非NULL;否則無法使用需要的埠(/proc/ioports包含了系統當前所有埠的分配資訊, 若request_region分配失敗時,可以檢視該檔案,看誰先用了你要的埠) */ struct resource *request_region(unsigned long first, unsigned long n, const char *name); |
(2)訪問IO埠:
在驅動成功請求到I/O 埠後,就可以讀寫這些埠了。大部分硬體會將8位、16位和32位埠區分開,無法像訪問記憶體那樣混淆使用。驅動程式必須呼叫不同的函式來訪問不同大小的埠。
Linux 核心標頭檔案(體系依賴的標頭檔案<asm/io.h>) 定義了下列行內函數來存取I/O埠:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* inb/outb:讀/寫位元組埠(8位寬)。有些體系將port引數定義為unsigned long; 而有些平臺則將它定義為unsigned short。inb的返回型別也是依賴體系的 */ unsigned inb(unsigned port); void outb(unsigned char byte, unsigned port); /* inw/outw:讀/寫字埠(16位寬) */ unsigned inw(unsigned port); void outw(unsigned short word, unsigned port); /* inl/outl:讀/寫32位埠。longword也是依賴體系的,有的體系為unsigned long;而有的為unsigned int */ unsigned inl(unsigned port); void outl(unsigned longword, unsigned port); |
(3)釋放IO埠:
1 2 |
/* 用完I/O埠後(可能在模組解除安裝時),應當呼叫release_region將I/O埠返還給系統。引數start和n應與之前傳遞給request_region一致 */ void release_region(unsigned long start, unsigned long n); |
2 操作IO記憶體
(1)申請I/O 記憶體:
I/O 記憶體區在使用前必須先分配。分配記憶體區的函式介面在<linux/ioport.h>定義中:
1 2 3 4 |
/* request_mem_region分配一個開始於start,len位元組的I/O記憶體區。分配成功,返回一個非NULL指標;否則返回NULL。 系統當前所有I/O記憶體分配資訊都在/proc/iomem檔案中列出,你分配失敗時,可以看看該檔案,看誰先佔用了該記憶體區 */ struct resource *request_mem_region(unsigned long start, unsigned long len, char *name); |
(2)對映:
在訪問I/O記憶體之前,分配I/O記憶體並不是唯一要求的步驟,你還必須保證核心可存取該I/O記憶體。訪問I/O記憶體並不只是簡單解引用指標,在許多體系中,I/O 記憶體無法以這種方式直接存取。因此,還必須通過ioremap 函式設定一個對映。
1 2 3 4 5 |
/* ioremap用於將I/O記憶體區對映到虛擬地址。 引數phys_addr為要對映的I/O記憶體起始地址, 引數size為要對映的I/O記憶體的大小,返回值為被對映到的虛擬地址 */ void *ioremap(unsigned long phys_addr, unsigned long size); |
(3)訪問IO記憶體:
經過 ioremap之後,裝置驅動就可以存取任何I/O記憶體地址。注意,ioremap返回的地址不可以直接解引用;相反,應當使用核心提供的訪問函式。訪問I/O記憶體的正確方式是通過一系列專門用於實現此目的的函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <asm/io.h> /* I/O記憶體讀函式。引數addr應當是從ioremap獲得的地址(可能包含一個整型偏移); 返回值是從給定I/O記憶體讀取到的值 */ unsigned int ioread8(void *addr); unsigned int ioread16(void *addr); unsigned int ioread32(void *addr); /* I/O記憶體寫函式。引數addr同I/O記憶體讀函式,引數value為要寫的值 */ void iowrite8(u8 value, void *addr); void iowrite16(u16 value, void *addr); void iowrite32(u32 value, void *addr); /* 以下這些函式讀和寫一系列值到一個給定的 I/O 記憶體地址,從給定的buf讀或寫count個值到給定的addr。引數count表示要讀寫的資料個數,而不是位元組大小 */ void ioread8_rep(void *addr, void *buf, unsigned long count); void ioread16_rep(void *addr, void *buf, unsigned long count); void ioread32_rep(void *addr, void *buf, unsigned long count); void iowrite8_rep(void *addr, const void *buf, unsigned long count); void iowrite16_rep(void *addr, const void *buf, unsigned long count); void iowrite32_rep(void *addr,,onst void *buf,,nsigned long count); /* 需要操作一塊I/O 地址時,使用下列函式(這些函式的行為類似於它們的C庫類似函式): */ void memset_io(void *addr, u8 value, unsigned int count); void memcpy_fromio(void *dest, void *source, unsigned int count); void memcpy_toio(void *dest, void *source, unsigned int count); /* 舊的I/O記憶體讀寫函式,不推薦使用 */ unsigned readb(address); unsigned readw(address); unsigned readl(address); void writeb(unsigned value, address); void writew(unsigned value, address); void writel(unsigned value, address); |
(4)釋放IO記憶體步驟:
1 2 |
void iounmap(void * addr); /* iounmap用於釋放不再需要的對映 */ void release_mem_region(unsigned long start, unsigned long len); /* iounmap用於釋放不再需要的對映 */ |
幾乎每一種外設都是通過讀寫裝置上的暫存器來進行的,通常包括控制暫存器、狀態暫存器和資料暫存器三大類,外設的暫存器通常被連續地編址。根據CPU體系結構的不同,CPU對IO埠的編址方式有兩種:
(1)I/O對映方式(I/O-mapped)
典型地,如X86處理器為外設專門實現了一個單獨的地址空間,稱為”I/O地址空間”或者”I/O埠空間”,CPU通過專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元。
(2)記憶體對映方式(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)中,原型如下:
1 |
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags); |
iounmap函式用於取消ioremap()所做的對映,原型如下:
1 |
void iounmap(void * addr); |
在將I/O記憶體資源的實體地址對映成核心虛地址後,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O記憶體資源了。為了保證驅動程式的跨平臺的可移植性,我們應該使用Linux中特定的函式來訪問I/O記憶體資源,而不應該通過指向核心虛地址的指標來訪問。
最後,我們要特別強調驅動程式中mmap函式的實現方法。用mmap對映一個裝置,意味著使使用者空間的一段地址關聯到裝置記憶體上,這使得只要程式在分配的地址範圍內進行讀取或者寫入,實際上就是對裝置的訪問。≠
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式