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

來源:互聯網
上載者:User
地址空間、核心空間、IO地址空間(轉)

(2011-03-17 21:30:06)

轉載
標籤:

地址空間核心空間io地址空間雜談
分類:作業系統

有這麼一系列的問題,是否在困擾著你:使用者程式編譯串連形成的地址空間在什麼範圍內?核心編譯後地址空間在什麼範圍內?要對外設進行訪問,I/O的地址空間又是什麼樣的?

先 回答第一個問題。Linux最常見的可執行檔格式為elf(Executable and LinkableFormat)。在elf格式的可執行代碼中,ld總是從0x8000000開始安排程式的“程式碼片段”,對每個程式都是這樣。至於程式執行時在實體記憶體中的實際地址,則由核心為其建立記憶體映射時臨時分配,具體地址取決於當時所分配的實體記憶體頁面。
我們可以用Linux的公用程式objdump對你的程式進行反組譯碼,從而知曉其位址範圍。

例如:假定我們有一個簡單的C程式Hello.c
   # include<stdio.h>
   greeting ( )
   {
         printf(“Hello,world!\n”);
   }
   main()
{
     greeting();
}
之所以把這樣簡單的程式寫成兩個函數,是為了說明指令的轉移過程。我們用gcc和ld對其進行編譯和串連,得到可執行代碼hello。然後,用Linux的公用程式objdump對其進行反組譯碼:
$objdump –d hello
得到的主要片段為:
08048568 <greeting>:
8048568:    pushl   �p
8048569:    movl   %esp, �p
804856b:    pushl   $0x809404
8048570:    call 8048474  <_init+0x84>
8048575:    addl $0x4, %esp
8048578:    leave
8048579:    ret
804857a:    movl   %esi, %esi
0804857c <main>:
804857c:    pushl   �p
804857d:    movl   %esp, �p
804857f:    call 8048568  <greeting>
8048584:    leave
8048585:    ret
8048586:    nop
8048587:    nop
其中,像08048568這樣的地址,就是我們常說的虛地址(這個地址實實在在的存在,只不過因為物理地址的存在,顯得它是“虛”的罷了)。

.虛擬記憶體、核心空間和使用者空間
Linux虛擬記憶體的大小為2^32(在32位的x86機器上),核心將這4G位元組的空間分為兩部分。最高的1G位元組(從虛地址0xC0000000到0xFFFFFFFF)供核心使用,稱為“核心空間”。而較低的3G位元組(從虛地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為“使用者空間”。因為每個進程可以通過系統調用進入核心,因此,Linux核心空間由系統內的所有進程共用。於是,從具體進程的角度來看,每個進程可以擁有4G位元組的虛擬位址空間(也叫虛擬記憶體)。

每個進程有各自的私人使用者空間(0~3G),這個空間對系統中的其他進程是不可見的。最高的1GB核心空間則為所有進程以及核心所共用。另外,進程的“使用者空間”也叫“地址空間”,在後面的敘述中,我們對這兩個術語不再區分。

使用者空間不是進程共用的,而是進程隔離的。每個進程最大都可以有3GB的使用者空間。一個進程對其中一個地址的訪問,與其它進程對於同一地址的訪問絕不衝突。比如,一個進程從其使用者空間的地址0x1234ABCD處可以讀出整數8,而另外一個進程從其使用者空間的地址0x1234ABCD處可以讀出整數20,這取決於進程自身的邏輯。

任意一個時刻,在一個CPU上只有一個進程在運行。所以對於此CPU來講,在這一時刻,整個系統只存在一個4GB的虛擬位址空間,這個虛擬位址空間是面向此進程的。當進程發生切換的時候,虛擬位址空間也隨著切換。由此可以看出,每個進程都有自己的虛擬位址空間,只有此進程啟動並執行時候,其虛擬位址空間才被運行它的CPU所知。在其它時刻,其虛擬位址空間對於CPU來說,是不可知的。所以儘管每個進程都可以有4GB的虛擬位址空間,但在CPU眼中,只有一個虛擬位址空間存在。虛擬位址空間的變化,隨著進程切換而變化。

從上面我們知道,一個程式編譯串連後形成的地址空間是一個虛擬位址空間,但是程式最終還是要運行在實體記憶體中。因此,應用程式所給出的任何虛地址最終必須被轉化為物理地址,所以,虛擬位址空間必須被映射到實體記憶體空間中,這個映射關係需要通過硬體體繫結構所規定的資料結構來建立。這就是我們所說的段描述符表和頁表,Linux主要通過頁表來進行映射。

於是,我們得出一個結論,如果給出的頁表不同,那麼CPU將某一虛擬位址空間中的地址轉化成的物理地址就會不同。所以我們為每一個進程都建立其頁表,將每個進程的虛擬位址空間根據自己的需要映射到物理地址空間上。既然某一時刻在某一CPU上只能有一個進程在運行,那麼當進程發生切換的時候,將頁表也更換為相應進程的頁表,這就可以實現每個進程都有自己的虛擬位址空間而互不影響。所以,在任意時刻,對於一個CPU來說,只需要有當前進程的頁表,就可以實現其虛擬位址到物理地址的轉化。

.核心空間到實體記憶體的映射  
核心空間對所有的進程都是共用的,其中存放的是核心代碼和資料,而進程的使用者空間中存放的是使用者程式的代碼和資料,不管是核心程式還是使用者程式,它們被編譯和串連以後,所形成的指令和符號地址都是虛地址(參見2.5節中的例子),而不是實體記憶體中的物理地址。

雖然核心空間佔據了每個虛擬空間中的最高1GB位元組,但映射到實體記憶體卻總是從最低地址(0x00000000)開始的,4.2所示,之所以這麼規定,是為了在核心空間與實體記憶體之間建立簡單的線性映射關係。其中,3GB(0xC0000000)就是物理地址與虛擬位址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET。
          
我們來看一下在include/asm/i386/page.h標頭檔中對核心空間中地址映射的說明及定義:
#define __PAGE_OFFSET        (0xC0000000)
……
#define PAGE_OFFSET        ((unsignedlong)__PAGE_OFFSET)
#define __pa(x)             ((unsignedlong)(x)-PAGE_OFFSET)
#define __va(x)             ((void*)((unsigned long)(x)+PAGE_OFFSET))
對於核心空間而言,給定一個虛地址x,其物理地址為“x- PAGE_OFFSET”,給定一個物理地址x,其虛地址為“x+PAGE_OFFSET”。
這裡再次說明,宏__pa()僅僅把一個核心空間的虛地址映射到物理地址,而決不適用於使用者空間,使用者空間的地址映射要複雜得多,它通過分頁機制完成。

核心空間為3GB~4GB,這1GB的空間分為如下幾部分,1所示:
先說明圖中符號的含義:
PAGE_OFFSET:0XC0000000,即3GB
high_memory:這個變數的字面含義是高端記憶體,到底什麼是高端記憶體,Linux核心規定,RAM的前896為所謂的低端記憶體,而896~1GB共128MB為高端記憶體。如果你的記憶體是512M,那麼high_memory是多少?是3GB+512,也就是說,物理地址x<=896M,就有核心地址0xc0000000+x,否則,high_memory=0xc0000000+896M
或者說high_memory最大值為0xc0000000+896M ,實際值為0xc0000000+x
在原始碼中函數mem_init中,有這樣一行:
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE);
其中,max_low_pfn為實體記憶體的最大頁數。
所以在圖中,PAGE_OFFSET到high_memory之間就是所謂的實體記憶體映射。只有這一段之間,物理地址與虛地址之間是簡單的線性關係。
  還要說明的是,要在這段記憶體配置記憶體,則調用kmalloc()函數。反過來說,通過kmalloc()分配的記憶體,其物理頁是連續的。

VMALLOC_START:非連續區的的起始地址。
VMALLOC_END:非連續區的的末尾地址
在非連續區中,實體記憶體映射的末端與第一個VMalloc之間有一個8MB的安全區,目的是為了“捕獲”對記憶體的越界訪問。處於同樣的理由,插入其他4KB的安全區來隔離非連續區。

非連續區的分配調用VMalloc()函數。

vmalloc()與 kmalloc()都是在核心代碼中用來分配記憶體的函數,但二者有何區別?
從前面的介紹已經看出,這兩個函數所分配的記憶體都處於核心空間,即從3GB~4GB;但位置不同,kmalloc()分配的記憶體處於3GB~high_memory之間,這一段核心空間與實體記憶體的映射一一對應,而vmalloc()分配的記憶體在VMALLOC_START~4GB之間,這一段非連續記憶體區映射到實體記憶體也可能是非連續的。
vmalloc()工作方式與kmalloc()類似,其主要差別在於前者分配的物理地址無需連續,而後者確保頁在物理上是連續的(虛地址自然也是連續的)。
儘管僅僅在某些情況下才需要物理上連續的記憶體塊,但是,很多核心代碼都調用kmalloc(),而不是用vmalloc()獲得記憶體。這主要是出於效能的考慮。vmalloc()函數為了把物理上不連續的頁面轉換為虛擬位址空間上連續的頁,必須專門建立頁表項。還有,通過vmalloc()獲得的頁必須一個一個的進行映射(因為它們物理上不是連續的),這就會導致比直接記憶體映射大得多的緩衝區重新整理。因為這些原因,vmalloc()僅在絕對必要時才會使用——典型的就是為了獲得大塊記憶體時,例如,當模組被動態插入到核心中時,就把模組裝載到由vmalloc()分配的記憶體上。
vmalloc()函數用起來比較簡單:
char *buf;
buf =vmalloc(16*PAGE_SIZE);  
if (!buf)

在使用完分配的記憶體之後,一定要釋放它:
vfree(buf);
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記憶體,三者之間有什麼區別,在驅動程式開發中如何具體應用?

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.