內存與I/O訪問

本章節帶大家一起來探討一下Linux驅動中的內存與I/O訪問


CPU與內核和I/O
I/O空間:在X86處理器中存在着I/O空間的概念,I/O空間是相對於內存空間而言的。
它通過特定的指令in、out來訪問

指令格式:IN 累加器,{端口號|DX}
OUT {端口號|DX},累加器


注意:目前大多數嵌入式微控制例如ARM、PowerPC等不提供I/O空間,而僅存在內存空間。
內存空間可以直接通過地址、指針來訪問


爲什麼內存空間是必須的,I/O空間是可選的?
答:我們可以將外設只掛在到內存空間,此時CPU就可以像訪問一個內存空間一樣訪問外設i/o端口了

如下圖所示:





內存管理單元MMU
作用:輔助操作系統進行內存管理,提供虛擬地址和物理地址的映射、內存訪問權限保護
和Cache緩存控制等硬件支持。


其中的虛擬內存機制可以讓用戶感覺好像程序可以使用非常大的內存空間

MMU操作原理:
(1).TLB:轉換旁路緩存,是MMU的核心部件,緩存少量虛擬地址和物理地址的轉換關係,
是轉換表的Cache,也成爲快表

(2).TTW:轉換表漫遊,當TLB中沒有對應緩衝對應的地址轉換關係時,需要通過對內存中轉換表(一般爲多級頁表)
的訪問來得到虛擬地址和物理地的對應關係。TTW成功後,會將對應的轉換關係寫入TLB,

方便下次轉換,具體如圖所示:





下面給出一個典型的ARM處理器訪問內存的過程:





在次敘述一下虛擬地址和物理地址的轉換過程:
若TLB中沒有虛擬地址的入口,則轉換表遍歷硬件從存放於主存儲器的轉換表中獲取
地址轉換信息和訪問權限(也就是獲得TTW啦),同時將信息放入TLB,它或者被放在一個沒有
使用的入口或者替換一個已經存在的端口,以後當再次訪問這些地址時,對
真是物理地址的訪問將在Cache或者在內存中發生
具體如下圖:





TLB進階:
ARM中的TLB條目中的控制信息用於控制對對應地址的訪問權限及Cache的操作
---C(高速緩存)和B(緩衝)位被用來控制對應地址的高速緩存和寫緩衝,並決定是否進行
告訴緩存

---訪問權限和域位作用用來控制讀寫訪問是否被允許,如果不允許將發送一個異常




注意:Linux內核使用了三級頁表PGD、PMD、PTE









Linux內存管理:
Linux系統中,進程4GB的內存空間被分成兩個部分:用戶空間和內核空間

用戶空間地址:0~3GB
內核空間地址:3~4GB


用戶進程通常只能訪問用戶空間的虛擬地址,不能訪問內核空間的虛擬地址,用戶進程
只有通過系統調用的方式才能訪問到內核空間





內核空間與用戶空間的區別:
每個進程的用戶空間都是完全獨立的用戶進程各自擁有不同的頁表。
內核空間是由內核負責映射的,它不會跟着進程改變,是固定的,內核地址空間有自己獨立的頁表




Linux內核空間(1GB)的劃分:
常規內存:
物理內映射區(896MB):系統物理內存被順序映射在內核空間的這個區域

高端內存:
虛擬內存分配區(其實地址:VMALLOC_START~VMALLOC_END,用vmalloc()函數分配)
高端頁面映射區(起始地址:PKMAP_BASE)
專用頁面應設置區(地址爲FIXADDR_START~FIXADDR_TOP)
系統保留映射區


如下圖所示:










如果物理內存超過4GB怎麼辦:
此時必須使用CPU的擴展分頁(PAE)模式提供的64位頁目錄才能取到4GB以上的物理內存









內存讀取:
用戶空間內存動態申請
申請:malloc()
釋放:free()





內核空間內存動態申請:
相關函數:
kmalloc()、__get_free_pages()和vmalloc()


詳細介紹:
kmalloc()和__get_free_pages()申請的內存位於物理內存映射區,並且是連續的,它與真實的物理地址一般
只差一個固定的偏移

vmalloc()在虛擬地址空間給出一塊連續的內存區,實質上這段連續的虛擬內存在物理內存中並不一定連續。也沒簡單的換算關係


進階:
kmalloc(size_t size,int flags);
參數介紹:
size:分配大小
flag:分配標識
GFP_KERNEL(最常用),在內核空間進程中申請內存
GFP_USER,用來爲用戶空間分配內存,可能阻塞
GFP_HINSTANCE,與GFP_USER類似但是是從高端內存分配
GFP_NOIO,不允許任何IO初始化
GFP_NOFS,不允許任何文件系統調用
__GFP_DMA,要求分配在能夠DMA的內存區
__GFP_HINSTANCE,指示分配的內存區可以位於高端內存
...


小知識:kmalloc()其實就是依賴於_get_free_pages()函數實現的


__get_free_pages(unsigned int flags,unsigned int order);
介紹:此宏是Linux內核本質上最底層用於獲取空閒內存的方法
因爲底層的夥伴算法總是以頁的2的n次方爲單位管理空閒內存,所以最底層的內存申請總是以頁
爲單位的

相關宏還包括:
__get_zeroed_pages()(申請的同時將頁清空)、__get_free_page()(申請一頁)

參數介紹:
order:分配的頁數是2^order
flags:同kmalloc


__get_free_pages()和get_zeroed_page()的實現中調用了alloc_pages()函數,alloc_pages()既可以在內核空間分配,也
可以在用戶空間分配
struct page*alloc_pages(int gfp_mask,unsigned long order);

//__get_free_pages()函數對應的釋放函數
void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order);




vmalloc函數
void *vmalloc(unsigned long size);
void vfree(void *addr);







slab與內存池:
引入slab的原因:
(1).完全使用頁爲單元申請和釋放內存容易導致浪費
(2).在操作系統的運行過程中,經常涉及到對大量對象的重複生成、使用和釋放問題,
此時使用slab可以大大提高效率

實際上kmalloc()就是使用slab機制實現的
使用方法:
暫且略過,詳細實現,可以在用到的時候再去了解(Linux驅動程序開發詳解)






                
虛擬地址和物理地址的關係:
虛擬地址到物理地址的轉換
static inline unsigned long virt_to_phys(void *x);

物理地址到虛擬地址的轉換
static inline void *phys_to_virt(unsigned long x);


注意:上述方法僅使用於896M以下的低端內存









設備I/O端口和I/O內存的訪問

控制寄存器、數據寄存器和狀態寄存器:
設備通常提供這些寄存器用來控制設備、讀寫寄存器和獲取設備狀態


I/O內存:當這些寄存器位於內存空間時
I/O端口:當這些寄存器位於I/O空間時




設備I/O端口和I/O內存的訪問
Linux I/O端口和I/O內存的訪問接口
1.I/O端口


(1)讀寫字節端口(8位寬)
unsigned inb(unsigned port);
void outb(unsigned char byte,unsigned port);
(2)讀寫字端口(16位寬)
unsigned inw(unsigned port);
void outw(unsigned char byte,unsigned port);
(3)讀雙字節端口(32位寬)
unsigned inl(unsigned port);
void outl(unsigned char byte,unsigned port);

(4)讀寫一串字節
void insb(unsigned port,void *addr,unsigned long count);
void outsb(unsigned port,void *addr,unsigned long count);
//insb()從端口port開始讀count個字節端口,並將讀取的結果寫入addr指向的內存

(5)讀寫一串字
void insw(unsigned port,void *addr,unsigned long count);
void outsw(unsigned port,void *addr,unsigned long count);
(6)讀寫一串長字
void insl(unsigned port,void *addr,unsigned long count);
void outsl(unsigned port,void *addr,unsigned long count);




2.I/O內存


前面我們說過在arm、powerPC中一般都取消了I/O空間的說法,一般都直接將外設掛在到內存
而我們首先應該做的工作就是將設備所處的物理地址映射到虛擬地址

將物理地址映射到虛擬地址
void *ioremap(unsigned long offset,unsigned long size)
函數介紹:會建立頁表

釋放:
void ionumap(void * addr);


在將物理地址與設備地址映射完成之後,就可以通過指針訪問這些地址
I/O內存讀寫操作:
(1).讀I/O內存
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
//上述函數版本較早,但在Linux2.6中仍然可以使用:
新版本:
unsigned readb(void *addr);
unsigned readw(void *addr);
unsigned readl(void *addr);


(2).寫I/O內存
void writeb(unsigned value,void *addr);
void readw(unsigned value,void *addr);
void readl(unsigned value,void *addr);


(3).讀一串I/O內存
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);


(4).寫一串I/O內存
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,const void *buf,unsigned long count);


(5).複製I/O內存
void memcpy_fromio(void *dset,void *source,unsigned int count);
void memcpy_toio(void *dest,void *source,unsigend int count);

(6).設置I/O內存
void memset_io(void *addr,u8 value,unsigned int count);

3.把I/O端口映射到內存空間(很簡便)
void *ioport_map(unsigned long port,unsigned int count);
//通過此函數可以把port開始的count個連續的I/O空間重映射爲一段“內存空間”
就可以在返回的地址上像訪問I/O內存一樣訪問這些I/O端口。當不需要時撤銷映射
void ioport_umap(void*addr);

小知識:對這個函數進行深層剖析後你可以發現,ioport_map函數中所謂的
映射其實是一個假象,並沒有映射到內核虛擬地址,僅僅是爲了讓工程師使用同一的I/O內存訪問
接口訪問I/O端口,即沿用第二種方法





申請和釋放設備I/O端口和內存
1.I/O端口申請
相關操作:
struct resource *request_region(unsigned long first,unsigned long n,const char*name);
函數介紹:向內核申請n個I/O端口,這些端口從first開始,name參數爲設備的名稱
返回值爲NULL表示失敗

將I/O端口歸還給系統
void release_region(unsigned long start,unsigned long n);


2.I/O內存的申請

struct resource *request_mem_region(unsigned long start,unsigned long len,char*name);
函數簡介:向內核申請n個內存地址,這些地址從first開始,name參數爲設備名稱
返回NULL表示分配失敗

釋放
void release_mem_region(unsigned long start,unsigned long len);
//這兩個函數不是必須的但是建議使用




設備I/O端口和I/O內存訪問流程

I/O端口的訪問:
最簡單的方法
request_region()+inb()/outb()+release_region()

具體如下圖所示

方法二:
request_region()+ioport_map()+ioread/iowrite+ioport_unmap()+release_region()




I/O內存的訪問:
將I/O端口映射到內存,對內存進行訪問
request_mem_region()+ioport_remap()+ioread/iowrite+ioport_uremap()+release_region()


具體見下圖:











將設備地址映射到用戶空間
1.內存映射與VMA
一般情況下用戶空間是不能也不應該直接訪問設備
但是我們可以通過在設備驅動程序中實現mmap()函數,這個函數可以使得用戶空間能直接訪問物理設備

mmap()函數的實質:將用戶空間的一段內存與設備內存關聯,當用戶訪問用戶空間的這段地址範圍時,實際上會轉化爲對應的設備的訪問。


注意:mmap()函數必須以頁爲單位進行映射


mmap()函數原型:
int (*mmap)(struct file *,struct vm_area_struct *);
驅動中的mmap()函數將在用戶空間進行mmap()系統調用時被調用


用戶空間的mmap()函數原型:
caddr_t mmap(caddr_t addr,size_t len,int prot,int flags,int fd,off_t offset);
參數介紹:
fd:文件描述符
len:是映射到用戶空間的字節數
prot指定訪問權限:PROT_READ PROT_WRITE PROT_EXEC PROT_NONE
caddr:指定文件應該被映射的起始地址,一般被指定爲NULL,由內核分配



用戶調用mmap()時所進行的工作
(1)在進程的虛擬地址空間查找一塊VMA
(2)將這塊VMA進行映射
(3)如果設備驅動程序或者文件系統的file_operations定義了mmap()操作則調用它
(4)將這個VMA插入到進程的VMA表中


小知識:驅動程序mmap()的實現機制也是建立頁表


VMA結構體:
虛擬地址的描述通過VMA結構體來實現
struct vm_area_struct
{
struct mm_struct *vm_mm;/*所處的地址空間*/
unsigned long vm_start;/*開始虛擬地址*/
unsigned long vm_end;/*結束虛擬地址*/


pgprot_t vm_page_prot;/*訪問權限*/
unsigned long vm_flags;/*標識,VM_READ,WM_WRITE,VM_EXEC,VM_SHARED*/
...
/*VMA的函數的指針*/
struct vm_operations_struct *vm_ops;


unsigned long vm_pgoff;/*偏移(頁幀號)*/
struct file *vm_file;
void *vm_private_data;
....

};

簡介:VMA結構體描述的虛擬地址位於vm_start ~vm_end之間


vm_operations_structk結構體
此結構體體描述了對VMA的相關操作
struct vm_operations_struct{
void(*open)(struct vm_area_struct *area);/*打開vma的操作*/
void (*close)(..)
struct page*(nopage)(...)/*訪問的頁不存在時調用*/
...
};



注意:當用戶進行系統調用mmap()後,內核不會調用VMA的open函數,
通常需要在驅動的mmap()函數中顯示調用vma->vm_ops->open()




/*vm_operations_struct操作範例*/
static int xxx_map(struct file*filp,struct vm_area_struct *vma)
{
   /*建立頁表*/
if(remap_pfn_range(vma,vma->start,vm->vm_pgoff,vm->vm_end-vma->start,vma->page_prot))/*建立頁表*/
return -EAGAIN;
vma->ops = &xxx_remap_vmops;
xxx_vma_open(vma);
return 0;
}


/*vma打開函數*/
void xxx_vm_open(struct vm_area_struct *vma)
{
...
printk(KERNEL "xxx VMA open,virt %1x,phys %1x\n",vma->vm_start,
vma->vm_pgoff<<PAGE_SHIFT);
}

/*vma關閉函數*/
void xxx_vma_close(struct vm_area_struct *vma)
{
...
printk(KERN_NOTICE "xxx VMA close.\n");
}


static struct vm_operations_struct xxx_remap_vm_ops = {
/*VMA操作結構體*/
.open =  xxx_vm_open,
.close = xxx_vma_close,
...
};


簡介:remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,
unsigned long pfn,unsigned long size,pgprot_t prot);
作用:創建頁表,映射的虛擬地址訪問爲vma->cm_start~vma->cm_end
參數介紹:addr:表示內存映射開始處的虛擬地址
pfn:是虛擬地址應該映射到的物理地址的頁幀號

小知識:何爲頁幀號
內核地址無論是虛擬的還是物理的,都是由兩部分構成,往往是高N位爲頁號,低M位爲頁內偏移量。當我們將地址中的低M位
偏移量拋棄時,高N位移動到右端得到這個結果稱爲頁幀號,宏PAGE_SHIFT告訴我們要右移多少位才能得到頁幀號


prot:是新頁的保護屬性

/*映射kmalloc申請的內存到用戶空間*/
/*內核模塊加載函數*/
int __init kmalloc_map_init(void)
{
...
/*申請設備號
添加cdev結構體*/
buffer = kmalloc(BUFSIZE,GFP_KERNEL);//申請buffer

/*virt_to_page,獲取對應的虛擬頁*/
for(page = virt_to_page(buffer);page<vir_to_page(buffer+BUFSIZE);page++)
mem_map_reverse(page);/*設置爲保留頁*/
}

/*mmap()函數*/
static int kmalloc_map_mmap(struct file*filp,struct vm_area_struct *vma)
{
unsigned long page,pos;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size  = (unsigned long)(vma->vm_end-vma->vm_start);
printk(KERNEL_INFO "mmaptest_mmap called\n");

/*用戶要映射的區域太大*/
if(size>BUFSIZE)
return -EINVAL;

pos = (unsigned long)buffer;
/*映射buffer中的所有頁*/
while(size > 0){
/*每次映射一頁*/
page = virt_to_phys((void *)pos);//先將在內核中用malloc分配的空間轉換爲對應的物理頁地址
if(remap_page_range(start,page,PAGE_SIZE,PAGE_SHARED));/*將物理頁地址映射到vma,並且每次只映射一頁*/
return - EAGAIN;
start += PAGE_SIZE;
pos +=PAGE_SIZE;
size -=PAGE_SIZE;
}return 0;
}



注意:通常I/O內存被映射時需要nocahe的,這個時候需要對vma_page_prot設置nocache標識之後再進行映射
(暫略)





2.nopage()函數
簡介:除了remap_pfn_range函數以外,在驅動程序中實現VMA的nopage()函數
可以爲設備提供,更加靈活的映射途徑,當訪問的頁不存在(發生缺頁異常)時,
nopage()會被內核自動調用


當發生缺頁異常時系統做出的響應:
(1).找到缺頁的虛擬地址所在的VMA
(2).如果不要,分配中間頁目錄表和頁表
(3).如果也表項不存在,調用VMA的nopage()方法,返回物理頁面的描述符
(4).將物理頁面的地址填充到頁表中







I/O內存的靜態映射
簡介:假如我們已經做好目標電路板,而要將Linux移植到目標電路板,此時通常
會建立外I/O內存物理地址到虛擬地址的靜態映射,
這裏你只需要理解何爲靜態映射就可以了。具體用到的時候可以再回來研究,
不妨礙我們鄉下學習







DMA(重點)
簡介:
DMA:是一種無序CPU幫助就可以讓外設與系統之間進行雙向數據傳輸的硬件機制
簡單點說就是這個樣子
外設<---------->內存
而不是傳統的
外設<----cpu----->內存


DMA與Cache的一致性問題:
假設DMA針對內存的目的地址與Cache緩存的對象有重疊區域,那麼經過DMA操作
後,Cache緩存對應的內存的數據就會被修改,而CPU卻並不知道,它仍然會認爲Cache
中的數據就是內存中的數據,此時會產生Cache與內存之間的數據"不一致"錯誤

具體如下圖所示:




解決方法:
禁止DMA目標地址範圍內內存的cache功能







Linux下的DMA編程
知識儲備:
內存中用於與外設交互數據的一塊區域被稱爲DMA緩衝區,一般情況下DMA
在物理上連續的


1.DMA ZONE
對於X86系統的ISA設備而言,DMA操作只能在16MB一下的內存中使用,因此在用kmalloc()
和__get_free_pages()及類似的函數申請DMA緩衝區時應使用GFP_DMA標誌,這樣
獲得的DMA ZONE是具備DMA能力的

Linux內核已經把此操作爲我們封裝好了
__get_dma_page()//它在申請時已經添加了GFP_DMA標誌
#define __get_dma_pages(gfp_mask,order)\
__get_free_pages((gfp_mask)|GFP_DMA,(order))

上述函數是以2^order爲大小分配的


也可以使用下面這個函數
static unsigend long dma_mem_alloc(int size);


注意:上數只是針對X86,對於大多數嵌入式設備而言,DMA操作可以在整個常規內存區域進行




2.虛擬地址、物理地址和總線地址
總線地址:是從設備的角度上看到的內存地址
物理地址:是從CPU MMU控制器外圍角度上看到的內存地址


具體如下圖所示:




Linux內核提供如下函數用於簡單的虛擬地址/總線地址的轉換
unsigned long virt_to_bus(volate void *address);
void *bus_to_virt(unsigned long address);




3.DMA地址掩碼
設備不一定在所有的內存地址上執行DMA操作,此時應該通過下列函數執行DMA地址掩碼
int dma_set_mask(struct device*dev,u64 mask);


例如:對於只能在24位地址上執行DMA操作的設備,就應該使用如下方法
dma_set_mask(dev,0xffffff)


4.一致性DMA緩衝區
DMA緩衝區包括兩個方面的工作:
1.分配一篇DMA緩衝區(爲這篇緩衝區產生設備可以訪問的地址)
2.DMA映射必須考慮Cache一致性問題



內核提供如下函數用於分配就一個DMA一致性的內存區域
void *ama_alloc_coherent(struct device *dev,size_t size,dma_addr_t handle,gfp_t gfp)
返回值:爲申請到的DMA緩衝區地址




/*DMA其他操作暫略,這裏暫且之明白DMA的含義等到工程中具體
用到的時候再回來纖細學習*/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章