1. 程式人生 > >IO的埠對映和記憶體對映 (Port mapped I/O 和 Memory mapped I/O說明)

IO的埠對映和記憶體對映 (Port mapped I/O 和 Memory mapped I/O說明)

IO埠和IO記憶體的區別及分別使用的函式介面 

         每個外設都是通過讀寫其暫存器來控制的。外設暫存器也稱為I/O,通常包括:控制暫存器、狀態暫存器和資料暫存器三大類。根據訪問外設暫存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68K,Power PC等)把這些暫存器看作記憶體的一部分,暫存器參與記憶體統一編址訪問暫存器就通過訪問一般的記憶體指令進行,所以,這種CPU沒有專門用於裝置I/O的指令。這就是所謂的“I/O記憶體”方式。另一類CPU(典型的如X86),將外設的暫存器看成一個獨立的地址空間,所以訪問記憶體的指令不能用來訪問這些暫存器,而要為對外設暫存器的讀/寫設定專用指令,如IN和OUT指令。這就是所謂的“ I/O

”方式。但是,用於I/O指令的“地址空間”相對來說是很小的,如x86 CPU的I/O空間就只有64KB(0-0xffff)。

    結合下圖,我們徹底講述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的訪問方式即可。

3.    CPU是ARM或PPC架構的情況

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

1.使用I/O 埠

I/O 埠是驅動用來和很多裝置通訊的方法。

1.1、分配I/O 埠

在驅動還沒獨佔裝置之前,不應對埠進行操作。核心提供了一個註冊介面,以允許驅動宣告其需要的埠:

#include <linux/ioport.h>
/* request_region告訴核心:要使用first開始的n個埠。引數name為裝置名。如果分配成功返回值是非NULL;否則無法使用需要的埠(/proc/ioports包含了系統當前所有埠的分配資訊,若request_region分配失敗時,可以檢視該檔案,看誰先用了你要的埠) */
struct resource *request_region

(unsigned long first, unsigned long n, const char *name);
/* 用完I/O埠後(可能在模組解除安裝時),應當呼叫release_region將I/O埠返還給系統。引數start和n應與之前傳遞給request_region一致 */
void release_region(unsigned long start, unsigned long n); 
/* check_region用於檢查一個給定的I/O埠集是否可用。如果給定的埠不可用,check_region返回一個錯誤碼。不推薦使用該函式,因為即便它返回0(埠可用),它也不能保證後面的埠分配操作會成功,因為檢查和後面的埠分配並不是一個原子操作。而request_region通過加鎖來保證操作的原子性,因此是安全的 */
int check_region(unsigned long first, unsigned long n);


      1.2、操作I/O埠

在驅動成功請求到I/O 埠後,就可以讀寫這些埠了。大部分硬體會將8位、16位和32位埠區分開,無法像訪問記憶體那樣混淆使用。驅動程式必須呼叫不同的函式來訪問不同大小的埠。

如同前面所講的,僅支援單地址空間的計算機體系通過將I/O埠地址重新對映到記憶體地址來偽裝埠I/O 。為了提高移植性,核心對驅動隱藏了這些細節。Linux 核心標頭檔案(體系依賴的標頭檔案<asm/io.h>) 定義了下列行內函數來存取I/O埠:

/* inb/outb:讀/寫位元組埠(8位寬)。有些體系將port引數定義為unsigned long;而有些平臺則將它定義為unsigned short。inb的返回型別也是依賴體系的 */
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
/* inw/outw:讀/寫字埠(16位寬) */
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
/* inl/outl:讀/32位埠。longword也是依賴體系的,有的體系為unsigned long;而有的為unsigned int */
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

從現在開始,當我們使用unsigned 沒有進一步指定型別時,表示是一個依賴體系的定義。

注意,沒有64位的I/O埠操作函式。即便在64位體系中,埠地址空間使用一個32位(最大)的資料通路。

1.3、從使用者空間訪問I/O埠

1.2節介紹的函式主要是提供給驅動使用,但它們也可在使用者空間使用,至少在PC機上可以。GNU C 庫在 <sys/io.h> 中定義它們。如果在使用者空間使用這些函式,必須滿足下列條件:

1)、程式必須使用-O選項編譯來強制擴充套件行內函數

2)、必須使用ioperm和iopl系統呼叫(#include <sys/perm.h>) 來獲得進行操作I/O埠的許可權。ioperm 為獲取單個埠的操作許可,iopl 為獲取整個I/O空間許可。這2個函式都是x86特有的

3)、程式必須以root來呼叫ioperm或者iopl,或者其父程序(祖先)必須以root獲得的埠操作許可權

如果平臺不支援ioperm和iopl系統呼叫,通過使用/dev/prot裝置檔案,使用者空間仍然可以存取I/O 埠。但是要注意的是,這個檔案的定義也是依賴平臺的。

1.4、字串操作

除了一次傳遞一個數據的I/O操作,某些處理器實現了一次傳遞一序列資料(單位可以是位元組、字和雙字)的特殊指令。這些所謂的字串指令,它們完成任務比一個C語言迴圈更快。下列巨集定義實現字串操作,在某些體系上,它們通過使用單個機器指令實現;但如果目標處理器沒有進行字串I/O指令,則通過執行一個緊湊的迴圈實現。

字串函式的原型是:

/* insb:從I/O埠port讀取count個數據(單位位元組)到以記憶體地址addr為開始的記憶體空間 */
void insb(unsigned port, void *addr, unsigned long count);
/* outsb:將記憶體地址addr開始的count個數據(單位位元組)寫到I/O埠port */
void outsb(unsigned port, void *addr, unsigned long count);
/* insw:從I/O埠port讀取count個數據(單位字)到以記憶體地址addr為開始的記憶體空間 */
void insw(unsigned port, void *addr, unsigned long count);
/* outsw:將記憶體地址addr開始的count個數據(單位字)寫到I/O埠port */
void outsw(unsigned port, void *addr, unsigned long count);
/* insl:從I/O埠port讀取count個數據(單位雙字)到以記憶體地址addr為開始的記憶體空間 */
void insl(unsigned port, void *addr, unsigned long count);
/* outsl:將記憶體地址addr開始的count個數據(單位雙字)寫到I/O埠port */
void outsl(unsigned port, void *addr, unsigned long count);

注意:使用字串函式時,它們直接將位元組流從埠中讀取或寫入。當埠和主機系統有不同的位元組序時,會導致不可預期的結果。使用 inw讀取埠應在必要時自行轉換位元組序,以匹配主機位元組序。

1.5、暫停式I/O操作函式

由於處理器的速率可能與外設(尤其是低速裝置)的並不匹配,當處理器過快地傳送資料到或自匯流排時,這時可能就會引起問題。解決方法是:如果在I/O 指令後面緊跟著另一個相似的I/O 指令,就必須插入一個小的延時。為此,Linux提供了暫停式I/O操作函式,這些函式的名子只是在非暫停式I/O操作函式(前面提到的那些I/O操作函式都是非暫停式的)名後加上_p ,如inb_p、outb_p等。大部分體系都支援這些函式,儘管它們常常被擴充套件為與非暫停 I/O 同樣的程式碼,因為如果體系使用一個合理的現代外設匯流排,沒有必要額外暫停。

以下是ARM體系暫停式I/O巨集的定義:

#define outb_p(val,port)    outb((val),(port))
#define outw_p(val,port)    outw((val),(port))
#define outl_p(val,port)    outl((val),(port))
#define inb_p(port)        inb((port))
#define inw_p(port)        inw((port))
#define inl_p(port)        inl((port))
#define outsb_p(port,from,len)    outsb(port,from,len)
#define outsw_p(port,from,len)    outsw(port,from,len)
#define outsl_p(port,from,len)    outsl(port,from,len)
#define insb_p(port,to,len)    insb(port,to,len)
#define insw_p(port,to,len)    insw(port,to,len)
#define insl_p(port,to,len)    insl(port,to,len)

因為ARM使用內部匯流排,就沒有必要額外暫停,所以暫停式的I/O函式被擴充套件為與非暫停式I/O同樣的程式碼。

1.6、平臺依賴性

由於自身的特性,I/O指令高度依賴於處理器,非常難以隱藏各體系間的不同。因此,大部分的關於埠 I/O的原始碼是平臺依賴的。以下是x86和ARM所使用函式的總結:

IA-32 (x86)

x86_64

這個體系支援本章介紹的所有函式;port引數的型別為unsigned short。

ARM

埠對映到記憶體,並且支援本章介紹的所有函式;port引數的型別為unsigned int;字串函式用C語言實現。

       2、使用 I/O 記憶體

儘管 I/O 埠在x86世界中非常流行,但是用來和裝置通訊的主要機制是通過記憶體對映的暫存器和裝置記憶體,兩者都稱為I/O 記憶體,因為暫存器和記憶體之間的區別對軟體是透明的。

I/O 記憶體僅僅是一個類似於RAM 的區域,處理器通過匯流排訪問該區域,以實現對裝置的訪問。同樣,讀寫這個區域是有邊際效應

根據計算機體系和匯流排不同,I/O 記憶體可分為可以或者不可以通過頁表來存取。若通過頁表存取,核心必須先重新編排實體地址,使其對驅動程式可見,這就意味著在進行任何I/O操作之前,你必須呼叫ioremap;如果不需要頁表,I/O記憶體區域就類似於I/O埠,你可以直接使用適當的I/O函式讀寫它們。

由於邊際效應的緣故,不管是否需要 ioremap,都不鼓勵直接使用I/O記憶體指標,而應使用專門的I/O記憶體操作函式。這些I/O記憶體操作函式不僅在所有平臺上是安全,而且對直接使用指標操作 I/O 記憶體的情況進行了優化。

2.1、I/O 記憶體分配和對映

I/O 記憶體區在使用前必須先分配。分配記憶體區的函式介面在<linux/ioport.h>定義中:

/* request_mem_region分配一個開始於start,len位元組的I/O記憶體區。分配成功,返回一個非NULL指標;否則返回NULL。系統當前所有I/O記憶體分配資訊都在/proc/iomem檔案中列出,你分配失敗時,可以看看該檔案,看誰先佔用了該記憶體區 */
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);
/* release_mem_region用於釋放不再需要的I/O記憶體區 */
void release_mem_region(unsigned long start, unsigned long len); 
/* check_mem_region用於檢查I/O記憶體區的可用性。同樣,該函式不安全,不推薦使用 */
int check_mem_region(unsigned long start, unsigned long len);

在訪問I/O記憶體之前,分配I/O記憶體並不是唯一要求的步驟,你還必須保證核心可存取該I/O記憶體。訪問I/O記憶體並不只是簡單解引用指標,在許多體系中,I/O 記憶體無法以這種方式直接存取。因此,還必須通過ioremap 函式設定一個對映

#include <asm/io.h>
/* ioremap用於將I/O記憶體區對映到虛擬地址。引數phys_addr為要對映的I/O記憶體起始地址,引數size為要對映的I/O記憶體的大小,返回值為被對映到的虛擬地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);

/* ioremap_nocache為ioremap的無快取版本。實際上,在大部分體系中,ioremap與ioremap_nocache的實現一樣的,因為所有 I/O 記憶體都是在無快取的記憶體地址空間中 */
void *ioremap_nocache(unsigned long phys_addr, unsigned long size);
/* iounmap用於釋放不再需要的對映 */
void iounmap(void * addr);

經過 ioremap (iounmap)之後,裝置驅動就可以存取任何I/O記憶體地址。注意,ioremap返回的地址不可以直接解引用;相反,應當使用核心提供的訪問函式。

2.2、訪問I/O記憶體

訪問I/O記憶體的正確方式是通過一系列專門用於實現此目的的函式:

#include <asm/io.h>
/* I/O記憶體讀函式。引數addr應當是從ioremap獲得的地址(可能包含一個整型偏移); 返回值是從給定I/O記憶體讀取到的值 */
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
/* I/O記憶體寫函式。引數addr同I/O記憶體讀函式,引數value為要寫的值 */
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
/* 以下這些函式讀和寫一系列值到一個給定的 I/O 記憶體地址,從給定的buf讀或寫count個值到給定的addr。引數count表示要讀寫的資料個數,而不是位元組大小 */
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr,,onst void *buf,,nsigned long count);
/* 需要操作一塊I/O 地址時,使用下列函式(這些函式的行為類似於它們的C庫類似函式): */
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);
/* 舊的I/O記憶體讀寫函式,不推薦使用 */
unsigned readb(address);
unsigned readw(address);
unsigned readl(address); 
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address); 

2.3、像I/O 記憶體一樣使用埠

一些硬體有一個有趣的特性: 有些版本使用 I/O 埠;而有些版本則使用 I/O 記憶體。不管是I/O 埠還是I/O 記憶體,處理器見到的裝置暫存器都是相同的,只是訪問方法不同。為了統一程式設計介面,使驅動程式易於編寫,2.6核心提供了一個ioport_map函式:

/* ioport_map重新對映countI/O,使它們看起來I/O記憶體。此後,驅動程式可以在ioport_map返回的地址上使用ioread8和同類函式。這樣,就可以在程式設計時,消除了I/O埠和I/O 記憶體的區別 */
void *ioport_map(unsigned long port, unsigned int count); 
/* ioport_unmap用於釋放不再需要的對映 */
void ioport_unmap(void *addr);

注意,I/O 埠在重新對映前必須使用request_region分配所需的I/O 埠。

       3、ARM體系的I/O操作介面

s3c24x0處理器使用的是I/O記憶體,也就是說:s3c24x0處理器使用統一編址方式,I/O暫存器和記憶體使用的是單一地址空間,並且讀寫I/O暫存器和讀寫記憶體的指令是相同的。所以推薦使用I/O記憶體的相關指令和函式。但這並不表示I/O埠的指令在s3c24x0中不可用。如果你注意過s3c24x0關於I/O方面的核心原始碼,你就會發現:其實I/O埠的指令只是一個外殼,內部還是使用和I/O記憶體一樣的程式碼。

下面是ARM體系原始的I/O操作函式。其實後面I/O埠和I/O記憶體操作函式,只是對這些函式進行再封裝。從這裡也可以看出為什麼我們不推薦直接使用I/O埠和I/O記憶體地址指標,而是要求使用專門的I/O操作函式——專門的I/O操作函式會檢查地址指標是否有效是否為IO地址(通過__iomem__chk_io_ptr

#include <asm-arm/io.h>

/*
 * Generic IO read/write. These perform native-endian accesses. Note
 * that some architectures will want to re-define __raw_{read,write}w.
 */
extern void __raw_writesb(void __iomem *addr, const void *data, int bytelen);
extern void __raw_writesw(void __iomem *addr, const void *data, int wordlen);
extern void __raw_writesl(void __iomem *addr, const void *data, int longlen);
extern void __raw_readsb(const void __iomem *addr, void *data, int bytelen);
extern void __raw_readsw(const void __iomem *addr, void *data, int wordlen);
extern void __raw_readsl(const void __iomem *addr, void *data, int longlen);
#define __raw_writeb(v,a)    (__chk_io_ptr(a), *(volatile unsigned char __force *)(a) =(v))
#define __raw_writew(v,a)    (__chk_io_ptr(a), *(volatile unsigned short __force *)(a) =(v))
#define __raw_writel(v,a)    (__chk_io_ptr(a), *(volatile unsigned int __force *)(a) =(v))
#define __raw_readb(a)        (__chk_io_ptr(a), *(volatile unsigned char __force *)(a))
#define __raw_readw(a)        (__chk_io_ptr(a), *(volatile unsigned short __force *)(a))
#define __raw_readl(a)        (__chk_io_ptr(a), *(volatile unsigned int __force *)(a))

關於__force和__iomem

#include <linux/compiler.h>

/* __force表示所定義的變數型別是可以做強制型別轉換的 */
#define __force __attribute__((force)) 
/* __iomem是用來修飾一個變數的,這個變數必須是非解引用(no dereference)的,即這個變數地址必須是有效的,而且變數所在的地址空間必須是2,即裝置地址對映空間。0表示normal space,即普通地址空間,對核心程式碼來說,當然就是核心空間地址了。1表示使用者地址空間,2表示是裝置地址對映空間 */
#define __iomem __attribute__((noderef, address_space(2)))

I/O埠

#include <asm-arm/io.h>

#define outb(v,p)        __raw_writeb(v,__io(p))
#define outw(v,p)        __raw_writew((__force __u16) \
                    cpu_to_le16(v),__io(p))
#define outl(v,p)        __raw_writel((__force __u32) \
                    cpu_to_le32(v),__io(p))
#define inb(p)    ({ __u8 __v = __raw_readb(__io(p)); __v; })
#define inw(p)    ({ __u16 __v = le16_to_cpu((__force __le16) \
            __raw_readw(__io(p))); __v; })
#define inl(p)    ({ __u32 __v = le32_to_cpu((__force __le32) \
            __raw_readl(__io(p))); __v; })
#define outsb(p,d,l)        __raw_writesb(__io(p),d,l)
#define outsw(p,d,l)        __raw_writesw(__io(p),d,l)
#define outsl(p,d,l)        __raw_writesl(__io(p),d,l)
#define insb(p,d,l)        __raw_readsb(__io(p),d,l)
#define insw(p,d,l)        __raw_readsw(__io(p),d,l)
#define insl(p,d,l)        __raw_readsl(__io(p),d,l)

I/O記憶體

#include <asm-arm/io.h>

#define ioread8(p)    ({ unsigned int __v = __raw_readb(p); __v; })
#define ioread16(p)    ({ unsigned int __v = le16_to_cpu((__force __le16)__raw_readw(p));__v; })
#define ioread32(p)    ({ unsigned int __v = le32_to_cpu((__force __le32)__raw_readl(p));__v; })
#define iowrite8(v,p)    __raw_writeb(v, p)
#define iowrite16(v,p)    __raw_writew((__force __u16)cpu_to_le16(v), p)
#define iowrite32(v,p)    __raw_writel((__force __u32)cpu_to_le32(v), p)
#define ioread8_rep(p,d,c)    __raw_readsb(p,d,c)
#define ioread16_rep(p,d,c)    __raw_readsw(p,d,c)
#define ioread32_rep(p,d,c)    __raw_readsl(p,d,c)
#define iowrite8_rep(p,s,c)    __raw_writesb(p,s,c)
#define iowrite16_rep(p,s,c)    __raw_writesw(p,s,c)
#define iowrite32_rep(p,s,c)    __raw_writesl(p,s,c)

注意:

1)、所有的讀寫指令(I/O操作函式)所賦的地址必須都是虛擬地址,你有兩種選擇:使用核心已經定義好的地址,如在include/asm-arm/arch-s3c2410/regs-xxx.h中定義了s3c2410處理器各外設暫存器地址(其他處理器晶片也可在類似路徑找到核心定義好的外設暫存器的虛擬地址;另一種方法就是使用自己用ioremap對映的虛擬地址。絕對不能使用實際的實體地址,否則會因為核心無法處理地址而出現oops。

2)、在使用I/O指令時,可以不使用request_region和request_mem_region,而直接使用outb、ioread等指令。因為request的功能只是告訴核心埠被誰佔用了,如再次request,核心會制止(資源busy)。但是不推薦這麼做,這樣的程式碼也不規範,可能會引起併發問題(很多時候我們都需要獨佔裝置)。

3)、在使用I/O指令時,所賦的地址資料有時必須通過強制型別轉換為 unsigned long,不然會有警告。

4)、在include\asm-arm\arch-s3c2410\hardware.h中定義了很多io口的操作函式,有需要可以在驅動中直接使用,很方便。

Linux系統對IO埠和IO記憶體的管理

http://blog.csdn.net/ce123/article/details/7204458

一、I/O埠

      埠(port)是介面電路中能被CPU直接訪問的暫存器的地址。幾乎每一種外設都是通過讀寫裝置上的暫存器來進行的。CPU通過這些地址即埠向介面電路中的暫存器傳送命令,讀取狀態和傳送資料。外設暫存器也稱為“I/O埠”,通常包括:控制暫存器、狀態暫存器和資料暫存器三大類,而且一個外設的暫存器通常被連續地編址。

二、IO記憶體

       例如,在PC上可以插上一塊圖形卡,有2MB的儲存空間,甚至可能還帶有ROM,其中裝有可執行程式碼。

三、IO埠和IO記憶體的區分及聯絡

         這兩者如何區分就涉及到硬體知識,X86體系中,具有兩個地址空間:IO空間和記憶體空間,而RISC指令系統的CPU(如ARM、PowerPC等)通常只實現一個實體地址空間,即記憶體空間。
記憶體空間:記憶體地址定址範圍,32位作業系統記憶體空間為2的32次冪,即4G。
IO空間:X86特有的一個空間,與記憶體空間彼此獨立的地址空間,32位X86有64K的IO空間。

IO埠:當暫存器或記憶體位於IO空間時,稱為IO埠。一般暫存器也俗稱I/O埠,或者說I/O ports,這個I/O埠可以被對映在Memory Space,也可以被對映在I/O Space。

IO記憶體:當暫存器或記憶體位於記憶體空間時,稱為IO記憶體。

四、外設IO埠實體地址的編址方式

        CPU對外設IO埠實體地址的編址方式有兩種:一種是I/O對映方式(I/O-mapped),另一種是記憶體對映方式(Memory-mapped)。而具體採用哪一種則取決於CPU的體系結構。

1、統一編址

  RISC指令系統的CPU(如,PowerPC、m68k、ARM等)通常只實現一個實體地址空間(RAM)。在這種情況下,外設I/O埠的實體地址就被對映到CPU的單一實體地址空間中,而成為記憶體的一部分。此時,CPU可以象訪問一個記憶體單元那樣訪問外設I/O埠,而不需要設立專門的外設I/O指令。

       統一編址也稱為“I/O記憶體”方式,外設暫存器位於“記憶體空間”(很多外設有自己的記憶體、緩衝區,外設的暫存器和記憶體統稱“I/O空間”)。

2、獨立編址

        而另外一些體系結構的CPU(典型地如X86)則為外設專門實現了一個單獨地地址空間,稱為“I/O地址空間”或者“I/O埠空間”。這是一個與CPU地RAM實體地址空間不同的地址空間,所有外設的I/O埠均在這一空間中進行編址。CPU通過設立專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元(也即I/O埠)。與RAM實體地址空間相比,I/O地址空間通常都比較小,如x86 CPU的I/O空間就只有64KB(0-0xffff)。這是“I/O對映方式”的一個主要缺點。

        獨立編址也稱為“I/O埠”方式,外設暫存器位於“I/O(地址)空間”。

3、優缺點

獨立編址主要優點是:
1)、I/O埠地址不佔用儲存器空間;使用專門的I/O指令對埠進行操作,I/O指令短,執行速度快。
2)、並且由於專門I/O指令與儲存器訪問指令有明顯的區別,使程式中I/O操作和儲存器操作層次清晰,程式的可讀性強。
3)、同時,由於使用專門的I/O指令訪問埠,並且I/O埠地址和儲存器地址是分開的,故I/O埠地址和儲存器地址可以重疊,而不會相互混淆。 
4)、譯碼電路比較簡單(因為I/0埠的地址空間一般較小,所用地址線也就較少)。
其缺點是:只能用專門的I/0指令,訪問埠的方法不如訪問儲存器的方法多。

統一編址優點:
1)、由於對I/O裝置的訪問是使用訪問儲存器的指令,所以指令型別多,功能齊全,這不僅使訪問I/O埠可實現輸入/輸出操作,而且還可對埠內容進行算術邏輯運算,移位等等;
2)、另外,能給埠有較大的編址空間,這對大型控制系統和資料通訊系統是很有意義的。
這種方式的缺點是端口占用了儲存器的地址空間,使儲存器容量減小,另外指令長度比專門I/O指令要長,因而執行速度較慢。

        究竟採用哪一種取決於系統的總體設計。在一個系統中也可以同時使用兩種方式,前提是首先要支援I/O獨立編址。Intel的x86微處理器都支援I/O 獨立編址,因為它們的指令系統中都有I/O指令,並設定了可以區分I/O訪問和儲存器訪問的控制訊號引腳。而一些微處理器或微控制器,為了減少引腳,從而減 少晶片佔用面積,不支援I/O獨立編址,只能採用儲存器統一編址。

五、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埠。

1、I/O對映方式

       直接使用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資料結構中:

  1. struct resource {  
  2.          resource_size_t start;// 資源範圍的開始  
  3.          resource_size_t end;// 資源範圍的結束  
  4.          const char *name; //資源擁有者的名字  
  5.          unsigned long flags;// 各種標誌  
  6.          struct resource *parent, *sibling, *child;// 指向資源樹中父親,兄弟和孩子的指標  
  7. };  


          所有的同種資源都插入到一個樹型資料結構(父親、兄弟和孩子)中;例如,表示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檔案中獲得。

2、記憶體對映方式

         將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);

流程如下:

六、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 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()申請到的)

流程如下:

六、ioremap和ioport_map

下面具體看一下ioport_map和ioport_umap的原始碼:

  1. void __iomem *ioport_map(unsigned long port, unsigned int nr)  
  2. {  
  3.     if (port > PIO_MASK)  
  4.         return NULL;  
  5.     return (void __iomem *) (unsigned long) (port + PIO_OFFSET);  
  6. }  
  7. void ioport_unmap(void __iomem *addr)  
  8. {  
  9.     /* Nothing to do */  
  10. }  

          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來讀寫。

  1. unsigned int fastcall ioread8(void __iomem *addr)  
  2. {  
  3.     IO_COND(addr, return inb(port), return readb(addr));  
  4. }  
  5. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  
  6. 相關推薦

    IO對映記憶體對映 Port mapped I/O Memory mapped I/O說明

    IO埠和IO記憶體的區別及分別使用的函式介面           每個外設都是通過讀寫其暫存器來控制的。外設暫存器也稱為I/O埠,通常包括:控制暫存器、狀態暫存器和資料暫存器三大類。根據訪問外設暫存器的不同方式,可以把CPU分成兩大類。一類CPU(如M68K,Power

    QT 共享記憶體 QSharedMemory 記憶體對映

    QSharedMemory 使用本地 Key (即平臺相關,setNativeKey)時,可以和其他非QT應用互動。 例如,Windows平臺下API建立共享記憶體 CreateFileMapping

    記憶體洩漏記憶體溢位不看後悔,一看必懂

    ​​​​​​​記憶體洩漏 記憶體洩漏是指不再被程式使用的物件或者變數還在記憶體中佔儲存空間 記憶體洩漏主要有兩種情況:一是在堆中申請的空間沒有被釋放;二是物件已經不再使用,但仍然在記憶體中保留著,垃圾回收器則無法保證不再使用的物件會被釋放.因此java語言中的記憶

    程序間通訊之記憶體對映(mmap記憶體對映)

    讀資料端程序 //mmanp_r.c #include<stdio.h> #include<fcntl.h> #include<stdlib.h> #include<sys/mman.h> #include<unistd.h> #inc

    Java多執行緒記憶體模型:程序執行緒基礎

    Java多執行緒和記憶體模型(一) 由於java是執行在 JVM上 的,所以需要涉及到 JVM 的記憶體模型概念,需要理解記憶體模型,就需要多執行緒的基礎; 而執行緒是基於載體執行緒裡的,所以我們藉由作業系統的程序來講一講。 程序 什麼是程序?

    讀書筆記:Objective-C高階程式設計 iOS與OS X多執行緒記憶體管理 ——持續

    1.記憶體管理的思考方式: .自己生成的物件,自己所持有.非自己生成的物件,自己也能持有不再需要自己持有的物件時釋放非自己持有的物件無法釋放由NSObject類擔任下面管理職責 eg:  /

    linux 記憶體對映 PCI記憶體對映 DMA對映

    記憶體對映, 就是指把外設的記憶體對映到使用者空間訪問。系統呼叫為:        #include <sys/mman.h>        void *mmap(void *addr, size_t length, int prot, int flags, i

    記憶體洩漏memory leak記憶體溢位out of memory

    一、概念: memory leak---記憶體洩漏:是指程式申請記憶體空間後,無法釋放。然而,記憶體洩漏不斷疊加會導致系統把記憶體花完,以致於發生記憶體溢位。(用了不還) out of memory-

    Java記憶體區域執行時資料區域記憶體模型JMM

    Java 記憶體區域和記憶體模型是不一樣的東西,記憶體區域是指 Jvm 執行時將資料分割槽域儲存,強調對記憶體空間的劃分。 而記憶體模型(Java Memory Model,簡稱 JMM )是定義了執行緒和主記憶體之間的抽象關係,即 JMM 定義了 JVM 在計算機記憶體(RAM)中的工作方式,如果我們要想深

    《小白滴滴系列》-線程進程小白學習,內容均參考網絡資料

    多個 系統 資料 搶占式 線程 並發 強制 資源 資源分配 1、進程就是操作系統將資源分配成一塊一塊的內存 2、線程就是在進程中運行的多個程序 3、線程是程序運行的最小單位,而進程則是分配資源的最小單位。 4、一個進程可以有多個線程 5、任務調度:采取時間片輪轉搶占式執行,

    2008 R2 輔域安裝卸載加域、退域及組策略的測試

    主域 輔域 組策略 加域 退域 一、輔域安裝 1、運行——dcpromo 2、向現有域添加域控制器 3、輸入主域域名fdwxyjy.com 4、選擇站點(默認即可,此處wuxi是因為在AD中重命名了) 5、輔域安裝完後重啟,在主域上可以看到原來Domain Contr

    C++學習之分支語句邏輯運算子switch語句,breakcontinue語句

    1.switch語句 switch(integer-expression) { case label1:statement(s) case label2:statement(s) ....... default :statement(s

    中序線索二叉樹的建立、線索化遍歷前序遍歷後序遍歷

    線索二叉樹的概念 線索二叉樹的原理:線索二叉樹是將普通二叉樹左右孩子中的空鏈域利用起來,將左孩子空鏈域指向當前節點的線性遍歷前驅,將右孩子空鏈域指向當前節點的線性遍歷後繼,指向該線性序列中的前驅或後繼

    使用Matlab進行影象的讀寫、顯示縮放最近臨插值雙線性內插值法

    上次我們開始進行數字影象處理這門課程的實驗,直到現在才抽空出來寫寫文章,記錄一下知識點。介紹一下,使用Matlab對數字影象的簡單處理。 1、 讀取與顯示輸入影象: %輸入影象和顯示影象 funct

    js正則驗證數字,字母下劃線至少包含其中兩種,數字不能開頭

    最近做了一個註冊驗證使用者名稱,要求數字,字母和下劃線(至少包含其中兩種,數字不能開頭),在網上找了好多例子,要麼只能實現字母和下劃線(數字不能開頭),要麼就只能實現數字,字母和特殊字元(至少包含其中兩種,數字不能開頭),無法排除其他的特殊字元,在嘗試了很多次以後,我試著將這兩種情況的判

    雜湊表之簡易數學原理簡易實現史上最簡單易懂的雜湊表介紹

           什麼是雜湊表呢? 我先不說, 但其思想確實厲害。 下面, 我以最簡單易懂的方式來介紹雜湊表。        你要是去看教科書啊, 還沒有理解雜湊表的原理, 他就給你介紹近10種防衝突的方法, 這就是中國的教育。 你要是去網上搜點資料問為什麼雜湊表查詢的時間複雜

    SharePoint 2016:效能優化高可用十一,SP中的WEB內容管理功能

    SHAREPOINT SERVER 2016中的WEB內容管理功能 根據SharePoint MPP 課程,下面幾個文章介紹,SP16 效能優化和高可用的下一部分,Web內容管理功能。 SharePoint Server 2016中的Web內容管理包含用於配置,自定義,優

    30分鐘學會用scikit-learn的基本回歸方法線性、決策樹、SVM、KNN整合方法隨機森林,AdaboostGBRT

    注:本教程是本人嘗試使用scikit-learn的一些經驗,scikit-learn真的超級容易上手,簡單實用。30分鐘學會用呼叫基本的迴歸方法和整合方法應該是夠了。 本文主要參考了scikit-learn的官方網站 前言:本教程主要使用了numpy的最最基

    Java的阻塞中斷機制 wait notify使用 waitsleep區別 interrupt使用其他中斷方法

    wait、notify和notifyAll wait和notify(notifyAll)一般是成對搭配出現的,用來資源調控。wait用來將當然執行緒掛起,notify/notifyAll用來恢復執行緒。它是類Object的方法,也就是所有的物件都可以使用。一個簡單的例子

    記Spring搭建功能完整的個人博客「Oyster」全過程[其二] Idea中Maven+SpringBoot多模塊項目開發的設計各種坑模塊間依賴打包問題

    也不能 -c restfu 訪問 存在 後臺 api 新增 idea 大家好嘞,今天閑著沒事幹開寫寫博客,記錄一下Maven+SpringBoot的多模塊設計和遇到的坑。 多模塊設計 簡單說明一下截止目前的需求: 需要RESTful API:對文章、標簽、分類和評論等的C