1. 程式人生 > >裝置I/O埠和I/O記憶體的訪問

裝置I/O埠和I/O記憶體的訪問

裝置通常會提供一組暫存器來控制裝置、讀寫裝置和獲取裝置狀態,即控制暫存器、資料暫存器和狀態暫存器。

這些寄器可能位於I/O空間中,也可能位於記憶體空間中。當位於I/O空間時,通常被稱為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 CPU的I/O空間就只有64KB(0-0xffff)。

1 Linux I/O埠和I/O記憶體訪問介面

1.I/O埠

在Linux裝置驅動中,應使用Linux核心提供的函式來訪問
定位於I/O空間的埠,這些函式包括如下幾種。
1)讀寫位元組埠(8位寬)
 

unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);

2)讀寫字埠(16位寬)
 

unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);

3)讀寫長字埠(32位寬)
 

unsigned inl(unsigned port);
void outl(unsigned longword, 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指向的記憶體;

outsb()將addr指向的記憶體中的count個位元組連續寫入以port開始的埠。

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

上述各函式中I/O埠號port的型別高度依賴於具體的硬體平臺,因此,這裡只是寫出了unsigned。


2.I/O記憶體

在核心中訪問I/O記憶體(通常是晶片內部的各個I2C、SPI、 USB等控制器的暫存器或者外部記憶體總線上的裝置)之前,需首先使用ioremap()函式將裝置所處的實體地址對映到虛擬地址上。 ioremap()的原型如下:
 

void *ioremap(unsigned long offset, unsigned long size);

ioremap()與vmalloc()類似,也需要建立新的頁表,但是它並不進行vmalloc()中所執行的記憶體分配行為。
ioremap()返回一個特殊的虛擬地址,該地址可用來存取特定的實體地址範圍,這個虛擬地址位於vmalloc對映區域。

通過ioremap()獲得的虛擬地址應該被iounmap()函式釋放,其原型如下:
 

void iounmap(void * addr);

ioremap()有個變體是devm_ioremap(),類似於其他以devm_開頭的函式,通過devm_ioremap()進行的對映通常不需要在驅動退出和出錯處理的時候進行iounmap()。devm_ioremap()的原型為:
 

void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
                            unsigned long size);

在裝置的實體地址(一般都是暫存器)被對映到虛擬地址之後,儘管可以直接通過指標訪問這些地址,但是Linux核心推薦用一組標準的API來完成裝置記憶體對映的虛擬地址的讀寫。

讀暫存器用readb_relaxed()、 readw_relaxed()、readl_relaxed()、 readb()、 readw()、 readl()這一組API

以分別讀8bit、 16bit、 32bit的暫存器,沒有_relaxed字尾的版本與有_relaxed字尾的版本的區別是沒有_relaxed字尾的版本包含一個記憶體屏障,如:
 

#define readb(c)		({ u8  __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c)		({ u16 __v = readw_relaxed(c); __iormb(); __v; })
#define readl(c)		({ u32 __v = readl_relaxed(c); __iormb(); __v; })

寫暫存器用writeb_relaxed()、 writew_relaxed()、writel_relaxed()、 writeb()、 writew()、 writel()這一組API,

以分別寫8bit、 16bit、 32bit的暫存器,沒有_relaxed字尾的版本與有_relaxed字尾的版本的區別是前者包含一個記憶體屏障,如:
 

#define writeb(v,c)		({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c)		({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c)		({ __iowmb(); writel_relaxed(v,c); })

 

2 申請與釋放裝置的I/O埠和I/O記憶體

1.I/O埠申請

Linux核心提供了一組函式以申請和釋放I/O埠,表明該驅動要訪問這片區域
 

#define request_region(start,n,name)		__request_region(&ioport_resource, (start), (n), (name), 0)
struct resource * __request_region(struct resource *parent,
				   resource_size_t start, resource_size_t n,
				   const char *name, int flags)

這個函式向核心申請n個埠,這些埠從first開始,name引數為裝置的名稱。如果分配成功,則返回值不是NULL,如果返回NULL,則意味著申請埠失敗。當用request_region()申請的I/O埠使用完成後,應當使用release_region()函式將它們歸還給系統,這個函式的原型如下

#define release_region(start,n)	__release_region(&ioport_resource, (start), (n))
void __release_region(struct resource *parent, resource_size_t start,
			resource_size_t n)

2.I/O記憶體申請(也就是得到使用權)

同樣, Linux核心也提供了一組函式以申請和釋放I/O記憶體的範圍。此處的“申請”表明該驅動要訪問這片區域,它不會做任何記憶體對映的動作,更多的是類似於“reservation”的概念。

#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
struct resource * __request_region(struct resource *parent,
				   resource_size_t start, resource_size_t n,
				   const char *name, int flags)

這個函式向核心申請n個記憶體地址,這些地址從first開始,name引數為裝置的名稱。如果分配成功,則返回值不是NULL,如果返回NULL,則意味著申請I/O記憶體失敗。當用request_mem_region()申請的I/O記憶體使用完成後,應當使用release_mem_region()函式將它們歸還給系統,這個函式的原型如下:

#define release_mem_region(start,n)	__release_region(&iomem_resource, (start), (n))
void __release_region(struct resource *parent, resource_size_t start,
			resource_size_t n)

request_region()和request_mem_region()也分別有變體,其為devm_request_region()和devm_request_mem_region()。
 

3 裝置I/O埠和I/O記憶體訪問流程

歸納出裝置驅動訪問I/O埠和I/O記憶體的步驟。I/O埠訪問的一種途徑是直接使用I/O埠操作函式:在
裝置開啟或驅動模組被載入時申請I/O埠區域,之後使用inb()、 outb()等進行埠訪問,最後,在裝置關閉或驅動被解除安裝時釋放I/O埠範圍。整個流程如圖所示

I/O記憶體的訪問步驟如圖11.11所示,首先是呼叫request_mem_region()申請資源,接著將暫存器地址通過
ioremap()對映到核心空間虛擬地址,之後就可以通過Linux裝置訪問程式設計介面訪問這些裝置的暫存器了。訪問完成後,應對ioremap()申請的虛擬地址進行釋放,並釋放release_mem_region()申請的I/O記憶體資源。

有時候,驅動在訪問暫存器或I/O埠前,會省去request_mem_region()、 request_region()這樣的呼叫。
 

4 將裝置地址對映到使用者空間

1.記憶體對映與VMA

一般情況下,使用者空間是不可能也不應該直接訪問裝置
的,但是,裝置驅動程式中可實現mmap()函式,這個函式
可使得使用者空間能直接訪問裝置的實體地址。實際上,
mmap()實現了這樣的一個對映過程:它將使用者空間的一段
記憶體與裝置記憶體關聯,當用戶訪問使用者空間的這段地址範圍
時,實際上會轉化為對裝置的訪問。
這種能力對於顯示介面卡一類的裝置非常有意義,如果用
戶空間可直接通過記憶體對映訪問視訊記憶體的話,螢幕幀的各點畫素
將不再需要一個從使用者空間到核心空間的複製的過程。
mmap()必須以PAGE_SIZE為單位進行對映,實際上,
記憶體只能以頁為單位進行對映,若要對映非PAGE_SIZE整數
倍的地址範圍,要先進行頁對齊,強行以PAGE_SIZE的倍數
大小進行對映。
從file_operations檔案操作結構體可以看出,驅動中
mmap()函式的原型如下:
 

int(*mmap)(struct file *, struct vm_area_struct*);

驅動中的mmap()函式將在使用者進行mmap()系統呼叫
時最終被呼叫, mmap()系統呼叫的原型與file_operations中
mmap()的原型區別很大,如下所示:

void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);


引數fd為檔案描述符,一般由open()返回, fd也可以指
定為-1,此時需指定flags引數中的MAP_ANON,表明進行的
是匿名對映。
len是對映到呼叫使用者空間的位元組數,它從被對映檔案開
頭offset個位元組開始算起, offset引數一般設為0,表示從檔案頭
開始對映。
prot引數指定訪問許可權,可取如下幾個值的“或”:
PROT_READ(可讀)、 PROT_WRITE(可寫)、
PROT_EXEC(可執行)和PROT_NONE(不可訪問)。
引數addr指定檔案應被對映到使用者空間的起始地址,一般
被指定為NULL,這樣,選擇起始地址的任務將由核心完成,
而函式的返回值就是對映到使用者空間的地址。其型別caddr_t實
際上就是void*。
當用戶呼叫mmap()的時候,核心會進行如下處理。
1)在程序的虛擬空間查詢一塊VMA。
2)將這塊VMA進行對映。
3)如果裝置驅動程式或者檔案系統的file_operations定義
了mmap()操作,則呼叫它。
4)將這個VMA插入程序的VMA連結串列中。
file_operations中mmap()函式的第一個引數就是步驟1)
找到的VMA。
由mmap()系統呼叫對映的記憶體可由munmap()解除映
射,這個函式的原型如下:
 

int munmap(caddr_t addr, size_t len );

驅動程式中mmap()的實現機制是建立頁表,並填充
VMA結構體中vm_operations_struct指標。 VMA就是
vm_area_struct,用於描述一個虛擬記憶體區域, VMA結構體的
定義如程式碼清單11.4所示。
 

struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */
	
	unsigned long vm_start; /* Our start address within vm_mm. */
	unsigned long vm_end; /* The first byte after our end address within vm_mm. */
	
	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;
	
	struct rb_node vm_rb;
	
	/* Second cache line starts here. */
	
	struct mm_struct *vm_mm; /* The address space we belong to.*/
	pgprot_t vm_page_prot; /* Access permissions of this VMA.*/
	unsigned long vm_flags; /* Flags, see mm.h. */
	
	
	const struct vm_operations_struct *vm_ops;
}

VMA結構體描述的虛地址介於vm_start和vm_end之間,而
其vm_ops成員指向這個VMA的操作集。針對VMA的操作都被
包含在vm_operations_struct結構體中, vm_operations_struct結
構體的定義如程式碼清單11.5所示。
 

struct vm_operations_struct {
	void (*open)(struct vm_area_struct * area);
	void (*close)(struct vm_area_struct * area);
	int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

	int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

	int (*access)(struct vm_area_struct *vma, unsigned long addr,
		      void *buf, int len, int write);
};

整個vm_operations_struct結構體的實體會在file_operations
的mmap()成員函式裡被賦值給相應的vma->vm_ops,而上
述open()函式也通常在mmap()裡呼叫, close()函式會
在使用者呼叫munmap()的時候被呼叫到。程式碼清單11.6給出
了一個vm_operations_struct的操作範例。
 

vm_operations_struct操作範例
 

static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)
{
	if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end -
	vma->vm_start, vma->vm_page_prot))/* 建立頁表 */
	return -EAGAIN;
	vma->vm_ops = &xxx_remap_vm_ops;
	xxx_vma_open(vma);
	return 0;
}

static void xxx_vma_open(struct vm_area_struct *vma)/* VMA開啟函式*/
{
	//...
	printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma->vm_sta
	vma->vm_pgoff << PAGE_SHIFT);
}

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

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

第3行呼叫的remap_pfn_range()建立頁表項,以VMA結
構體的成員(VMA的資料成員是核心根據使用者的請求自己填
充的)作為remap_pfn_range()的引數,對映的虛擬地址範圍
是vma->vm_start至vma->vm_end。
remap_pfn_range()函式的原型如下
 

int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
		    unsigned long pfn, unsigned long size, pgprot_t prot)

其中的addr引數表示記憶體對映開始處的虛擬地址。
remap_pfn_range()函式為addr~addr+size的虛擬地址構造頁
表。
pfn是虛擬地址應該對映到的實體地址的頁幀號,實際上
就是實體地址右移PAGE_SHIFT位。若PAGE_SIZE為4KB,則
PAGE_SHIFT為12,因為PAGE_SIZE等於1<<PAGE_SHIFT。
prot是新頁所要求的保護屬性。
在驅動程式中,我們能使用remap_pfn_range()對映記憶體
中的保留頁、裝置I/O、 framebuffer、 camera等記憶體。在
remap_pfn_range()上又可以進一步封裝出
io_remap_pfn_range()、 vm_iomap_memory()等API。
程式碼清單11.7給出了LCD驅動對映framebuffer實體地址到
使用者空間的典型範例,程式碼取自
drivers/video/fbdev/core/fbmem.c。
 

程式碼清單11.7 LCD驅動對映framebuffer的mmap
 

static int fb_mmap(struct file *file, struct vm_area_struct * vma)
{
	struct fb_info *info = file_fb_info(file);
	struct fb_ops *fb;
	unsigned long mmio_pgoff;
	unsigned long start;
	u32len;
	
	if (!info)
	return -ENODEV;
	fb = info->fbops;
	if (!fb)
	return -ENODEV;
	mutex_lock(&info->mm_lock);
	if (fb->fb_mmap) {
		int res;
		res = fb->fb_mmap(info, vma);
		mutex_unlock(&info->mm_lock);
		return res;
	}
	
	/*
	* Ugh. This can be either the frame buffer mapping, or
	* if pgoff points past it, the mmio mapping.
	*/
	start = info->fix.smem_start;
	len = info->fix.smem_len;
	mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
	if (vma->vm_pgoff >= mmio_pgoff) {
		if (info->var.accel_flags) {
			mutex_unlock(&info->mm_lock);
			return -EINVAL;
		}
	
		vma->vm_pgoff -= mmio_pgoff;
		start = info->fix.mmio_start;
		len = info->fix.mmio_len;
	}
	mutex_unlock(&info->mm_lock);
	
	vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
	fb_pgprotect(file, vma, start);
	
	return vm_iomap_memory(vma, start, len);
}

通常, I/O記憶體被對映時需要是nocache的,這時候,我們
應該對vma->vm_page_prot設定nocache標誌之後再對映,如代
碼清單11.8所示。
程式碼清單11.8 以nocache方式將核心空間對映到使用者空間
 

static int xxx_nocache_mmap(struct file *filp, struct vm_area_struct *
{
	vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);/*賦nocache標誌*/
	vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);
	/* 對映 */
	if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end -
	vma->vm_start, vma->vm_page_prot))
		return -EAGAIN;
	return 0;
}

上述程式碼第3行的pgprot_noncached()是一個巨集,它高度
依賴於CPU的體系結構, ARM的pgprot_noncached()定義如
下:

#define pgprot_noncached(prot) \
	__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_UNCACHED)

另一個比pgprot_noncached()稍微少一些限制的巨集是
pgprot_writecombine(),它的定義如下:

#define pgprot_writecombine(prot) \
	__pgprot_modify(prot, L_PTE_MT_MASK, L_PTE_MT_BUFFERABLE)

pgprot_noncached()實際禁止了相關頁的Cache和寫緩衝
(Write Buffer), pgprot_writecombine()則沒有禁止寫緩
衝。 ARM的寫緩衝器是一個非常小的FIFO儲存器,位於處理
器核與主存之間,其目的在於將處理器核和Cache從較慢的主
存寫操作中解脫出來。寫緩衝區與Cache在儲存層次上處於同
一層次,但是它只作用於寫主存。

2.fault()函式
除了remap_pfn_range()以外,在驅動程式中實現VMA
的fault()函式通常可以為裝置提供更加靈活的記憶體對映途
徑。當訪問的頁不在記憶體裡,即發生缺頁異常時, fault()會
被核心自動呼叫,而fault()的具體行為可以自定義。這是因
為當發生缺頁異常時,系統會經過如下處理過程。
1)找到缺頁的虛擬地址所在的VMA。
2)如果必要,分配中間頁目錄表和頁表。
3)如果頁表項對應的物理頁面不存在,則呼叫這個VMA
的fault()方法,它返回物理頁面的頁描迏符。
4)將物理頁面的地址填充到頁表中。
fault()函式在Linux的早期版本中命名為nopage(),後
來變更為了fault()。程式碼清單11.9給出了一個裝置驅動中使
用fault()的典型範例
程式碼清單11.9 fault()函式使用範例
 

static int xxx_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
{
	unsigned long paddr;
	unsigned long pfn;
	pgoff_t index = vmf->pgoff;
	struct vma_data *vdata = vma->vm_private_data;
	
	//...
	
	pfn = paddr >> PAGE_SHIFT;
	
	vm_insert_pfn(vma, (unsigned long)vmf->virtual_address, pfn);
	
	return VM_FAULT_NOPAGE;
}

大多數裝置驅動都不需要提供裝置記憶體到使用者空間的對映能力,因為,對於串列埠等面向流的裝置而言,實現這種對映毫無意義。而對於顯示、視訊等裝置,建立對映可減少使用者空間和核心空間之間的記憶體複製。