iomem—I/O映射方式的I/O端口和内存映射方式的I/O端口
Linux將基于I/O映射方式的I/O端口和基于內(nèi)存映射方式的I/O端口資源統(tǒng)稱為“I/O區(qū)域”(I/O Region)。I/O Region仍然是一種I/O資源,因此它仍然可以用resource結(jié)構(gòu)類型來描述。下面我們就來看看Linux是如何管理I/O Region的。
3.3.1 I/O Region的分配
在函數(shù)__request_resource()的基礎(chǔ)上,Linux實(shí)現(xiàn)了用于分配I/O區(qū)域的函數(shù)__request_region(),如下:
?
?1?struct?resource?*?__request_region(struct?resource?*parent,?2? unsigned?long?start,?unsigned?long?n,?const?char?*name)
?3? {
?4? struct?resource?*res?=?kmalloc(sizeof(*res),?GFP_KERNEL);
?5?
?6? if?(res)?{
?7? memset(res,?0,?sizeof(*res));
?8? res->name?=?name;
?9? res->start?=?start;
10? res->end?=?start?+?n?-?1;
11? res->flags?=?IORESOURCE_BUSY;
12?
13? write_lock(&resource_lock);
14?
15? for?(;;)?{
16? struct?resource?*conflict;
17?
18? conflict?=?__request_resource(parent,?res);
19? if?(!conflict)
20? break;?
21?
22? if?(conflict?!=?parent)?{
23? parent?=?conflict;
24? if?(!(conflict->flags?&?IORESOURCE_BUSY))
25? continue;
26? }
27?
28? /*?Uhhuh,?that?didn't?work?out..?*/
29? kfree(res);
30? res?=?NULL;
31? break;
32? }
33? write_unlock(&resource_lock);
34? }
35? return?res;
36? }
37?
NOTE:
①首先,調(diào)用kmalloc()函數(shù)在SLAB分配器緩存中分配一個(gè)resource結(jié)構(gòu)。
②然后,相應(yīng)的根據(jù)參數(shù)來填充resource結(jié)構(gòu)。注意!flags成員被初始化為IORESOURCE_BUSY。
③接下來,用一個(gè)for循環(huán)開始進(jìn)行資源分配,循環(huán)體的步驟如下:
l 首先,調(diào)用__request_resource()函數(shù)進(jìn)行資源分配。如果返回NULL,說明分配成功,因此就執(zhí)行break語句推出for循環(huán),返回所分配的resource結(jié)構(gòu)的指針,函數(shù)成功地結(jié)束。
l 如果__request_resource()函數(shù)分配不成功,則進(jìn)一步判斷所返回的沖突資源節(jié)點(diǎn)是否就是父資源節(jié)點(diǎn)parent。如果不是,則將分配行為下降一個(gè)層次,即試圖在當(dāng)前沖突的資源節(jié)點(diǎn)中進(jìn)行分配(只有在沖突的資源節(jié)點(diǎn)沒有設(shè)置IORESOURCE_BUSY的情況下才可以),于是讓 parent指針等于conflict,并在conflict->flags&IORESOURCE_BUSY為0的情況下執(zhí)行 continue語句繼續(xù)for循環(huán)。
l 否則如果相沖突的資源節(jié)點(diǎn)就是父節(jié)點(diǎn)parent,或者相沖突資源節(jié)點(diǎn)設(shè)置了IORESOURCE_BUSY標(biāo)志位,則宣告分配失敗。于是調(diào)用kfree ()函數(shù)釋放所分配的resource結(jié)構(gòu),并將res指針置為NULL,最后用break語句推出for循環(huán)。
④最后,返回所分配的resource結(jié)構(gòu)的指針。
3.3.2 I/O Region的釋放
函數(shù)__release_region()實(shí)現(xiàn)在一個(gè)父資源節(jié)點(diǎn)parent中釋放給定范圍的I/O Region。實(shí)際上該函數(shù)的實(shí)現(xiàn)思想與__release_resource()相類似。其源代碼如下:
?2? unsigned?long?start,?unsigned?long?n)
?3? {
?4? struct?resource?**p;
?5? unsigned?long?end;
?6?
?7? p?=?&parent->child;
?8? end?=?start?+?n?-?1;
?9?
10? for?(;;)?{
11? struct?resource?*res?=?*p;
12?
13? if?(!res)
14? break;
15? if?(res->start?<=?start?&&?res->end?>=?end)?{
16? if?(!(res->flags?&?IORESOURCE_BUSY))?{
17? p?=?&res->child;
18? continue;
19? }
20? if?(res->start?!=?start'?'res->end?!=?end)
21? break;
22? *p?=?res->sibling;
23? kfree(res);
24? return;
25? }
26? p?=?&res->sibling;
27? }
28? printk(Trying?to?free?nonexistent?resource?<%08lx-%08lx>
29? ,?start,?end);
30? }
31?
類似地,該函數(shù)也是通過一個(gè)for循環(huán)來遍歷父資源parent的child鏈表。為此,它讓指針res指向當(dāng)前正被掃描的子資源節(jié)點(diǎn),指針p指向前一個(gè)子資源節(jié)點(diǎn)的sibling成員變量,p的初始值為指向parent->child。For循環(huán)體的步驟如下:
①讓res指針指向當(dāng)前被掃描的子資源節(jié)點(diǎn)(res=*p)。
②如果res指針為NULL,說明已經(jīng)掃描完整個(gè)child鏈表,所以退出for循環(huán)。
③如果res指針不為NULL,則繼續(xù)看看所指定的I/O區(qū)域范圍是否完全包含在當(dāng)前資源節(jié)點(diǎn)中,也即看看[start,start+n-1]是否包含在res->[start,end]中。如果不屬于,則讓p指向當(dāng)前資源節(jié)點(diǎn)的sibling成員,然后繼續(xù)for循環(huán)。如果屬于,則執(zhí)行下列步驟:
l 先看看當(dāng)前資源節(jié)點(diǎn)是否設(shè)置了IORESOURCE_BUSY標(biāo)志位。如果沒有設(shè)置該標(biāo)志位,則說明該資源節(jié)點(diǎn)下面可能還會(huì)有子節(jié)點(diǎn),因此將掃描過程下降一個(gè)層次,于是修改p指針,使它指向res->child,然后執(zhí)行continue語句繼續(xù)for循環(huán)。
l 如果設(shè)置了IORESOURCE_BUSY標(biāo)志位。則一定要確保當(dāng)前資源節(jié)點(diǎn)就是所指定的I/O區(qū)域,然后將當(dāng)前資源節(jié)點(diǎn)從其父資源的child鏈表中去除。這可以通過讓前一個(gè)兄弟資源節(jié)點(diǎn)的sibling指針指向當(dāng)前資源節(jié)點(diǎn)的下一個(gè)兄弟資源節(jié)點(diǎn)來實(shí)現(xiàn)(即讓*p=res->sibling),最后調(diào)用kfree()函數(shù)釋放當(dāng)前資源節(jié)點(diǎn)的resource結(jié)構(gòu)。然后函數(shù)就可以成功返回了。
3.3.3 檢查指定的I/O Region是否已被占用
函數(shù)__check_region()檢查指定的I/O Region是否已被占用。其源代碼如下:
?2? {
?3? struct?resource?*?res;
?4?
?5? res?=?__request_region(parent,?start,?n,?check-region);
?6? if?(!res)
?7? return?-EBUSY;
?8?
?9? release_resource(res);
10? kfree(res);
11? return?0;
12? }
13?
該函數(shù)的實(shí)現(xiàn)與__check_resource()的實(shí)現(xiàn)思想類似。首先,它通過調(diào)用__request_region()函數(shù)試圖在父資源 parent中分配指定的I/O Region。如果分配不成功,將返回NULL,因此此時(shí)函數(shù)返回錯(cuò)誤值-EBUSY表示所指定的I/O Region已被占用。如果res指針不為空則說明所指定的I/O Region沒有被占用。于是調(diào)用__release_resource()函數(shù)將剛剛分配的資源釋放掉(實(shí)際上是將res結(jié)構(gòu)從parent的 child鏈表去除),然后調(diào)用kfree()函數(shù)釋放res結(jié)構(gòu)所占用的內(nèi)存。最后,返回0值表示指定的I/O Region沒有被占用。 中國網(wǎng)管論壇bbs.bitsCN.com
3.4 管理I/O端口資源
我們都知道,采用I/O映射方式的X86處理器為外設(shè)實(shí)現(xiàn)了一個(gè)單獨(dú)的地址空間,也即“I/O空間”(I/O Space)或稱為“I/O端口空間”,其大小是64KB(0x0000-0xffff)。Linux在其所支持的所有平臺(tái)上都實(shí)現(xiàn)了“I/O端口空間” 這一概念。
由于I/O空間非常小,因此即使外設(shè)總線有一個(gè)單獨(dú)的I/O端口空間,卻也不是所有的外設(shè)都將其I/O端口(指寄存器)映射到“I/O端口空間”中。比如,大多數(shù)PCI卡都通過內(nèi)存映射方式來將其I/O端口或外設(shè)內(nèi)存映射到CPU的RAM物理地址空間中。而老式的 ISA卡通常將其I/O端口映射到I/O端口空間中。
Linux是基于“I/O Region”這一概念來實(shí)現(xiàn)對(duì)I/O端口資源(I/O-mapped 或 Memory-mapped)的管理的。
3.4.1 資源根節(jié)點(diǎn)的定義
Linux在kernel/Resource.c文件中定義了全局變量ioport_resource和iomem_resource,來分別描述基于I/O映射方式的整個(gè)I/O端口空間和基于內(nèi)存映射方式的I/O內(nèi)存資源空間(包括I/O端口和外設(shè)內(nèi)存)。其定義如下:
2? {?PCI?IO,?0x0000,?IO_SPACE_LIMIT,?IORESOURCE_IO?};
3? struct?resource?iomem_resource?=
4? {?PCI?mem,?0x00000000,?0xffffffff,?IORESOURCE_MEM?};
其中,宏IO_SPACE_LIMIT表示整個(gè)I/O空間的大小,對(duì)于X86平臺(tái)而言,它是0xffff(定義在include/asm-i386/io.h頭文件中)。顯然,I/O內(nèi)存空間的大小是4GB。
3.4.2 對(duì)I/O端口空間的操作
基于I/O Region的操作函數(shù)__XXX_region(),Linux在頭文件include/linux/ioport.h中定義了三個(gè)對(duì)I/O端口空間進(jìn)行操作的宏:①request_region()宏,請(qǐng)求在I/O端口空間中分配指定范圍的I/O端口資源。②check_region()宏,檢查 I/O端口空間中的指定I/O端口資源是否已被占用。③release_region()宏,釋放I/O端口空間中的指定I/O端口資源。這三個(gè)宏的定義如下:
2? __request_region(&ioport_resource,?(start),?(n),?(name))
3? #define?check_region(start,n)
4? __check_region(&ioport_resource,?(start),?(n))
5? #define?release_region(start,n)
6? __release_region(&ioport_resource,?(start),?(n))
7?
其中,宏參數(shù)start指定I/O端口資源的起始物理地址(是I/O端口空間中的物理地址),宏參數(shù)n指定I/O端口資源的大小。
3.4.3 對(duì)I/O內(nèi)存資源的操作
基于I/O Region的操作函數(shù)__XXX_region(),Linux在頭文件include/linux/ioport.h中定義了三個(gè)對(duì)I/O內(nèi)存資源進(jìn)行操作的宏:①request_mem_region()宏,請(qǐng)求分配指定的I/O內(nèi)存資源。②check_ mem_region()宏,檢查指定的I/O內(nèi)存資源是否已被占用。③release_ mem_region()宏,釋放指定的I/O內(nèi)存資源。這三個(gè)宏的定義如下:
2? __request_region(&iomem_resource,?(start),?(n),?(name))
3? #define?check_mem_region(start,n)
4? __check_region(&iomem_resource,?(start),?(n))
5? #define?release_mem_region(start,n)
6? __release_region(&iomem_resource,?(start),?(n))
7?
其中,參數(shù)start是I/O內(nèi)存資源的起始物理地址(是CPU的RAM物理地址空間中的物理地址),參數(shù)n指定I/O內(nèi)存資源的大小。
3.4.4 對(duì)/proc/ioports和/proc/iomem的支持
Linux在ioport.h頭文件中定義了兩個(gè)宏:
get_ioport_list()和get_iomem_list(),分別用來實(shí)現(xiàn)/proc/ioports文件和/proc/iomem文件。其定義如下:
2? #define?get_mem_list(buf)?get_resource_list(&iomem_resource,?buf,?PAGE_SIZE)
3?
3.5 訪問I/O端口空間
在驅(qū)動(dòng)程序請(qǐng)求了I/O端口空間中的端口資源后,它就可以通過CPU的IO指定來讀寫這些I/O端口了。在讀寫I/O端口時(shí)要注意的一點(diǎn)就是,大多數(shù)平臺(tái)都區(qū)分8位、16位和32位的端口,也即要注意I/O端口的寬度。
Linux在include/asm/io.h頭文件(對(duì)于i386平臺(tái)就是include/asm-i386/io.h)中定義了一系列讀寫不同寬度I/O端口的宏函數(shù)。如下所示:
⑴讀寫8位寬的I/O端口
2? void?outb(unsigned?char?value,unsigned?port);
3?
其中,port參數(shù)指定I/O端口空間中的端口地址。在大多數(shù)平臺(tái)上(如x86)它都是unsigned short類型的,其它的一些平臺(tái)上則是unsigned int類型的。顯然,端口地址的類型是由I/O端口空間的大小來決定的。 網(wǎng)管朋友網(wǎng)www_bitscn_net
⑵讀寫16位寬的I/O端口
2? void?outw(unsigned?short?value,unsigned?port);
3?
⑶讀寫32位寬的I/O端口
2? void?outl(unsigned?int?value,unsigned?port);
3.5.1 對(duì)I/O端口的字符串操作
除了上述這些“單發(fā)”(single-shot)的I/O操作外,某些CPU也支持對(duì)某個(gè)I/O端口進(jìn)行連續(xù)的讀寫操作,也即對(duì)單個(gè)I/O端口讀或?qū)懸幌盗凶止?jié)、字或32位整數(shù),這就是所謂的“字符串I/O指令”(String Instruction)。這種指令在速度上顯然要比用循環(huán)來實(shí)現(xiàn)同樣的功能要快得多。
Linux同樣在io.h文件中定義了字符串I/O讀寫函數(shù):
⑴8位寬的字符串I/O操作
2? void?outsb(unsigned?port?,void?*?addr,unsigned?long?count);
3?
⑵16位寬的字符串I/O操作
2? void?outsw(unsigned?port?,void?*?addr,unsigned?long?count);
3?
⑶32位寬的字符串I/O操作
2? void?outsl(unsigned?port?,void?*?addr,unsigned?long?count);
3.5.2 Pausing I/O
在一些平臺(tái)上(典型地如X86),對(duì)于老式總線(如ISA)上的慢速外設(shè)來說,如果CPU讀寫其I/O端口的速度太快,那就可能會(huì)發(fā)生丟失數(shù)據(jù)的現(xiàn)象。對(duì)于這個(gè)問題的解決方法就是在兩次連續(xù)的I/O操作之間插入一段微小的時(shí)延,以便等待慢速外設(shè)。這就是所謂的“Pausing I/O”。
對(duì)于Pausing I/O,Linux也在io.h頭文件中定義了它的I/O讀寫函數(shù),而且都以XXX_p命名,比如:inb_p()、outb_p()等等。下面我們就以out_p()為例進(jìn)行分析。
將io.h中的宏定義__OUT(b,”b”char)展開后可得如下定義:
?2? __asm__?__volatile__?(outb?%?b?0,%?w?1
?3? :?:?a?(value),?Nd?(port));
?4? }
?5?
?6? extern?inline?void?outb_p(unsigned?char?value,?unsigned?short?port)?{
?7? __asm__?__volatile__?(outb?%?b?0,%?w?1
?8? __FULL_SLOW_DOWN_IO
?9? :?:?a?(value),?Nd?(port));
10? }
11?
可以看出,outb_p()函數(shù)的實(shí)現(xiàn)中被插入了宏__FULL_SLOWN_DOWN_IO,以實(shí)現(xiàn)微小的延時(shí)。宏__FULL_SLOWN_DOWN_IO在頭文件io.h中一開始就被定義:
?
?1? #ifdef?SLOW_IO_BY_JUMPING?2? #define?__SLOW_DOWN_IO
?3? jmp?1f
?4? 1:?jmp?1f
?5? 1:
?6? #else
?7? #define?__SLOW_DOWN_IO
?8? outb?%%al,$0x80
?9? #endif
10?
11? #ifdef?REALLY_SLOW_IO
12? #define?__FULL_SLOW_DOWN_IO?__SLOW_DOWN_IO
13? __SLOW_DOWN_IO?__SLOW_DOWN_IO?__SLOW_DOWN_IO
14? #else
15? #define?__FULL_SLOW_DOWN_IO?__SLOW_DOWN_IO
16? #endif
17?
顯然,__FULL_SLOW_DOWN_IO就是一個(gè)或四個(gè)__SLOW_DOWN_IO(根據(jù)是否定義了宏REALLY_SLOW_IO來決定),而宏__SLOW_DOWN_IO則被定義成毫無意義的跳轉(zhuǎn)語句或?qū)懚丝?x80的操作(根據(jù)是否定義了宏SLOW_IO_BY_JUMPING來決定)。 網(wǎng)管朋友網(wǎng)www_bitscn_net
3.6 訪問I/O內(nèi)存資源
盡管I/O端口空間曾一度在x86平臺(tái)上被廣泛使用,但是由于它非常小,因此大多數(shù)現(xiàn)代總線的設(shè)備都以內(nèi)存映射方式(Memory-mapped)來映射它的I/O端口(指I/O寄存器)和外設(shè)內(nèi)存。基于內(nèi)存映射方式的I/O端口(指I/O寄存器)和外設(shè)內(nèi)存可以通稱為“I/O內(nèi)存”資源(I/O Memory)。因?yàn)檫@兩者在硬件實(shí)現(xiàn)上的差異對(duì)于軟件來說是完全透明的,所以驅(qū)動(dòng)程序開發(fā)人員可以將內(nèi)存映射方式的I/O端口和外設(shè)內(nèi)存統(tǒng)一看作是 “I/O內(nèi)存”資源。
從前幾節(jié)的闡述我們知道,I/O內(nèi)存資源是在CPU的單一內(nèi)存物理地址空間內(nèi)進(jìn)行編址的,也即它和系統(tǒng)RAM同處在一個(gè)物理地址空間內(nèi)。因此通過CPU的訪內(nèi)指令就可以訪問I/O內(nèi)存資源。
一般來說,在系統(tǒng)運(yùn)行時(shí),外設(shè)的I/O內(nèi)存資源的物理地址是已知的,這可以通過系統(tǒng)固件(如BIOS)在啟動(dòng)時(shí)分配得到,或者通過設(shè)備的硬連線(hardwired)得到。比如,PCI卡的I/O內(nèi)存資源的物理地址就是在系統(tǒng)啟動(dòng)時(shí)由PCI BIOS分配并寫到PCI卡的配置空間中的BAR中的。而ISA卡的I/O內(nèi)存資源的物理地址則是通過設(shè)備硬連線映射到640KB-1MB范圍之內(nèi)的。但是CPU通常并沒有為這些已知的外設(shè)I/O內(nèi)存資源的物理地址預(yù)定義虛擬地址范圍,因?yàn)樗鼈兪窃谙到y(tǒng)啟動(dòng)后才已知的(某種意義上講是動(dòng)態(tài)的),所以驅(qū)動(dòng)程序并不能直接通過物理地址訪問I/O內(nèi)存資源,而必須將它們映射到核心虛地址空間內(nèi)(通過頁表),然后才能根據(jù)映射所得到的核心虛地址范圍,通過訪內(nèi)指令訪問這些I/O內(nèi)存資源。
3.6.1 映射I/O內(nèi)存資源
Linux在io.h頭文件中聲明了函數(shù)ioremap(),用來將I/O內(nèi)存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,如下:
void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
void iounmap(void * addr);
函數(shù)用于取消ioremap()所做的映射,參數(shù)addr是指向核心虛地址的指針。這兩個(gè)函數(shù)都是實(shí)現(xiàn)在mm/ioremap.c文件中。具體實(shí)現(xiàn)可參考《情景分析》一書。
3.6.2 讀寫I/O內(nèi)存資源
在將I/O內(nèi)存資源的物理地址映射成核心虛地址后,理論上講我們就可以象讀寫RAM那樣直接讀寫I/O內(nèi)存資源了。但是,由于在某些平臺(tái)上,對(duì) I/O內(nèi)存和系統(tǒng)內(nèi)存有不同的訪問處理,因此為了確保跨平臺(tái)的兼容性,Linux實(shí)現(xiàn)了一系列讀寫I/O內(nèi)存資源的函數(shù),這些函數(shù)在不同的平臺(tái)上有不同的實(shí)現(xiàn)。但在x86平臺(tái)上,讀寫I/O內(nèi)存與讀寫RAM無任何差別。如下所示(include/asm-i386/io.h):
?2? #define?readw(addr)?(*(volatile?unsigned?short?*)?__io_virt(addr))?
?3?
?4?
?5? #define?readl(addr)?(*(volatile?unsigned?int?*)?__io_virt(addr))
?6?
?7? #define?writeb(b,addr)?(*(volatile?unsigned?char?*)?__io_virt(addr)?=?(b))
?8? #define?writew(b,addr)?(*(volatile?unsigned?short?*)?__io_virt(addr)?=?(b))
?9? #define?writel(b,addr)?(*(volatile?unsigned?int?*)?__io_virt(addr)?=?(b))
10?
11? #define?memset_io(a,b,c)?memset(__io_virt(a),(b),(c))
12? #define?memcpy_fromio(a,b,c)?memcpy((a),__io_virt(b),(c))
13? #define?memcpy_toio(a,b,c)?memcpy(__io_virt(a),(b),(c))
14?
上述定義中的宏__io_virt()僅僅檢查虛地址addr是否是核心空間中的虛地址。該宏在內(nèi)核2.4.0中的實(shí)現(xiàn)是臨時(shí)性的。具體的實(shí)現(xiàn)函數(shù)在arch/i386/lib/Iodebug.c文件。
顯然,在x86平臺(tái)上訪問I/O內(nèi)存資源與訪問系統(tǒng)主存RAM是無差別的。但是為了保證驅(qū)動(dòng)程序的跨平臺(tái)的可移植性,我們應(yīng)該使用上面的函數(shù)來訪問I/O內(nèi)存資源,而不應(yīng)該通過指向核心虛地址的指針來訪問。
轉(zhuǎn)載于:https://www.cnblogs.com/b2tang/archive/2009/07/07/1518175.html
總結(jié)
以上是生活随笔為你收集整理的iomem—I/O映射方式的I/O端口和内存映射方式的I/O端口的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 渔民出海偶遇100多只海豚逐浪嬉戏:场面
- 下一篇: 主板的北桥芯片与南桥芯片