蛻變成蝶~Linux設備驅動之CPU與內存和I/O

那是世上最遠的距離 思念讓我沒法去呼吸 你的一動和一舉 佔據我內心 陪我每一個孤獨無盡的夜裏 html

用我心中盛放的畫筆 描繪你微笑時的絢麗 愛讓人痛徹心底 我卻不懷疑 你的存在是我生命的奇蹟linux

感覺你的每一次的呼吸 多想告訴你我有多愛你 若是我說我願意 爲你而死去 能否你的夢裏留下我痕跡編程

無數悸動變換歲月裏 你會依偎在誰的懷裏 那些埋藏在內心 最深的祕密 是我生命裏最脆弱的美麗緩存

感覺你的每一次的呼吸 多想告訴你我有多愛你 若是我說我願意 爲你而死去 能否告訴我你心底曾是惟一app

感覺你的每一次的呼吸 多想告訴你我有多愛你 若是我說我願意 爲你而死去 能否告訴我你心底 我曾是惟一函數

這是今天的旋律,在腦海中回放,在心靈裏思考,在閉上眼睛時掙扎,在睜開雙眼時去看清這個不能停留過久的世界  ui

  因爲Linux系統提供了複雜的內存管理功能,本節將講解的是內存和I/O的訪問編程。
  在X86中,I/O空間是相對於內存空間而言的,經過特定的in、out來訪問,in、out指令格式以下:操作系統

IN 累加器,{端口號|DX}
OUT {端口號|DX},累加器

  下面說說MMU(內存管理單元),操做系統藉助MMU可讓用戶感受到好像程序可使用很是大的內核空間,實際上就是咱們平時瞭解的虛擬地址同樣的。爲了好好了解一下MMU,先看兩個概念設計

TLB:MMU的核心部件,緩存少許的虛擬地址與物理地址的轉換關係,是轉換表的Cache指針

TTW:當TLB中沒有緩衝對應的地址轉換關係時候,須要經過對內存中轉換表的訪問來獲取虛擬地址和物理地址的對應關係,TTW成功後,結果應該寫入TLB。

爲了說明MMU在內存中使用的關係,下圖能夠說明以下關係

  對於提供了 MMU(存儲管理器,輔助操做系統進行內存管理,提供虛實地址轉換等硬件支持)的處理器而言,Linux 提供了複雜的存儲管理系統,使得進程所能訪問的內存達到4GB。進程的 4GB 內存空間被人爲的分爲兩個部分——用戶空間與內核空間。用戶空間地址分佈從0 到3GB(PAGE_OFFSET,在0x86 中它等於0xC0000000),3GB 到4GB 爲內核空間,

  內核空間中,從3G 到vmalloc_start 這段地址是物理內存映射區域(該區域中包含了內核鏡像、物理頁框表mem_map 等等)kmalloc 和get_free_page 申請的內存位於物理內存映射區域,並且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,所以存在較簡單的轉換關係,virt_to_phys()能夠實現內核虛擬地址轉化爲物理地址:

#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
 return __pa(address);
}

  上面轉換過程是將虛擬地址減去3G(PAGE_OFFSET=0XC000000)。與之對應的函數爲phys_to_virt(),將內核物理地址轉化爲虛擬地址:

#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
 return __va(address);
}

  virt_to_phys()和phys_to_virt()都定義在include\asm-i386\io.h中。而vmalloc申請的內存則位於vmalloc_start~vmalloc_end之間,與物理地址沒有簡單的轉換關係,雖然在邏輯上它們也是連續的,可是在物理上它們不要求連續。咱們用下面的程序來演示kmalloc、get_free_page和vmalloc的區別:

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL"); 
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;

int __init mem_module_init(void)
{
 //最好每次內存申請都檢查申請是否成功
 //下面這段僅僅做爲演示的代碼沒有檢查
 pagemem = (unsigned char*)get_free_page(0);
 printk("<1>pagemem addr=%x", pagemem);

 kmallocmem = (unsigned char*)kmalloc(100, 0);
 printk("<1>kmallocmem addr=%x", kmallocmem);

 vmallocmem = (unsigned char*)vmalloc(1000000);
 printk("<1>vmallocmem addr=%x", vmallocmem);

 return 0;
}

void __exit mem_module_exit(void)
{
 free_page(pagemem);
 kfree(kmallocmem);
 vfree(vmallocmem);
}

module_init(mem_module_init);
module_exit(mem_module_exit);

  設備一般會提供一組寄存器來用於控制設備、讀寫設備、獲取設備狀態,那麼Linux設備驅動究竟怎樣訪問設備的I/O端口(寄存器)和I/O內存的訪問的呢

1.操做I/O口

(1)申請I/O 端口:

在驅動還沒獨佔設備以前,不該對端口進行操做。內核提供了一個註冊接口,以容許驅動聲明其須要的端口:

/* request_region告訴內核:要使用first開始的n個端口。參數name爲設備名。若是分配成功返回值是非NULL;不然沒法使用須要的端口(/proc/ioports包含了系統當前全部端口的分配信息,若request_region分配失敗時,能夠查看該文件,看誰先用了你要的端口) */
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

(2)訪問IO端口:

在驅動成功請求到I/O 端口後,就能夠讀寫這些端口了。大部分硬件會將8位、16位和32位端口區分開,沒法像訪問內存那樣混淆使用。驅動程序必須調用不一樣的函數來訪問不一樣大小的端口。

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

(3)釋放IO端口:

/* 用完I/O端口後(可能在模塊卸載時),應當調用release_region將I/O端口返還給系統。參數start和n應與以前傳遞給request_region一致 */
void release_region(unsigned long start, unsigned long n);

2 操做IO內存

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

(2)映射:
  在訪問I/O內存以前,分配I/O內存並非惟一要求的步驟,你還必須保證內核可存取該I/O內存。訪問I/O內存並不僅是簡單解引用指針,在許多體系中,I/O 內存沒法以這種方式直接存取。所以,還必須經過ioremap 函數設置一個映射。

/* ioremap用於將I/O內存區映射到虛擬地址。參數phys_addr爲要映射的I/O內存起始地址,參數size爲要映射的I/O內存的大小,返回值爲被映射到的虛擬地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);

(3)訪問IO內存:
  通過 ioremap以後,設備驅動就能夠存取任何I/O內存地址。注意,ioremap返回的地址不能夠直接解引用;相反,應當使用內核提供的訪問函數。訪問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);

(4)釋放IO內存步驟:

void iounmap(void * addr); /* iounmap用於釋放再也不須要的映射 */
void release_mem_region(unsigned long start, unsigned long len); /* iounmap用於釋放再也不須要的映射 */

  幾乎每一種外設都是經過讀寫設備上的寄存器來進行的,一般包括控制寄存器、狀態寄存器和數據寄存器三大類,外設的寄存器一般被連續地編址。根據CPU體系結構的不一樣,CPU對IO端口的編址方式有兩種:

(1)I/O映射方式(I/O-mapped)
  典型地,如X86處理器爲外設專門實現了一個單獨的地址空間,稱爲"I/O地址空間"或者"I/O端口空間",CPU經過專門的I/O指令(如X86的IN和OUT指令)來訪問這一空間中的地址單元。 

(2)內存映射方式(Memory-mapped)
  RISC指令系統的CPU(如ARM、PowerPC等)一般只實現一個物理地址空間,外設I/O端口成爲內存的一部分。此時,CPU能夠象訪問一個內存單元那樣訪問外設I/O端口,而不須要設立專門的外設I/O指令。 可是,這二者在硬件實現上的差別對於軟件來講是徹底透明的,驅動程序開發人員能夠將內存映射方式的I/O端口和外設內存統一看做是"I/O內存"資源。
  通常來講,在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定。可是CPU一般並無爲這些已知的外設I/O內存資源的物理地址預約義虛擬地址範圍,驅動程序並不能直接經過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內(經過頁表),而後才能根據映射所獲得的核心虛地址範圍,經過訪內指令訪問這些I/O內存資源。Linux在io.h頭文件中聲明瞭函數ioremap(),用來將I/O內存資源的物理地址映射到核心虛地址空間(3GB-4GB)中,原型以下:

void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

  iounmap函數用於取消ioremap()所作的映射,原型以下:

void iounmap(void * addr);

  在將I/O內存資源的物理地址映射成核心虛地址後,理論上講咱們就能夠象讀寫RAM那樣直接讀寫I/O內存資源了。爲了保證驅動程序的跨平臺的可移植性,咱們應該使用Linux中特定的函數來訪問I/O內存資源,而不該該經過指向核心虛地址的指針來訪問。

  最後,咱們要特別強調驅動程序中mmap函數的實現方法。用mmap映射一個設備,意味着使用戶空間的一段地址關聯到設備內存上,這使得只要程序在分配的地址範圍內進行讀取或者寫入,實際上就是對設備的訪問。

 

  版權全部,轉載請註明轉載地址:http://www.cnblogs.com/lihuidashen/p/4468223.html

相關文章
相關標籤/搜索