天天看點

nc 端口占用_詳解IO端口與IO記憶體

nc 端口占用_詳解IO端口與IO記憶體

(一)位址的概念

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  IDE0x1900 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 : dma10020-003f : pic10040-005f : timer0060-006f : keyboard0070-007f : rtc0080-008f : dma page reg00a0-00bf : pic200c0-00df : dma200f0-00ff : fpu0170-0177 : ide1……

不過Intelx86平台普通使用了名為記憶體映射(MMIO)的技術,該技術是PCI規範的一部分,IO裝置端口被映射到記憶體空間,映射後,CPU通路IO端口就如同通路記憶體一樣。看IntelTA 719文檔給出的x86/x64系統典型記憶體位址配置設定表:

系統資源  占用

BIOS  1M本地APIC 4K晶片組保留 2MIO APIC  4KPCI裝置 256MPCI Express裝置256MPCI裝置(可選) 256M顯示幀緩存 16MTSEG  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端口。

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

流程如下:

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

流程如下:

3)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.   

8. void ioport_unmap(void __iomem *addr)  

9. {  

10.       

11. }  

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.   

6. #define VERIFY_PIO(port) BUG_ON((port & ~PIO_MASK) != PIO_OFFSET)  

7. #define IO_COND(addr, is_pio, is_mmio) do { \  

8.     unsigned long port = (unsigned long __force)addr; \  

9.         if (port 

10.             VERIFY_PIO(port); \  

11.             port &= PIO_MASK; \  

12.             is_pio; \  

13.         } else { \  

14.             is_mmio; \  

15.         } \  

16. } while (0)  

17.   

18. 展開:

19. unsigned int fastcall ioread8(void __iomem *addr)  

20. {  

21.     unsigned long port = (unsigned long __force)addr;  

22.     if( port 

23.         BUG_ON( (port & ~PIO_MASK) != PIO_OFFSET );  

24.         port &= PIO_MASK;  

25.         return inb(port);  

26.     }else{  

27.         return readb(addr);  

28.     }  

29. } 

-END-

nc 端口占用_詳解IO端口與IO記憶體

繼續閱讀