一段摘自《Linux設備驅動程序》的話:
每種外設都通過讀寫寄存器進行控制。大部分外設都有多個寄存器,不管是內存地址空間還是I/O地址空間,這些寄存器的訪問地址都是連續的。
在硬件層,內存區域和I/O區域沒有概念上的區別:它們都通過向地址總線和控制總線發送電平信號進行訪問,在通過數據總線讀寫數據。一些CPU制造廠商在它們的芯片中使用單一的地址空間,而另一些則為外設保留了獨立的地址空間,以便和內存區分開來。一些處理器(主要是X86家族的)還為I/O端口的讀寫提供了單獨的線路,並且使用特殊的CPU指令訪問端口。
I/O端口和I/O內存是兩種概念上的方法,用以支持設備驅動程序和設備之間的通信。為使得各種不同的驅動程序彼此互不干擾,有必要事先為驅動程序分配端口和I/O內存范圍。這確保幾種設備驅動程序不會試圖訪問同樣的資源。
6.6.1 資源管理
我們首先看看管理資源的數據結構和函數。
1. 樹數據結構
Linux提供了一個通用框架,用於在內存中構建數據結構。這些結構描述了系統可用的資源,使得內核代碼能夠管理和分配資源。注意,其中關鍵的數據結構式resource,定義如下:
/* ./include/linux/ioport.h */ struct resource { resource_size_t start; resource_size_t end; // start 和 end 通常表示某個地址空間中的一個區域 const char *name; // 資源名稱,實際與內核無關,只是在以可讀形式輸出資源列表(在proc文件系統中)時比較有用 unsigned long flags; // 更准確的描述資源及其當前狀態 struct resource *parent, *sibling, *child; // 這3個指向resource的指針建立了一個樹形層次的結構 };
樹形結構中的資源管理
從上圖可以發現,resource的parent、child、sibling成員的規則如下:
- 每個子結點只有一個父結點
- 一個父結點可以有人以數目的子結點
- 同一個父結點的所有子結點,會連接到兄弟結點連表上
在內存中,表示數據結構時,必須注意以下問題:
- 盡管每個子結點都有一個指針指向父結點,但父結點只有一個指針指向第一個子結點。所有其他子結點都通過兄弟結點訪問鏈表。
- 指向父結點的指針同樣可以為NULL,在這種情況下,說明已經沒有更高層次的結點了。
如何將層次結構一年關於設備驅動程序呢?我們來考察一個系統總線的例子,其附接了一塊網卡。網卡支持兩個輸出,每個都分配一塊特定的內存區域,用於數據的輸入和輸出。總線自身也有一個I/O內存區域,其中一些部分由網卡使用。
該方案可以完美地融入到樹形層次結構中。總線的內存區域理論上占用了0和1000之間的內存范圍,充當根結點(最高的父結點)。網卡要求使用100和199之間的內存區域,這是根結點的一個子結點。網卡的子結點表示各個網絡輸出,分配I/O內存區分別為100到149和150到199.原來較大的資源區域被划分為了較小的部分,每次細分都表示了抽象模型中的一個層次。因此,子結點可用於將內存區划分為越來越小、功能越來越具體的部分。
2. 請求和釋放資源
為確保可靠地配置資源,內核必須提供一種機制來分配和釋放資源。一旦資源已經被分配,則不能由任何其他驅動程序使用。
請求和釋放資源,無非是從資源樹中添加和刪除而已。
- 請求資源
內核提供了一個__request_resource函數,用於請求一個資源區域。這函數需要一系列參數,包括一個指向父結點的指針,資源區域的起始地址和結束地址,表示該區域名稱的字符串。
/* ./kernel/resource.c */ /* Return the conflict entry if you can't request it */ static struct resource * __request_resource(struct resource *root, struct resource *new) { resource_size_t start = new->start; resource_size_t end = new->end; struct resource *tmp, **p; if (end < start) return root; if (start < root->start) return root; if (end > root->end) return root; // 判斷要request source是否在root source的范圍內 p = &root->child; for (;;) { // 連續地掃描現存資源,將新資源添加到正確的位置,或者發現與已分配區域的沖突 tmp = *p; if (!tmp || tmp->start > end) { // 在合適的位置插入 new->sibling = tmp; *p = new; new->parent = root; return NULL; } p = &tmp->sibling; // 僅僅遍歷同一層次的兄弟結點,不會掃描更底層子結點的鏈表 if (tmp->end < start) continue; return tmp; } } /** * request_resource_conflict - request and reserve an I/O or memory resource * @root: root resource descriptor * @new: resource descriptor desired by caller * * Returns 0 for success, conflict resource on error. */ struct resource *request_resource_conflict(struct resource *root, struct resource *new) { struct resource *conflict; write_lock(&resource_lock); conflict = __request_resource(root, new); write_unlock(&resource_lock); return conflict; } /** * request_resource - request and reserve an I/O or memory resource * @root: root resource descriptor * @new: resource descriptor desired by caller * * Returns 0 for success, negative error code on error. */ int request_resource(struct resource *root, struct resource *new) { struct resource *conflict; conflict = request_resource_conflict(root, new); return conflict ? -EBUSY : 0; } EXPORT_SYMBOL(request_resource);
- 釋放資源
static int __release_resource(struct resource *old) { struct resource *tmp, **p; p = &old->parent->child; for (;;) { // 遍歷與old同一層的所有結點,發現與old相同的結點即old結點,刪除該結點並鏈接鏈表 tmp = *p; if (!tmp) break; if (tmp == old) { *p = tmp->sibling; old->parent = NULL; return 0; } p = &tmp->sibling; } return -EINVAL; } /** * release_resource - release a previously reserved resource * @old: resource pointer */ int release_resource(struct resource *old) { int retval; write_lock(&resource_lock); retval = __release_resource(old); write_unlock(&resource_lock); return retval; }
6.6.2 I/O內存
資源管理還有一個很重要的方面是I/O內存的分配方式,因為在所有平台上這都是與外設通信的主要方法(IA-32除外,其中I/O端口更為主要)。
I/O內存不僅包括與擴展設備通信直接使用的內存區域,還包括系統中可用的物理內存和ROM存儲器,以及包含在資源列表中的內存。
看下kernel 3.08的iomem信息:
root@paramount:/ # cat /proc/iomem
00000000-0fefffff : System RAM
00010000-005c7e73 : Kernel code
005c7e74-00770d87 : Kernel data
10003000-100030ff : jz-rtc.0
10003000-100030ff : jz-rtc
10020000-1002006f : dsp.0
10020000-1002006f : dsp
10030000-10030fff : jz-uart.0
10033000-10033fff : jz-uart.3
10050000-10050fff : jz-i2c.0
10051000-10051fff : jz-i2c.1
10052000-10052fff : jz-i2c.2
10071000-10071000 : dsp.1
10071000-10071000 : dsp
13040000-130407ff : galcore register region
13050000-130517ff : jz-fb.0
13050000-130517ff : jz-fb
13080000-13087fff : jz-ipu.0
13080000-13087fff : jz-ipu
13200000-132effff : jz-vpu.0
13300000-13350000 : ovisp-camera
13300000-13350000 : ovisp-camera
13420000-1342ffff : jz-dma
13420000-1342ffff : jz-dma
13450000-13450fff : jzmmc_v1.2.0
13460000-13460fff : jzmmc_v1.2.1
13500000-1353ffff : jz-dwc2
13500000-1353ffff : jz-dwc2
13500000-1353ffff : dwc2
arm或mips架構,對kernel而言,其實所有的系統資源就是memory,或者所有的系統資源就是iomem。
比如,以我們使用的mips32架構為例:
/* kernel/arch/mips/xburst/soc-m200/common/setup.c */ void __init plat_mem_setup(void) { /* jz mips cpu special */ __asm__ ( "li $2, 0xa9000000 \n\t" "mtc0 $2, $5, 4 \n\t" "nop \n\t" ::"r"(2)); /* use IO_BASE, so that we can use phy addr on hard manual * directly with in(bwlq)/out(bwlq) in io.h. */ set_io_port_base(IO_BASE); ioport_resource.start = 0x00000000; ioport_resource.end = 0xffffffff; iomem_resource.start = 0x00000000; iomem_resource.end = 0xffffffff; // iomem_resource是表示整個系統的memory空間,所有其他資源都是從屬於該子資源的child或Grandson setup_init(); init_all_clk(); #ifdef CONFIG_ANDROID_PMEM /* reserve memory for pmem. */ board_pmem_setup(); #endif return; }
在使用I/O內存時,分配內存區域並不是所需的唯一操作。取決於總線系統和處理器類型,可能必須將擴展設備的地址空間映射到內核地址空間中,才能訪問該設備(陳志偉軟件I/O映射)。這是通過使用ioremap內核函數適當的設置系統頁表而實現的,內核源代碼中有若干不同地方使用了該函數,其定義與體系結構相關。同樣地,提供了特定於體系結構的iounmap函數來解除映射。
在某種程度上,實現對進城頁表的操作冗長而復雜。特別地,不同系統的實現有很大的差別,而且它對理解設備驅動程序並不重要,所以此處不再討論其實現。一般地說,更重要的是:將一個物理地址映射到處理器的虛擬地址空間中,使得內核可以使用該地址。就設備驅動而言,這意味着擴展總線的地址空間映射到處理器的虛擬地址空間中,使得能夠用普通內存訪問函數操作總線/設備。
1. request_source分配內存區域 2. ioremap建立擴展總線地址空間到內核虛擬地址空間的映射。
通過上述兩步,是否意味着在驅動程序中需要訪問io時,可以直接對ioremap的指針進行反引用的?答案是否定的。
6.3.3 I/O端口
I/O端口是一種與設備和總線通信的流行方法,特別是在IA-32平台上。
可以使用 cat /proc/ioports 查看I/O端口。