天天看點

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

從使用者态進入到核心态的方式一般有三種:

通過<code>系統調用</code>進入,比如printf就是調用write函數

通過<code>軟中斷</code>進入,常見的是程序突然發生了異常。比如android中的應用crash發生以後,程序就會進入核心态調用中斷服務。

通過<code>硬體中斷</code>進入,通常是外部裝置的中斷。當外圍裝置完成使用者的請求操作後,會像CPU發出中斷信号,此時,CPU就會暫停執行下一條即将要執行的指令,轉而去執行中斷信号對應的處理程式,如果先前執行的指令是在使用者态下,則自然就發生從使用者态到核心态的轉換。比如網卡,鍵盤等,一打字,程序就會陷入到核心态。

上面所說到的應用通過軟中斷和硬中斷進入核心态以後,都會去查找和調用相對應的中斷服務程式。Linux的中斷服務程式都不在程序上下文中執行,而是有一個程序無關的<code>中斷上下文</code>中運作,保證中斷服務程式能第一時間響應和處理,然後快速退出。

是以程序,或者說CPU,在任何指定時間點上的活動必然為三者之一:

運作于使用者空間,執行使用者程序

運作于核心空間,處于程序上下文,代表某個特定程序執行

運作于核心空間,處于中斷上下文,與任何程序無關,處理某個特定中斷。

轉自:Linux使用者空間與核心空間(了解高端記憶體)

參考:

1. 程序核心棧、使用者棧

2. 解惑-Linux核心空間

3. linux kernel學習筆記-5 記憶體管理

Linux 作業系統和驅動程式運作在核心空間,應用程式運作在使用者空間,兩者不能簡單地使用指針傳遞資料,因為Linux使用的虛拟記憶體機制,使用者空間的資料可能被換出,當核心空間使用使用者空間指針時,對應的資料可能不在記憶體中。

Linux核心位址映射模型

x86 CPU采用了段頁式位址映射模型。程序代碼中的位址為邏輯位址,經過段頁式位址映射後,才真正通路實體記憶體。

段頁式機制如下圖。

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

Linux使用者空間與核心空間(了解高端記憶體)_核心空間"&gt;

Linux核心位址空間劃分

通常32位Linux核心位址空間劃分0~3G為使用者空間,3~4G為核心空間。注意這裡是32位核心位址空間劃分,64位核心位址空間劃分是不同的。

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

Linux使用者空間與核心空間(了解高端記憶體)_核心空間_02"&gt;

Linux核心高端記憶體的由來

當核心子產品代碼或線程通路記憶體時,代碼中的記憶體位址都為邏輯位址,而對應到真正的實體記憶體位址,需要位址一對一的映射,如邏輯位址0xc0000003對應的實體位址為0×3,0xc0000004對應的實體位址為0×4,… …,邏輯位址與實體位址對應的關系為

實體位址 = 邏輯位址 – 0xC0000000

邏輯位址

實體記憶體位址

0xc0000000

0×0

0xc0000001

0×1

0xc0000002

0×2

0xc0000003

0×3

0xe0000000

0×20000000

0xffffffff

0×40000000 ??

設按照上述簡單的位址映射關系,那麼核心邏輯位址空間通路為0xc0000000 ~ 0xffffffff,那麼對應的實體記憶體範圍就為0×0 ~

0×40000000,即隻能通路1G實體記憶體。若機器中安裝8G實體記憶體,那麼核心就隻能通路前1G實體記憶體,後面7G實體記憶體将會無法通路,因為核心

的位址空間已經全部映射到實體記憶體位址範圍0×0 ~

0×40000000。即使安裝了8G實體記憶體,那麼實體位址為0×40000001的記憶體,核心該怎麼去通路呢?代碼中必須要有記憶體邏輯位址

的,0xc0000000 ~ 0xffffffff的位址空間已經被用完了,是以無法通路實體位址0×40000000以後的記憶體。

顯然不能将核心位址空間0xc0000000 ~

0xfffffff全部用來簡單的位址映射。是以x86架構中将核心位址空間劃分三部分:ZONE_DMA、ZONE_NORMAL和

ZONE_HIGHMEM。ZONE_HIGHMEM即為高端記憶體,這就是記憶體高端記憶體概念的由來。

在x86結構中,三種類型的區域如下:

ZONE_DMA        記憶體開始的16MB

ZONE_NORMAL       16MB~896MB

ZONE_HIGHMEM       896MB ~ 結束

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

Linux使用者空間與核心空間(了解高端記憶體)_實體記憶體_03"&gt;

Linux核心高端記憶體的了解

前面我們解釋了高端記憶體的由來。

Linux将核心位址空間劃分為三部分ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,高端記憶體HIGH_MEM位址空間範圍為

0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那麼核心是如何借助128MB高端記憶體位址空間實作通路所有實體記憶體?

當核心想通路高于896MB實體位址記憶體時,從0xF8000000 ~ 0xFFFFFFFF位址空間範圍内找一段相應大小空閑的邏輯位址空間,借用一會。借用這段邏輯位址空間,建立映射到想通路的那段實體記憶體(即填充核心PTE頁面表),臨時用一會,用完後歸還。這樣别人也可以借用這段位址空間通路其他實體記憶體,實作了使用有限的位址空間,通路所有所有實體記憶體。如下圖。

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

Linux使用者空間與核心空間(了解高端記憶體)_使用者空間_04"&gt;

例如核心想通路2G開始的一段大小為1MB的實體記憶體,即實體位址範圍為0×80000000 ~

0x800FFFFF。通路之前先找到一段1MB大小的空閑位址空間,假設找到的空閑位址空間為0xF8700000 ~

0xF87FFFFF,用這1MB的邏輯位址空間映射到實體位址空間0×80000000 ~ 0x800FFFFF的記憶體。映射關系如下:

0xF8700000

0×80000000

0xF8700001

0×80000001

0xF8700002

0×80000002

0xF87FFFFF

0x800FFFFF

當核心通路完0×80000000 ~ 0x800FFFFF實體記憶體後,就将0xF8700000 ~ 0xF87FFFFF核心線性空間釋放。這樣其他程序或代碼也可以使用0xF8700000 ~ 0xF87FFFFF這段位址通路其他實體記憶體。

從上面的描述,我們可以知道高端記憶體的最基本思想:借一段位址空間,建立臨時位址映射,用完後釋放,達到這段位址空間可以循環使用,通路所有實體記憶體。

看到這裡,不禁有人會問:萬一有核心程序或子產品一直占用某段邏輯位址空間不釋放,怎麼辦?若真的出現的這種情況,則核心的高端記憶體位址空間越來越緊張,若都被占用不釋放,則沒有建立映射到實體記憶體都無法通路了。

在香港尖沙咀有些寫字樓,洗手間很少且有門鎖的。客戶要去洗手間的話,可以向前台拿鑰匙,友善完後,把鑰匙歸還到前台。這樣雖然隻有一個洗

手間,但可以滿足所有客戶去洗手間的需求。要是某個客戶一直占用洗手間、鑰匙不歸還,那麼其他客戶都無法上洗手間了。Linux核心高端記憶體管理的思想類似。

Linux核心高端記憶體的劃分

核心将高端記憶體劃分為3部分:VMALLOC_START~VMALLOC_END、KMAP_BASE~FIXADDR_START和FIXADDR_START~4G。

linux使用者空間和核心空間(核心高端記憶體)_轉 Linux使用者空間與核心空間(了解高端記憶體)

Linux使用者空間與核心空間(了解高端記憶體)_位址空間_05"&gt;

對于高端記憶體,可以通過 alloc_page()

或者其它函數獲得對應的 page,但是要想通路實際實體記憶體,還得把 page 轉為線性位址才行(為什麼?想想 MMU是如何通路實體記憶體的),也就是說,我們需要為高端記憶體對應的 page 找一個線性空間,這個過程稱為高端記憶體映射。

對應高端記憶體的3部分,高端記憶體映射有三種方式:

映射到”核心動态映射空間”(noncontiguous memory allocation)

這種方式很簡單,因為通過 vmalloc() ,在”核心動态映射空間”申請記憶體的時候,就可能從高端記憶體獲得頁面(參看 vmalloc 的實作),是以說高端記憶體有可能映射到”核心動态映射空間”中。

持久核心映射(permanent kernel mapping)

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

核心專門為此留出一塊線性空間,從 PKMAP_BASE 到 FIXADDR_START ,用于映射高端記憶體。在 2.6核心上,這個位址範圍是

4G-8M 到 4G-4M

之間。這個空間起叫”核心永久映射空間”或者”永久核心映射空間”。這個空間和其它空間使用同樣的頁目錄表,對于核心來說,就是

swapper_pg_dir,對普通程序來說,通過 CR3 寄存器指向。通常情況下,這個空間是 4M 大小,是以僅僅需要一個頁表即可,核心通過來

pkmap_page_table 尋找這個頁表。通過 kmap(),可以把一個 page 映射到這個空間來。由于這個空間是 4M

大小,最多能同時映射1024 個 page。是以,對于不使用的的 page,應該及時從這個空間釋放掉(也就是解除映射關系),通過

kunmap() ,可以把一個 page 對應的線性位址從這個空間釋放出來。

臨時映射(temporary kernel mapping)

核心在 FIXADDR_START 到 FIXADDR_TOP 之間保留了一些線性空間用于特殊需求。這個空間稱為”固定映射空間”在這個空間中,有一部分用于高端記憶體的臨時映射。

這塊空間具有如下特點:

(1)每個 CPU 占用一塊空間

(2)在每個 CPU 占用的那塊空間中,又分為多個小空間,每個小空間大小是 1 個 page,每個小空間用于一個目的,這些目的定義在 kmap_types.h 中的 km_type 中。

當要進行一次臨時映射的時候,需要指定映射的目的,根據映射目的,可以找到對應的小空間,然後把這個空間的位址作為映射位址。這意味着一次臨時映射會導緻以前的映射被覆寫。通過 kmap_atomic() 可實作臨時映射。

 可以參考一下:Linux高端記憶體映射等等

常見問題:

1、使用者空間(程序)是否有高端記憶體概念?

使用者程序沒有高端記憶體概念。隻有在核心空間才存在高端記憶體。使用者程序最多隻可以通路3G實體記憶體,而核心程序可以通路所有實體記憶體。

2、64位核心中有高端記憶體嗎?

目前現實中,64位Linux核心不存在高端記憶體,因為64位核心可以支援超過512GB記憶體。若機器安裝的實體記憶體超過核心位址空間範圍,就會存在高端記憶體。

3、使用者程序能通路多少實體記憶體?核心代碼能通路多少實體記憶體?

32位系統使用者程序最大可以通路3GB,核心代碼可以通路所有實體記憶體。

64位系統使用者程序最大可以通路超過512GB,核心代碼可以通路所有實體記憶體。

4、高端記憶體和實體位址、邏輯位址、線性位址的關系?

高端記憶體隻和邏輯位址有關系,和線性位址、實體位址沒有直接關系。

5、使用者空間程序切換時虛拟記憶體怎樣切換,怎樣做到通路不沖突?

使用者空間不是程序共享的,而是程序隔離的。每個程序最大都可以有3GB的使用者空間。一個程序對其中一個位址的通路,與其它程序對于同一位址的通路絕不沖突。比如,一個程序從其使用者空間的位址0x1234ABCD處可以讀出整數8,而另外一個程序從其使用者空間的位址0x1234ABCD處可以讀出整數20,這取決于程序自身的邏輯。

任意一個時刻,在一個CPU上隻有一個程序在運作。是以對于此CPU來講,在這一時刻,整個系統隻存在一個4GB的虛拟位址空間,這個虛拟位址空間是面向此程序的。當程序發生切換的時候,虛拟位址空間也随着切換。由此可以看出,每個程序都有自己的虛拟位址空間,隻有此程序運作的時候,其虛拟位址空間才被運作它的CPU所知。在其它時刻,其虛拟位址空間對于CPU來說,是不可知的。是以盡管每個程序都可以有4 GB的虛拟位址空間,但在CPU眼中,隻有一個虛拟位址空間存在。虛拟位址空間的變化,随着程序切換而變化。

從上面我們知道,一個程式編譯連接配接後形成的位址空間是一個虛拟位址空間,但是程式最終還是要運作在實體記憶體中。是以,應用程式所給出的任何虛位址最終必須被轉化為實體位址,是以,虛拟位址空間必須被映射到實體記憶體空間中,這個映射關系需要通過硬體體系結構所規定的資料結構來建立。這就是我們所說的段描述符表和頁表,Linux主要通過頁表來進行映射。

于是,我們得出一個結論,如果給出的頁表不同,那麼CPU将某一虛拟位址空間中的位址轉化成的實體位址就會不同。是以我們為每一個程序都建立其頁表,将每個程序的虛拟位址空間根據自己的需要映射到實體位址空間上。既然某一時刻在某一CPU上隻能有一個程序在運作,那麼當程序發生切換的時候,将頁表也更換為相應程序的頁表,這就可 以實作每個程序都有自己的虛拟位址空間而互不影響。是以,在任意時刻,對于一個CPU來說,隻需要有目前程序的頁表,就可以實作其虛拟位址到實體位址的轉化。

6、為什麼不把所有的位址空間都配置設定給核心?

若把所有位址空間都給記憶體,那麼使用者程序怎麼使用記憶體?怎麼保證核心使用記憶體和使用者程序不起沖突?

(1)讓我們忽略Linux對段式記憶體映射的支援。

在保護模式下,我們知道無論CPU運作于使用者态還是核心态,CPU執行程式所通路的位址都是虛拟位址,MMU

必須通過讀取控制寄存器CR3中的值作為目前頁面目錄的指針,進而根據分頁記憶體映射機制(參看相關文檔)将該虛拟位址轉換為真正的實體位址才能讓CPU真正的通路到實體位址。

(2)對于32位的Linux,

其每一個程序都有4G的尋址空間,但當一個程序通路其虛拟記憶體空間中的某個位址時又是怎樣實作不與其它程序的虛拟空間混淆

的呢?每個程序都有其自身的頁面目錄PGD,Linux将該目錄的指針存放在與程序對應的記憶體結構task_struct.(struct

mm_struct)mm-&gt;pgd中。每當一個程序被排程(schedule())即将進入運作态時,Linux核心都要用該程序的PGD指針設定CR3(switch_mm())。

(3)當建立一個新的程序時,都要為新程序建立一個新的頁面目錄PGD,并從核心的頁面目錄swapper_pg_dir中複制核心區間頁面目錄項至建立程序頁面目錄PGD的相應位置,具體過程如下:

do_fork()

--&gt; copy_mm() --&gt; mm_init() --&gt; pgd_alloc() --&gt;

set_pgd_fast() --&gt; get_pgd_slow() --&gt; memcpy(&amp;PGD +

USER_PTRS_PER_PGD, swapper_pg_dir + USER_PTRS_PER_PGD, (PTRS_PER_PGD -

USER_PTRS_PER_PGD) * sizeof(pgd_t))

樣一來,每個程序的頁面目錄就分成了兩部分,第一部分為“使用者空間”,用來映射其整個程序空間(0x0000 0000-0xBFFF

FFFF)即3G位元組的虛拟位址;第二部分為“系統空間”,用來映射(0xC000 0000-0xFFFF

FFFF)1G位元組的虛拟位址。可以看出Linux系統中每個程序的頁面目錄的第二部分是相同的,是以從程序的角度來看,每個程序有4G位元組的虛拟空間,

較低的3G位元組是自己的使用者空間,最高的1G位元組則為與所有程序以及核心共享的系統空間。

(4)現在假設我們有如下一個情景:

在程序A中通過系統調用sethostname(const char *name,seze_t len)設定計算機在網絡中的“主機名”.

在該情景中我們勢必涉及到從使用者空間向核心空間傳遞資料的問題,name是使用者空間中的位址,它要通過系統調用設定到核心中的某個位址中。讓我們看看這個過程中的一些細節問題:系統調用的具體實作是将系統調用的參數依次存入寄存器ebx,ecx,edx,esi,edi(最多5個參數,該情景有兩個

name和len),接着将系統調用号存入寄存器eax,然後通過中斷指令“int

80”使程序A進入系統空間。由于程序的CPU運作級别小于等于為系統調用設定的陷阱門的準入級别3,是以可以暢通無阻的進入系統空間去執行為int

80設定的函數指針system_call()。由于system_call()屬于核心空間,其運作級别DPL為0,CPU要将堆棧切換到核心堆棧,即程序A的系統空間堆棧。我們知道核心為建立程序建立task_struct結構時,共配置設定了兩個連續的頁面,即8K的大小,并将底部約1k的大小用于

task_struct(如#define alloc_task_struct() ((struct task_struct *)

__get_free_pages(GFP_KERNEL,1))),而其餘部分記憶體用于系統空間的堆棧空間,即當從使用者空間轉入系統空間時,堆棧指針

esp變成了(alloc_task_struct()+8192),這也是為什麼系統空間通常用宏定義current(參看其實作)擷取目前程序的

task_struct位址的原因。每次在程序從使用者空間進入系統空間之初,系統堆棧就已經被依次壓入使用者堆棧SS、使用者堆棧指針ESP、EFLAGS、

使用者空間CS、EIP,接着system_call()将eax壓入,再接着調用SAVE_ALL依次壓入ES、DS、EAX、EBP、EDI、ESI、

EDX、ECX、EBX,然後調用sys_call_table+4*%EAX,本情景為sys_sethostname()。

在sys_sethostname()中,經過一些保護考慮後,調用copy_from_user(to,from,n),其中to指向核心空間

system_utsname.nodename,譬如0xE625A000,from指向使用者空間譬如0x8010FE00。現在程序A進入了核心,在系統空間中運作,MMU根據其PGD将虛拟位址完成到實體位址的映射,最終完成從使用者空間到系統空間資料的複制。準備複制之前核心先要确定使用者空間位址和

長度的合法性,至于從該使用者空間位址開始的某個長度的整個區間是否已經映射并不去檢查,如果區間内某個位址未映射或讀寫權限等問題出現時,則視為壞位址,

就産生一個頁面異常,讓頁面異常服務程式處理。過程如

下:copy_from_user()-&gt;generic_copy_from_user()-&gt;access_ok()+__copy_user_zeroing()。

(5)小結:

*程序尋址空間0~4G  

*程序在使用者态隻能通路0~3G,隻有進入核心态才能通路3G~4G  

*程序通過系統調用進入核心态

*每個程序虛拟空間的3G~4G部分是相同的  

*程序從使用者态進入核心态不會引起CR3的改變但會引起堆棧的改變

Linux

簡化了分段機制,使得虛拟位址與線性位址總是一緻,是以,Linux的虛拟位址空間也為0~4G。Linux核心将這4G位元組的空間分為兩部分。将最高的

1G位元組(從虛拟位址0xC0000000到0xFFFFFFFF),供核心使用,稱為“核心空間”。而将較低的3G位元組(從虛拟位址

0x00000000到0xBFFFFFFF),供各個程序使用,稱為“使用者空間)。因為每個程序可以通過系統調用進入核心,是以,Linux核心由系統内的所有程序共享。于是,從具體程序的角度來看,每個程序可以擁有4G位元組的虛拟空間。

Linux使用兩級保護機制:0級供核心使用,3級供使用者程式使用。從圖中可以看出(這裡無法表示圖),每個程序有各自的私有使用者空間(0~3G),這個空間對系統中的其他程序是不可見的。最高的1GB位元組虛拟核心空間則為所有程序以及核心所共享。

》》虛拟核心空間到實體空間的映射

核心空間中存放的是核心代碼和資料,而程序的使用者空間中存放的是使用者程式的代碼和資料。不管是核心空間還是使用者空間,它們都處于虛拟空間中。讀者會問,系統啟動時,核心的代碼和資料不是被裝入到實體記憶體嗎?它們為什麼也處于虛拟記憶體中呢?這和編譯程式有關,後面我們通過具體讨論就會明白這一點。

雖然核心空間占據了每個虛拟空間中的最高1GB位元組,但映射到實體記憶體卻總是從最低位址(0x00000000)開始。對核心空間來說,其位址映射是很簡單的線性映射,0xC0000000就是實體位址與線性位址之間的位移量,在Linux代碼中就叫做PAGE_OFFSET。

我們來看一下在include/asm/i386/page.h中對核心空間中位址映射的說明及定義:

/*

* This handles the memory map.. We could make this a config

* option, but too many people screw it up, and too few need

* it.

*

* A __PAGE_OFFSET of 0xC0000000 means that the kernel has

* a virtual address space of one gigabyte, which limits the

* amount of physical memory you can use to about 950MB. 

* If you want more physical memory than this then see the CONFIG_HIGHMEM4G

* and CONFIG_HIGHMEM64G options in the kernel configuration.

*/

#define __PAGE_OFFSET           (0xC0000000)

……

#define PAGE_OFFSET             ((unsigned long)__PAGE_OFFSET)

#define __pa(x)                 ((unsigned long)(x)-PAGE_OFFSET)

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

源代碼的注釋中說明,如果你的實體記憶體大于950MB,那麼在編譯核心時就需要加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G選

項,這種情況我們暫不考慮。如果實體記憶體小于950MB,則對于核心空間而言,給定一個虛位址x,其實體位址為“x-

PAGE_OFFSET”,給定一個實體位址x,其虛位址為“x+ PAGE_OFFSET”。

這裡再次說明,宏__pa()僅僅把一個核心空間的虛位址映射到實體位址,而決不适用于使用者空間,使用者空間的位址映射要複雜得多。

》》核心映像

在下面的描述中,我們把核心的代碼和資料就叫核心映像(kernel

image)。當系統啟動時,Linux核心映像被安裝在實體位址0x00100000開始的地方,即1MB開始的區間(第1M留作它用)。然而,在正常運作時,

整個核心映像應該在虛拟核心空間中,是以,連接配接程式在連接配接核心映像時,在所有的符号位址上加一個偏移量PAGE_OFFSET,這樣,核心映像在核心空間的起始位址就為0xC0100000。

例如,程序的頁目錄

PGD(屬于核心資料結構)就處于核心空間中。在程序切換時,要将寄存器CR3設定成指

向新程序的頁目錄PGD,而該目錄的起始位址在核心空間中是虛位址,但CR3所需要的是實體位址,這時候就要用__pa()進行位址轉換。在

mm_context.h中就有這麼一行語句:

asm volatile(“movl %0,%%cr3”: :”r” (__pa(next-&amp;gt;pgd));

這是一行嵌入式彙編代碼,其含義是将下一個程序的頁目錄起始位址next_pgd,通過__pa()轉換成實體位址,存放在某個寄存器中,然後用mov指令将其寫入CR3寄存器中。經過這行語句的處理,CR3就指向新程序next的頁目錄表PGD了。

繼續閱讀