解惑-Linux內核空間(二)

從前一講我們知道,內核空間爲3GB~4GB,這1GB的空間分爲如下幾部分,如圖1所示:

這裏寫圖片描述

       圖1  從PAGE_OFFSET開始的1GB地址空間  

先說明圖中符號的含義:

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,Power PC等)把這些寄存器看作內存的一部分,寄存器參與內存統一編址,訪問寄存器就通過訪問一般的內存指令進行,所以,這種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()函數:
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()申請到的)

爲什麼要申請虛擬內存然後才進行映射?

發佈了6 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章