天天看點

c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

一、I/O端口

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

二、IO記憶體

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

c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

三、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獨立編址,隻能采用存儲器統一編址。

需要C/C++ Linux伺服器架構師學習資料加qun擷取(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等),免費分享
c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

五、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個位元組為一組的連續位元組序列。

流程如下:

c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

雖然通路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;// 指向資源樹中父親,兄弟和孩子的指針  
};  
           

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

流程如下:

c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

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

流程如下:

c++ 讀取記憶體資料 基址_Linux系統對IO端口和IO記憶體的管理

六、ioremap和ioport_map

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

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

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

七、總結

外設IO寄存器位址獨立編址的CPU,這時應該稱外設IO寄存器為IO端口,通路IO寄存器可通過ioport_map将其映射到虛拟位址空間,但實際上這是給開發人員制造的一個“假象”,并沒有映射到核心虛拟位址,僅僅是為了可以使用和IO記憶體一樣的接口通路IO寄存器;也可以直接使用in/out指令通路IO寄存器。

例如:Intel x86平台普通使用了名為記憶體映射(MMIO)的技術,該技術是PCI規範的一部分,IO裝置端口被映射到記憶體空間,映射後,CPU通路IO端口就如同訪 問記憶體一樣。

外設IO寄存器位址統一編址的CPU,這時應該稱外設IO寄存器為IO記憶體,通路IO寄存器可通過ioremap将其映射到虛拟位址空間,然後再使用read/write接口通路。

繼續閱讀