(一)地址的概念
1)物理地址:CPU地址總線傳來的地址,由硬件電路控制其具體含義。物理地址中很大一部分是留給內存條中的內存的,但也常被映射到其他存儲器上(如顯存、BIOS等)。在程序指令中的虛擬地址經過段映射和頁面映射後,就生成了物理地址,這個物理地址被放到CPU的地址線上。
物理地址空間,一部分給物理RAM(內存)用,一部分給總線用,這是由硬件設計來決定的,因此在32bits地址線的x86處理器中,物理地址空間是2的32次方,即4GB,但物理RAM一般不能上到4GB,因爲還有一部分要給總線用(總線上還掛着別的許多設備)。在PC機中,一般是把低端物理地址給RAM用,高端物理地址給總線用。
2)總線地址:總線的地址線或在地址週期上產生的信號。外設使用的是總線地址,CPU使用的是物理地址。
物理地址與總線地址之間的關係由系統的設計決定的。在x86平臺上,物理地址就是總線地址,這是因爲它們共享相同的地址空間——這句話有點難理解,詳見下面的“獨立編址”。在其他平臺上,可能需要轉換/映射。比如:CPU需要訪問物理地址是0xfa000的單元,那麼在x86平臺上,會產生一個PCI總線上對0xfa000地址的訪問。因爲物理地址和總線地址相同。
3)虛擬地址:現代操作系統普遍採用虛擬內存管理(VirtualMemory Management)機制,這需要MMU(MemoryManagement Unit)的支持。MMU通常是CPU的一部分,如果處理器沒有MMU,或者有MMU但沒有啓用,CPU執行單元發出的內存地址將直接傳到芯片引腳上,被內存芯片(物理內存)接收,這稱爲物理地址(Physical Address),如果處理器啓用了MMU,CPU執行單元發出的內存地址將被MMU截獲,從CPU到MMU的地址稱爲虛擬地址(Virtual Address),而MMU將這個地址翻譯成另一個地址發到CPU芯片的外部地址引腳上,也就是將虛擬地址映射成物理地址。
Linux中,進程的4GB(虛擬)內存分爲用戶空間、內核空間。用戶空間分佈爲0~3GB(即PAGE_OFFSET,在0X86中它等於0xC0000000),剩下的1G爲內核空間。程序員只能使用虛擬地址。系統中每個進程有各自的私有用戶空間(0~3G),這個空間對系統中的其他進程是不可見的。
CPU發出取指令請求時的地址是當前上下文的虛擬地址,MMU再從頁表中找到這個虛擬地址的物理地址,完成取指。同樣讀取數據的也是虛擬地址,比如mov ax, var. 編譯時var就是一個虛擬地址,也是通過MMU從也表中來找到物理地址,再產生總線時序,完成取數據的。
(二)編址方式
1)對外設的編址
外設都是通過讀寫設備上的寄存器來進行的,外設寄存器也稱爲“I/O端口”,而IO端口有兩種編址方式:獨立編址和統一編制。
統一編址:外設接口中的IO寄存器(即IO端口)與主存單元一樣看待,每個端口占用一個存儲單元的地址,將主存的一部分劃出來用作IO地址空間,如,在 PDP-11中,把最高的4K主存作爲IO設備寄存器地址。端口占用了存儲器的地址空間,使存儲量容量減小。
統一編址也稱爲“I/O內存”方式,外設寄存器位於“內存空間”(很多外設有自己的內存、緩衝區,外設的寄存器和內存統稱“I/O空間”)。
如,Samsung的S3C2440,是32位ARM處理器,它的4GB地址空間被外設、RAM等瓜分:
0x8000 1000 LED 8*8點陣的地址
0x4800 0000 ~ 0x6000 0000 SFR(特殊暫存器)地址空間
0x3800 1002 鍵盤地址
0x3000 0000 ~ 0x3400 0000 SDRAM空間
0x2000 0020 ~ 0x2000 002e IDE
0x1900 0300 CS8900
獨立編址(單獨編址):IO地址與存儲地址分開獨立編址,I/0端口地址不佔用存儲空間的地址範圍,這樣,在系統中就存在了另一種與存儲地址無關的IO地址,CPU也必須具有專用與輸入輸出操作的IO指令(IN、OUT等)和控制邏輯。獨立編址下,地址總線上過來一個地址,設備不知道是給IO端口的、還是給存儲器的,於是處理器通過MEMR/MEMW和IOR/IOW兩組控制信號來實現對I/O端口和存儲器的不同尋址。如,intel80x86就採用單獨編址,CPU內存和I/O是一起編址的,就是說內存一部分的地址和I/O地址是重疊的。
獨立編址也稱爲“I/O端口”方式,外設寄存器位於“I/O(地址)空間”。
對於x86架構來說,通過IN/OUT指令訪問。PC架構一共有65536個8bit的I/O端口,組成64K個I/O地址空間,編號從0~0xFFFF,有16位,80x86用低16位地址線A0-A15來尋址。連續兩個8bit的端口可以組成一個16bit的端口,連續4個組成一個 32bit的端口。I/O地址空間和CPU的物理地址空間是兩個不同的概念,例如I/O地址空間爲64K,一個32bit的CPU物理地址空間是4G。如,在Intel 8086+Redhat9.0 下用“more/proc/ioports”可看到:
0000-001f : dma1
0020-003f : pic1
0040-005f : timer
0060-006f : keyboard
0070-007f : rtc
0080-008f : dma page reg
00a0-00bf : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : ide1
……
不過Intelx86平臺普通使用了名爲內存映射(MMIO)的技術,該技術是PCI規範的一部分,IO設備端口被映射到內存空間,映射後,CPU訪問IO端口就如同訪問內存一樣。看IntelTA 719文檔給出的x86/x64系統典型內存地址分配表:
系統資源 佔用
------------------------------------------------------------------------
BIOS 1M
本地APIC 4K
芯片組保留 2M
IO APIC 4K
PCI設備 256M
PCI Express設備256M
PCI設備(可選) 256M
顯示幀緩存 16M
TSEG 1M
對於某一既定的系統,它要麼是獨立編址、要麼是統一編址,具體採用哪一種則取決於CPU的體系結構。如,PowerPC、m68k等採用統一編址,而X86等則採用獨立編址,存在IO空間的概念。目前,大多數嵌入式微控制器如ARM、PowerPC等並不提供I/O空間,僅有內存空間,可直接用地址、指針訪問。但對於Linux內核而言,它可能用於不同的CPU,所以它必須都要考慮這兩種方式,於是它採用一種新的方法,將基於I/O映射方式的或內存映射方式的I/O端口通稱爲“I/O區域”(I/O region),不論你採用哪種方式,都要先申請IO區域:request_resource(),結束時釋放它:release_resource()。
幾乎每一種外設都是通過讀寫設備上的寄存器來進行的。外設寄存器也稱爲“I/O端口”,通常包括:控制寄存器、狀態寄存器和數據寄存器三大類,而且一個外設的寄存器通常被連續地編址。CPU對外設IO端口物理地址的編址方式有兩種:一種是I/O映射方式(I/O-mapped),另一種是內存映射方式(Memory-mapped)。而具體採用哪一種則取決於CPU的體系結構。
有些體系結構的CPU(如,PowerPC、m68k等)通常只實現一個物理地址空間(RAM)。在這種情況下,外設I/O端口的物理地址就被映射到CPU的單一物理地址空間中,而成爲內存的一部分。此時,CPU可以象訪問一個內存單元那樣訪問外設I/O端口,而不需要設立專門的外設I/O指令。這就是所謂的“內存映射方式”(Memory-mapped)。
而另外一些體系結構的CPU(典型地如X86)則爲外設專門實現了一個單獨地地址空間,稱爲“I/O地址空間”或者“I/O端口空間”。這是一個與CPU地RAM物理地址空間不同的地址空間,所有外設的I/O端口均在這一空間中進行編址。CPU通過設立專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元(也即I/O端口)。這就是所謂的“I/O映射方式”(I/O-mapped)。與RAM物理地址空間相比,I/O地址空間通常都比較小,如x86 CPU的I/O空間就只有64KB(0-0xffff)。這是“I/O映射方式”的一個主要缺點。
Linux將基於I/O映射方式的或內存映射方式的I/O端口通稱爲“I/O區域”(I/Oregion)。在討論對I/O區域的管理之前,我們首先來分析一下Linux是如何實現“I/O資源”這一抽象概念的.
(四)IO端口與IO內存區別
在驅動程序編寫過程中,很少會注意到IO Port和IO Mem的區別。雖然使用一些不符合規範的代碼可以達到最終目的,這是極其不推薦使用的。
結合下圖,我們徹底講述IO端口和IO內存以及內存之間的關係。主存16M字節的SDRAM,外設是個視頻採集卡,上面有16M字節的SDRAM作爲緩衝區。
1. CPU是i386架構的情況
在i386系列的處理中,內存和外部IO是獨立編址,也是獨立尋址的。MEM的內存空間是32位可以尋址到4G,IO空間是16位可以尋址到64K。
在Linux內核中,訪問外設上的IO Port必須通過IO Port的尋址方式。而訪問IO Mem就比較羅嗦,外部MEM不能和主存一樣訪問,雖然大小上不相上下,可是外部MEM是沒有在系統中註冊的。訪問外部IO MEM必須通過remap映射到內核的MEM空間後才能訪問。
爲了達到接口的同一性,內核提供了IO Port到IO Mem的映射函數。映射後IO Port就可以看作是IO Mem,按照IO Mem的訪問方式即可。
2. CPU是ARM 或PPC架構的情況
在這一類的嵌入式處理器中,IO Port的尋址方式是採用內存映射,也就是IO bus就是Mem bus。系統的尋址能力如果是32位,IO Port+Mem(包括IO Mem)可以達到4G。
訪問這類IO Port時,我們也可以用IO Port專用尋址方式。至於在對IO Port尋址時,內核是具體如何完成的,這個在內核移植時就已經完成。在這種架構的處理器中,仍然保持對IO Port的支持,完全是i386架構遺留下來的問題,在此不多討論。而訪問IO Mem的方式和i386一致。
3、IO端口和IO內存的區分及聯繫
這兩者如何區分就涉及到硬件知識,X86體系中,具有兩個地址空間:IO空間和內存空間,而RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個物理地址空間,即內存空間。
內存空間:內存地址尋址範圍,32位操作系統內存空間爲2的32次冪,即4G。
IO空間:X86特有的一個空間,與內存空間彼此獨立的地址空間,32位X86有64K的IO空間。
IO端口:當寄存器或內存位於IO空間時,稱爲IO端口。一般寄存器也俗稱I/O端口,或者說I/Oports,這個I/O端口可以被映射在MemorySpace,也可以被映射在I/OSpace。
IO內存:當寄存器或內存位於內存空間時,稱爲IO內存。
(五)在Linux下對IO端口與IO內存訪問方式總結
1)在Linux下訪問IO端口
對於某一既定的系統,它要麼是獨立編址、要麼是統一編址,具體採用哪一種則取決於CPU的體系結構。如,PowerPC、m68k等採用統一編址,而X86等則採用獨立編址,存在IO空間的概念。目前,大多數嵌入式微控制器如ARM、PowerPC等並不提供I/O空間,僅有內存空間,可直接用地址、指針訪問。但對於Linux內核而言,它可能用於不同的CPU,所以它必須都要考慮這兩種方式,於是它採用一種新的方法,將基於I/O映射方式的或內存映射方式的I/O端口通稱爲“I/O區域”(I/O region),不論你採用哪種方式,都要先申請IO區域:request_resource(),結束時釋放它:release_resource()。
IO region是一種IO資源,因此它可以用resource結構類型來描述。
訪問IO端口有2種途徑:I/O映射方式(I/O-mapped)、內存映射方式(Memory-mapped)。前一種途徑不映射到內存空間,直接使用 intb()/outb()之類的函數來讀寫IO端口;後一種MMIO是先把IO端口映射到IO內存(“內存空間”),再使用訪問IO內存的函數來訪問 IO端口。
直接使用IO端口操作函數:在設備打開或驅動模塊被加載時申請IO端口區域,之後使用inb(),outb()等進行端口訪問,最後在設備關閉或驅動被卸載時釋放IO端口範圍。
in、out、ins和outs彙編語言指令都可以訪問I/O端口。內核中包含了以下輔助函數來簡化這種訪問:
inb( )、inw( )、inl( )
分別從I/O端口讀取1、2或4個連續字節。後綴“b”、“w”、“l”分別代表一個字節(8位)、一個字(16位)以及一個長整型(32位)。
inb_p( )、inw_p( )、inl_p( )
分別從I/O端口讀取1、2或4個連續字節,然後執行一條“啞元(dummy,即空指令)”指令使CPU暫停。
outb( )、outw( )、outl( )
分別向一個I/O端口寫入1、2或4個連續字節。
outb_p( )、outw_p( )、outl_p( )
分別向一個I/O端口寫入1、2或4個連續字節,然後執行一條“啞元”指令使CPU暫停。
insb( )、insw( )、insl( )
分別從I/O端口讀入以1、2或4個字節爲一組的連續字節序列。字節序列的長度由該函數的參數給出。
outsb( )、outsw( )、outsl( )
分別向I/O端口寫入以1、2或4個字節爲一組的連續字節序列。
流程如下:
雖然訪問I/O端口非常簡單,但是檢測哪些I/O端口已經分配給I/O設備可能就不這麼簡單了,對基於ISA總線的系統來說更是如此。通常,I/O設備驅動程序爲了探測硬件設備,需要盲目地向某一I/O端口寫入數據;但是,如果其他硬件設備已經使用這個端口,那麼系統就會崩潰。爲了防止這種情況的發生,內核必須使用“資源”來記錄分配給每個硬件設備的I/O端口。資源表示某個實體的一部分,這部分被互斥地分配給設備驅動程序。在這裏,資源表示I/O端口地址的一個範圍。每個資源對應的信息存放在resource數據結構中:
[plain] view plaincopy
- struct resource {
- resource_size_t start;// 資源範圍的開始
- resource_size_t end;// 資源範圍的結束
- const char *name; //資源擁有者的名字
- unsigned long flags;// 各種標誌
- struct resource *parent, *sibling, *child;// 指向資源樹中父親,兄弟和孩子的指針
- };
所有的同種資源都插入到一個樹型數據結構(父親、兄弟和孩子)中;例如,表示I/O端口地址範圍的所有資源都包括在一個根節點爲ioport_resource的樹中。節點的孩子被收集在一個鏈表中,其第一個元素由child指向。sibling字段指向鏈表中的下一個節點。
爲什麼使用樹?例如,考慮一下IDE硬盤接口所使用的I/O端口地址-比如說從0xf000 到 0xf00f。那麼,start字段爲0xf000 且end 字段爲0xf00f的這樣一個資源包含在樹中,控制器的常規名字存放在name字段中。但是,IDE設備驅動程序需要記住另外的信息,也就是IDE鏈主盤使用0xf000 到0xf007的子範圍,從盤使用0xf008 到0xf00f的子範圍。爲了做到這點,設備驅動程序把兩個子範圍對應的孩子插入到從0xf000 到0xf00f的整個範圍對應的資源下。一般來說,樹中的每個節點肯定相當於父節點對應範圍的一個子範圍。I/O端口資源樹(ioport_resource)的根節點跨越了整個I/O地址空間(從端口0到65535)。
任何設備驅動程序都可以使用下面三個函數,傳遞給它們的參數爲資源樹的根節點和要插入的新資源數據結構的地址:
request_resource() //把一個給定範圍分配給一個I/O設備。
allocate_resource() //在資源樹中尋找一個給定大小和排列方式的可用範圍;若存在,將這個範圍分配給一個I/O設備(主要由PCI設備驅動程序使用,可以使用任意的端口號和主板上的內存地址對其進行配置)。
release_resource() //釋放以前分配給I/O設備的給定範圍。
內核也爲以上函數定義了一些應用於I/O端口的快捷函數:request_region( )分配I/O端口的給定範圍,release_region( )釋放以前分配給I/O端口的範圍。當前分配給I/O設備的所有I/O地址的樹都可以從/proc/ioports文件中獲得。
將IO端口映射爲內存進行訪問,在設備打開或驅動模塊被加載時,申請IO端口區域並使用ioport_map()映射到內存,之後使用IO內存的函數進行端口訪問,最後,在設備關閉或驅動模塊被卸載時釋放IO端口並釋放映射。
映射函數的原型爲:
void *ioport_map(unsigned long port, unsigned int count);
通過這個函數,可以把port開始的count個連續的I/O端口重映射爲一段“內存空間”。然後就可以在其返回的地址上像訪問I/O內存一樣訪問這些I/O端口。但請注意,在進行映射前,還必須通過request_region()分配I/O端口。
當不再需要這種映射時,需要調用下面的函數來撤消:
void ioport_unmap(void *addr);
在設備的物理地址被映射到虛擬地址之後,儘管可以直接通過指針訪問這些地址,但是宜使用Linux內核的如下一組函數來完成訪問I/O內存:讀I/O內存
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
與上述函數對應的較早版本的函數爲(這些函數在Linux 2.6中仍然被支持):
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
·寫I/O內存
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
與上述函數對應的較早版本的函數爲(這些函數在Linux 2.6中仍然被支持):
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);
流程如下:
2)Linux下訪問IO內存
IO內存的訪問方法是:首先調用request_mem_region()申請資源,接着將寄存器地址通過ioremap()映射到內核空間的虛擬地址,之後就可以Linux設備訪問編程接口訪問這些寄存器了,訪問完成後,使用ioremap()對申請的虛擬地址進行釋放,並釋放release_mem_region()申請的IO內存資源。
struct resource*requset_mem_region(unsigned long start, unsigned long len,char *name);
這個函數從內核申請len個內存地址(在3G~4G之間的虛地址),而這裏的start爲I/O物理地址,name爲設備的名稱。注意,如果分配成功,則返回非NULL,否則,返回NULL。
另外,可以通過/proc/iomem查看系統給各種設備的內存範圍。
要釋放所申請的I/O內存,應當使用release_mem_region()函數:
void release_mem_region(unsigned longstart, unsigned long len)
申請一組I/O內存後,調用ioremap()函數:
void* ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);
其中三個參數的含義爲:
phys_addr:與requset_mem_region函數中參數start相同的I/O物理地址;
size:要映射的空間的大小;
flags:要映射的IO空間的和權限有關的標誌;
功能:將一個I/O地址空間映射到內核的虛擬地址空間上(通過requset _mem_region()申請到的)
流程如下:
下面具體看一下ioport_map和ioport_umap的源碼:
[plain] view plaincopy
- void __iomem *ioport_map(unsigned long port, unsigned int nr)
- {
- if (port > PIO_MASK)
- return NULL;
- return (void __iomem *) (unsigned long) (port + PIO_OFFSET);
- }
- void ioport_unmap(void __iomem *addr)
- {
- /* Nothing to do */
- }
ioport_map僅僅是將port加上PIO_OFFSET(64k),而ioport_unmap則什麼都不做。這樣portio的64k空間就被映射到虛擬地址的64k~128k之間,而ioremap返回的虛擬地址則肯定在3G之上。ioport_map函數的目的是試圖提供與ioremap一致的虛擬地址空間。分析ioport_map()的源代碼可發現,所謂的映射到內存空間行爲實際上是給開發人員製造的一個“假象”,並沒有映射到內核虛擬地址,僅僅是爲了讓工程師可使用統一的I/O內存訪問接口ioread8/iowrite8(......)訪問I/O端口。
最後來看一下ioread8的源碼,其實現也就是對虛擬地址進行了判斷,以區分IO端口和IO內存,然後分別使用inb/outb和readb/writeb來讀寫。
[plain] view plaincopy
- unsigned int fastcall ioread8(void __iomem *addr)
- {
- IO_COND(addr, return inb(port), return readb(addr));
- }
- #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)
- #define IO_COND(addr, is_pio, is_mmio) do { \
- unsigned long port = (unsigned long __force)addr; \
- if (port < PIO_RESERVED) { \
- VERIFY_PIO(port); \
- port &= PIO_MASK; \
- is_pio; \
- } else { \
- is_mmio; \
- } \
- } while (0)
- 展開:
- unsigned int fastcall ioread8(void __iomem *addr)
- {
- unsigned long port = (unsigned long __force)addr;
- if( port < 0x40000UL ) {
- BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );
- port &= PIO_MASK;
- return inb(port);
- }else{
- return readb(addr);
- }
- }