學習 Linux 時,經常可以看到兩個詞:User space(用戶空間)和 Kernel space(內核空間)。
簡單說,Kernel space 是 Linux 內核的運行空間,User space 是用戶程序的運行空間。為了安全,它們是隔離的,即使用戶的程序崩潰了,內核也不受影響。
Kernel space 可以執行任意命令,調用系統的一切資源;User space 只能執行簡單的運算,不能直接調用系統資源,必須通過系統接口(又稱 system call),才能向內核發出指令。
str = "my string" // 用戶空間 x = x + 2 file.write(str) // 切換到內核空間 y = x + 4 // 切換回用戶空間
上面代碼中,第一行和第二行都是簡單的賦值運算,在 User space 執行。第三行需要寫入文件,就要切換到 Kernel space,因為用戶不能直接寫文件,必須通過內核安排。第四行又是賦值運算,就切換回 User space。
查看 CPU 時間在 User space 與 Kernel Space 之間的分配情況,可以使用top
命令。它的第三行輸出就是 CPU 時間分配統計。
這一行有 8 項統計指標。
其中,第一項24.8 us
(user 的縮寫)就是 CPU 消耗在 User space 的時間百分比,第二項0.5 sy
(system 的縮寫)是消耗在 Kernel space 的時間百分比。
隨便也說一下其他 6 個指標的含義。
ni
:niceness 的縮寫,CPU 消耗在 nice 進程(低優先級)的時間百分比id
:idle 的縮寫,CPU 消耗在閑置進程的時間百分比,這個值越低,表示 CPU 越忙wa
:wait 的縮寫,CPU 等待外部 I/O 的時間百分比,這段時間 CPU 不能干其他事,但是也沒有執行運算,這個值太高就說明外部設備有問題hi
:hardware interrupt 的縮寫,CPU 響應硬件中斷請求的時間百分比si
:software interrupt 的縮寫,CPU 響應軟件中斷請求的時間百分比st
:stole time 的縮寫,該項指標只對虛擬機有效,表示分配給當前虛擬機的 CPU 時間之中,被同一台物理機上的其他虛擬機偷走的時間百分比
如果想查看單個程序的耗時,一般使用time
命令。
程序名之前加上time
命令,會在程序執行完畢以后,默認顯示三行統計。
real
:程序從開始運行到結束的全部時間,這是用戶能感知到的時間,包括 CPU 切換去執行其他任務的時間。user
:程序在 User space 執行的時間sys
:程序在 Kernel space 執行的時間
user
和sys
之和,一般情況下,應該小於real
。但如果是多核 CPU,這兩個指標反映的是所有 CPU 的總耗時,所以它們之和可能大於real
。
Java文件讀寫原理和虛擬內存
1.內核空間和用戶空間
這兩個概念對於初次接觸的小伙伴來說並不是很好理解,舉個簡單例子如下圖:

上圖中的儲戶是沒法直接從金庫中存錢獲取取錢的,如果這么做了,那么就非法了。這里用戶空間相當於儲戶,內核空間相當於銀行職員,而硬盤相當於金庫,也就是用戶空間中的進程沒法直接操作讀寫硬盤中的數據,我們需要通過內核空間來處理,這樣對於這兩個概念應該會容易理解些。
空間 |
描述 |
---|---|
用戶空間 |
是非特權區域,在該區域執行的代碼不能直接訪問硬件設備,常規進程就在本區域執行,JVM就是常規進程,所以JVM進程駐守在用戶空間 |
內核空間 |
是操作系統所在區域,有特別的權利:能與設備控制器通訊,控制着用戶區域進程的運行狀態等等,最重要的是,所有I/O都直接或間接的通過內核空間 |
2.普通IO操作
了解了用戶空間和內核空間的概念和作用后我們來看下普通IO的執行原理。

根據上圖,當進程請求一個I/O操作,它會執行一個系統(open() , read() , writer() , close())調用將控制權移交給內核。當內核以這種方式被調用,它隨即采取任何必要步驟,找到進程所需數據,並把數據傳送到用戶空間內指定的緩沖區中,這時常規進程就可以對緩沖區中的數據處理操作了,而內核試圖對數據進行高速緩存或預讀取,因此進程所需數據可能已經在內核空間里了,如果是這樣,該數據只需簡單地拷貝出來即可,如果數據不在內核空間,則進程被掛起,內核着手把數據讀進內場。
問題
數據從內核空間拷貝到用戶空間似乎多余,為什么不直接讓磁盤把數據送到用戶空間的緩沖區呢?
- 硬盤通常不能直接訪問用戶空間
- 磁盤基於塊存儲的硬件設備操作的固定大小的數據塊,用戶進程請求的可能是任意大小或者非對齊的數據塊,在這兩者數據交互過程中內核負責數據的分解、再組合工作,起到一個中間人的角色。
3.虛擬內存
通過上面的介紹,我們知道當應用程序需要讀取文件的時候,內核首先通過DMA技術將文件內容從磁盤讀入內核中的buffer,然后Java應用進程再從內核的buffer將數據讀取到應用程序的buffer。也就是有兩次的文件復制,為了提升I/O效率和處理能力,操作系統采用虛擬內存的機制。虛擬內存意為使用虛假(或虛擬)地址取代物理(硬件RAM)內存地址。這樣做好處頗多,總結起來可分為兩大類:
- 一個以上的虛擬地址可指向同一個物理內存地址。
- 虛擬內存空間可大於實際可用的硬件內存

這樣做的好處是省去了內核與用戶空間的往來拷貝。
3.1 一個以上的虛擬地址可指向同一個物理內存地址
在進行IO操作時就可以將用戶空間的buffer區和內核空間的buffer區指向同一個物理內存。這樣用戶空間的程序就不需要再去內核空間再取回數據,而是可以直接訪問,節省內存空間。

3.2 虛擬內存空間可大於實際可用的硬件內存
當用戶程序訪問內存地址時,一般的操作如下:首先虛擬內存系統會到物理內存去查找該虛擬地址是否存在。如果存在,如A1,則直接從物理內存中讀取;如果不存在,如A4則會拋出一個信號。這時虛擬內存系統會去磁盤空間中找,找到后再按一定的策略,將其置入到內存中,如將B2和A4交換。然后由用戶程序就可以使用A4中的數據。這樣就保證了用戶程序可以讀取一些大型的文件。

從本質上說,物理內存充當了分頁區的高速緩存;而所謂分頁區,即從物理內存置換出來,轉而存儲於磁盤上的內存頁面.
把內存頁大小設定為磁盤塊大小的倍數,這樣內核就可直接向磁盤控制硬件發布命令,把內存頁寫入磁盤,在需要時再重新裝入。結果是,所有磁盤 I/O 都在頁層面完成。對於采用分頁技術的,現代操作系統而言,這也是數據在磁盤與物理內存之間往來的唯一方式

3.3內存管理單元
現代 CPU 包含一個稱為內存管理單元(MMU)的子系統,邏輯上位於CPU 與物理內存之間。該設備包含虛擬地址向物理內存地址轉換時所需映射信息。當 CPU 引用某內存地址時,MMU負責確定該地址所在頁(往往通過對地址值進行移位或屏蔽位操作實現),並將虛擬頁號轉換為物理頁號(這一步由硬件完成,速度極快)。如果當前不存在與該虛擬頁形成有效映射的物理內存頁,MMU會向CPU 提交一個頁錯誤。

頁錯誤隨即產生一個陷阱(類似於系統調用),把控制權移交給內核,附帶導致錯誤的虛擬地址信息,然后內核采取步驟驗證頁的有效性。內核會安排頁面調入操作,把缺失的頁內容讀回物理內存。這往往導致別的頁被移出物理內存,好給新來的頁讓地方。在這種情況下,如果待移出的頁已經被碰過了(自創建或上次頁面調入以來,內容已發生改變),還必須首先執行頁面調出,把頁內容拷貝到磁盤上的分頁區。
如果所要求的地址不是有效的虛擬內存地址(不屬於正在執行的進程的任何一個內存段),則該頁不能通過驗證,段錯誤隨即產生。於是,控制權轉交給內核的另一部分,通常導致的結果就是進程被強令關閉。
一旦出錯的頁通過了驗證,MMU 隨即更新,建立新的虛擬到物理的映射(如有必要,中斷被移出頁的映射),用戶進程得以繼續。造成頁錯誤的用戶進程對此不會有絲毫察覺,一切都在不知不覺中進行
用戶空間_內核空間以及內存映射
內核空間和用戶空間
現代操作系統采用虛擬存儲器,對於32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的權限。為了保證用戶進程不能直接操作內核,保證內核安全,操作系統將虛擬空間划分為兩部分,一部分是內核空間,一部分是用戶空間。針對Linux操作系統,將最高的1G字節(從虛擬地址0xC0000000到0xFFFFFFFF)供內核使用,稱為內核空間,而較低的3G字節(從虛擬地址0x00000000到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程都可以通過系統調用進入到內核。其中在Linux系統中,進程的用戶空間是獨立的,而內核空間是共有的,進程切換時,用戶空間切換,內核空間不變。

有了用戶空間和內核空間的划分后,整個linux內部結構可以分為三部分,從最底層到最上層依次是:硬件->內核空間->用戶空間,如下圖所示:

用戶態和內核態
當一個進程執行系統調用而陷入內核代碼中執行時,稱進行處於內核運行態(內核態)。此時處理器處於特權級別最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每個進程都有自己的內核棧。
當進行在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。此時處理器在特權級最低的(3級)用戶代碼中運行。當正在執行用戶程序而突然被中斷程序中斷時,此時用戶程序也可象征性的稱為處於進行的內核態,因為中斷處理程序使用當前進程的內核棧。
邏輯地址、線性地址和物理地址
在解釋高端內存和內存映射前,先復習一下什么是邏輯地址、線性地址和物理地址吧,大家要是知道的話就可以直接跳過。
邏輯地址
邏輯地址(Logical Address) 是指由程序產生的和段相關的偏移地址部分。例如,你在進行C語言指針編程中,能讀取指針變量本身值(&操作),實際上這個值就是邏輯地址,他是相對於你當前進程數據段的地址,不和絕對物理地址相干。只有在Intel實模式下,邏輯地址才和物理地址相等(因為實模式沒有分段或分頁機制,Cpu不進行自動地址轉換);邏輯也就是在Intel保護模式下程序執行代碼段限長內的偏移地址(假定代碼段、數據段如果完全相同)。應用程式員僅需和邏輯地址打交道,而分段和分頁機制對你來說是完全透明的,僅由系統編程人員涉及。應用程式員雖然自己能直接操作內存,那也只能在操作系統給你分配的內存段操作。
線性地址
線性地址(Linear Address) 是邏輯地址到物理地址變換之間的中間層。程式代碼會產生邏輯地址,或說是段中的偏移地址,加上相應段的基地址就生成了一個線性地址。如果啟用了分頁機制,那么線性地址能再經變換以產生一個物理地址。若沒有啟用分頁機制,那么線性地址直接就是物理地址。Intel 80386的線性地址空間容量為4G(2的32次方即32根地址總線尋址)。
物理地址
物理地址(Physical Address) 是指出目前CPU外部地址總線上的尋址物理內存的地址信號,是地址變換的最終結果地址。如果啟用了分頁機制,那么線性地址會使用頁目錄和頁表中的項變換成物理地址。如果沒有啟用分頁機制,那么線性地址就直接成為物理地址了。
虛擬地址
虛擬內存(Virtual Memory)是指計算機呈現出要比實際擁有的內存大得多的內存量。因此他允許程式員編制並運行比實際系統擁有的內存大得多的程式。這使得許多大型項目也能夠在具有有限內存資源的系統上實現。一個非常恰當的比喻是:你不必非常長的軌道就能讓一列火車從上海開到北京。你只需要足夠長的鐵軌(比如說3公里)就能完成這個任務。采取的方法是把后面的鐵軌即時鋪到火車的前面,只要你的操作足夠快並能滿足需求,列車就能象在一條完整的軌道上運行。這也就是虛擬內存管理需要完成的任務。在Linux0.11內核中,給每個程式(進程)都划分了總容量為64MB的虛擬內存空間。因此程式的邏輯地址范圍是0x0000000到0x4000000。有時我們也把邏輯地址稱為 虛擬地址。因為和虛擬內存空間的概念類似,邏輯地址也是和實際物理內存容量無關的。邏輯地址和物理地址的“差距”是0xC0000000,是由於虛擬地址->線性地址->物理地址映射正好差這個值。這個值是由操作系統指定的。機理 邏輯地址(或稱為虛擬地址)到線性地址是由CPU的段機制自動轉換的。如果沒有開啟分頁管理,則線性地址就是物理地址。如果開啟了分頁管理,那么系統程式需要參和線性地址到物理地址的轉換過程。具體是通過設置頁目錄表和頁表項進行的。
高端內存
高端內存的由來
在傳統的Linux x86 32位系統中,內核模塊的代碼或者線程訪問內存時,代碼中的內存地址都為邏輯地址,而對應到真正的物理內存地址時,還需要地址的一一映射。如果邏輯地址位0xC0000003,那么對應的物理地址就是0x3,如果邏輯地址位0xC0000004,那么對應的物理地址就是0x4,所以物理地址和邏輯地址的關系如下:
物理地址 = 邏輯地址 – 0xC0000000
根據上面的內核地址空間的地址轉換關系,注意內核的虛擬地址在“高端”,但是ta映射的物理內存地址在低端。會發現,內核模塊能夠訪問的邏輯地址為0xC0000000-0xFFFFFFFF,對應的物理地址為0x00000000-0x40000000,總共1G的內存。也就是說如果計算機的總物理內存大於1G,按照上面的映射關系,高於1G的部分,內核就無法訪問到了。為了解決這種狀況,就出現了高端內存一說。
因為不能直接將內和空間的1G內存直接做一一映射,所以Linux內核將內核空間分成了三個部分,分別是:ZONE_DMA,ZONE_NORMAL和ZONE_HIGHMEM。這三個區域的內存分配情況如下:
ZONE_DMA | 內存開始的16MB空間 |
---|---|
ZONE_NORMAL | 16MB-896MB |
ZONE_HIGHMEM | 896MB-結束(1G) |
對高端內存的理解
上一小節就說到高端內存是用來解決內核無法訪問大於1G內存地址空間的問題的。那么具體是怎么實現的呢?總的來說非常簡單,當內核需要訪問高於1G的內存空間的時候,例如內核需要訪問0x50000000-0x500FFFFF這1MB內存空間的時候,只需要在ZONE_HIGHMEM這一個區域內臨時申請一個1MB的內存空間,然后將其映射到上述需要訪問的內存區域即可。當內核使用完后,釋放申請的1MB內存空間便完成對高於1G內存空間的訪問了。
內存映射(mmap)
mmap基本概念
mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。如下圖所示:

由上圖可以看出,進程的虛擬地址空間,由多個虛擬內存區域構成。虛擬內存區域是進程的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址范圍。上圖中所示的text數據段(代碼段)、初始數據段、BSS數據段、堆、棧和內存映射,都是一個獨立的虛擬內存區域。而為內存映射服務的地址空間處在堆棧之間的空余部分。
linux內核使用vm_area_struct結構來表示一個獨立的虛擬內存區域,由於每個不同質的虛擬內存區域功能和內部機制都不同,因此一個進程使用多個vm_area_struct結構來分別表示不同類型的虛擬內存區域。各個vm_area_struct結構使用鏈表或者樹形結構鏈接,方便進程快速訪問,如下圖所示:

vm_area_struct結構中包含區域起始和終止地址以及其他相關信息,同時也包含一個vm_ops指針,其內部可引出所有針對這個區域可以使用的系統調用函數。這樣,進程對某一虛擬內存區域的任何操作需要用要的信息,都可以從vm_area_struct中獲得。mmap函數就是要創建一個新的vm_area_struct結構,並將其與文件的物理磁盤地址相連。具體步驟請看下一節。
mmap內存映射原理
mmap內存映射的實現過程,總的來說可以分為三個階段:
進程啟動映射過程,並在虛擬地址空間中為映射創建虛擬映射區域
- 進程在用戶空間調用庫函數mmap,原型:void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset);
- 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址
- 為此虛擬區分配一個vm_area_struct結構,接着對這個結構的各個域進行了初始化
- 將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中
調用內核空間的系統調用函數mmap(不同於用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
- 為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護着和這個已打開文件相關各項信息。
- 通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型為:int mmap(struct file filp, struct vm_area_struct vma),不同於用戶空間庫函數。
- 內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。
- 通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址並沒有任何數據關聯到主存中。
進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
注:前兩個階段僅在於創建虛擬區間並完成地址映射,但是並沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。
- 程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。
- 缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程。
- 調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。
- 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面並不會立即更新回文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件里了。
vm_struct和vm_area_struct
關於vm_struct和vm_area_struct這兩個結構體,需要簡單說明一下,vm_struct和vm_area_struct都是用於表示一片連續的虛擬地址空間的,但是映射到物理地址空間后可以是不連續的。其次,vm_area_struct表示的虛擬地址是給進程使用的,而vm_struct表示的虛擬地址是給內核使用的。從上面的內容可以知道,內核空間的地址分成三個部分,ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM,其中前面兩部分是用來和物理地址進行一一映射的,而ZONE_HIGHMEM通過臨時借用以及映射的方法管理高於1G的內存,vm_struct所使用的內核虛擬地址就是ZONE_HIGHMEM部分地址。