歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

關於linux內存管理 原理知識詳解

linux的內存管理主要分為兩部分:物理地址到虛擬地址的映射,內核內存分配管理(主要基於slab)。


1、概念
  物理地址(physical address)

  用於內存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應。——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機器上那根內存本身,把內存看成一個從0字節一直到最大空量逐字節的編號的大數組,然後把這個數組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內存的尋址方式並不是這樣。所以,說它是“與地址總線相對應”,是更貼切一些,不過拋開對物理內存尋址方式的考慮,直接 把物理地址與物理的內存一一對應,也是可以接受的。也許錯誤的理解更利於形而上的抽像。

  虛擬內存(virtual memory)

  這是對整個內存(不要與機器上插那條對上號)的抽像描述。它是相對於物理內存來講的,可以直接理解成“不直實的”,“假的”內存,例如,一個0×08000000內存地址,它並不對就物理地址上那個大數組中0×08000000 – 1那個地址元素;

  之所以是這樣,是因為現代操作系統都提供了一種內存管理的抽像,即虛擬內存(virtual memory)。進程使用虛擬內存中的地址,由操作系統協助相關硬件,把它“轉換”成真正的物理地址。這個“轉換”,是所有問題討論的關鍵。有了這樣的抽像,一個程序,就可以使用比真實物理地址大得多的地址空間。(拆東牆,補西牆,銀行也是這樣子做的),甚至多個進程可以使用相同的地址。不奇怪,因為轉換 後的物理地址並非相同的。可以把連接後的程序反編譯看一下,發現連接器已經為程序分配了一個地址,例如,要調用某個函數A,代碼不是call A,而是call 0×0811111111 ,也就是說,函數A的地址已經被定下來了。沒有這樣的“轉換”,沒有虛擬地址的概念,這樣做是根本行不通的。

  打住了,這個問題再說下去,就收不住了。

  邏輯地址(logical address)

  Intel為了兼容,將遠古時代的段式內存管理方式保留了下來。邏輯地址指的是機器語言指令中,用來指定一個操作數或者是一條指令的地址。以上例,我們說的連接器為A分配的0×08111111這個地址就是邏輯地址。——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯地址要求,“一個邏輯地址,是由一個段標識符加上一個指定段內相對地址的偏移量,表示為 [段標識符:段內偏移量],也就是說,上例中那個0×08111111,應該表示為[A的代碼段標識符: 0x08111111],這樣,才完整一些”

  線性地址(linear address)或也叫虛擬地址(virtual address)

  跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬件平台段式管理轉換前地址的話,那麼線性地址則對應了硬件頁式內存的轉換前地址。

  CPU將一個虛擬內存空間中的地址轉換為物理地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內偏移量,這個一定要理解!!!),CPU 要利用其段式內存管理單元,先將為個邏輯地址轉換成一個線程地址,再利用其頁式內存管理單元,轉換為最終物理地址。這樣做兩次轉換,的確是非常麻煩而且沒有必要的,因為直接可以把線性地址抽像給進程。之所以這樣冗余,Intel完全是為了兼容而已。

  2、CPU段式內存管理,邏輯地址如何轉換為線性地址

  一個邏輯地址由兩部份組成,段標識符: 段內偏移量。段標識符是由一個16位長的字段組成,稱為段選擇符。其中前13位是一個索引號。後面3位包含一些硬件細節,如圖:

最後兩位涉及權限檢查,本貼中不包含。

  索引號,或者直接理解成數組下標——那它總要對應一個數組吧,它又是什麼東東的索引呢?這個東東就是“段描述符(segment descriptor)”,呵呵,段描述符具體地址描述了一個段(對於“段”這個字眼的理解,我是把它想像成,拿了一把刀,把虛擬內存,砍成若干的截—— 段)。這樣,很多個段描述符,就組了一個數組,叫“段描述符表”,這樣,可以通過段標識符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描 述符就描述了一個段,我剛才對段的抽像不太准確,因為看看描述符裡面究竟有什麼東東——也就是它究竟是如何描述的,就理解段究竟有什麼東東了,每一個段描 述符由8個字節組成,如下圖:


這些東東很復雜,雖然可以利用一個數據結構來定義它,不過,我這裡只關心一樣,就是Base字段,它描述了一個段的開始位置的線性地址。

  Intel設計的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個進程自己的,就放在所謂的“局部段 描述符表(LDT)”中。那究竟什麼時候該用GDT,什麼時候該用LDT呢?這是由段選擇符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

  GDT在內存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。好多概念,像繞口令一樣。這張圖看起來要直觀些:


 首先,給定一個完整的邏輯地址[段選擇符:段內偏移地址],

  1、看段選擇符的T1=0還是1,知道當前要轉換是GDT中的段,還是LDT中的段,再根據相應寄存器,得到其地址和大小。我們就有了一個數組了。

  2、拿出段選擇符中前13位,可以在這個數組中,查找到對應的段描述符,這樣,它了Base,即基地址就知道了。

  3、把Base + offset,就是要轉換的線性地址了。

  還是挺簡單的,對於軟件來講,原則上就需要把硬件轉換所需的信息准備好,就可以讓硬件來完成這個轉換了。OK,來看看Linux怎麼做的。

  3、Linux的段式管理

  Intel要求兩次轉換,這樣雖說是兼容了,但是卻是很冗余,呵呵,沒辦法,硬件要求這樣做了,軟件就只能照辦,怎麼著也得形式主義一樣。

  另一方面,其它某些硬件平台,沒有二次轉換的概念,Linux也需要提供一個高層抽像,來提供一個統一的界面。所以,Linux的段式管理,事實上只是“哄騙”了一下硬件而已。按照Intel的本意,全局的用GDT,每個進程自己的用LDT——不過Linux則對所有的進程都使用了相同的段來對 指令和數據尋址。即用戶數據段,用戶代碼段,對應的,內核中的是內核數據段和內核代碼段。這樣做沒有什麼奇怪的,本來就是走形式嘛,像我們寫年終總結一樣。
include/asm-i386/segment.h

#define GDT_ENTRY_DEFAULT_USER_CS        14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
#define GDT_ENTRY_DEFAULT_USER_DS        15
#define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
#define GDT_ENTRY_KERNEL_BASE        12
#define GDT_ENTRY_KERNEL_CS                (GDT_ENTRY_KERNEL_BASE + 0)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
#define GDT_ENTRY_KERNEL_DS                (GDT_ENTRY_KERNEL_BASE + 1)
#define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)

把其中的宏替換成數值,則為:

#define __USER_CS 115        [00000000 1110  0  11]
#define __USER_DS 123        [00000000 1111  0  11]
#define __KERNEL_CS 96      [00000000 1100  0  00]
#define __KERNEL_DS 104    [00000000 1101  0  00]

方括號後是這四個段選擇符的16位二制表示,它們的索引號和T1字段值也可以算出來了

__USER_CS              index= 14   T1=0
__USER_DS               index= 15   T1=0
__KERNEL_CS           index=  12  T1=0
__KERNEL_DS           index= 13   T1=0

T1均為0,則表示都使用了GDT,再來看初始化GDT的內容中相應的12-15項(arch/i386/head.S):

        .quad 0x00cf9a000000ffff        /* 0×60 kernel 4GB code at 0×00000000 */
        .quad 0x00cf92000000ffff        /* 0×68 kernel 4GB data at 0×00000000 */
        .quad 0x00cffa000000ffff        /* 0×73 user 4GB code at 0×00000000 */
        .quad 0x00cff2000000ffff        /* 0x7b user 4GB data at 0×00000000 */

  按照前面段描述符表中的描述,可以把它們展開,發現其16-31位全為0,即四個段的基地址全為0。

  這樣,給定一個段內偏移地址,按照前面轉換公式,0 +段內偏移,轉換為線性地址,可以得出重要的結論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的偏移量字段的值與線性地址的值總是相同的。!!!”

  忽略了太多的細節,例如段的權限檢查。呵呵。Linux中,絕大部份進程並不例用LDT,除非使用Wine ,仿真Windows程序的時候。

Copyright © Linux教程網 All Rights Reserved