Android內存映射

內存映射

想理解這個知識點,我們首先要知道內存的概念和映射的概念。

內存的基本概念

我們先看一張計算的組成圖:

在這裏插入圖片描述
內存一般分爲只讀存儲器(ROM)和隨機存儲器(RAM),以及最強悍的高速緩衝存儲器(CACHE),只讀存儲器應用廣泛,它通常是一塊在硬件上集成的可讀芯片,作用是識別與控制硬件,它的特點是隻可讀取,不能寫入。隨機存儲器的特點是可讀可寫,斷電後一切數據都消失,這兩者一起就構成了我們硬件上的內存條就是我們常說的4G 8G。

高速緩衝存儲器就是數據交換的緩衝區(稱作Cache),當某一硬件要讀取數據時,會首先從緩存中查找需要的數據,如果找到了則直接執行,找不到的話則從內存中找。由於緩存的運行速度比內存快得多(接近CPU內部速度),故緩存的作用就是幫助硬件更快地運行,緩解CPU和主存之間的速度差異。

cpu不能直接訪問硬盤的數據,只能通過把硬盤的數據先放到內存裏, 然後再從內存裏訪問硬盤的數據。

在這裏插入圖片描述
主存就要物理內存,真實的插在板子上的內存是多大就是多大了。而在CPU中的概念,物理內存就是CPU的地址線可以直接進行尋址的內存空間大小。

地址線是用來傳輸地址信息用的。舉個簡單的例子:cpu在內存或硬盤裏面尋找一個數據時通過地址總線找到該內存單元,然後通過控制總線確定操作方法,在通過數據總線將其數據送回來 如果有地址線32根.就可以訪問2的32次方的空間,也就是4GB。

如何找到內存?

光有內存還不夠,還需要找到它,才能使用起來。這裏有個專業的叫法:尋址,分爲物理尋址,和虛擬尋址。

尋址空間一般指的是CPU對於內存尋址的能力。通俗地說,就是能最多用到多少內存的一個問題。數據在存儲器(RAM)中存放是有規律的 ,CPU在運算的時候需要把數據提取出來就需要知道數據在那裏 ,這時候就需要挨家挨戶的找,這就叫做尋址,但如果地址太多超出了CPU的能力範圍,CPU就無法找到數據了。 CPU最大能查找多大範圍的地址叫做尋址能力 ,CPU的尋址能力以字節爲單位。 通常人們認爲,內存容量越大,處理數據的能力也就越強,但內存容量不可能無限的大,它要受到系統結構、硬件設計、製造成本等多方面因素的制約,一個最直接的因素取決於系統的地址總線的地址寄存器的寬度(位數)。 計算機的尋找範圍由總線寬度(處理器的地址總線的位數)決定的,也可以理解爲cpu寄存器位數,這二者一般是匹配的

物理尋址:內存通常被組織爲一個由M個連續的字節大小的單元組成的數組,每個字節都有一個唯一的物理地址(Physical Address PA),作爲到數組的索引。CPU訪問內存最簡單直接的方法就是使用物理地址,這種尋址方式被稱爲物理尋址。

虛擬尋址:現代處理器使用的是一種稱爲虛擬尋址(Virtual Addressing)的尋址方式。使用虛擬尋址,CPU需要將虛擬地址翻譯成物理地址,這樣才能訪問到真實的物理內存。怎麼翻譯等下說。

虛擬內存

上面所說的還是物理內存,我們當然可以直接使用物理內存運行程序,但是這樣會有幾個問題:

1.現在有多個程序需要運行,但是內存空間不足了,就需要將其他程序暫時拷貝到硬盤當中,然後將新的程序裝入內存運行.由於大量的數據裝入裝出,內存的使用效率會十分低。

2.由於程序都是直接訪問物理內存的,所以一個進程可以修改其他進程的內存數據,甚至修改內核地址空間中的數據。

3.因爲內存地址是隨機分配的,所以程序運行的地址也是不正確的。

爲了解決以上問題,虛擬內存就出來了。程序不再直接使用物理內存。而是使用 操作linux給每個進程分配的虛擬內存。此內存結構:

在這裏插入圖片描述

內核區:用戶代碼不可見的區域,頁表就存放在這個區域中。 用戶區:

a、只讀段:只可讀,不可寫,程序代碼段。

b、數據段:保存全局變量,靜態變量的區域。

c、堆區:就是動態內存,通過malloc,new申請內存,有一個堆指針,可以通過brk系統調用調整堆指針。

d、文件映射區域:通過mmap系統調用,如動態庫,共享內存等映射物理空間的內存區域。可以單獨釋放,不會產生內存碎片。

e、棧區:用於維護函數調用的上下文空間,用ulimit -s 查看。一般默認爲8M

Stack空間(進棧和出棧)由操作系統控制,其中主要存儲函數地址、函數參數、局部變量等等,所以Stack空間不需要很大,一般爲幾MB大小。

Heap空間由程序控制,程序員可以使用malloc、new、free、delete等函數調用來操作這片地址空間。Heap爲程序完成各種複雜任務提供內存空間,所以空間比較大,一般爲幾百MB到幾GB。正是因爲Heap空間由程序員管理,所以容易出現使用不當導致嚴重問題。

進程內存分配

每個進程運行的時候,都會拿到4G的虛擬內存,在32位Linux下,其中3G是交給用戶的,1G是交給內核的,而task_struct就是存儲在這1G的內核系統空間中。

  1. 每個進程都有各自的私有用戶空間(0-3G),這個空間對系統中的其他進程是不可見的。
  2. 最高的1GB內核空間則爲所有進程以及內核所共享。
  3. 至於爲什麼需要這個1G的內核空間,是因爲進程需要調用一些系統調用,來交給內核跑,程序的一部分邏輯可能是要交給內核去跑的,所以一部分虛擬地址必須要留給內核使用。
    在這裏插入圖片描述
    於是,通過上圖我們可以發現,每個進程的PCB都是存在所有進程共享的內核空間的中,這也就是我們說操作系統管理進程,也就是在內核空間中管理的,在內核空間中通過鏈表管理所有進程的PCB,如果有一個進程要被創建,實際上多分配了這麼一個4G的虛擬內存,並在共享的內核空間中的雙向鏈表中加入了自己的PCB。

什麼是進程和PCB

對於一個進程,它在被執行前其實是一個可執行程序。這個程序是被放在磁盤上的,當它要被執行的時候,它先被加載到內存當中,然後再放入到寄存器中,最後再讓cpu執行該程序,這個時候一個靜態的程序就變成了進程。

那麼操作系統是怎麼來管理這些進程的呢?操作系統通過一個雙向鏈表把進程連起來。但是,對於進程其實它是一個抽象的概念,系統肯定要通過一個東西來描述進程,然後才能管理進程。於是PCB就出來了,操作系統通過PCB來描述進程,於是這個雙向鏈表連接的其實是PCB(progress control block)進程控制塊,它就是一個C語言中的結構體,用來描述進程,在Linux下,就是task_struct結構體。 主要有以下內容:

標識相關:pid,ppid等等
文件相關:進程需要記錄打開的文件信息,於是需要文件描述符表
內存相關:內存指針,指向進程的虛擬地址空間(用戶空間)信息
優先級相關:進程相對於其他進程的調度優先級
上下文信息相關:CPU的所有寄存器中的值、進程的狀態以及
堆棧上的內容,當內核需要切換到另一個進程時,需要保存當前進程的所有狀態,即保存當前進程的進程上下文,以便再次執行該進程時,能夠恢復切換時的狀態,繼續執行。
狀態相關:進程當前的狀態,說明該進程處於什麼狀態
信號相關:進程的信號處理函數,以及記錄當前進程是否還有待處理的信號
9.I/O相關:記錄進程與各種I/O設備之間的交互

映射

兩個非空集合A與B間存在着對應關係f,而且對於A中的每一個元素x,B中總有有唯一的一個元素y與它對應,就這種對應爲從A到B的映射,記作f:A→B。其中,b稱爲元素a在映射f下的象,記作:b=f(a)。a稱爲b關於映射f的原象。集合A中所有元素的象的集合稱爲映射f的值域,記作f(A)

這個是數學上的定義,在計算中也是類似。hashmap就是映射,哈希函數就是 映射關係。這個函數你可以自己定義。簡答來說就是 集合A和集合B是映射關係的話,你通過集合A和映射函數就能得到B。建立某種關係 是的2者 對其他人來說就是同一個。不正確的描述:2個池子A B。如果沒有建立映射,那麼我存取水,要分別 往A B池子倒水和抽水。但是如果我 把池子A和池子B用一根水管連接起來 建立了映射關係。那麼當我想從B池子倒水取水時,我往A池子倒水取水就行了。

內存映射

總算到主題了,虛擬內存畢竟只是虛擬的真實是不存在的。只是邏輯上存在。如果有效果那就需要把虛擬內存和真實的物理內存 建立起聯繫。這樣 進程訪問 虛擬內存的地址 比如0x00008 經過處理後 可以訪問到 物理內存的地址 比如0x00004。這個建立聯繫的過程就叫映射。處理就叫做地址翻譯。

內存映射:指物理內存與虛擬內存建立映射關係。使得進程A訪問虛擬內存時就像訪問物理內存一樣。

到這裏大家肯定會有很多疑問如:

這個映射關係具體是怎麼樣的?
要是物理內存不夠怎麼辦?
在回答這些問題前,我們需要了解 linux操作系統是怎麼去管理內存的。

Linux管理內存方式。

常見的內存管理方式有塊式管理、頁式管理、段式和段頁式管理。

(1)塊式管理:把主存分爲一大塊一大塊的,當所需的程序片段不在主存時就分配一塊主存空間,把程序片段load入主存,就算所需的程序片段只有幾個字節也只能把這一塊分配給它。這樣會造成很大的浪費,平均浪費了50%的內存空間,但是易於管理。

(2)頁式管理:把主存分爲一頁一頁的,每一頁的空間要比一塊一塊的空間小很多,這種方法的空間利用率要比塊式管理高很多

(3)段式管理:把主存分爲一段一段的,每一段的空間又要比一頁一頁的空間小很多,這種方法在空間利用率上又比頁式管理高得多,但是也有另外一個缺點。一個程序片段可能會被分爲幾十段,這樣很多時間就會被浪費在計算每一段的物理地址上。

(4)段頁式管理:結合了段式管理和頁式管理的優點。把主存先分成若干段,每個段又分成若干頁。段頁式管理每取一護具,要訪問3次內存。

計算機會對虛擬內存地址空間(32位爲4G)分頁產生頁(page),對物理內存地址空間(假設256M)分頁產生頁幀(page frame),這個頁和頁幀的大小是一樣大的都是4KB,所以呢,在這裏,虛擬內存頁的個數勢必要大於物理內存頁幀的個數。在計算機上有一個頁表(page table),就是映射虛擬內存頁到物理內存頁的,更確切的說是頁號到頁幀號的映射,而且是一對一的映射。這裏要好好理解。

但是問題來了,虛擬內存頁的個數 > 物理內存頁幀的個數,豈不是有些虛擬內存頁的地址永遠沒有對應的物理內存地址空間?不是的,操作系統是這樣處理的。操作系統有個頁面失效(page fault)功能。操作系統找到一個最少使用的頁幀,讓他失效,並把它寫入磁盤,隨後把需要訪問的頁放到頁幀中,並修改頁表中的映射,這樣就保證所有的頁都有被調度的可能了。這就是處理虛擬內存地址到物理內存的步驟。這就回答了第一個問題。

虛擬內存地址由頁號和偏移量組成。頁號對應的映射到一個頁幀。那麼,說說偏移量。偏移量就是我上面說的頁(或者頁幀)的大小,即這個頁(或者頁幀)到底能存多少數據。舉個例子,有一個虛擬地址它的頁號是4,偏移量是20,那麼他的尋址過程是這樣的:首先到頁表中找到頁號4對應的頁幀號(比如爲8),如果頁不在內存中,則用失效機制調入頁,否則把頁幀號和偏移量傳給MMU(CPU的內存管理單元)組成一個物理上真正存在的地址,接着就是訪問物理內存中的數據了。總結起來說,虛擬內存地址的大小是與地址總線位數相關,物理內存地址的大小跟物理內存條的容量相關。

進程是如何訪問物理內存

進程開始要訪問一個地址,它可能會經歷下面的過程

每次我要訪問地址空間上的某一個地址,都需要把地址翻譯爲實際物理內存地址
所有進程共享這整一塊物理內存,每個進程只把自己目前需要的虛擬地址空間映射到物理內存上
進程需要知道哪些地址空間上的數據在物理內存上,哪些不在(可能這部分存儲在磁盤上),還有在物理內存上的哪裏,這就需要通過頁表來記錄
頁表的每一個表項分兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(如果在的話)
當進程訪問某個虛擬地址的時候,就會先去看頁表,如果發現對應的數據不在物理內存上,就會發生缺頁異常
缺頁異常的處理過程,操作系統立即阻塞該進程,並將硬盤裏對應的頁換入內存,然後使該進程就緒,如果內存已經滿了,沒有空地方了,那就找一個頁覆蓋,至於具體覆蓋的哪個頁,就需要看操作系統的頁面置換算法是怎麼設計的了。

在這裏插入圖片描述
比如:

我們的cpu想訪問虛擬地址所在的虛擬頁(VP3),根據頁表,找出頁表中第三條的值.判斷有效位。如果有效位爲1,DRMA緩存命中,根據物理頁號,找到物理頁當中的內容,返回。
若有效位爲0,參數缺頁異常,調用內核缺頁異常處理程序。內核通過頁面置換算法選擇一個頁面作爲被覆蓋的頁面,將該頁的內容刷新到磁盤空間當中。然後把VP3映射的磁盤文件緩存到該物理頁上面。然後頁表中第三條,有效位變成1,第二部分存儲上了可以對應物理內存頁的地址的內容。
缺頁異常處理完畢後,返回中斷前的指令,重新執行,此時緩存命中,執行1。
將找到的內容映射到告訴緩存當中,CPU從告訴緩存中獲取該值,結束。
簡單說就是:當每個進程創建的時候,內核會爲進程分配4G的虛擬內存,當進程還沒有開始運行時,這只是一個內存佈局。實際上並不立即就把虛擬內存對應位置的程序數據和代碼(比如.text .data段)拷貝到物理內存中,只是建立好虛擬內存和磁盤文件之間的映射就好(叫做存儲器映射)。這個時候數據和代碼還是在磁盤上的。當運行到對應的程序時,進程去尋找頁表,發現頁表中地址沒有存放在物理內存上,而是在磁盤上,於是發生缺頁異常,於是將磁盤上的數據拷貝到物理內存中。

另外在進程運行過程中,要通過malloc來動態分配內存時,也只是分配了虛擬內存 new也是如此,即爲這塊虛擬內存對應的頁表項做相應設置,當進程真正訪問到此數據時,才引發缺頁異常。

可以認爲虛擬空間都被映射到了磁盤空間中(事實上也是按需要映射到磁盤空間上,通過mmap,mmap是用來建立虛擬空間和磁盤空間的映射關係的)

利用虛擬內存機制的優點

既然每個進程的內存空間都是一致而且固定的(32位平臺下都是4G),所以鏈接器在鏈接可執行文件時,可以設定內存地址,而不用去管這些數據最終實際內存地址,這交給內核來完成映射關係

當不同的進程使用同一段代碼時,比如庫文件的代碼,在物理內存中可以只存儲一份這樣的代碼,不同進程只要將自己的虛擬內存映射過去就好了,這樣可以節省物理內存

在程序需要分配連續空間的時候,只需要在虛擬內存分配連續空間,而不需要物理內存時連續的,實際上,往往物理內存都是斷斷續續的內存碎片。這樣就可以有效地利用我們的物理內存

使用虛存也是有代價的,主要表現在以下幾個方面: (1)虛存的管理需要建立很多數據結構,這些數據結構要佔用額外的內存 (2)虛擬地址到物理地址的轉換,增加了指令的執行時間 (3)頁面的換入換出需要磁盤I/O,這是很耗時間的。 (4)如果一頁中只有一部分數據,會很浪費內存。

總結

存儲單元一般應具有存儲數據和讀寫數據的功能,一般以8位二進制作爲一個存儲單元,也就是一個字節。每個單元有一個地址,是一個整數編碼,可以表示爲二進制整數。 程序中的變量和主存儲器的存儲單元相對應。變量的名字對應着存儲單元的地址,變量內容對應着單元所存儲的數據。

由程序產生的地址被稱爲虛擬地址,它們構成了一個虛擬地址空間。在使用虛擬存儲器的情況下,虛擬地址不是被直接送到內存總線上,而且是被送到內存管理單元(Memory Management Unt,MMU),MMU把虛擬地址映射爲物理內存地址。

虛擬內存空間被組織爲一個存放在硬盤上的M個連續的字節大小的單元組成的數組,每個字節都有一個唯一的虛擬地址,作爲到數組的索引(這點其實與物理內存是一樣的)。虛擬尋址需要硬件與操作系統之間互相合作。CPU中含有一個被稱爲內存管理單元(Memory Management Unit, MMU)的硬件,它的功能是將虛擬地址轉換爲物理地址。MMU需要藉助存放在內存中的頁表來動態翻譯虛擬地址,該頁表由操作系統管理。

操作系統通過將虛擬內存分割爲大小固定的塊來作爲硬盤和內存之間的傳輸單位,這個塊被稱爲虛擬頁(Virtual Page, VP),每個虛擬頁的大小爲P=2^p字節。物理內存也會按照這種方法分割爲物理頁(Physical Page, PP),大小也爲P字節。

頁表是一種數組結構,存放着各個虛擬頁的狀態,是否映射,是否緩存.進程要知道那些內存地址上的數據在物理內存上,那些不在,還有物理內存上的哪裏需要用頁表來記錄.頁表的每一個表項分爲兩部分,第一部分記錄此頁是否在物理內存上,第二部分記錄物理內存頁的地址(如果在的話)當進程訪問某個虛擬地址,去查看頁表的時候如果發現對應的數據不在物理內存中,則發生缺頁異常.但是我們馬上就要使用這個數據啊.所以我們需要儘快解決掉缺頁異常.缺頁異常的處理過程:就是把進程需要的數據從磁盤上面拷貝到物理內存中,如果物理內存已經滿了,沒有空地方了,那就找一個頁覆蓋,當然如果被覆蓋的頁曾經被修改過,需先將此頁寫回磁盤.頁表的狀態>> 內存管理器會將物理內存頁(通常大小爲 4 KB)保存到磁盤文件。數據或代碼頁會根據需要在物理內存與磁盤之間移動。

mmap() 文件映射

void* mmap ( void * addr , size_t len , int prot , int flags , int fd , off_t offset )

mmap的作用是映射文件描述符fd指定文件的 [off,off + len]區域至調用進程的[addr, addr + len]的內存區域。

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的映射關係後,進程就可以採用指針的方式讀寫操作這一段內存,而系統會自動回寫髒頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。 如:

在這裏插入圖片描述

參數fd爲即將映射到進程空間的文件描述字,一般由open()返回,同時,fd可以指定爲-1,此時須指定flags參數中的

MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關係的

進程間通信)。

len是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。

prot 參數指定共享內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀) , PROT_WRITE (可寫), PROT_EXEC (可執行), PROT_NONE(不可訪問)。

flags由以下幾個常值指定:MAP_SHARED , MAP_PRIVATE , MAP_FIXED,其中,MAP_SHARED , MAP_PRIVATE必

選其一,而MAP_FIXED則不推薦使用。

offset參數一般設爲0,表示從文件頭開始映射。

參數addr指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。函

數的返回值爲最後文件映射到進程空間的地址,進程可直接操作起始地址爲該值的有效地址。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章