內(nèi)核空間與用戶空間
Kernel space 是 Linux 內(nèi)核的運行空間栖忠,User space 是用戶程序的運行空間咽瓷。為了安全设凹,它們是隔離的,即使用戶的程序崩潰了茅姜,內(nèi)核也不受影響闪朱。
內(nèi)核空間中存放的是內(nèi)核代碼和數(shù)據(jù)。內(nèi)核空間是操作系統(tǒng)所在區(qū)域钻洒。內(nèi)核代碼有特別的權力:它能與設備控制器通訊奋姿,控制著用戶區(qū)域進程的運行狀態(tài),等等素标。最重要的是称诗,所有 I/O 都直接或間接通過內(nèi)核空間。
用戶空間是常規(guī)進程所在區(qū)域头遭,進程的用戶空間中存放的是用戶程序的代碼和數(shù)據(jù)寓免。
Linux使用兩級保護機制:0級供內(nèi)核使用,3級供用戶程序使用任岸。
當一個任務(進程)執(zhí)行系統(tǒng)調(diào)用而陷入內(nèi)核代碼中執(zhí)行時再榄,我們就稱進程處于內(nèi)核運行態(tài)(內(nèi)核態(tài))。此時處理器處于特權級最高的(0級)內(nèi)核代碼中執(zhí)行享潜,CPU可執(zhí)行任何指令。當進程在執(zhí)行用戶自己的代碼時嗅蔬,則稱其處于用戶運行態(tài)(用戶態(tài))剑按。
32位Linux的虛擬地址空間為0~4G。Linux內(nèi)核將這4G字節(jié)的空間分為兩部分澜术。將最高的1G字節(jié)(從虛擬地址0xC0000000到0xFFFFFFFF)艺蝴,供內(nèi)核使用,稱為“內(nèi)核空間”鸟废。而將較低的3G字節(jié)(從虛擬地址 0x00000000到0xBFFFFFFF)猜敢,供各個進程使用,稱為“用戶空間)。每個進程有各自的私有用戶空間(0~3G)缩擂,這個空間對系統(tǒng)中的其他進程是不可見的鼠冕。最高的1GB字節(jié)虛擬內(nèi)核空間則為所有進程以及內(nèi)核所共享。
str = "my string" // 用戶空間
x = x + 2
file.write(str) // 切換到內(nèi)核空間
y = x + 4 // 切換回用戶空間
上面代碼中胯盯,第一行和第二行都是簡單的賦值運算懈费,在 User space 執(zhí)行。第三行需要寫入文件博脑,就要切換到 Kernel space憎乙,因為用戶不能直接寫文件,必須通過內(nèi)核安排叉趣。第四行又是賦值運算泞边,就切換回 User space。
分頁存儲
操作系統(tǒng)在運行程序時疗杉,需要為每一個進程分配內(nèi)存阵谚。比如A進程需要200m,B進程需要300m乡数,c進程需要100m椭蹄。那么操作系統(tǒng)應該如何為他們分配這些內(nèi)存呢?
一種想法是直接分配連續(xù)的內(nèi)存净赴。操作系統(tǒng)維護一個內(nèi)存列表绳矩,每次申請內(nèi)存時就去這個列表中尋找合適的連續(xù)內(nèi)存塊,分配給用戶進程玖翅。這樣會帶來一個問題翼馆,那就是內(nèi)存碎片化。由于程序申請內(nèi)存的大小是不規(guī)律的金度,在經(jīng)過多次分配之后应媚,內(nèi)存空間就會變得零碎,產(chǎn)生很多不連續(xù)的小的內(nèi)存碎片猜极,這些碎片無法被程序使用(因為碎片化的內(nèi)存不是連續(xù)的中姜,也不夠大)。
可以通過‘緊湊’的方法將這些碎片拼接成可用的大塊內(nèi)存空間跟伏,但是必須要付出很大的開銷丢胚。因此產(chǎn)生了離散化的分配方式:允許直接將一個緊湊直接分散的裝入到許多不相鄰的內(nèi)存塊當中。就可以充分的利用內(nèi)存空間受扳。
離散分配其中之一的分配方式就是分頁:將用戶程序的地址空間分為若干個固定大小的區(qū)域携龟,稱為頁。比如勘高,每個頁為1kb峡蟋。相應的將內(nèi)存空間也分為若干個物理塊坟桅,和頁的大小相同。這樣就可以將用戶程序的任一頁放入任一物理塊當中蕊蝗,實現(xiàn)了離散分配仅乓。
在分頁系統(tǒng)中,允許將進程的各個頁離散的存儲在內(nèi)存的任一物理塊當中匿又,為了保證進程能夠正確運行方灾,即能夠在內(nèi)存中找到每個頁面所對應的物理塊,系統(tǒng)為每一個進程建立了一張頁面映像表碌更,簡稱頁表裕偿。在進程地址空間內(nèi)的所有頁,依次在頁表中有一頁表項痛单,其中記錄了相應頁在內(nèi)存中的物理塊號嘿棘。
在配置了頁表之后,進程執(zhí)行時旭绒,通過查找該表鸟妙,即可找到每頁在內(nèi)存中的物理塊號』映常可見重父,頁表的作用是實現(xiàn)從頁號到物理塊號的地址映射。
虛擬內(nèi)存
所有現(xiàn)代操作系統(tǒng)都使用虛擬內(nèi)存忽匈。虛擬內(nèi)存意為使用虛假(或虛擬)地址取代物理(硬件RAM)內(nèi)存地址房午。這樣做好處頗多,總結起來可分為兩大類:
一個以上的虛擬地址可指向同一個物理內(nèi)存地址丹允。
虛擬內(nèi)存空間可大于實際可用的硬件內(nèi)存郭厌。
那么,這是如何做到的呢雕蔽?
我們會同時運行多個進程折柠,而每個進程占用的內(nèi)存大小不固定,但是這些進程所需要的內(nèi)存大小加起來卻會超過我們實際的物理內(nèi)存(比如4g內(nèi)存)批狐,用戶感覺到的內(nèi)存容量會比實際內(nèi)存容量大的多扇售。這是因為:
應用程序在運行之前沒有必要將之全部裝入內(nèi)存,而僅需將那些當前要運行的少數(shù)頁面裝入內(nèi)存便可運行嚣艇,其余部分暫留在磁盤上缘眶。程序在運行時,如果他要訪問的頁已經(jīng)調(diào)入內(nèi)存髓废,便可繼續(xù)執(zhí)行下去;但如果程序所要訪問的頁面尚未調(diào)入內(nèi)存(缺頁)该抒,便發(fā)出缺頁請求(頁錯誤)慌洪,此時操作系統(tǒng)將利用請求調(diào)頁功能將他們調(diào)入內(nèi)存顶燕,以便程序能夠繼續(xù)執(zhí)行下去。如果此時內(nèi)存已滿冈爹,無法再裝入新的頁涌攻,操作系統(tǒng)還需再利用頁的置換功能,將內(nèi)存中暫時不用的頁調(diào)到磁盤上频伤,騰出足夠的內(nèi)存空間后恳谎,再將要訪問的頁調(diào)入內(nèi)存,使程序繼續(xù)執(zhí)行下去憋肖。這樣因痛,可以使一個或多個大的用戶程序在較小的內(nèi)存空間中運行。
聯(lián)想一下Linux系統(tǒng)在硬盤分區(qū)時需要讓我們選擇一個swap分區(qū)岸更,結合上面的知識鸵膏,可知這個swap分區(qū)就是上面置換時提到的磁盤。摘抄一段百度百科對swap的定義:
Swap分區(qū)在系統(tǒng)的物理內(nèi)存不夠用的時候怎炊,把硬盤空間中的一部分空間釋放出來谭企,以供當前運行的程序使用。那些被釋放的空間可能來自一些很長時間沒有什么操作的程序评肆,這些被釋放的空間被臨時保存到Swap分區(qū)中债查,等到那些程序要運行時,再從Swap分區(qū)中恢復保存的數(shù)據(jù)到內(nèi)存中瓜挽。
因此盹廷,虛擬內(nèi)存的實現(xiàn)利用了上面提到的分頁存儲的方法,同時秸抚,需要存儲系統(tǒng)需要增加頁面置換和頁面調(diào)度功能速和。
我們知道頁表的基本作用就是將用戶地址空間中的邏輯地址映射為內(nèi)存空間中的物理地址,為了滿足頁面的換進換出功能剥汤,在頁表中增加幾個字段:
對上面字段的解釋:
狀態(tài)位P: 由于在請求分頁系統(tǒng)中颠放,只將應用程序的一部分調(diào)入內(nèi)存,還有一部分在磁盤上吭敢,所以需要在頁表中增加一個存在位字段碰凶,指示該夜是否已調(diào)入內(nèi)存,供應用程序參考鹿驼。
訪問字段A:用于記錄本頁在一段時間內(nèi)的訪問次數(shù)欲低,或已有多長時間未被訪問,提供給置換算法在選擇換出頁面時參考畜晰。
修改位M:標識該頁在調(diào)入內(nèi)存后是否被修改過砾莱。由于內(nèi)存中的每一頁都在外存上保留一個副本,因此凄鼻,在置換該頁時腊瑟,若未被修改聚假,就不需要將該頁再寫回到外存,減少磁盤交互的次數(shù)闰非;若已被修改膘格,則必須將該頁重寫到外存上,保證外存中所保留的副本是最新的财松。
外存地址:指出該頁在外存上的地址瘪贱,通常是物理塊號,供調(diào)入該頁時參考辆毡。
回想一下菜秦,在前面 **內(nèi)核空間與用戶空間 這一節(jié)當中,提到了 Linux的虛擬地址空間為0~4G胚迫,從0x00000000到0xFFFFFFFF喷户。這里的虛擬地址,經(jīng)過MMU的轉換访锻,可以映射為物理頁號褪尝。每一個進程都維護自己的虛擬地址,從虛擬地址中分配內(nèi)存期犬,實際上底層將這些虛擬地址河哑,通過查詢頁表映射到物理塊號,然后進行相應的置換或者讀入龟虎。實際上璃谨,是所有的進程共享這些物理內(nèi)存,此時的物理內(nèi)存相當于一個池(聯(lián)想 線程池鲤妥?)佳吞。
IO原理
有了上面的基礎,我們再來看一下操作系統(tǒng)中的IO:
進程使用read()系統(tǒng)調(diào)用棉安,要求其緩沖區(qū)被填滿底扳。內(nèi)核隨即向磁盤控制硬件發(fā)出命令,要求其從磁盤讀取數(shù)據(jù)贡耽。磁盤控制器把數(shù)據(jù)直接寫入內(nèi)核內(nèi)存緩沖區(qū)衷模,這一步通過 DMA 完成,無需主CPU協(xié)助蒲赂。一旦磁盤控制器把緩沖區(qū)裝滿阱冶,內(nèi)核即把數(shù)據(jù)從內(nèi)核空間的臨時緩沖區(qū)拷貝到進程執(zhí)行read()調(diào)用時指定的緩沖區(qū)。
我們可能會覺得滥嘴,把數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間似乎有些多余木蹬。為什么不直接讓磁盤控制器把數(shù)據(jù)送到用戶空間的緩沖區(qū)呢?這樣做有幾個問題若皱。首先届囚,硬件通常不能直接訪問用戶空間有梆。其次,像磁盤這樣基于塊存儲的硬件設備操作的是固定大小的數(shù)據(jù)塊意系,而用戶進程請求的可能是任意大小的或非對齊的數(shù)據(jù)塊。在數(shù)據(jù)往來于用戶空間與存儲設備的過程中饺汹,內(nèi)核負責數(shù)據(jù)的分解蛔添、再組合工作,因此充當著中間人的角色兜辞。
采用分頁技術的操作系統(tǒng)執(zhí)行 I/O 的全過程可總結為以下幾步:
確定請求的數(shù)據(jù)分布在文件系統(tǒng)的哪些頁(磁盤扇區(qū)組)迎瞧。磁盤上的文件內(nèi)容和元數(shù)據(jù)可能跨越多個文件系統(tǒng)頁,而且這些頁可能也不連續(xù)逸吵。
在內(nèi)核空間分配足夠數(shù)量的內(nèi)存頁凶硅,以容納得到確定的文件系統(tǒng)頁。
在內(nèi)存頁與磁盤上的文件系統(tǒng)頁之間建立映射扫皱。
為每一個內(nèi)存頁產(chǎn)生頁錯誤足绅。
虛擬內(nèi)存系統(tǒng)俘獲頁錯誤,安排頁面調(diào)入韩脑,從磁盤上讀取頁內(nèi)容,使頁有效。
一旦頁面調(diào)入操作完成地粪,文件系統(tǒng)即對原始數(shù)據(jù)進行解析荞驴,取得所需文件內(nèi)容或?qū)傩孕畔ⅰ?/p>
內(nèi)存映射文件
傳統(tǒng)的文件 I/O 是通過用戶進程發(fā)布read()和write()系統(tǒng)調(diào)用來傳輸數(shù)據(jù)的。比如FileInputStream.read(byte b[])
进苍,實際上是調(diào)用了read()系統(tǒng)調(diào)用完成數(shù)據(jù)的讀取加缘。回想上一篇文章觉啊,FileInputStream.read(byte b[])
會造成幾次數(shù)據(jù)拷貝呢拣宏?
從磁盤到內(nèi)核緩沖區(qū)的拷貝
內(nèi)核緩沖區(qū)到JVM進程直接緩沖區(qū)的拷貝
JVM直接緩沖區(qū)到
FileInputStream.read(byte b[])
中byte數(shù)組b指向的堆內(nèi)存的拷貝
可見,傳統(tǒng)的IO要經(jīng)歷至少三次數(shù)據(jù)拷貝才可以把數(shù)據(jù)讀出來柄延,即使是使用直接緩沖區(qū)DirectBuffer蚀浆,也需要至少兩次拷貝過程。
我們知道搜吧,設備控制器不能通過 DMA 直接存儲到用戶空間市俊,但是利用虛擬內(nèi)存一個以上的虛擬地址可指向同一個物理內(nèi)存地址這個特點,則可以把內(nèi)核空間地址與用戶空間的虛擬地址映射到同一個物理地址滤奈,這樣摆昧,DMA 硬件(只能訪問物理內(nèi)存地址)就可以填充對內(nèi)核與用戶空間進程同時可見的緩沖區(qū)。
這樣的話蜒程,就省去了內(nèi)核與用戶空間的往來拷貝绅你,但前提條件是伺帘,內(nèi)核與用戶緩沖區(qū)必須使用相同的頁對齊,緩沖區(qū)的大小還必須是磁盤控制器塊大小的倍數(shù)忌锯。
內(nèi)存映射 I/O 使用文件系統(tǒng)建立從用戶空間直到可用文件系統(tǒng)頁的虛擬內(nèi)存映射伪嫁。這樣做有幾個好處:
用戶進程把文件數(shù)據(jù)當作內(nèi)存,所以無需發(fā)布read()或write()系統(tǒng)調(diào)用偶垮。
當用戶進程碰觸到映射內(nèi)存空間张咳,頁錯誤會自動產(chǎn)生,從而將文件數(shù)據(jù)從磁盤讀進內(nèi)存似舵。如果用戶修改了映射內(nèi)存空間脚猾,相關頁會自動標記為臟,隨后刷新到磁盤砚哗,文件得到更新龙助。
操作系統(tǒng)的虛擬內(nèi)存子系統(tǒng)會對頁進行智能高速緩存,自動根據(jù)系統(tǒng)負載進行內(nèi)存管理蛛芥。
數(shù)據(jù)總是按頁對齊的提鸟,無需執(zhí)行緩沖區(qū)拷貝。
大型文件使用映射常空,無需耗費大量內(nèi)存沽一,即可進行數(shù)據(jù)拷貝。
MappedByteBuffer
了解了上面的內(nèi)容漓糙,我們知道在操作系統(tǒng)和硬件層面實際上是為我們提供了內(nèi)存映射文件這樣的機制的铣缠。在java1.4之后,java也提供了對應的接口昆禽,可以讓我們利用操作系統(tǒng)這一特性蝗蛙,提高文件讀寫性能,那就是MappedByteBuffer醉鳖。
MappedByteBuffer繼承自ByteBuffer捡硅,MappedByteBuffer被abstract修飾,所以他不能被實例化盗棵。我們可以調(diào)用FileChannel.map()
方法獲取一個MappedByteBuffer:
FileInputStream inputStream = new FileInputStream(file);
FileChannel channel = inputStream.getChannel();
MappedByteBuffer map = channel.map(MapMode.READ_WRITE, 0, file.length());
這個MappedByteBuffer實際上是其子類DirectByteBuffer實例的引用壮韭。也就是說,我們獲得的MappedByteBuffer實際上是DirectBuffer類型的緩沖區(qū)纹因。也就是說喷屋,使用MappedByteBuffer并不會消耗Java虛擬機內(nèi)存堆。
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {
// 這里僅列出部分API
public abstract MappedByteBuffer map(MapMode mode, long position, long size)
public static class MapMode
{
public static final MapMode READ_ONLY
public static final MapMode READ_WRITE
public static final MapMode PRIVATE
}
}
我們可以創(chuàng)建一個MappedByteBuffer來代表一個文件中字節(jié)的某個子范圍瞭恰。例如屯曹,要映射100到299(包含299)位置的字節(jié),可以使用下面的代碼:buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 100, 200);
如果要映射整個文件則使用:buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
文件映射可以是可寫的或只讀的。前兩種映射模式MapMode.READ_ONLY和MapMode.READ_WRITE意義是很明顯的恶耽,它們表示希望獲取的映射只讀還是允許修改映射的文件密任。請求的映射模式將受被調(diào)用map()方法的FileChannel對象的訪問權限所限制。如果通道是以只讀的權限打開的卻請求MapMode.READ_WRITE模式偷俭,那么map()方法會拋出一個NonWritableChannelException異常浪讳;如果在一個沒有讀權限的通道上請求MapMode.READ_ONLY映射模式,那么將產(chǎn)生NonReadableChannelException異常社搅。
第三種模式MapMode.PRIVATE表示想要一個寫時拷貝(copy-on-write)的映射驻债。這意味著通過put()方法所做的任何修改都會導致產(chǎn)生一個私有的數(shù)據(jù)副本并且該副本中的數(shù)據(jù)只有MappedByteBuffer實例可以看到。該過程不會對底層文件做任何修改形葬。盡管寫時拷貝的映射可以防止底層文件被修改,但也必須以read/write權限來打開文件以建立MapMode.PRIVATE映射暮的。只有這樣笙以,返回的MappedByteBuffer對象才能允許使用put()方法。
一個映射一旦建立之后將保持有效冻辩,直到MappedByteBuffer對象被施以垃圾收集動作為止猖腕。關閉相關聯(lián)的FileChannel不會破壞映射,只有丟棄緩沖區(qū)對象本身才會破壞該映射恨闪。
MappedByteBuffer主要用在對大文件的讀寫或?qū)崟r性要求比較高的程序當中倘感。
For most operating systems, mapping a file into memory is more expensive than reading or writing a few tens of kilobytes of data via the usual read and write methods. From the standpoint of performance it is generally only worth mapping relatively large files into memory.
參考
《計算機操作系統(tǒng)(第四版)》 西安電子科技大學出版社