1. 程式人生 > >CPU地址空間,IO埠和IO記憶體

CPU地址空間,IO埠和IO記憶體

1)實體地址:CPU地址匯流排傳來的地址,由硬體 電路控制其具體含義。實體地址中很大一部分是留給記憶體條中的記憶體的,但也常被對映到其他儲存器上(如視訊記憶體、BIOS等)。在程式指令中的虛擬地址經過段映 射和頁面對映後,就生成了實體地址,這個實體地址被放到CPU的地址線上。
        實體地址空間,一部分給物理RAM(記憶體)用,一部分給匯流排用,這是由硬體設計來決定的,因此在32 bits地址線的x86處理器中,實體地址空間是2的32次方,即4GB,但物理RAM一般不能上到4GB,因為還有一部分要給匯流排用(總線上還掛著別的 許多裝置)。在PC機中,一般是把低端實體地址給RAM用,高階實體地址給匯流排用。
 
2)匯流排地址:匯流排的地址線或在地址週期上產生的訊號。外設使用的是匯流排地址,CPU使用的是實體地址。
        實體地址與匯流排地址之間的關係由系統的設計決定的。在x86平臺上,實體地址就是匯流排地址,這是因為它們共享相同的地址空間——這句話有點難理解,詳見下 面的“獨立編址”。在其他平臺上,可能需要轉換/對映。比如:CPU需要訪問實體地址是0xfa000的單元,那麼在x86平臺上,會產生一個PCI匯流排 上對0xfa000地址的訪問。因為實體地址和匯流排地址相同,所以憑眼睛看是不能確定這個地址是用在哪兒的,它或者在記憶體中,或者是某個卡上的儲存單元, 甚至可能這個地址上沒有對應的儲存器。

3)虛擬地址:現代

作業系統普遍採用虛擬記憶體管理(Virtual Memory Management)機制,這需要MMU(Memory Management 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從也表中來找到實體地址,再產生匯流排時序,完成取資料的。

4)地址空間對映
 這 裡要說的是Intel構架下的CPU地址空間佈局,注意這裡沒有說是記憶體地址空間佈局。 我們說的記憶體通常是指DRAM,DRAM相對於CPU也可以算是外部裝置,CPU地址空間是CPU訪問外部裝置過程中的一個概念,CPU除了訪問DRAM 外還會訪問許多其他的裝置。可以粗略的認為CPU地址空間包含DRAM地址空間,但兩者卻是不同的概念。而且DRAM地址空間是由記憶體控制器直接訪問的, 由CPU間接訪問的。 過去很長一段時間Intel CPU是32位的,也就是可以訪問到4GB的地址空間,但是當時的DRAM通常也就是512MB到2GB之間,現在假設DRAM是1GB,那麼就是3GB 的地址空間是空的。在計算機裡面,地址也是資源。這空的地址空間就用來訪問外部裝置IO所用,這部分被稱為MMIO(Memory Mapped I/O)
。MMIO的空間是很大的,它包含了PCI的配置空間(256MB或者更大),內建整合視訊記憶體(256MB,或者更大),還有其他很多東西 。所以這部分的大小是不容忽視的。
  以上主要說明:假設CPU是32位(i386),其可訪問的地址範圍大小是4G地址空間,在X86平臺上,實體地址和匯流排地址共享這4G地址空間(實體地址就是匯流排地址),但物理RAM一般不能上到4GB,因為還有一部分要給匯流排用(總線上還掛著別的許多裝置),假設RAM是1G,那麼3G的地址空間就是空的,而在計算機裡面,地址也是資源,這空的地址空間就用來訪問外部裝置IO資源所用,及產生記憶體對映時就用到這段地址空間。

幾乎每一種外設都是通過讀寫裝置上的暫存器來進行的,通常包括控制暫存器、狀態暫存器和資料暫存器三大類,外設的暫存器通常被連續地編址。根據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記憶體資源的實體地址是已知的(通過request_mem_region()),由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O記憶體資源的實體地址 預定義虛擬地址範圍,驅動程式並不能直接通過實體地址訪問I/O記憶體資源,而必須將它們對映到核心虛地址空間內(通過頁表),然後才能根據對映所得到的核 心虛地址範圍,通過訪內指令訪問這些I/O記憶體資源。Linux在io.h標頭檔案中聲明瞭函式ioremap(),(在核心驅動程式的初始化階段,通過ioremap()將實體地址對映到核心虛擬空間(3GB-4GB);在驅動程式的mmap系統呼叫中,使用remap_page_range()將該塊ROM對映到使用者虛擬空間。這樣核心空間和使用者空間都能訪問這段被對映後的虛擬地址。)

4GB以下的地址空間的佈局情況

同樣的圖中紅色字型部分為暫存器,這些暫存器與地址空間佈局有著密切的關係。這 些暫存器的詳細說明可以參考spec。比如說“Egress Port Registers”這個4KB的視窗,會根據EPBAR的設定被放置到MMIO的任意一個DMI Interface的位置,但讓該視窗不能與其他任何視窗重疊。

       另外我也將我所瞭解的情況說明一下。


      1.先看TOLUD-4GB的位置,可以看到有幾處都是DMI Interface(Subtractive Decode)。DMI是南橋與北橋的介面,訪問DMI,也就是訪問南橋。

      另外要解釋的是Substactive decode,在計算機中地址譯碼有三種形式,當主裝置通過指定地址訪問總線上的從裝置,一個是Positive decode,有從裝置解碼後發現是訪問自己的,於是它就會響應,否則就沒有從裝置響應;一個是Negative decode,從裝置收到該地址經解碼後發現不屬於自己的地址範圍,從裝置就轉發出去;一個是Subtractive decode,在4個時鐘週期內沒有從裝置響應,該地址就會發送到擴充套件的總線上面解碼。

      

       DMI Interface(Subtractive Decode)的意思就是CPU傳送一地址先到北橋上解碼,如果該地址沒有北橋上的裝置佔用,那麼就用該地址就會被傳送到南橋上解碼,,也就是訪問南橋上 的裝置。可以假想為一開始4GB空間都是DMI Interface(Subtractive Decode),然後0-TOLUD被DRAM宣告佔用,TOLUD-4GB也紛紛被各種裝置佔用,於是就剩下了支離破碎的幾個DMI Interface。(目前看上去這樣理解是通順的,但我希望它也是正確的)。

IO PortIO Mem的區別

在驅動程式編寫過程中,很少會注意到IO PortIO Mem的區別。雖然使用一些不符合規範的程式碼可以達到最終目的,這是極其不推薦使用的。

結合下圖,我們徹底講述IO埠和IO記憶體以及記憶體之間的關係。主存16M位元組的SDRAM,外設是個視訊採集卡,上面有16M位元組的SDRAM作為緩衝區。

1.      CPUi386架構的情況在i386系列的處理中,記憶體和外部IO是獨立編址,也是獨立定址的。MEM的記憶體空間是32位可以定址到4GIO空間是16位可以定址到64K

2.      Linux核心中,訪問外設上的IO Port必須通過IO Port的定址方式。而訪問IO Mem就比較羅嗦,外部MEM不能和主存一樣訪問,雖然大小上不相上下,可是外部MEM是沒有在系統中註冊的。訪問外部IO MEM必須通過remap對映到核心的MEM空間後才能訪問。為了達到介面的同一性,核心提供了IO PortIO Mem的對映函式。對映後IO Port就可以看作是IO Mem,按照IO Mem的訪問方式即可。

3.      CPUARM PPC架構的情況

在這一類的嵌入式處理器中,IO Port的定址方式是採用記憶體對映,也就是IO bus就是Mem bus。系統的定址能力如果是32位,IO PortMem(包括IO Mem)可以達到4G

訪問這類IO Port時,我們也可以用IO Port專用定址方式。至於在對IO Port定址時,核心是具體如何完成的,這個在核心移植時就已經完成。在這種架構的處理器中,仍然保持對IO Port的支援,完全是i386架構遺留下來的問題,在此不多討論。而訪問IO Mem的方式和i386一致。

注意:linux核心給我提供了完全對IO PortIO Mem的支援,然而具體去看看driver目錄下的驅動程式,很少按照這個規範去組織IO PortIO Mem資源。對這二者訪問最關鍵問題就是地址的定位C語言中,使用volatile 就可以實現。很多的程式碼訪問IO Port中的暫存器時,就使用volatile關鍵字,雖然功能可以實現,我們還是不推薦使用。就像最簡單的延時莫過於while,可是在多工的系統中是堅決避免的!

RISC指令系統的CPU(如ARMPowerPC等)通常只實現一個實體地址空間,外設I/O埠成為記憶體的一部分。此時,CPU可以象訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的外設I/O指令。
但是,這兩者在硬體實現上的差異對於軟體來說是完全透明的,驅動程式開發人員可以將記憶體對映方式的I/O埠和外設記憶體統一看作是"I/O記憶體"資源。

一般來說,在系統執行時,外設的I/O記憶體資源的實體地址是已知的,由硬體的設計決定。但是CPU通常並沒有為這些已知的外設I/O記憶體資源的實體地址預定義虛擬地址範圍,驅動程式並不能直接通過實體地址訪問I/O記憶體資源,而必須將它們對映到核心虛地址空間內(通過頁表),然後才能根據對映所得到的核心虛地址範圍,通過訪內指令訪問這些I/O記憶體資源。Linuxio.h標頭檔案中聲明瞭函式ioremap(),用來將I/O記憶體資源的實體地址對映到核心虛地址空間(3GB4GB)中,原型如下:

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對映一個裝置,意味著使使用者空間的一段地址關聯到裝置記憶體上,這使得只要程式在分配的地址範圍內進行讀取或者寫入,實際上就是對裝置的訪問。

筆者在Linux原始碼中進行包含"ioremap"文字的搜尋,發現真正出現的ioremap的地方相當少。所以筆者追根索源地尋找I/O操作的實體地址轉換到虛擬地址的真實所在,發現Linux有替代ioremap的語句,但是這個轉換過程卻是不可或缺的。

CPU外設埠實體地址的編址方式有兩種:

一種是IO對映方式,另一種是記憶體對映方式。

  Linux將基於IO對映方式的和記憶體對映方式的IO埠統稱為IO區域(IO region)。

  IO region仍然是一種IO資源,因此它仍然可以用resource結構型別來描述。

  Linux管理IO region

  1) request_region()

  把一個給定區間的IO埠分配給一個IO裝置。

  2) check_region()

  檢查一個給定區間的IO埠是否空閒,或者其中一些是否已經分配給某個IO裝置。

  3) release_region()

  釋放以前分配給一個IO裝置的給定區間的IO埠。

  Linux中可以通過以下輔助函式來訪問IO埠:

  inb(),inw(),inl(),outb(),outw(),outl()

  “b”“w”“l”分別代表8位,16位,32位。

IO記憶體資源的訪問

  1) request_mem_region()

  請求分配指定的IO記憶體資源。

  2) check_mem_region()

  檢查指定的IO記憶體資源是否已被佔用。

  3) release_mem_region()

  釋放指定的IO記憶體資源。

  其中傳給函式的start address引數是記憶體區的實體地址(以上函式引數表已省略)。

  驅動開發人員可以將記憶體對映方式的IO埠和外設記憶體統一看作是IO記憶體資源。

ioremap()用來將IO資源的實體地址對映到核心虛地址空間(3GB - 4GB)中,引數addr是指向核心虛地址的指標。

  Linux中可以通過以下輔助函式來訪問IO記憶體資源:

  readb(),readw(),readl(),writeb(),writew(),writel()

  Linuxkernel/resource.c檔案中定義了全域性變數ioport_resourceiomem_resource,來分別描述基於IO對映方式的整個IO埠空間和基於記憶體對映方式的IO記憶體資源空間(包括IO埠和外設記憶體)。

記憶體對映(IO地址和記憶體地址)

ARM體系結構下面記憶體和i/o對映區別
1)關於IO與記憶體空間:
  在X86處理器中存在著I/O空間的概念,I/O空間是相對於記憶體空間而言的,它通過特定的指令inout來訪問。埠號標識了外設的暫存器地址Intel語法的inout指令格式為:
   IN 累加器, {埠號│DX}
   OUT {埠號│DX},累加器
  目前,大多數嵌入式微控制器如ARMPowerPC等中並不提供I/O空間,而僅存在記憶體空間。記憶體空間可以直接通過地址、指標來訪問,程式和程式執行中使用的變數和其他資料都存在於記憶體空間中。
  即便是在X86處理器中,雖然提供了I/O空間,如果由我們自己設計電路板,外設仍然可以只掛接在記憶體空間。此時,CPU可以像訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的I/O指令。因此,記憶體空間是必須的,而I/O空間是可選的。2inboutb
Linux裝置驅動中,宜使用Linux核心提供的函式來訪問定位於I/O空間的埠,這些函式包括:
·  讀寫位元組埠(8位寬)
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
·  讀寫字埠(16位寬)
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
·  讀寫長字埠(32位寬)
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
·  讀寫一串位元組
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
·  insb()從埠port開始讀count個位元組埠,並將讀取結果寫入addr指向的記憶體;outsb()addr指向的記憶體的count個位元組連續地寫入port開始的埠。
·  讀寫一串字
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
·  讀寫一串長字
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
上述各函式中I/O埠號port的型別高度依賴於具體的硬體平臺,因此,只是寫出了unsigned3readbwriteb:
在裝置的實體地址被對映到虛擬地址之後,儘管可以直接通過指標訪問這些地址,但是工程師宜使用Linux核心的如下一組函式來完成裝置記憶體對映的虛擬地址的讀寫,這些函式包括:
·  讀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);
4)把I/O埠對映到“記憶體空間”:
void *ioport_map(unsigned long port, unsigned int count);
通過這個函式,可以把port開始的count個連續的I/O埠重對映為一段“記憶體空間”。然後就可以在其返回的地址上像訪問I/O記憶體一樣訪問這些I/O埠。當不再需要這種對映時,需要呼叫下面的函式來撤消:
void ioport_unmap(void *addr);
實際上,分析ioport_map()的原始碼可發現,所謂的對映到記憶體空間行為實際上是給開發人員製造的一個“假象”,並沒有對映到核心虛擬地址,僅僅是為了讓工程師可使用統一的I/O記憶體訪問介面訪問I/O埠。

11.2.7 I/O 空間的對映
很多硬體裝置都有自己的記憶體,通常稱之為I/O空間。例如,所有比較新的圖形卡都有幾MBRAM,稱為視訊記憶體,用它來存放要在螢幕上顯示的螢幕影像。
1.地址對映
  根據裝置和匯流排型別的不同,PC體系結構中的I/O空間可以在三個不同的實體地址範圍之間進行對映:
1)對於連線到ISA總線上的大多數裝置
I/O空間通常被對映到從0xa00000xfffff的實體地址範圍,這就在640K1MB之間留出了一段空間,這就是所謂的“洞”。
2)對於使用VESA本地匯流排(VLB)的一些老裝置
  這是主要由圖形卡使用的一條專用匯流排:I/O空間被對映到從0xe000000xffffff的地址範圍中,也就是14MB16MB之間。因為這些裝置使頁表的初始化更加複雜,因此已經不生產這種裝置。
3)對於連線到PCI匯流排的裝置
   I/O空間被對映到很大的實體地址區間,位於RAM實體地址的頂端。這種裝置的處理比較簡單。
2.訪問I/O空間
  核心如何訪問一個I/O空間單元?讓我們從PC體系結構開始入手,這個問題很容易就可以解決,之後我們再進一步討論其他體系結構。
  不要忘了核心程式作用於虛擬地址,因此I/O空間單元必須表示成大於PAGE_OFFSET的地址。在後面的討論中,我們假設PAGE_OFFSET等於0xc0000000,也就是說,核心虛擬地址是在第4G
  核心驅動程式必須把I/O空間單元的實體地址轉換成核心空間的虛擬地址。PC體系結構中,這可以簡單地把32位的實體地址和0xc0000000常量進行或運算得到。例如,假設核心需要把實體地址為0x000b0fe4I/O單元的值存放在t1中,把實體地址為0xfc000000I/O單元的值存放在t2中,就可以使用下面的表示式來完成這項功能:

   t1 = *((unsigned char *)(0xc00b0fe4));
   t2 = *((unsigned char *)(0xfc000000));

  在第六章我們已經介紹過,在初始化階段,核心已經把可用的RAM實體地址對映到虛擬地址空間第4G的最初部分。因此,分頁機制把出現在第一個語句中的虛擬地址0xc00b0fe4映射回到原來的I/O實體地址0x000b0fe4,這正好落在從640K1MB的這段“ISA洞”中。這正是我們所期望的。  但是,對於第二個語句來說,這裡有一個問題,因為其I/O實體地址超過了系統RAM的最大實體地址。因此,虛擬地址0xfc000000就不需要與實體地址0xfc000000相對應。在這種情況下,為了在核心頁表中包括對這個I/O實體地址進行對映的虛擬地址,必須對頁表進行修改:這可以通過呼叫ioremap( )函式來實現。ioremap( )vmalloc( )函式類似,都呼叫get_vm_area( ) 建立一個新的vm_struct描述符,其描述的虛擬地址區間為所請求I/O空間區的大小。然後,ioremap( )函式適當地更新所有程序的對應頁表項。
因此,第二個語句的正確形式應該為:

   io_mem = ioremap(0xfb000000, 0x200000);
   t2 = *((unsigned char *)(io_mem + 0x100000));
第一條語句建立一個2MB的虛擬地址區間,從0xfb000000開始;第二條語句讀取地址0xfc000000的記憶體單元。驅動程式以後要取消這種對映,就必須使用iounmap( )函式。

  現在讓我們考慮一下除PC之外的體系結構。在這種情況下,把I/O實體地址加上0xc0000000常量所得到的相應虛擬地址並不總是正確的。為了提高核心的可移植性,Linux特意包含了下面這些巨集來訪問I/O空間:
   readb, readw, readl
  分別從一個I/O空間單元讀取12或者4個位元組
  writeb, writew, writel
  分別向一個I/O空間單元寫入12或者4個位元組
  memcpy_fromio, memcpy_toio
  把一個數據塊從一個I/O空間單元拷貝到動態記憶體中,另一個函式正好相反,把一個數據塊從動態記憶體中拷貝到一個I/O空間單元
  memset_io
 用一個固定的值填充一個I/O空間區域對於0xfc000000 I/O單元的訪問推薦使用這樣的方法: io_mem = ioremap(0xfb000000, 0x200000);
 t2 = readb(io_mem + 0x100000);
使用這些巨集,就可以隱藏不同平臺訪問I/O空間所用方法的差異。

 從本質上來說是一樣的,IO埠在Linux驅動中是指IO埠的暫存器,通過操作暫存器來控制IO埠。而IO記憶體是指一些裝置把IO暫存器對映到某個記憶體區域,因為訪問記憶體就不要特殊的指令。