1. 程式人生 > >地址空間、核心空間、IO地址空間

地址空間、核心空間、IO地址空間

1.I/O埠和I/O記憶體

裝置驅動程式要直接訪問外設或其介面卡上的物理電路,這部分通常都是以暫存器的形式出現。外設暫存器也稱為I/O埠,通常包括:控制暫存器、狀態暫存器和資料暫存器三大類。根據訪問外設暫存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68K,PowerPC等)把這些暫存器看作記憶體的一部分,暫存器參與記憶體統一編址,訪問暫存器就通過訪問一般的記憶體指令進行,所以,這種CPU沒有專門用於裝置I/O的指令。這就是所謂的“I/O記憶體”方式。另一類CPU(典型地如X86)將外設的暫存器看成一個獨立的地址空間,所以訪問記憶體的指令不能用來訪問這些暫存器,而要為對外設暫存器的讀/寫設定專用指令,如IN和OUT指令。這就是所謂的” I/O埠”方式。但是,用於I/O指令的“地址空間”相對來說是很小的。事實上,現在x86的I/O地址空間已經非常擁擠。

但是,隨著計算機技術的發展,單純的I/O埠方式無法滿足實際需要了,因為這種方式只能對外設中的幾個暫存器進行操作。而實際上,需求在不斷髮生變化,例如,在PC上可以插上一塊圖形卡,有2MB的儲存空間,甚至可能還帶有ROM,其中裝有可執行程式碼。自從PCI匯流排出現後,不管是CPU的設計採用I/O埠方式還是I/O記憶體方式,都必須將外設卡上的儲存器對映到記憶體空間,實際上是採用了虛存空間的手段,這樣的對映是通過ioremap()來建立的。

2.   訪問I/O埠
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資料結構中:
struct resource {
     resource_size_t start;
     resource_size_t end;
     const char *name;
     unsigned long flags;
     struct resource *parent,*sibling, *child;
};
其欄位如表1所示。所有的同種資源都插入到一個樹型資料結構(父親、兄弟和孩子)中;例如,表示I/O埠地址範圍的所有資源都包括在一個根節點為ioport_resource的樹中。
表1: resource資料結構中的欄位

型別 欄位 描述

const char * name
資源擁有者的名字

unsigned long
start
資源範圍的開始

unsigned long
end
資源範圍的結束

unsigned long
flags
各種標誌

struct resource *
parent
指向資源樹中父親的指標

struct resource *
sibling
指向資源樹中兄弟的指標

struct resource *
child
指向資源樹中第一個孩子的指標

節點的孩子被收集在一個連結串列中,其第一個元素由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檔案中獲得。

3.把I/O埠對映到記憶體空間-訪問I/O埠的另一種方式
對映函式的原型為:
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);

4. 訪問I/O記憶體
  Linux核心也提供了一組函式申請和釋放某一範圍的I/O記憶體:
   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()函式:
   voidrelease_mem_region(unsigned long start, 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地址空間對映到核心的虛擬地址空間上(通過release_mem_region()申請到的)
為什麼要申請虛擬記憶體然後才進行對映?
————————————————————————————————————
留給讀者的思考:直接訪問I/O埠、把I/O埠對映到記憶體進行訪問,以及訪問I/O記憶體,三者之間有什麼區別,在驅動程式開發中如何具體應用?