linux驅動程式執行在什麼空間

2022-11-10 22:01:34

linux驅動程式執行在「核心」空間。一般情況下驅動程式中都是呼叫kmalloc()來給資料結構分配記憶體,呼叫vmalloc()為活動的交換區分配資料結構,為某些I/O驅動程式分配緩衝區,或為模組分配空間;kmalloc和vmalloc分配的是核心的記憶體。

程式設計師必備介面測試偵錯工具:

本教學操作環境:linux7.3系統、Dell G3電腦。

linux驅動程式執行在「核心」空間。

對於一般編寫的微控制器程式來說應用程式和驅動程式往往是雜糅的,擁有一定能力水平的微控制器程式程式設計人員可以實現應用和驅動的分層。而在Linux系統中已經強制將應用和驅動進行了分層。

在微控制器程式中,應用可以直接操作底層的暫存器。而在Linux系統中卻禁止這樣的行為,舉個例子:Linux應用的編寫人員故意在應用中呼叫了驅動中關於電源管理的驅動,關閉了系統,那不就得不償失了?

具體的Linux應用程式對驅動的呼叫如圖所示:

1.png

應用程式執行在使用者空間,驅動程式執行在核心空間。處於使用者空間應用程式如果想要實現對核心的操作,必須經過一種"系統呼叫"的方法,實現從使用者空間進入核心空間,實現對底層的操作。

Linux中的核心空間

核心也是程式,也應該具有自己的虛存空間,但是作為一種為使用者程式服務的程式,核心空間有它自己的特點。

核心空間與使用者空間的關係

在一個32位元系統中,一個程式的虛擬空間最大可以是4GB,那麼最直接的做法就是,把核心也看作是一個程式,使它和其他程式一樣也具有4GB空間。但是這種做法會使系統不斷的切換使用者程式的頁表和核心頁表,以致影響計算機的效率。解決這個問題的最好做法就是把4GB空間分成兩個部分:一部分為使用者空間,另一部分為核心空間,這樣就可以保證核心空間固定不變,而當程式切換時,改變的僅是程式的頁表。這種做法的唯一缺點便是核心空間和使用者空間均變小了。

例如:在i386這種32位元的硬體平臺上,Linux在檔案page.h中定義了一個常數PAGE_OFFSET:

#ifdef CONFIG_MMU
#define __PAGE_OFFSET  (0xC0000000)        //0xC0000000為3GB
#else
#define __PAGE_OFFSET  (0x00000000)
#endif

#define PAGE_OFFSET		((unsigned long)__PAGE_OFFSET)
登入後複製

Linux以PAGE_OFFSET為界將4GB的虛擬記憶體空間分成了兩部分:地址0~3G-1這段低地址空間為使用者空間,大小為3GB;地址3GB~4GB-1這段高地址空間為核心空間,大小為1GB。

當系統中執行多個程式時,多個使用者空間與核心空間的關係可以表示如下圖:

1.png

如圖中所示,程式1、2……n共用核心空間。當然,這裡的共用指得是分時共用,因為在任何時刻,對於單核處理器系統來說,只能有一個程式在執行。

核心空間的總體佈局

Linux在發展過程中,隨著硬體裝置的更新和技術水平的提高,其核心空間佈局的發展也是一種不斷打修補程式的方式。這樣的後果就是使得核心空間被分成不同的幾個區域,而且在不同的區域具有不同的對映方式。通常,人們認為Linux核心空間有三個區域,即DMA區(ZONE_DMA)、普通區(ZONE_NORMAL)和高階記憶體區(ZONE_HIGHMEM)。

實際實體記憶體較小時核心空間的直接對映

早期計算機實際設定的實體記憶體通常只有幾MB,所以為了提高核心通過虛擬地址存取實體地址記憶體的速度,核心空間的虛擬地址與實體記憶體地址採用了一種從低地址向高地址依次一一對應的固定對映方式,如下圖所示:

2.png

可以看到,這種固定對映方式使得虛擬地址與實體地址的關係變得很簡單,即核心虛擬地址與實際實體地址只在數值上相差一個固定的偏移量PAGE_OFFSET,所以當核心使用虛擬地址存取物理頁框時,只需在虛擬地址上減去PAGE_OFFSET即可得到實際實體地址,比使用頁表的方式要快得多!

由於這種做法幾乎就是直接使用實體地址,所以這種按固定對映方式的核心空間也就叫做「實體記憶體空間」,簡稱實體記憶體。另外,由於固定對映方式是一種線性對映,所以這個區域也叫做線性對映區。

當然,這種情況下(計算機實際實體記憶體較小時),核心固定對映空間僅佔整個1GB核心空間的一部分。例如:在設定32MB實際實體記憶體的x86計算機系統時,核心的固定對映區便是PAGE_OFFSET~(PAGE_OFFSET+0x02000000)這個32MB空間。那麼核心空間剩餘的核心虛擬空間怎麼辦呢?

當然還是按照普通虛擬空間的管理方式,以頁表的非線性對映方式使用實體記憶體。具體來說,在整個1GB核心空間中去除固定對映區,然後在剩餘部分中再去除其開頭部分的一個8MB隔離區,餘下的就是對映方式與使用者空間相同的普通虛擬記憶體對映區。在這個區,虛擬地址和實體地址不僅不存在固定對映關係,而且通過呼叫核心函數vmalloc()獲得動態記憶體,故這個區就被稱為vmalloc分配區,如下圖所示:

3.png

對於設定32MB實際實體記憶體的x86計算機系統來說,vmalloc分配區的起始位置為PAGE_OFFSET+0x02000000+0x00800000。

這裡說明一下:這裡說的核心空間與物理頁框的固定對映,實質上是核心頁對物理頁框的一種「預定」,並不是說這些頁就「霸佔」了這些物理頁框。即只有當虛擬頁真正需要存取物理頁框時,虛擬頁才與物理頁框繫結。而平時,當某個物理頁框不被與它對應的虛擬頁所使用時,該頁框完全可以被使用者空間以及後面所介紹的核心kmalloc分配區使用。

總之,在實際實體記憶體較小的系統中,實際記憶體的大小就是核心空間的實體記憶體區與vmalloc分配區的邊界。

ZONE_DMA區與ZONE_NORMAL區

對於整個1GB的核心空間,人們還把該空間頭部的16MB叫做DMA區,即ZONE_DMA區,因為以往硬體將DMA空間固定在了實體記憶體的低16MB空間;其餘區則叫做普通區,即ZONE_NORMAL。

核心空間的高階記憶體

隨著計算機技術的發展,計算機的實際實體記憶體越來越大,從而使得核心固定對映區(線性區)也越來越大。顯然,如果不加以限制,當實際實體記憶體達到1GB時,vmalloc分配區(非線性區)將不復存在。於是以前開發的、呼叫了vmalloc()的核心程式碼也就不再可用,顯然為了相容早期的核心程式碼,這是不能允許的。

下圖就表示了這種核心空間所面臨的局面:

4.png

顯然,出現上述問題的原因就是沒有預料到實際實體記憶體可以超過1GB,因而沒有為核心固定對映區的邊界設定限制,而任由其隨著實際實體記憶體的增大而增大。

解決上述問題的方法就是:對核心空間固定對映區的上限加以限制,使之不能隨著實體記憶體的增加而任意增加。Linux規定,核心對映區的上邊界的值最大不能大於一個小於1G的常數high_menory,當實際實體記憶體較大時,以3G+high_memory為邊界來確定實體記憶體區。

例如:對於x86系統,high_memory的值為896M,於是1GB核心空間餘下的128MB為非線性對映區。這樣就確保在任何情況下,核心都有足夠的非線性對映區以相容早期程式碼並可以按普通虛存方式存取實際實體記憶體的1GB以上的空間。

也就是說,高階記憶體的最基本思想:借一段地址空間,建立臨時地址對映,用完後釋放,達到這段地址空間可以迴圈使用,存取所有實體記憶體。當計算機是實體記憶體較大時,核心空間的示意圖如下:

5.png

習慣上,Linux把核心空間3G+high_memory~4G-1的這個部分叫做高階記憶體區(ZONE_HIGHMEM)。

總結一下:在x86結構的核心空間,三種型別的區域(從3G開始計算)如下:

  • ZONE_DMA:核心空間開始的16MB
  • ZONE_NORMAL:核心空間16MB~896MB(固定對映)
  • ZONE_HIGHMEM :核心空間896MB ~ 結束(1G)

根據應用目標不同,高階記憶體區分vmalloc區、可持久對映區和臨時對映區。核心空間中高階記憶體的佈局如下圖所示:

6.png

vmalloc對映區

vmalloc對映區時高階記憶體的主要部分,該區間的頭部與核心線性對映空間之間有一個8MB的隔離區,尾部與後續的可持久對映區有一個4KB的隔離區。

vmalloc對映區的對映方式與使用者空間完全相同,核心可以通過呼叫函數vmalloc()在這個區域獲得記憶體。這個函數的功能相當於使用者空間的malloc(),所提供的記憶體空間在虛擬地址上連續(注意,不保證實體地址連續)。

可持久核心對映區

如果是通過 alloc_page() 獲得了高階記憶體對應的 page,如何給它找個線性空間?

核心專門為此留出一塊線性空間,從PKMAP_BASE開始,用於對映高階記憶體,就是可持久核心對映區。

在可持久核心對映區,可通過呼叫函數kmap()在物理頁框與核心虛擬頁之間建立長期對映。這個空間通常為4MB,最多能對映1024個頁框,數量較為稀少,所以為了加強頁框的週轉,應及時呼叫函數kunmap()將不再使用的物理頁框釋放。

臨時對映區

臨時對映區也叫固定對映區和保留區。該區主要應用在多處理器系統中,因為在這個區域所獲得的記憶體空間沒有所保護,故所獲得的記憶體必須及時使用;否則一旦有新的請求,該頁框上的內容就會被覆蓋,所以這個區域叫做臨時對映區。

關於高階記憶體區一篇很不錯的文章:。

核心記憶體分配修飾符gfp

為了在核心記憶體請求函數對請求進行必要的說明,Linux定義了多種記憶體分配修飾符gfp。它們是行為修飾符、區修飾符、型別修飾符。

行為修飾符

在記憶體分配函數中的行為修飾符說明核心應當如何分配記憶體。主要行為修飾符如下:

Linux的主要核心記憶體分配行為修飾符
修飾符說明
__GFP_WAIT分配器可以休眠
__GFP_HIGH分配器可以存取緊急事件緩衝池
__GFP_IO分配器可以啟動磁碟IO
__GFP_FS分配器可以啟動檔案系統IO
__GFP_COLD分配器應該使用高速緩衝中快要淘汰的頁框
__GFP_NOWARN分配器不發出警告
__GFP_REPEAT分配失敗時重新分配
__GFP_NOFAILT分配失敗時重新分配,直至成功
__GFP_NORETRY分配失敗時不再重新分配

區修飾符

區修飾符說明需要從核心空間的哪個區域中分配記憶體。記憶體分配器預設從核心空間的ZONE_NORMAL開始逐漸向高階獲取為記憶體請求者分配記憶體區,如果使用者特意需要從ZONE_DMA或ZONE_HOGNMEM獲得記憶體,那麼就需要記憶體請求者在記憶體請求函數中使用以下兩個區修飾符說明:

Linux的主要核心記憶體分配區修飾符
修飾符說明
__GFP_DMA從ZONE_DMA區分配記憶體
__GFP_HIGHMEM從ZONE_HIGHMEM區分配記憶體

型別修飾符

型別修飾符實質上是上述所述修飾符的聯合應用。也就是:將上述的某些行為修飾符和區修飾符,用「|」進行連線並另外取名的修飾符。這裡就不多介紹了。

核心常用記憶體分配及地址對映函數

函數vmalloc()

函數vmalloc()在vmalloc分配區分配記憶體,可獲得虛擬地址連續,但並不保證其物理頁框連續的較大記憶體。與物理空間的記憶體分配函數malloc()有所區別,vmalloc()分配的物理頁不會被交換出去。函數vmalloc()的原型如下:

void *vmalloc(unsigned long size)
{
       return __vmalloc(size, GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL);
}
登入後複製
void *__vmalloc(unsigned long size, gfp_t gfp_mask, pgprot_t prot)
{
	return kmalloc(size, (gfp_mask | __GFP_COMP) & ~__GFP_HIGHMEM);
}
登入後複製

其中,引數size為所請求記憶體的大小,返回值為所獲得記憶體虛擬地址指標。

與vmalloc()配套的釋放函數如下:

void vfree(const void *addr)
{
	kfree(addr);
}
登入後複製

其中,引數addr為待釋放記憶體指標。

函數kmalloc()

kmalloc()是核心另一個常用的核心分配函數,它可以分配一段未清零的連續實體記憶體頁,返回值為直接對映地址。由kmalloc()可分配的記憶體最大不能超過32頁。其優點是分配速度快,缺點是不能分配大於128KB的記憶體頁(出於跨平臺考慮)。

在linux/slab.h檔案中,該函數的原型宣告如下:

static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
	struct kmem_cache *cachep;
	void *ret;

	if (__builtin_constant_p(size)) {
		int i = 0;

		if (!size)
			return ZERO_SIZE_PTR;

#define CACHE(x) \
		if (size <= x) \
			goto found; \
		else \
			i++;
#include <linux/kmalloc_sizes.h>
#undef CACHE
		return NULL;
found:
#ifdef CONFIG_ZONE_DMA
		if (flags & GFP_DMA)
			cachep = malloc_sizes[i].cs_dmacachep;
		else
#endif
			cachep = malloc_sizes[i].cs_cachep;

		ret = kmem_cache_alloc_notrace(cachep, flags);

		trace_kmalloc(_THIS_IP_, ret,
			      size, slab_buffer_size(cachep), flags);

		return ret;
	}
	return __kmalloc(size, flags);
}
登入後複製

其中,引數size為以位元組為單位表示的所申請空間的大小;引數flags決定了所分配的記憶體適合什麼場合。

與函數kmalloc()對應的釋放函數如下:

void kfree(const void *objp)
{
	struct kmem_cache *c;
	unsigned long flags;

	trace_kfree(_RET_IP_, objp);

	if (unlikely(ZERO_OR_NULL_PTR(objp)))
		return;
	local_irq_save(flags);
	kfree_debugcheck(objp);
	c = virt_to_cache(objp);
	debug_check_no_locks_freed(objp, obj_size(c));
	debug_check_no_obj_freed(objp, obj_size(c));
	__cache_free(c, (void *)objp);
	local_irq_restore(flags);
}
登入後複製

小結一下,kmalloc、vmalloc、malloc的區別:

  • kmalloc和vmalloc是分配的是核心的記憶體,malloc分配的是使用者的記憶體;
  • kmalloc保證分配的記憶體在物理上是連續的,vmalloc保證的是在虛擬地址空間上的連續,malloc不保證任何東西;
  • kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相對較大;
  • vmalloc比kmalloc要慢。

也就是說:kmalloc、vmalloc這兩個函數所分配的記憶體都處於核心空間,即從3GB~4GB;但位置不同,kmalloc()分配的記憶體處於3GB~high_memory(ZONE_DMA、ZONE_NORMAL)之間,而vmalloc()分配的記憶體在VMALLOC_START~4GB(ZONE_HIGHMEM)之間,也就是非連續記憶體區。一般情況下在驅動程式中都是呼叫kmalloc()來給資料結構分配記憶體,而vmalloc()用在為活動的交換區分配資料結構,為某些I/O驅動程式分配緩衝區,或為模組分配空間。

7.png

可參考文章:。

函數alloc_pages()

與上述在虛擬空間分配記憶體的函數不同,alloc_pages()是在實體記憶體空間分配物理頁框的函數,其原型如下:

static inline struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
{
	if (unlikely(order >= MAX_ORDER))
		return NULL;

	return alloc_pages_current(gfp_mask, order);
}
登入後複製

其中,引數order表示所分配頁框的數目,該數目為2^order。order的最大值由include/Linux/Mmzone.h檔案中的宏MAX_ORDER決定。引數gfp_mask為說明記憶體頁框分配方式及使用場合。

函數返回值為頁框塊的第一個頁框page結構的地址。

呼叫下列函數可以獲得頁框的虛擬地址:

void *page_address(struct page *page)
{
	unsigned long flags;
	void *ret;
	struct page_address_slot *pas;
 
	if (!PageHighMem(page))
		return lowmem_page_address(page);
 
	pas = page_slot(page);
	ret = NULL;
	spin_lock_irqsave(&pas->lock, flags);
	if (!list_empty(&pas->lh)) {
		struct page_address_map *pam;
 
		list_for_each_entry(pam, &pas->lh, list) {
			if (pam->page == page) {
				ret = pam->virtual;
				goto done;
			}
		}
	}
done:
	spin_unlock_irqrestore(&pas->lock, flags);
	return ret;
}
登入後複製

使用函數alloc_pages()獲得的記憶體應該使用下面的函數釋放:

void __free_pages(struct page *page, unsigned int order)
{
	if (put_page_testzero(page)) {
		if (order == 0)
			free_hot_page(page);
		else
			__free_pages_ok(page, order);
	}
}
登入後複製

函數kmap()

kmap()是一個對映函數,它可以將一個物理頁框對映到核心空間的可持久對映區。這種對映類似於核心ZONE_NORMAL的固定對映,但虛擬地址與實體地址的偏移不一定是PAGE_OFFSET。由於核心可持久對映區的容量有限(總共只有4MB),因此當記憶體使用完畢後,應該立即釋放。

函數kmap()的函數原型如下:

void *kmap(struct page *page)
{
	might_sleep();
	if (!PageHighMem(page))
		return page_address(page);
	return kmap_high(page);
}
登入後複製

小結

由於CPU的地址匯流排只有32位元, 32的地址匯流排無論是從邏輯上還是從物理上都只能描述4G的地址空間(232=4Gbit),在物理上理論上最多擁有4G記憶體(除了IO地址空間,實際記憶體容量小於4G),邏輯空間也只能描述4G的線性地址空間。

為了合理的利用邏輯4G空間,Linux採用了3:1的策略,即核心佔用1G的線性地址空間,使用者佔用3G的線性地址空間。所以使用者程序的地址範圍從0~3G,核心地址範圍從3G~4G,也就是說,核心空間只有1G的邏輯線性地址空間。

如果Linux實體記憶體小於1G的空間,通常核心把實體記憶體與其地址空間做了線性對映,也就是一一對映,這樣可以提高存取速度。但是,當Linux實體記憶體超過1G時,線性存取機制就不夠用了,因為只能有1G的記憶體可以被對映,剩餘的實體記憶體無法被核心管理,所以,為了解決這一問題,Linux把核心地址分為線性區和非線性區兩部分,線性區規定最大為896M,剩下的128M為非線性區。從而,線性區對映的實體記憶體成為低端記憶體,剩下的實體記憶體被成為高階記憶體。與線性區不同,非線性區不會提前進行記憶體對映,而是在使用時動態對映。

低端記憶體又分成兩部分:ZONE_DMA:核心空間開始的16MB、ZONE_NORMAL:核心空間16MB~896MB(固定對映)。剩下的就是高階記憶體:ZONE_HIGHMEM :核心空間896MB ~ 結束(1G)。

根據應用目標不同,高階記憶體區分vmalloc區、可持久對映區和臨時對映區三部分。vmalloc區使用vmalloc()函數進行分配;可持久對映區使用allc_pages()獲得對應的 page,在利用kmap()函數直接對映;臨時對映區一般用於特殊需求。

使用者空間和核心空間
核心空間(3G~4G)

高階記憶體(3G+high_memory~4G)ZONE_HIGHMEM

非線性對映區

臨時對映區
可持久對映區
vmalloc區

低端記憶體(3G~3G+high_memory-1)

線性對映區(固定對映區)

ZONE_NORMAL
ZONE_DMA
使用者空間(0~3G-1)頁目錄-->中間頁目錄-->頁表

相關推薦:《Linux視訊教學

以上就是linux驅動程式執行在什麼空間的詳細內容,更多請關注TW511.COM其它相關文章!