首頁 > 運維 > linux運維 > 主體

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

青灯夜游
發布: 2022-11-10 20:12:56
原創
1943 人瀏覽過

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

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

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

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

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

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

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

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

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

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。

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

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

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

核心空間的整體佈局

Linux在發展過程中,隨著硬體設備的更新和技術水平的提高,其核心空間佈局的發展也是一種不斷打補丁的方式。這樣的後果就是使得內核空間被分成不同的幾個區域,而且在不同的區域有不同的映射方式。

通常,人們認為Linux核心空間有三個區域,分別是DMA區(ZONE_DMA)、普通區(ZONE_NORMAL)和高階記憶體區(ZONE_HIGHMEM)。

實際物理記憶體較小時內核空間的直接對應

#早期電腦實際配置的實體記憶體通常只有幾MB,所以

為了提高核心透過虛擬位址存取實體位址記憶體的速度,核心空間的虛擬位址與實體記憶體位址採用了一種從低位址向高位址依序一一對應的固定映射方式,如下圖所示:

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

可以看到,

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

由於這種做法幾乎就是直接使用物理位址,所以這種固定映射方式的核心空間也就叫做“物理記憶體空間”,簡稱物理記憶體。另外,由於固定映射方式是一種線性映射,所以這個區域也叫做線性映射區。

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

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

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

對於配置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()的核心程式碼也就不再可用,顯然為了相容早期的核心程式碼,這是不能允許的。

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

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

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

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

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

也就是說,高階記憶體的最基本想法:借一段位址空間,建立臨時位址映射,用完後釋放,達到這段位址空間可以循環使用,存取所有實體記憶體。 當電腦是物理記憶體較大時,內核空間的示意圖如下:

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

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

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

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

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

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

#vmalloc映射區

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

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

可持久核心映射區

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

核心專門為此留出一塊線性空間,從PKMAP_BASE開始,用來映射高階內存,就是可持久核心映射區。

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

暫時對映區

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

關於高階記憶體區一篇很不錯的文章:linux 使用者空間與核心空間-高階記憶體詳解

核心記憶體分配修飾符gfp

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

行為修飾符

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

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

區修飾符說明需要從核心空間的哪個區域分配記憶體。記憶體分配器預設從核心空間的ZONE_NORMAL開始逐漸向高端獲取為內存請求者分配內存區,

如果用戶特意需要從ZONE_DMA或ZONE_HOGNMEM獲得內存,那麼就需要內存請求者在內存請求函數中使用以下兩個區修飾符說明:

Linux的主要核心記憶體分配區修飾符

修飾符##說明從ZONE_DMA區分配記憶體從ZONE_HIGHMEM區分配記憶體
__GFP_DMA
__GFP_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驱动程序分配缓冲区,或为模块分配空间。

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

可参考文章:Kmalloc和Vmalloc的区别

函数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()函數直接映射;臨時映射區一般用於特殊需求。

##可持久映射區vmalloc區ZONE_NORMALZONE_DMA使用者空間(0~3G-1)頁目錄-- >中間頁目錄-->頁表
使用者空間與核心空間
核心空間(3G~4G)

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

非線性映射區

臨時映射區
低階記憶體(3G~3G high_memory-1)

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

相關推薦:《

Linux影片教學

以上是linux驅動程式運行在什麼空間的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板