裝置IO之一(mmap、直接IO以及非同步IO)
阿新 • • 發佈:2018-11-05
現在,在linux中經常可以看到在使用者空間編寫的驅動程式,比如X伺服器,一些廠商的私有驅動等等,這就意味著使用者空間取得了對硬體的訪問能力,這通常是通過mmap將裝置記憶體對映到了使用者程序空間,從而使得使用者可以通過讀寫這些記憶體來獲取對硬體的訪問能力。
核心一般會對I/O操作進行緩衝以獲取更好的效能,但是也提供了直接I/O以及非同步I/O的能力。
在和硬體進行資料互動時,有的硬體支援DMA,DMA可以降低處理器負擔;有的硬體的記憶體空間無法直接讀寫,需要使用特殊的指令。
可以通過檢視/proc/${pid}/maps來檢視程序的記憶體區域,其格式為:
start-end perm offset major:minor inode image
下邊是一個init程序的maps片段:
00000000-00000000 r-xp 00000000 01:00 149505 /sbin/init.sysvinit
00000000-00000000 rw-p 00000000 01:00 149505 /sbin/init.sysvinit
00000000-00000000 ---p 00000000 00:00 0
00000000-00000000 rw-p 00000000 00:00 0 [heap]
00000000-00000000 rw-p 00000000 00:00 0
00000000-00000000 rw-p 00000000 00:00 0 [stack]
其中各部分的含義如下:
當一個程序試圖存取一個合法的VMA頁,但是該頁當前不在記憶體中時,則核心會為該VMA呼叫它的nopage函式。該函式返回指向物理頁的page指標。如果該VMA沒有定義自己的nopage介面,則核心會為它分配一個空頁。
並不是所有的硬體都支援mmap,比如串列埠裝置就不支援mmap。mmap存在一個限制,就是它對映的粒度為PAGE_SIZE,因而核心只能在頁表一級對虛擬記憶體地址進行管理,因而使用mmap將裝置記憶體對映到使用者程序的虛擬記憶體空間時必須以頁為單位,並且核心被對映的實體地址也必須起始於PAGE_SIZE的整數倍,即被對映的實體地址的起始地址必須對齊到PAGE_SIZE上。
大多數PCI外設將其控制暫存器對映到了記憶體地址中,對於這類裝置只需要通過將這種記憶體對映到使用者空間就可以獲得對硬體的控制能力,相對於通過常規的ioctl方法,這是非常誘人的。
mmap是file_operations結構的一部分。由於在*nix中,一切皆檔案,因而核心部件很容易藉助該結構來實現自己的mmap。
使用者空間程式通過系統呼叫:
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
來呼叫fd上的mmap函式。當使用mmap系統呼叫時,核心會在呼叫fd上的mmap之前做一些準備工作。fd上的mmap函式在核心中的原型如下:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
filp為對映檔案,vma包含了訪問裝置的虛擬地址的資訊。fd上的mmap需要完成的是為vma包含的虛擬地址範圍建立合適的頁表,並且初始化vma中的函式指標,以便後續可以使用適當的函式。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
它將從pfn開始的長度為size大小(size會向上對齊到頁PAGE_SIZE)的地址空間對映到vma中從addr開始的位置。因為size可變,因而它可以用於對映整個區域,也可以用於僅對映其中的一部分。
phys_addr_t phys_addr, pgprot_t prot);
它將從phys_addr開始的大小為(end-addr+1並且向上對齊到PAGE_SIZE)的I/O記憶體對映到從addr開始的虛擬地址。
如果nopage的引數type不為NULL,則它可用於返回錯誤不同於返回值所返回的,一般為VM_FAULT_MINOR。由於nopage需要返回指向所獲得記憶體的page指標,但是PCI的儲存空間是沒有page指標的,因而nopage方法不適用於PCI地址空間。
當呼叫成功時nopage地返回一個指向 struct page 的指標。否則nopage將返回一個錯誤。如果 nopage 函式為NULL,則負責處理頁錯誤的核心程式碼將零記憶體頁對映到失效的虛擬地址上。零記憶體頁是一個特殊的頁,讀它將返回0,寫它將修改程序的私有拷貝。
雖然無法使用remap_pfn_range來對映RAM到使用者空間,但是有變通的方法,可以使用VMA的nopage方法來將RAM對映到使用者地址空間,也就是一次一頁的向用戶空間對映記憶體。如果一個核心部件想要將RAM地址對映到使用者地址空間,就要實現nopage函式介面,並且在該函式中一次一頁的返回取到的頁。
unsigned long start, unsigned long nr_pages, int write,
int force, struct page **pages, struct vm_area_struct **vmas)
該函式將使用者程序的頁對映到核心的地址空間,然後核心中的程式碼就可以直接訪問這些頁了。其引數的含義:
核心一般會對I/O操作進行緩衝以獲取更好的效能,但是也提供了直接I/O以及非同步I/O的能力。
在和硬體進行資料互動時,有的硬體支援DMA,DMA可以降低處理器負擔;有的硬體的記憶體空間無法直接讀寫,需要使用特殊的指令。
一、虛擬記憶體區
當使用mmap時,需要將核心的地址塊對映到使用者的地址空間,這就涉及到一個很關鍵的資料結構VMA,一個VMA表示在程序的虛擬地址空間中的同一類區域:擁有同樣的許可權並且被同樣的物件(一個檔案或者交換空間)所備份的一個連續的虛擬地址範圍。可以通過檢視/proc/${pid}/maps來檢視程序的記憶體區域,其格式為:
start-end perm offset major:minor inode image
下邊是一個init程序的maps片段:
00000000-00000000 r-xp 00000000 01:00 149505 /sbin/init.sysvinit
00000000-00000000 rw-p 00000000 01:00 149505 /sbin/init.sysvinit
00000000-00000000 ---p 00000000 00:00 0
00000000-00000000 rw-p 00000000 00:00 0 [heap]
00000000-00000000 rw-p 00000000 00:00 0
00000000-00000000 rw-p 00000000 00:00 0 [stack]
其中各部分的含義如下:
- start end:該片記憶體區的開始和結束虛擬地址.
- perm:記憶體區的讀,寫和執行許可的位掩碼,perm的最後一個字元要麼是p表示是私有的,要麼是s表示是共享的。
- offset:記憶體區在對映檔案中的偏移量(記憶體區是被對映到了一個檔案中)
- major minor:擁有對映檔案的裝置的主次編號。對裝置對映來說,主次裝置號指的是磁碟中代表該裝置的磁碟檔案的主次裝置號,而不是核心分配給該真實裝置的主次裝置號。
- inode:對映檔案的inode 號.
- image:對映檔名
1.1 open
其原型為:void (*open)(struct vm_area_struct *vma)
核心會在產生對一個VMA的新的引用時,呼叫它,以使得實現該VMA的核心部件有機會做自己的初始化。不過在建立新的VMA時不會呼叫它,而是會呼叫核心部件提供的mmap函式。
1.2 close
其原型為:void (*close)(struct vm_area_struct *vma)
記憶體區被銷燬時被呼叫,VMA沒有引用計數,因而一個程序只能開啟和關閉一個VMA區域一次。1.3 nopage
其原型為:struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
二、mmap
mmap使得可以將裝置記憶體對映到使用者空間,從而使得使用者程式獲得訪問硬體的能力,mmap的動作需要由核心中的驅動來實現。在使用mmap對映後,使用者程式對給定範圍的記憶體的讀寫就變成了對裝置記憶體的讀寫,也就是在訪問裝置了。並不是所有的硬體都支援mmap,比如串列埠裝置就不支援mmap。mmap存在一個限制,就是它對映的粒度為PAGE_SIZE,因而核心只能在頁表一級對虛擬記憶體地址進行管理,因而使用mmap將裝置記憶體對映到使用者程序的虛擬記憶體空間時必須以頁為單位,並且核心被對映的實體地址也必須起始於PAGE_SIZE的整數倍,即被對映的實體地址的起始地址必須對齊到PAGE_SIZE上。
大多數PCI外設將其控制暫存器對映到了記憶體地址中,對於這類裝置只需要通過將這種記憶體對映到使用者空間就可以獲得對硬體的控制能力,相對於通過常規的ioctl方法,這是非常誘人的。
mmap是file_operations結構的一部分。由於在*nix中,一切皆檔案,因而核心部件很容易藉助該結構來實現自己的mmap。
使用者空間程式通過系統呼叫:
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
來呼叫fd上的mmap函式。當使用mmap系統呼叫時,核心會在呼叫fd上的mmap之前做一些準備工作。fd上的mmap函式在核心中的原型如下:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
filp為對映檔案,vma包含了訪問裝置的虛擬地址的資訊。fd上的mmap需要完成的是為vma包含的虛擬地址範圍建立合適的頁表,並且初始化vma中的函式指標,以便後續可以使用適當的函式。
2.1 建立頁表
建立頁表是mmap需要完成的最重要的工作。有兩種方法可以用來建立頁表:- 呼叫remap_pfn_range 函式一次全部建成
- 通過nopage函式一次一頁的建立
2.1.1 使用 remap_pfn_range
remap_pfn_range 和 io_remap_page_range負責為一段實體地址建立新的頁表,它們的原型如下:int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
它將從pfn開始的長度為size大小(size會向上對齊到頁PAGE_SIZE)的地址空間對映到vma中從addr開始的位置。因為size可變,因而它可以用於對映整個區域,也可以用於僅對映其中的一部分。
- vma: 實體地址要對映到的使用者VMA
- addr: 實體地址對映到使用者VMA地址空間時的使用者空間起始地址(通常為vam->start),但是也可以不為vam->start。
- pfn: 對映的核心實體地址
- size: 區域大小
- prot: 該對映中的頁面的保護模式
phys_addr_t phys_addr, pgprot_t prot);
它將從phys_addr開始的大小為(end-addr+1並且向上對齊到PAGE_SIZE)的I/O記憶體對映到從addr開始的虛擬地址。
- addr:虛擬地址起始值
- end:虛擬地址結束值
- phys_addr:實體地址起始值
- prot:該區域的保護模式
2.1.2 使用 nopage 對映記憶體
一次性建立好整個頁表在大多數情況下是不錯的選擇,但是在有的情況下時候nopage更合適。因為它更靈活。使用nopage的兩種典型場景如下:- 應用程式呼叫mremap系統呼叫改變對映區域。當這個呼叫導致VMA區域變小時,核心不會通知驅動,而是會將不必要的頁重新整理掉;但是當這個呼叫導致VMA變大時,核心就會呼叫nopage方法來申請新頁。因此從這個意義上來說如果要支援該系統呼叫,就必須實現nopage方法。
- 當用戶訪問VMA中的頁,但是該頁又不在記憶體中時,nopage函式會被呼叫。
如果nopage的引數type不為NULL,則它可用於返回錯誤不同於返回值所返回的,一般為VM_FAULT_MINOR。由於nopage需要返回指向所獲得記憶體的page指標,但是PCI的儲存空間是沒有page指標的,因而nopage方法不適用於PCI地址空間。
當呼叫成功時nopage地返回一個指向 struct page 的指標。否則nopage將返回一個錯誤。如果 nopage 函式為NULL,則負責處理頁錯誤的核心程式碼將零記憶體頁對映到失效的虛擬地址上。零記憶體頁是一個特殊的頁,讀它將返回0,寫它將修改程序的私有拷貝。
2.2 新增VMA 的操作
mmap的另一個重要的動作是更新VMA的函式指標。也就是nopage,open,close等函式指標。2.3 重新對映 RAM
remap_pfn_range只能用於保留頁以及在實體記憶體頂之上的實體地址,實際上就是不被記憶體管理系統管理的記憶體。也就是說常規的記憶體是不能用它來對映的,包括用__get_free_page獲得的記憶體。因此如果想用用它來對映一片記憶體,就要在系統啟動時將這部分記憶體給預留出來(因為經過remap_pfn_range對映後,程序就可以發起對它的直接讀寫,而由內核心記憶體管理系統管理的記憶體可能會被分配做其它用途,這就存在潛在的衝突)。雖然無法使用remap_pfn_range來對映RAM到使用者空間,但是有變通的方法,可以使用VMA的nopage方法來將RAM對映到使用者地址空間,也就是一次一頁的向用戶空間對映記憶體。如果一個核心部件想要將RAM地址對映到使用者地址空間,就要實現nopage函式介面,並且在該函式中一次一頁的返回取到的頁。
需要注意的是使用nopage函式來返回page時,需要的是真正的page,因此需要找到真正的page指標,對於常規核心記憶體可以通過virt_to_page來獲取其page,但是對於vmalloc返回的地址,則要通過vmalloc_to_page來獲取page。
三、直接I/O
大部分I/O操作都要經過核心緩衝,這是為了提高I/O的效率,但是有的場景中緩衝並不一定能得到很好的效能。因此核心也為不想使用緩衝的場景提供了API,如果一個外設的驅動不想使用核心的緩衝機制,可以使用如下API: long get_user_pages(struct task_struct *tsk, struct mm_struct *mm,unsigned long start, unsigned long nr_pages, int write,
int force, struct page **pages, struct vm_area_struct **vmas)
該函式將使用者程序的頁對映到核心的地址空間,然後核心中的程式碼就可以直接訪問這些頁了。其引數的含義:
- tsk:一個指向進行 I/O 的任務的指標,作用在於告知核心誰該為頁錯誤負責,如果不需要記錄則可以設定為NULL
- mm:一個記憶體管理結構的指標,描述被對映的地址空間
- start:使用者空間起始地址
- nr_pages頁數
- write:呼叫者是否要往這部分頁中寫入資料whether pages will be written to by the caller
- force:如果設定了它,則即便使用的是隻讀的使用者對映程序對映區,也會強制進行寫入,通常這不是所想要的效果
- pages:指向獲得的page的指標陣列,該陣列大小應該至少為nr_pages,或者如果不想獲得這些資訊就設定為NULL。
- vmas:指向與每個page相對應的vma區域的指標陣列。如果呼叫者不想要這些資訊,則可以為NULL。
四、非同步I/O(AIO)
除了直接I/O外,核心還提供了另外一種I/O特性,非同步I/O。非同步 I/O 允許使用者程式來發起一個或多個I/O操作而不必等待操作的完成,核心提供了一套API來支援使用者程式發起AIO。4.1 使用者介面
核心提供給使用者空間的API及其介面如下:- io_setup:為當前程序建立一個非同步I/O上下文,它有一個引數可以指定該上下文最多可以提交多少個非同步I/O。
- io_submit:提交一個或多個非同步I/O請求
- io_getevents:獲取已經提交的非同步I/O請求的完成狀態
- io_cancel:取消提交的非同步I/O請求
- io_destroy:清除為本程序建立的非同步I/O上下文