虚拟内存概念3
1 物理內存
1.1 物理內存概述
1.2 直接使用物理內存的問題
1.2.1 多進程地址布局困難
1.2.2 進程地址空間小
1.2.3 程序鏈接不統一
2 虛擬內存
2.1 引入虛擬內存的目的
2.2 局部性原理與虛擬內存
2.3 虛擬內存到物理內存的映射
2.3.1 概述
2.3.2 頁面分配與映射
2.4 頁表結構
2.4.1 概述
2.4.2 頁表
2.4.3 頁目錄表
2.4.4 虛擬地址翻譯過程
3 頁面換入換出
4 內存的段式管理與頁式管理
4.1 段式管理
4.1.1 特征
4.1.2 優點
4.1.3 缺點
4.2 頁式管理
4.2.1 特征
4.2.2 優點
4.3 使用現狀
5 內存布局
5.1 抽象內存布局
5.1.1 內存布局組成
5.1.2 磁盤程序段與內存程序段
5.2 IA-32 + Linux的進程內存布局
5.3 Intel 64 + Linux的進程內存布局
5.3.1 使用部分地址線
5.3.2 canonical address
1?物理內存
1.1?物理內存概述
1. 物理內存由多個連續的存儲單元組成,每個單元稱為一個字節
2. 每個字節有一個唯一的物理地址(Physical Address,PA),地址編碼從0開始
3. 在早期的體系結構中(e.g. X86實模式),程序直接使用物理地址。也就是說,程序中每個數據存儲在內存中的位置,都由程序員負責
1.2?直接使用物理內存的問題
1.2.1?多進程地址布局困難
由于系統中存在多個進程,每個進程分配多少內存、如何保證指令中訪問內存地址的正確性并且與其他進程不沖突,都需要程序員完成。相當于將linker和loader的工作全部交給程序員手動完成
1.2.2?進程地址空間小
由于是多個進程共享物理內存,所以需要對每個進程可使用的物理內存區域進行分配,因此每個進程的地址空間都很小(最大也不會超過當前可使用的物理內存)
1.2.3?程序鏈接不統一
1. 對于程序而言,鏈接地址和運行地址需要一致,才能確保程序中的地址相關操作正確執行
2. 由于進程被分配在不同的物理地址運行,所以在鏈接時需要指定相應的鏈接地址。或者說,程序需要加載到鏈接地址處運行
說明:上述情況是針對沒有分段機制的處理器而言的,對于有分段機制的處理器(e.g. X86),程序可以使用統一的鏈接方式(e.g. 程序中的每個段都從0地址開始鏈接),此時程序中使用相對于段起始位置的偏移地址而不是使用絕對地址
在加載程序時,將程序中不同的段加載到當前空閑的物理內存中,之后用段寄存器記錄段的物理起始地址,即可以實現程序的重定位
相關內存可參考X86匯編語言從實模式到保護模式01:處理器、內存和指令_麥小兜的博客-CSDN博客?chapter 2,相關加載器的實現可參考X86匯編語言從實模式到保護模式07:硬盤和顯卡的訪問控制_麥小兜的博客-CSDN博客
2?虛擬內存
2.1?引入虛擬內存的目的
虛擬內存的出現,就是為了解決直接使用物理內存的問題
1. 使得每個進程有不依賴物理內存大小的虛擬地址空間,這個空間可以比可用的物理內存大得多
2. 使得每個進程的虛擬地址空間是私有的、獨立的,與其他進程的虛擬地址空間相互隔離,這就解決了多進程之間地址沖突的問題
3. 由于虛擬地址空間是進程獨占的,可以任意使用,因此可以將變量和函數分配地址的任務交給鏈接器自動安排
也就是說,每個進程的虛擬地址空間布局是相同的,因此鏈接器可以按統一的方式鏈接不同程序
2.2?局部性原理與虛擬內存
處理器和操作系統是基于局部性原理(Principle of locality)為程序員虛擬化了一層內存,也就是虛擬內存。局部性原理有如下2方面,
1. 時間局部性:被訪問過一次的內存地址很可能在不遠的將來會被再次訪問
2. 空間局部性:如果一個內存地址被訪問過,那么與他臨近的地址在不遠的將來也很可能會被訪問
從局部性原理就可以得出結論:無論一個進程占用的內存資源有多大,在任一時刻,他需要的物理內存都是很少的
在這個推論的基礎上,處理器只要為每個進程保留很少的物理內存就可以保證進程的正常執行
2.3?虛擬內存到物理內存的映射
2.3.1?概述
1. 任何虛擬內存中的的數據,最終還是要保存在真實的物理內存中。也就是說,虛擬內存需要映射到物理內存
2. 虛擬內存的大小遠大于物理內存的大小
3. 基于局部性原理,處理器和操作系統只將當前使用的虛擬內存映射到物理內存
2.3.2?頁面分配與映射
1. 首先,虛擬內存與物理內存的映射以頁為單位,常見的頁大小為4KB
2. 雖然虛擬內存提供了很大的地址空間,但是在進程啟動后,這些空間并不是全部被使用,而是處于未分配狀態
3. 當程序中通過malloc等內存分配接口獲取內存時,相應的虛擬內存頁面將從未分配轉變為已分配但未映射狀態
4. 當對這段分配到的虛擬內存進行讀寫時,操作系統才會在缺頁異常中為他們分配物理內存,當映射關系建立后,該頁面轉變為正常頁面
可見虛擬內存實現一個假象,讓程序員覺得整個虛擬內存空間可以隨時訪問,但真實的數據可能不在物理內存中,而是在需要用到時才被加載到內存中,并建立虛擬內存到物理內存的映射
說明1:虛擬地址中的"虛擬",是指不存在但能看見
①?本質上,虛擬地址是一套虛擬內存分配與映射機制,真正操作的還是物理內存。所以說虛擬內存本身是不存在的
② 但是對程序員而言,能直接操作的就是虛擬地址,因此是能看見的
說明2:引入虛擬內存后,分配內存分為2級
① 首先是在進程的虛擬地址空間中分配虛擬內存
② 之后是分配物理內存,并建立虛擬內存與物理內存的映射關系
分配物理內存被推遲到對虛擬內存的訪問觸發缺頁異常時才進行,實現了按需加載
說明3:當虛擬內存已分配但未映射時,他所對應的數據在哪里?
① 可能在磁盤上,比如文件
② 也可能是申請但未訪問的內存,比如malloc分配一個數組
說明4:在虛擬內存中連續的頁面,在物理內存中不必是連續的。只要維護好從虛擬內存到物理內存的映射關系,就能正確使用內存
說明5:虛擬地址空間的大小
虛擬地址空間的大小一般由處理器字寬決定,
① 對于32位處理器,寄存器是32位的,可以存儲32位指針,因此能表示的地址范圍為0 ~ 4GB
② 對于64位處理器,寄存器是64位的,可以存儲64位指針。但是一般并不實際使用全部位數,比如只使用低48位,此時的虛擬地址空間為256TB
說明6:有不遵循局部性原理的程序嗎?
① 這種局部性不好的程序是存在的,他會導致處理器頻繁進入缺頁異常,為其分配物理內存,從而影響性能
② 對于這種程序,在物理內存足夠的情況下,應該讓其使用的數據盡可能駐留在內存中
③ 可以在該程序啟動時,就分配虛擬內存,并且對分配的虛擬內存空間進行一次訪問,強制將未映射的頁面轉變為正常頁面,從而降低缺頁異常發生的概率
上述過程通常稱為內存的commit
2.4?頁表結構
2.4.1?概述
1. 上述虛擬內存和物理內存的映射關系由操作系統管理,而管理這種映射關系的數據結構就是頁表
2. 映射的過程由處理器的內存管理單元(Memory Management Unit,MMU)自動完成,但是他依賴操作系統設置的頁表。因此,虛擬內存是軟硬件一體化設計的典型代表
2.4.2?頁表
?
1. 頁表本質上是頁表項(Page Table Entry,PTE)的數組,虛擬地址空間中的每個虛擬頁在頁表中都有一個PTE與之對應
2. 以X86-32體系結構為例,頁大小為4KB,每個頁表項4B,因此將1024個頁表項組成一張頁表。這樣一張頁表的大小正好是4KB,占據一個內存頁
3. 而一張頁表的1024個頁表項能夠映射1024 * 4KB = 4MB內存
2.4.3?頁目錄表
如上文所述,一張頁表可以映射4MB內存。為了編碼更多地址,就需要更多頁表。因此引入了頁目錄表
1. 頁目錄表中的每一項叫做頁目錄項(Page Directory Entry,PDE),每個PDE都對應一張頁表
2. 每個頁目錄項也是4B,因此將1024個頁目錄項組成一張頁目錄表,可以映射1024 * 4MB = 4GB內存,可以覆蓋32位處理器的虛擬地址空間
說明:使用多級頁表結構,而不是使用單級頁表的原因,可參考X86匯編語言從實模式到保護模式19:分頁和動態頁面分配_麥小兜的博客-CSDN博客_匯編page?chapter 2.4.3
2.4.4?虛擬地址翻譯過程
以X86-32體系結構為例,使用兩級頁表,并將虛擬地址劃分為3段,分別作為頁目錄表索引、頁表索引和頁內偏移量
虛擬地址的翻譯過程如下,
1. 確定頁目錄基址
每個CPU都有一個頁目錄基址寄存器,記錄最高級頁表的物理基地址。在X86-32體系結構中,就是CR3寄存器
2. 定位頁目錄項(PDE)
頁目錄物理基址 + 虛擬地址高10位 * 4 = PDE物理地址
頁目錄項(PDE)中記錄了頁表的物理地址
3. 定位頁表項(PTE)
頁表物理基址 + 虛擬地址中間10位 * 4 = PTE物理地址
頁表項(PTE)中記錄了物理頁的物理地址
4. 確定物理地址
以虛擬地址低12位作為物理頁中的索引
說明1:頁表項(PTE)中處理記錄物理頁的物理地址外,還記錄了一些屬性(e.g. 頁的讀寫權限、標記頁是否存在),操作系統可以基于這些屬性實現內存保護
說明2:每個進程都有自己的頁目錄表,當進程切換時,會將目標進程的頁目錄表物理地址加載到CR3寄存器。所以在任意時刻,只有一個進程頁表是活躍的
說明3:在64位處理器上,由于虛擬地址空間更大,需要更多級的頁表
以X86-64體系結構為例,PDE和PTE都是8B,所以一頁之中只能存放512項,需要9位進行編碼。所以虛擬地址會被分割為9 + 9 + 9 + 9 + 12共4段,而頁表也是4級
由此可見,頁表的級數與虛擬地址的分段是匹配的
3?頁面換入換出
1. 由于程序運行符合局部性原理,對于那些沒有被經常使用的內存,可以將他們換出到內存之外,比如磁盤的swap分區
2. 當物理內存足夠時,操作系統會讓盡可能多的頁駐留在物理內存中,因為將內存中的數據寫入磁盤是非常耗時的操作
說明1:極端情況下,給一個進程4KB物理內存就可以。操作系統通過對一頁不斷的換入換出,使得進程可以運行
說明2:虛擬內存會耗盡嗎?
①?虛擬內存也是會耗盡的,也就是out of memory錯誤。以Linux 32位操作系統為例,用戶空間只有3GB,如果程序中一次性要申請4GB內存,則虛擬內存不足
②?雖然有了虛擬內存,但是物理內存還是會耗盡的。當物理內存不足時,可以將不活躍的物理頁換出到磁盤的swap分區。當swap分區耗盡時,物理內存就耗盡了
所以任何時候,及時釋放申請的內存是一個好習慣
4?內存的段式管理與頁式管理
說明:關于X86體系結構實模式與保護模式的相關內容,可參考X86匯編語言:從實模式到保護模式學習筆記
?X86匯編語言:從實模式到保護模式學習筆記
4.1?段式管理
4.1.1?特征
1. 按功能將內存劃分為不同的段,例如代碼段、數據段、只讀數據段、棧段等
2. 為不同的段設置不同的特權級和讀寫權限
4.1.2?優點
1. 按功能對內存進行劃分,符合人的直觀思維
2. 可以提供更好的安全性(這點依賴保護模式下段機制的檢查機制)
4.1.3?缺點
1. 段長度往往是不固定的,難以以段為單位進行內存的分配和回收
4.2?頁式管理
4.2.1?特征
1. 不按功能對內存進行劃分,而是按固定大小將內存劃分為大小相同的頁
2. 無論存放數據還是代碼,都需要先分配一個頁,再將內容加載到頁中
4.2.2?優點
1. 頁大小固定,易于內存的分配與回收
2. 段式管理提供的安全性,在現代CPU中可以被頁表項中的屬性替代
4.3?使用現狀
1. 現代操作系統都采用段式管理實現基本的權限管理(比如區分內核態和用戶態),而對于內存的分配、回收和調度則依靠頁式管理實現
2. 段式管理負責將邏輯地址轉換為線性地址,頁式管理負責將線性地址轉換為物理地址。通過使用段頁式混合管理模式,兼具了段式管理和頁式管理的優點
說明1:現代操作系統中,一般將段描述符中的段基址設置為0,段長度設置為最大,也就是使用平坦模型
說明2:段式管理與頁式管理的內存碎片
① 段式管理可以根據實際需求分配段的大小,因此段內部沒有內存碎片。但是由于每個段的長度不固定,所以多個段未必能敲好使用所有的內存空間,所以段與段之間會產生內存碎片
② 頁式管理中頁的大小固定,即使所需內存不足一頁,也會分配一頁,因此頁內部有內存碎片。但是頁與頁之間緊密排列,沒有內存碎片
說明3:操作系統弱化分段機制后如何補償
① 首先需要說明的是,X86體系結構的設計者希望大家繼續使用段機制,所以基于段寄存器構造了保護模式,同時在段和頁兩級提供權限管理機制
但是Linux操作系統使用平坦模型繞過了段機制,且大多數體系結構都沒有段基址,所以段機制逐漸被弱化。甚至在X86-64體系結構中,默認就是使用平坦模型,廢棄了段描述符中的段基址和長度
② Linux內核引入vm_area_struct結構,通過軟件方式進行權限管理,部分代替了段基址的權限管理工作
5?內存布局
5.1?抽象內存布局
5.1.1?內存布局組成
抽象內存布局描述的是運行一個程序所需的最小功能集,對于一個典型的進程,內存布局包括如下部分
1. 代碼段
存儲程序的機器指令,這段區域的內存一般可讀可執行,但不可寫
2. 數據段
① 存儲程序中已經初始化且不為0的全局變量和靜態變量
② 這些變量的初值會存儲在程序編譯后的二進制文件中,然后被加載到內存中
3. BSS(Block Started by Symbol)段
① 存儲程序中未初始化或初始化為0的全局變量和靜態變量
② 由于他們的初值為0,因此不需要在程序編譯后的二進制文件中存儲那么多0,只需要記錄他們的起止地址即可
③ 操作系統在加載程序時,會根據記錄的BSS段起止地址初始化相應的內存區域
④ BSS除了Block Started by Symbol,從功能上也可以理解為Better Save Space
4. 堆 & 棧
堆空間和棧空間不是從磁盤上加載,而是在程序運行過程中申請的內存空間
說明1:Linux 0.11內核支持的a.out文件格式與內存布局,就是如上圖所示
說明2:現代應用程序除了上面的內存段,還會包含如下內存區域
① 存放加載的共享庫的內存空間
如果一個進程使用共享庫(動態庫),該共享庫的代碼段、數據段和BSS段也需要被加載到進程的地址空間中
② 共享內存段
可以通過系統調用映射一塊匿名內存作為共享內存,用來進行進程間通信
③ 內存映射文件
可以將磁盤上的文件映射到內存中,用來進行文件編輯或以類似共享內存的方式進行進程間通信
5.1.2?磁盤程序段與內存程序段
?
1. 上圖中左邊是程序在磁盤中的文件布局,右邊是程序加載到內存中的內存布局
2. 對于磁盤的的程序,每個單元稱為一個Section,可以通過readelf -S命令查看可執行程序中所有的Section信息
3. 對于內存鏡像,每個單元稱為一個Segment,可以通過readelf -l命令查看可執行程序加載到內存之后的Segment布局
說明1:Section與Segment的對應關系
① 因為Segment是將具有相同權限的Section集合在一起,為他們分配同一塊內存空間,所以往往是多個Section對應一個Segment
② 對于磁盤文件中一些保存輔助信息的Section(e.g. symtab段、strtab段),不需要在內存中進行映射
說明2:Section與Segment對應關系實驗
編寫一個簡單的hello world程序,編譯后查看Section與Segment的對應關系。首先來看一下readelf命令中-S和-l選項的功能
-S:Displays the information contained in the file's section headers
-l:Displays the information contained in the file's segment headers
?
?
?
從執行結果看見,
.text段被映射到可讀(R)可執行(E)的內存段
.data和.bss段被映射到可讀(R)可寫(W)的內存段
5.2 IA-32 + Linux的進程內存布局
首先需要說明的是,在IA-32 + Linux的進程內存布局中,低3GB為用戶空間,高1GB為內核空間,此處我們關注用戶空間的組成部分
1. 保留區
① 從0地址開始的是一段不可訪問的保留區,用于防止程序跑飛。這是因為在大多數系統中,一般認為一個比較小數值的地址是一個不合法地址(e.g. NULL指針)
② 此處保留區大小為0x08048000,約128MB
2. 代碼段
① 從0x08048000開始是代碼段
② 以上地址需要GCC在編譯時不開啟PIE選項
③ 代碼段是從可執行文件鏡像中加載到內存中
3. 數據段
① 數據段緊接在代碼段之后
② 數據段也是從可執行文件鏡像中加載到內存中
4. BSS段
① BSS段緊接在數據段之后
② BSS段是根據BSS段所需大小,在加載時生成的一段以0填充的內存空間
③ 由于BSS段和數據段屬性相同,所以如之前的實驗所示,在內存中與數據段映射在相同的Segment
5. 堆(Heap)
① 堆空間地址向上增長
② 堆的指針叫做Program break
③ 每次進程向內核申請新的堆地址時,分配的地址值增加
④ 堆的最大值會受到操作系統限制,如果耗盡就會發生out of memory錯誤,分配不出新的內存
6. 棧(Stack)
① 棧空間地址向下增長
② 棧的指針叫做Stack Pointer
③ 每次進程申請新的棧地址時,分配的地址值減小
7. 文件映射與匿名映射區
這里最常見的就是程序所依賴的共享庫(e.g. libc.so),共享庫的代碼段、數據段和BSS段會被加載到這里
說明1:可以通過cat /proc/[pid]/maps命令查看指定進程的虛擬內存空間布局
示例程序如下,
將該程序在后臺運行,之后查看對應進程的虛擬內存空間布局
?
說明2:進程地址隨機化
① 上述內存布局是在Linux操作系統關閉進程地址隨機化時的情況,如果打開進程地址隨機化,其中的堆空間、棧空間和共享庫映射的地址,在每次程序運行時都會不一樣
② 在實現方法上,就是內核在加載程序時,會對這些區域的起始地址增加一個隨機的偏移量
③ 可以通過sysctl命令設置進程地址隨機化
sudo sysctl -w kernel.ramdomize_va_space=val # val = 0,表示關閉內存地址隨機化 # val = 1,表示mmap的基地址、棧地址和VDSO的地址隨機化 # val = 2,表示在1的基礎上,增加堆地址隨機化補充:地址隨機化是由Linux內核與GCC的PIE編譯選項共同決定的,操作系統的加載器負責生成隨機地址,編譯器負責產生的代碼地址無關(因此使能進程地址隨機化,需要編譯時攜帶PIE選項)
5.3 Intel 64 + Linux的進程內存布局
Intel 64 + Linux進程內存布局中的組成部分與Intel 32中是相同的,此處重點說明虛擬地址空間的劃分
5.3.1?使用部分地址線
1. 64位處理器的理論尋址范圍為2^64 = 16EB,但是目前的操作系統和應用程序往往用不到這么龐大的地址空間,因此只使用部分地址線
2. Intel 64位處理器目前支持48位虛擬地址,即尋址空間為2^48 = 256TB
3. 由于使用48位虛擬地址,所以地址的最高有效位為bit [47](從bit[0]開始)
5.3.2 canonical address
1. Intel 64位體系結構定義了canonical address的概念,即在64位模式下,如果地址位63到地址最高有效位被設置為全0或全1,那么該地址被認為是canonical form
2. 在Intel 64位體系結構中使用48位虛擬地址,因此根據canonical address的劃分,地址空間天然被劃分為兩個區間,分別是0x0 ~ 0x00007FFFFFFFFFFF和0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF這2個128TB空間(即根據地址最高有效位bit [47],用該值設置到bit [63])
3. 地址空間中的其他區域均不滿足canonical form,也就是上圖中的非canonical空間,對這部分內存不會建立頁表進行映射
4. 在實際使用中,將低128TB用作用戶空間,高128TB用作內核空間
說明1:在64位操作系統中查看進程虛擬地址內存布局
使用與32位操作系統中相同的hello world程序
?
從中可以看出,在代碼段(0x00400000 ~ 0x00401000)和數據段(0x00600000 ~ 0x00601000)之間存在一段空隙
對于64位應用程序,這是一段不可讀寫的保護區域,作用是防止程序在讀寫數據段時越界訪問到代碼段。這個保護段可以讓這種越界訪問行為直接崩潰,防止程序繼續運行下去
/************************************************************************************
在早期的計算機中,程序都是直接運行在物理內存上的,意思是運行時訪問的地址都是物理地址,而這要求程序使用的內存空間不超過物理內存的大小。
在現代計算機操作系統中,為了提高CPU的利用率計算機同時運行多個程序,為了將計算機上有限的物理內存分配給多個程序使用,并做到隔離各個程序的地址空間和提高內存利用率,操作系統應用虛擬內存機制來管理內存。
本文介紹的是一些與虛擬內存相關的概念,包括虛擬內存和物理內存之間的映射,一個進程的虛擬內存空間的劃分等。
目錄
1.物理內存 vs 虛擬內存
2.物理內存空間 和 虛擬內存空間
3.4GB虛擬內存
cpu位寬 vs cpu的地址總線位寬
4.虛擬內存的地址空間劃分
Windows 系統下,4G虛擬地址空間被分成了 4 部分: NULL 指針區+用戶區+ 64KB 禁入區+內核區。
1)NULL指針區和64KB禁入區:略
2)用戶區每個進程私有使用的,大約 2GB 左右 ( 最大可以調整到 3GB,3GB模式) ,稱為用戶地址空間。
3)內核區是所有進程共享的,為 2GB (3GB模式下就是1GB),稱為系統地址空間。
Linux下和Windows下的差不多,只不過Linux默認就是1GB內核空間:
1.保留區:
2.可執行文件映像,堆,棧,動態庫
棧空間(Stack)——函數調用:
通過例子1看匯編:
例子2:(VC9,i386,Debug模式)
PS1:編譯器 生成的函數 的 標準進入和退出指令序列?
PS2:編譯器 實現的hook技術
PS3:函數調用之調用慣例
PS4:函數調用之返回值的傳遞
PS5:函數調用之C++對象
堆空間(heap)——動態申請內存:
5.虛擬地址和物理地址的映射
6.物理內存和硬盤之間的置換
7.虛擬內存的重要性
8.進程的虛存空間分布——裝載(程序員的自我修養-鏈接裝載庫 第6.4節)
readelf -S鏈接視圖——25個section頭
執行視圖:7個program頭——程序頭表記錄著程序頭
VMA
Stack VMA[stack]
動態鏈接時的進程堆棧初始化信息
1.物理內存 vs 虛擬內存
物理內存就是內存條,實實在在的內存,即RAM。
虛擬內存是內存管理中的一個概念。對于一個進程來說,虛擬內存是進程運行時所有內存空間的總和,包括共享的,非共享的,存在物理內存中,存在分頁內存中(分頁后面會介紹),提交的,未提交的。【進程運行起來以后,虛擬內存映射=PP物理空間+DP硬盤空間+未使用使用映射的。】【虛擬內存映射的可能有一部分不在物理內存中,另外一部分在其他介質中,比如硬盤,舉個例子,當你的程序需要創建一個1G的數據區,但是此時剩余500M的可用物理內存了,那么此時勢必不是所有數據都能一起加載到內存(物理內存)中,勢必有一部分數據要放到其他介質中(比如硬盤),待進程需要訪問那部分在其他介質中的數據時,再通過調度進入物理內存。】
2.物理內存空間 和 虛擬內存空間
物理內存大小組成的地址空間就叫物理內存空間。物理內存空間表示的是實實在在的RAM物理內存,其地址空間可以看成一個由 M 個連續的字節大小的單元組成的數組,每個字節都有一個唯一的物理地址。【存儲單元,也就是每個字節都有其地址,cpu進行訪問的時候需要知道其地址。M就是RAM的大小】
虛擬內存大小組成的地址空間就叫虛擬內存空間。跟物理內存一樣,也是有地址空間的,用地址標識哪個內存位置,也看成一個連續的字節大小的單元組成的數組。【虛擬內存空間實際上并不存在的,需要的只是虛擬內存空間到物理內存空間的映射關系的一種數據結構。】
上面說的數組的大小,就是地址空間的長度。
即地址空間是一個抽象的概念,可以想象成一個很大的數組,每個數組的元素是一個字節,而這個數組的大小就是由地址空間的地址長度決定。一般畫圖也是這么畫的。
比如16G的物理內存條,具有0~0x3FFFFFFFF(16G)的地址長度的尋址能力。【另一方面,cpu的地址總線位寬決定了可以直接進行尋址物理內存空間最大值】
4G虛擬內存,具有0~0xFFFFFFFF的地址長度的尋址能力。
在一個系統中,物理內存空間只有一個,但是虛擬內存空間有很多個(運行著多個進程)。每個虛擬內存空間都有必須有一個映射關系對應于物理內存。【在進程啟動的時候會建議一個映射關系,在運行中維護這個關系,后面會講】
對于一般程序而言,虛擬內存空間中的很大部分的都是未使用的,【虛擬內存是4G的空間】
每個虛擬內存空間往往只能映射到很少一部分物理空間上。每個物理頁(將整個物理空間劃分成多個大小相等的頁)可能只是被映射到一個虛擬地址空間中,也有可能存在一個物理頁被映射到多個虛擬地址空間中,那么這些地址空間將共享此頁面(如果在一個虛擬地址空間改寫了此頁面的數據,這在其他的虛擬地址空間也可以看到變化)
3.4GB虛擬內存
操作系統(32位)會為每一個新創建的進程分配一個 4GB 大小的虛擬內存,從0到2^32-1。(這里說的分配4GB的虛擬內存并不是分配4GB的空間,而是創建一個映射表。映射表存放在物理內存中。以后會介紹到一級頁表和二級頁表結構,這個映射表肯定加載在物理內存中,這個映射表最好是設計得當然是越小越好了)
之前一直說是4G的虛擬地址空間,那么為什么是分配一個4GB的虛擬地址空間,因為在32位操作系統下一個32位的程序的一個指針長度是 4 字節(指針即為地址,指針里面存儲的數值被解釋成為內存里的一個地址。64位程序指針是64位,尋址能力2^64-1), 4 字節指針(地址)的尋址能力是從 0x00000000~0xFFFFFFFF ,最大值 0xFFFFFFFF 表示的即為 4GB 大小的容量。
這個虛擬空間地址大小和 實際物理內存RAM大小沒有關系。
補充:一個進程的虛擬內存的大小是由操作系統的位寬和程序是32位還是64位決定的。
下面解釋一下cpu位寬和cpu地址總線位寬的以及他們的位寬大小會影響什么。
cpu位寬 vs cpu的地址總線位寬
1.我們說的16位cpu,32位cpu,64位cpu,指的都是cpu的位寬,表示的是一次能夠處理的數據寬度,即一個時鐘周期內cpu能處理的2進制位數,即分別是16bit,32bit和64bit。不是地址總線的數目。
(通用寄存器的寬度決定了cpu可以直接表示的數據范圍。我們說的16位cpu,32位cpu,64位cpu,指的就是通用寄存器的位數(寬度)。見 匯編語言 那篇文章)
2.cpu的地址總線位寬決定了可以直接進行尋址物理內存空間。所以如果cpu的地址總線是32位的,也就是它可以尋址0~0xFFFFFFFF(4G)的物理內存空間。但是如果你的計算機上只裝了512M的內存條,那么物理地址的有效部分只有0x00000000~0x1FFFFFFF,其他部分都是無效的物理地址。(這里無視一些外部的I/O設備映射到物理空間。)
3.cpu位寬不等于cpu的地址總線位寬,16位cpu的地址總線位寬可以是20位,32位cpu的地址總線可以是36位,64位cpu的地址總線位寬可以是36位或者40位(cpu能夠尋址的物理地址空間為64GB或者1T)。
4.在cpu訪問任何存儲單元必須知道其物理地址,所以在一定程度上,cpu的地址總線寬度影響了最大支持的物理內存RAM大小(操作系統的位數也會影響,32位系統,最大就是支持4GB物理內存RAM)
32位系統最大只能支持4GB內存之由來 - Matrix海子 - 博客園
4.虛擬內存的地址空間劃分
Windows 系統下,4G虛擬地址空間被分成了 4 部分: NULL 指針區+用戶區+ 64KB 禁入區+內核區。
為了高效的調用和執行操作系統的各種服務,Windows會把操作系統的內核數據和代碼(內核提供了各種服務供應用程序使用)映射到所有進程的進程空間中。即內核態,也稱為系統空間,也可以叫做系統空間。
所以內核態只有一個,會映射到所有的進程空間中。從這個角度來看,用戶空間是每個進程獨立的,內核空間是共享的。
?
?
?
1)NULL指針區和64KB禁入區:略
2)用戶區每個進程私有使用的,大約 2GB 左右 ( 最大可以調整到 3GB,3GB模式) ,稱為用戶地址空間。
用戶區存放的是程序代碼和數據, 堆, 共享庫, 棧。
默認的windows配置下是2GB+2GB,稱為2GB模式。可以修改windows配置,可以設置3GB用戶地址空間+1GB的系統地址空間,稱為3GB模式。
如上圖:(以下都是用戶態的,用戶態的,用戶態的,ntdll.dll是用戶態的,而且都是共享,每個進程都是同一個虛擬地址的。一些固定的地址。)
0x80000000附近的:如系統庫(ntdll.dll:windows操作系統用戶層的最底層的dll,負責windows子系統與windows內核之間接口,比如堆管理器的核心接口,HeapCreate、HeapAlloc、HeapFree和HeapDestroy,向操作系統申請內存的時候WindowsAPI:VirtualAlloc進行申請。
ntdll.dll是NT內核派駐到用戶空間的領事館,ntdll.dll是用戶態和內核態溝通的橋梁。用戶空間的代碼通過這個dll,來調用內核空間的系統服務。操作系統會在啟動階段將這個加載到內存中,并把他映射到進程空間的同一個虛擬地址。
)
0x10000000:如運行時庫(msvcr90d.dll,microsoft的c運行時庫,運行時庫這這篇文章介紹過)
0x00400000:可執行程序映像exe,即程序代碼和數據(為什么叫映像,在這個文章介紹過)
stack棧的位置:在0x00300000和exe后面都有分布。因為有多少個線程就有多少個棧,線程默認棧大小1MB,也可以啟動線程的時候自行設定大小。(所以棧,是相對于線程來說的,這在調試的時候可能看出來)
heap堆的位置:在剩下的空間中進行分配。
3)內核區是所有進程共享的,為 2GB (3GB模式下就是1GB),稱為系統地址空間。
內核區保存的是系統線程調度、內存管理、設備驅動等數據,這部分數據供所有的進程共享以及操作系統的使用——程序在運行的時候處于操作系統的監管下,監管進程的虛擬空間,當進程進行非法訪問時強制結束程序。
(進程只能使用操作系統分配給進程的虛擬空間。錯誤提示“進程因非法操作需要關閉”就是訪問了未經允許的地址。)
Linux下和Windows下的差不多,只不過Linux默認就是1GB內核空間:
1.保留區:
對內存中收到保護而禁止訪問的內存區域的總稱。在大多數操作系統中,極小的地址都是不允許訪問的,如果訪問了就會拋出錯誤。如NULL。 通常C語言將無效指針賦值為0,因為0地址上正常情況下不可能有有效的可訪問的數據的。
2.可執行文件映像,堆,棧,動態庫
1)可執行文件映像:裝載的時候涉及到。可執行文件的裝載,進程和線程,運行時庫的入口函數(第六章)_u012138730的專欄-CSDN博客_運行時動態裝載鏈接至少需要用到以下哪些函數
2)堆:應用程序動態分配的 通常在棧的下方。地址從低到高分配。
3)動態鏈接庫(共享庫)映射區。動態鏈接的時候介紹。Linux之前是默認從0x4000000開始裝載的,但是在linux內核2.6中,共享庫的裝載地址已經挪到了靠近棧的位置,即0xbfxxxxxx附近。。
4)棧:用于維護函數調用的上下文。有函數調用就要用到棧。地址從高到低分配。
棧空間(Stack)——函數調用:
棧是虛擬內存空間的一段連續的地址。里面的內容是 調用函數的活動記錄,記錄目前為止函數調用之前的那些函數的維護信息。(函數調用,維護信息,堆棧幀,活動記錄)
一個函數的活動記錄(堆棧幀)可以用 ebp 和 esp 劃定范圍(其中參數和函數返回地址用ebp+x 可以得到,這兩個也是包括在活動記錄中的):
ebp和esp都是寄存器,詳情看匯編指令和寄存器_u012138730的專欄-CSDN博客。
以上就是一個堆棧幀,bp指向當前的堆棧幀的底部,sp指向當前的棧幀的頂部。隨著函數調用的進行以及返回,bp和sp也就是隨著改變,指向新的堆棧幀或者舊的堆棧幀。
如上圖,一條活動記錄包括一下幾個方面:
1)函數的返回地址(地址 ebp + 4)和函數的參數(地址 ebp + 8,ebp + 12等等)、
其中函數的返回地址是在call指令中push 的
其中函數的輸入實參是在call之前的指令壓入的
2)調用該函數之前的ebp的值(舊ebp值,上一個堆棧幀的底部的地址),目的是為了:這個函數返回的時候ebp的值可以恢復到上一個位置,回到上一個堆棧幀現場。
3)【可選】需要保持不變的寄存器的值(地址 ebp - 4,ebp - 8等等)
4)臨時數據,局部變量,調試信息——擴大的棧空間中。
通過例子1看匯編:
SimpleSection.c中的func1
看func1函數的反匯編的實現(這里是.AT&T的匯編格式,看文章的最后):——其實就是創建了一個堆棧幀的過程,從ebp開始,不包括輸入實參和返回地址。
前面幾句的含義寫到了圖片上,接下去幾句:
movl $0x0,(%esp) 的含義:
在esp指針寄存器指向的位置存入0x0(其實代表的是第一個參數“%d\n”,那為什是0呢,因為要重定位。這條指令就是printf函數的第一個實參,上一條指令就是printf的第二個參數參數i。相當于從右到左壓入實參)
(所以printf的兩個輸入參數就分別存在當前堆棧幀的esp+4 和 esp 中,下面一條語句就是call了再次進入函數調用了,所以這就是之前說的,輸入實參是在call之前壓入的。)
可以看到.text段(代碼段)的offset是0x10的地方正好是指令movl $0x0,(%esp) 中0x0的地址。(上上個圖中數數,第16個字節)
需要重定位的符號是.rodata,可以看到.rodata里存的正好就是%d。
【.rodata, .data, .bss應該也是鏈接的時候重定位,跟普通的符號printf和func1一樣,因為鏈接完了以后這些段的VMA已經確定了,就可以填上正確的值了。這里看的是目標文件.o,還沒有重定位過】
call 15<func1+0x15>的含義:
15<func1+0x15>,就是跳轉到func1+0x15 這里的的內存的地址進行執行,call指令做了兩件事情:
1)把當前指令的下一條指令的地址壓入棧中,即函數的返回地址。
2)進入新的函數調用了(新的函數開頭都是,重復開頭的ebp入棧,ebp=esp等等,像進入func1一樣的——進入printf的例行操作)
leave 的含義:(相當于執行了出棧的操作,把棧恢復到函數調用以前的樣子)
等價于下面兩條指令:
movl %ebp %esp ?恢復esp=ebp的值,即此時esp指向的是ebp的位置,就一開始的時候那樣。
popl %ebp 從棧中彈出值,其實就是舊的ebp的值,賦值給ebp寄存器,即ebp = 舊ebp。彈出舊ebp以后此時esp指向的就是函數的返回地址那個位置了。對應上面活動記錄的圖。
ret 的含義:
等效于以下匯編指令:
popl %eip ?從棧中取出函數的返回地址,并跳轉到該位置執行
這個例子中沒有push寄存器的值,退出的時候也就不需要pop回來。
例子2:(VC9,i386,Debug模式)
int foo()
{
? ? ? ?return 123;
}
下面是匯編:(第四步,在debug模式下會加入調試信息,把棧空間上的內容都初始化為CC,兩個連續排列的CC就是中文“燙”)
例子1是AT&T匯編格式,例子2是Intel匯編格式。
比例子1多了第3,4,6步。
以下的ps可以不用看,跟本章內容無關只是做個記錄。 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
PS1:編譯器 生成的函數 的 標準進入和退出指令序列?
不知道不懂藍色劃線的原因。
PS2:編譯器 實現的hook技術
鉤子技術:
下面是可被替換的函數(FUNCTION)的進入指令序列:
nop指令占用一個字節,一共5個字節的nop:
比如一替換函數(REPLACEMENT_FUNCTION)如下:
替換以后如下:
進入到Function以后,先執行了jmp LABEL(這個jump是近跳指令,只占用兩個字節,覆蓋原來的mov edi,edi)
LABEL下是jmp 到一個新的標簽即替換函數(這個jmp占5個字節,,覆蓋原來nop的五個字節)
這樣就實現了函數的替換了。
說實話,沒有很懂,具體實際應用中的流程。
PS3:函數調用之調用慣例
函數聲明的時候可以用關鍵字聲明調用慣例,默認是cdecl(C語言中):
可以看到調用慣例規定了 :
出棧方:出棧可以是調用方,也可是函數本身(將函數實參壓入棧的肯定是調用方)。上面例子中的leave就是調用方執行的。
參數傳遞:即調用方將實參壓入棧 的 順序 需要和函數本身 有一致的規定,這樣才能取到正確的值。
名字修飾:不同的調用慣例有不同的名字修飾規則,所以,如果不一致的話,就會找不到符號了
不同的編譯器對同一種調用慣例的實現也不盡相同,比如gcc和vc對于C++的thiscall(p298)
調用慣例調用方和被調用方不一致的例子:p299 ,要在動態鏈接中說明如何鏈接成功,這里先略了。
例子:
main函數:1 2-調用方對函數參數進行壓棧,從右到左,4-調用方對函數參數進行出棧。
f函數也是:棧在調用完以后都棧都恢復到原來的調用之前
vs中設置默認調用慣例:
cdecl調用慣例的用途——p337變長參數。
常用的調用約定類型有__cdecl、stdcall、PASCAL、fastcall。除了fastcall可以支持以寄存器的方式來傳遞函數參數外,其他的都是通過堆棧的方式來傳遞函數參數的。(這是網上看到的,不是書里面寫的)
PS4:函數調用之返回值的傳遞
上面的——例子2:(VC9,i386,Debug模式)——中可以看到返回值是由寄存器eax來傳遞的。但是如果返回值大于4個字節,不能存放在eax寄存器中應該怎么辦呢——解決辦法是:eax指向一個地址,返回值就存放在這個這個地址中。下面是返回是類的例子,其中 big_thing 大小為 128個字節。
typedef struct big_thing
{
?? ?char buf[128];
}big_thing;
?
big_thing return_test()
{
?? ?big_thing b;
?? ?b.buf[0] = 0;
?? ?return b;
}
?
int main() {
?
?? ?big_thing n = return_test();
?? ?return 0;
}
main函數的匯編:
int main() {
01041720 ?push ? ? ? ?ebp ?
01041721 ?mov ? ? ? ? ebp,esp ?
01041723 ?sub ? ? ? ? esp,258h ? ---》開辟258h的棧空間
01041729 ?push ? ? ? ?ebx ?
0104172A ?push ? ? ? ?esi ?
0104172B ?push ? ? ? ?edi ?
0104172C ?lea ? ? ? ? edi,[ebp-258h] ?
01041732 ?mov ? ? ? ? ecx,96h ?
01041737 ?mov ? ? ? ? eax,0CCCCCCCCh ?
0104173C ?rep stos ? ?dword ptr es:[edi] ? ----》把棧空間都初始化為cch,即漢字燙燙燙。。。 96h*4=258h。就是棧空間的大小。
? ? big_thing n = return_test();
0104173E ?lea ? ? ? ? eax,[ebp-254h] ?---》lea指令看文章匯編指令和寄存器_u012138730的專欄-CSDN博客
01041744 ?push ? ? ? ?eax ? ----》eax值是棧空間的最后一個地址,把eax壓入棧,緊接著下面就調用call,說明把這個當作了輸入參數 return_test(ebp-254h)
01041745 ?call ? ? ? ?return_test (010410E1h) ?
0104174A ?add ? ? ? ? esp,4 ?--》函數調用方負責把壓棧參數還原
0104174D ?mov ? ? ? ? ecx,20h ?
01041752 ?mov ? ? ? ? esi,eax ?
01041754 ?lea ? ? ? ? edi,[ebp-1CCh] ?
0104175A ?rep movs ? ?dword ptr es:[edi],dword ptr [esi] ?---》把eax的內容復制到ebp-1CCh中,ebp-1CCh是棧上的一個臨時的地址。20h*4=80h 就是正好128字節就是big_thing的大小。rep movs指令匯編指令和寄存器_u012138730的專欄-CSDN博客
0104175C ?mov ? ? ? ? ecx,20h ?
01041761 ?lea ? ? ? ? esi,[ebp-1CCh] ?
01041767 ?lea ? ? ? ? edi,[n] ?
0104176D ?rep movs ? ?dword ptr es:[edi],dword ptr [esi] ?---》再把臨時的地址的內容復制到真正的n中。
? ? return 0;
0104176F ?xor ? ? ? ? eax,eax ?
}
return_test的匯編
big_thing return_test()
{
01041690 ?push ? ? ? ?ebp ?
01041691 ?mov ? ? ? ? ebp,esp ?
01041693 ?sub ? ? ? ? esp,148h ?---》開辟148h的棧空間
01041699 ?push ? ? ? ?ebx ?
0104169A ?push ? ? ? ?esi ?
0104169B ?push ? ? ? ?edi ?
0104169C ?lea ? ? ? ? edi,[ebp-148h] ?
010416A2 ?mov ? ? ? ? ecx,52h ?
010416A7 ?mov ? ? ? ? eax,0CCCCCCCCh ?
010416AC ?rep stos ? ?dword ptr es:[edi] ? ---》初始化148h的棧空間 ?rep stos ?指令見文章 匯編指令和寄存器_u012138730的專欄-CSDN博客
? ? big_thing b;
? ? b.buf[0] = 0;
010416AE ?mov ? ? ? ? eax,1 ?
010416B3 ?imul ? ? ? ?ecx,eax,0 ?
010416B6 ?mov ? ? ? ? byte ptr b[ecx],0 ?---》假匯編 b其實是棧空間的一個地址?
? ? return b;
010416BE ?mov ? ? ? ? ecx,20h ?
010416C3 ?lea ? ? ? ? esi,[b] ?
010416C9 ?mov ? ? ? ? edi,dword ptr [ebp+8] ?---》ebp+8就是之前main函數調用return_test時,壓入了一個作為隱形參數出入到return_test中的,在main函數的棧的地址。 (數據應該是存入 [舊的ebp-254h]的內存地址中了 )
010416CC ?rep movs ? ?dword ptr es:[edi],dword ptr [esi] ?---》把b的內容復制到ebp+8中。
010416CE ?mov ? ? ? ? eax,dword ptr [ebp+8] ?---》把epb+8中存的地址復制給eax,也就是main函數的中的棧空間的某個地址,也就是返回值。
}
但是如果return_test返回類型太大,main中的棧空間也無法滿足要求,那么就是會使用一個臨時的棧上的內存作為中轉,返回值對象就會被拷貝2次。
即如果是函數的返回值大于4字節,調用的時候相當于多傳入一個輸入參數——一個指針,函數里面的返回值指向傳入的這個指針。這個指針賦值給eax。返回以后,調用方通過這個指針獲取到真正的返回值來進行使用。
PS5:函數調用之C++對象
#include <iostream>
using namespace std;
?
struct cpp_obj
{
?? ?cpp_obj()
?? ?{
?? ??? ?cout << "ctor\n";
?? ?}
?? ?~cpp_obj()
?? ?{
?? ??? ?cout << "dtor\n";
?? ?}
?? ?cpp_obj(const cpp_obj& )
?? ?{
?? ??? ?cout << "copy ctor\n";
?? ?}
?? ?cpp_obj& operator=(const cpp_obj& rhs)
?? ?{
?? ??? ?cout << "operator=\n";
?? ??? ?return *this;
?? ?}
};
?
cpp_obj return_test()
{
?? ?cpp_obj b;
?? ?cout << "before return\n";
?? ?return b;
}
?
int main() {
?
?? ?cpp_obj n = return_test();
?? ?return 0;
}
?輸出:(斷點下在main中的return那里,并且不啟用任何優化)
把函數return_test()換成(用了返回值優化技術:Return Value Optimization RVO優化,將對象的拷貝減少一次):
cpp_obj return_test()
{
?? ?return cpp_obj();
}
?輸出:?
總結:如果是c++對象的話,有臨時對象需要調用構造和析構函數。
函數傳遞大尺寸的返回值,在不同編譯器 ,不同平臺,不同的編譯參數,下都不相同,上述是win10,vs2015下的輸出。
堆空間(heap)——動態申請內存:
堆占據虛擬內存空間的絕大部分,在程序運行過程中進行動態的申請,在主動放棄申請的空間之前一直有效。(棧上的數據出了函數就無效了,全局變量要在編譯期間就定義好)
linux系統下堆分配,兩個系統調用(就是操作系統,提供的api的調用):
1)brk——擴大或縮小數據段(data段和bss段的合稱)
int brk(void* end_data_segment);
2)mmap——申請虛擬地址空間,可以申請的虛擬內存空間將映射到文件,也可以不映射到文件(不映射到文件的叫匿名空間)
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
windows系統下堆分配,的系統調用:
VirtualAlloc——空間大小必須是頁的整數倍,x86系統必須是4096字節
LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的內存區域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 分配的類型
DWORD flProtect // 該內存的初始保護屬性
};
運行時庫的malloc函數:
第一次先通過系統調用(申請虛擬地址空間:linux調用mmap申請,windows調用VirtualAlloc申請)申請一塊大的虛擬地址空間,再在這個虛擬地址空間中根據空閑進行分配。
分配算法有:空閑鏈表法,位圖法,對象池法
glibc的malloc:
小于64字節,采用類似于對象池的方法;
大于64,小于512字節,采用上述方法中的最佳折中策略;
大于512字節,采用最近適配算法;
大于128kb的申請,直接用mmap向操作系統申請內存。
msvc的入口函數使用了alloca(鏈接到入口函數的實現)
因為一開始的時候堆還沒有初始化,
alloca是唯一可以不使用堆的動態分配機制,
是在棧上分配任意空間,在函數返回時候釋放,跟局部變量一樣。
(那跟定義局部變量有什么區別?)
5.虛擬地址和物理地址的映射
一個程序要想執行(指令被cpu執行,cpu訪問內存),光有虛擬內存是不行的,必須運行在真實的內存上,所以必須在虛擬地址與物理地址間建立一種映射關系。
虛擬地址與物理地址間的映射關系由操作系統建立,當程序訪問虛擬地址空間上的某個地址值時,就相當于訪問了物理地址空間中的另一個值。
這種映射機制需要硬件的支持——cpu中的內存管理單元。cpu拿到一個需要虛擬地址(virtual address,VA),經過內存管理單元(Memory Management Unit, MMU)利用存放在物理內存中的映射表(頁表,由操作系統管理該表的內容)動態翻譯,轉換成物理內存地址,進而訪問物理內存。這也叫做cpu虛擬尋址方式。【對比文章開頭說的,在早期的計算機中,程序都是直接運行在物理內存上的,運行時訪問的地址都是物理地址】
整個的映射過程是由軟件(操作系統)來設置,而實際的地址轉換是由硬件(MMU)來完成。
應用程序和許多系統進程都是使用虛擬尋址。
只有操作系統內核的核心部分會使用cpu物理尋址,即地址翻譯,直接使用實際的物理內存地址。
虛擬尋址中的具體的映射方案后面介紹。段機制(段描述符)和頁機制(內存分頁)_u012138730的專欄-CSDN博客
6.物理內存和硬盤之間的置換
當物理內存RAM不夠使用的時候,操作系統會根據一些置換算法將物理內存中的數據置換到磁盤上的交換空間(swap),騰出空閑的物理內存頁來存儲需要在內存中運行的程序和相關數據。
前面文章說到任務管理器中的提交大小包含兩部分,一部分是獨占的物理內存(即專用工作集內存),另一部分是在分頁文件中的獨占內存映射。后者分頁文件即是這里說的交換空間。
windows下分頁文件叫,pagefile.sys,一般在硬盤的操作系統所在的分區中。
那么如何設置可以寫入硬盤的內存大小呢——我的電腦 右鍵 選擇【屬性】,左側欄里選擇【高級系統設置】,然后點擊如下圖所示:正如解釋的是,操作系統把這個分頁文件當作RAM使用,即硬盤中的虛擬內存的概念。(70526/1024=68G)
ps: windows下有兩種虛擬內存文件:
1)專用的頁面文件,位于磁盤根目錄,即上面說的pagefile.sys 。
2)使用文件映射機制加載過的磁盤文件,比如加載了的dll和exe,即成了映像文件(虛擬內存的映像文件)。一旦被加載到內存中,該文件就不能刪除了(這是內存管理器和文件系統之間的重要約定)。所以很多有時候刪除不了某文件,一般都是正在用著呢。
7.虛擬內存的重要性
1.每個進程使用的是一個一致的地址空間(從0到2^32-1),降低了程序員對內存管理的復雜性。讓操作系統來完成虛擬地址空間到物理地址空間的轉換。(對于程序來說,不需要關心物理地址的變化,最后被分配到哪對程序來說是透明的)
2.每個進程有自己獨立的虛擬地址空間,只能訪問自己的地址空間,有效地做到進程之間的隔離,保證進程的地址空間不會被其他進程破壞。(從進程角度來看,獨占cpu,獨占內存有單一的地址空間。)
3.提高物理內存的利用率。
8.進程的虛存空間分布——裝載(程序員的自我修養-鏈接裝載庫 第6.4節)
在進程創建的時候操作系統會根據可執行文件的頭部信息(記錄著這個可執行文件有哪些段)將可執行文件中的段和虛擬空間之間進行映射,這個過程就是裝載最重要的一步。
可執行文件從鏈接角度看,是按Section分段的,一般都有十幾個Section,二十幾個,三十幾個——鏈接視圖。
可執行文件從裝載角度看,是按Segment分段的,一般就是五六七八個——執行視圖。
我們可以用readelf命令看同一個執行文件(elf格式的)的兩個視圖:
readelf -S 命令看鏈接視圖(也就是看段表的命令)
readelf -l 命令看執行視圖
下面的例子不是書中的,是我自己電腦上之前編的一個可執行程序,angular的。然后是64位的不是32位的,書本中的例子是32位的。所以我這邊看地址都是8個字節的。
readelf -S鏈接視圖——25個section頭
執行視圖:7個program頭——程序頭表記錄著程序頭
程序頭的類Elf32_Phdr,其數據成員對應下面打印出來的8列:(ELF的目標文件不需要被裝載,所以他沒有程序頭表,而ELF的可執行文件和共享庫文件(linux下的so文件)都有。)
? ? ? 只有LOAD類型的Segment是真正需要被映射到虛擬空間的。其他類型的都是在裝載的時候起到輔助作用。
? ? ? 可以看到LOAD類型的Segment是按權限劃分的:
RE的代碼段
R的只讀數據段
RE的數據段和BSS段(因為有BSS段,所以FileSiz 和 MemSiz不一樣的大小)
? ? ? 可以看到其實權限相同的Section在可執行文件中的位置都是放在一起的。
? ? ? ?對于權限相同的Section把他們的合并到一起稱為一個Segment進行映射,因為從裝載的角度看操作系統不關心各個段的實際內容,最關心的是權限。
? ? ? ?ELF可執行文件被映射到虛擬空間的時候,是以系統的頁長度(4K)為單位的,所以每個段在映射到虛擬空間的時候都會按頁的整數倍的,多余部分占一個頁。按Segment作為段進行映射明顯(比按section進行映射)減少了內存的浪費。
VMA
? ? ? ?Linux中將進程虛擬空間中的一個段叫做虛擬內存區域VMA,windows叫虛擬段VirtualSection。
? ? ? 上述例子中,可執行文件映射到虛擬空間的就有三個VMA。除了有實際文件映射的VMA,還有匿名虛擬空間區域AVMA。
? ? ? 使用命令行看進程的虛擬空間的分布情況,就看上面例子中的main(9個VMA):
第1列:VMA的地址范圍 。這個例子中跟執行視圖中的Virtual地址范圍一樣,有的例子會略有不同(6.4.4節中的段地址對齊)。
第2列:VMA的權限 最后一個p是私有,s是共享。
第3列:VMA中的Segment在映像文件中偏移。(詳情看6.4.4節中描述道,VMA中的Segment和可執行文件中的Segment其實不是完全對應的)
第4列:映像文件所在設備的主設備號:次設備號
第5列:映像文件的節點號
第6列:映像文件的路徑
可以看到除了前3個的 最后幾列都是沒有的,說明最后的都是沒有映射到文件中,這種VMA也叫做匿名虛擬內存區域AVMA。(怎么沒有 [heap])
Stack VMA[stack]
操作系統在進程啟動前將系統環境變量和進程的運行參數提前保存到虛擬空間的棧中。
我們熟知的main()函數中的argc和argv就是從這里獲取的。
動態鏈接時的進程堆棧初始化信息
詳情看動態聯接 可執行文件的裝載,進程和線程,運行時庫的入口函數(第六章)_u012138730的專欄-CSDN博客_運行時動態裝載鏈接至少需要用到以下哪些函數 ? C/C++的編譯和鏈接過程_u012138730的專欄-CSDN博客
進程堆棧初始化信息中包含了動態鏈接器所需要的一些信息:
可執行文件的段(程序頭表),
可執行文件的入口地址等等。
這些輔助信息位于環境指針變量的后面,用一個輔助信息數組表示。
輔助信息結構:
輔助信息結構中的類型和值含義:
寫一個小程序打印出這些數據:
9.windows打開任務管理器
內存項含義
打開任務管理--詳細信息---右鍵 選擇列,選擇下面這4個。
1.工作集(內存)Working Set = 內存(專用工作集)+ 內存(共享工作集)【第2列=第3列+第4列】
工作集(內存)——進程當前正在使用的物理內存量——表示進程此時所占用的總物理內存(即占用RAM內存)。
這個值是由兩部分組成專用工作集 和 共享工作集:
專用工作集內存——由該進程正在使用,而其他進程無法使用的物理內存量——是此進程獨占的物理內存
共享工作集內存——由該進程正在使用,且可與其他進程共享的物理內存量——是指這個進程與其它進程共享的物理內存,比如加載了某一個dll所占用的內存。
2.提交大小 Comitted Memory——進程獨占的內存
提交大小是操作系統為該進程保留的虛擬內存量。
Committed Memory is the number of bytes that have been allocated by processes, and to which the operating system has committed a RAM page frame or a page slot in the page file (or both).
Windows allocates memory for processes in two stages. In the first stage, a series of memory addresses is reserved for a process. The process may reserve more memory than it actually needs or uses at one time, just to maintain ownership of a contiguous block of addresses. At any one time, the reserved memory addresses do not necessarily represent real space in either the physical memory (RAM) or on disk. In fact, a process can reserve more memory than is available on the system.
Before a memory address can be used by a process, the address must have a corresponding data storage location in actual memory (RAM or disk). Commit memory is memory that has been associated with a reserved address, and is therefore generally unavailable to other processes. Because it may be either in RAM or on disk (in the swap file), committed memory can exceed the RAM that is installed on the system
是進程獨占的內存。這個值也是包含兩部分,一部分是獨占的物理內存(即專用工作集內存),另一部分是在分頁文件中的獨占內存映射。分頁文件是硬盤中的虛擬內存,當RAM物理內存資源緊張,或者有數據長時間未使用時,操作系統通常會將數據占用的物理內存先映射到頁面文件(pagefile.sys)中,并拷貝數據到硬盤中,然后將本來占用的RAM空間釋放。這個也就是虛擬內存技術。
下面是一個Windows API,功能是向操作系統發送請求, 將此進程的不常用的內容從物理內存中換出到分頁文件中保存
EmptyWorkingSet
提交大小這部分內存在虛擬內存的線性地址中是連續的,不過在物理內存或者分頁內存中,不一定是連續的。提交但未使用的內存一般都在分頁內存里面,只有去使用的時候,才會換到物理內存里面。
提交大小 大于 專用工作集
3.PROCESS_MEMORY_COUNTERS ?類 和 GetProcessMemoryInfo 函數
MEMORYSTATUSEX 類 和 GlobalMemoryStatusEx 函數 可以獲得還可用的虛擬內存。
PROCESS_MEMORY_COUNTERS ?類 和 GetProcessMemoryInfo 函數可以獲得當前進程使用的內容,如下:
? MEMORYSTATUSEX ?MemoryInfo;
? memset( &MemoryInfo,0,sizeof(MEMORYSTATUSEX) );
? MemoryInfo.dwLength = sizeof(MEMORYSTATUSEX);
? GlobalMemoryStatusEx( &MemoryInfo );
? PROCESS_MEMORY_COUNTERS pmc;
? UINT uMemSwapUsed = 0;
? UINT uMemPhyUsed =0;
? if ( GetProcessMemoryInfo( GetCurrentProcess(), &pmc, sizeof(pmc)) )
? {
? ?uMemSwapUsed = pmc.PagefileUsage/1024/1024; ?// 即提交大小,進程獨占內存(物理+交換)
? ?uMemPhyUsed = pmc.WorkingSetSize/1024/1024; ?// 即工作集WorkingSet的大小,總占的物理內存(獨占+共享)
? }
? CString szInfo;
? szInfo.Format( "%s ?SVM:%u ?PM:%u ?AVM:%llu",GetTitle(),
? ?uMemSwapUsed,
? ?uMemPhyUsed,
? ?MemoryInfo.ullAvailVirtual/1024/1024
? ?);
? SetConsoleTitle( szInfo );
2 其他項目含義
cpu時間是cpu在這個進程用的總時間。
當系統沒有任務執行是,cpu就會執行空閑進程。空閑進程的pid總是0,他的線程數就是系統中總的cpu數目。他的運行時間cpu時間占用了絕大部分的時間。如果一個進程占用了小時以上,就算是重度的進程了。
cpu這一列指的是1s的cpu占用百分比。如果這一列的某個進程一直同一個值,可能某個線程陷入了死循環。
磁盤有關的問題:
IO讀取和IO寫入 次數
IO讀取字節和IO寫入字節 字節數
分析內存問題:
上文
其他觀察進程的exe
比如process explorer
?[轉載]windows任務管理器中的工作設置內存,內存專用工作集,提交大小詳解
10.硬件概念--存儲器芯片。
存儲器芯片從讀寫屬性分類可以分為:
? ? 1)RAM 隨機存儲器(可讀可寫),必須帶電存儲。
? ? ? ? A》主隨機存儲器(主存)
? ? ? ? ? ? 裝在主板上的RAM(主板上插槽,組裝電腦的時候)
? ? ? ? ? ? 裝在擴展插槽上的RAM(可以擴展到多少內存,自己買內存條)
? ? ? ? B》接口卡上的RAM
? ? ? ? ? ?接口卡(cpu通過地址總線與擴展插槽上的接口卡相連,從而與對應外設相通信)有的需要對大批量的輸入輸出數據進行暫存,其上需要裝RAM,最典型的為顯示器的接口卡,顯卡,上的RAM,稱之為顯存。?
? ? 2)ROM 只讀存儲器,關機后其內容不丟失。
? ? ? ? 存儲BIOS的ROM。BIOS是軟件系統,由主板和各類接口卡廠商提供。通過BIOS,對對應硬件設備進行最基本的輸入輸出。
? ? ? ? ? ? 主板上的ROM存儲系統的BIOS
? ? ? ? ? ? 顯卡上的ROM存儲顯卡的BIOS
? ? ? ? ? ? 網卡上的ROM存儲網卡的BIOS
劃重點:內存中存儲數據和指令用于與cpu溝通。內存是僅次于cpu的部件,性能再好的cpu,沒有內存也不能工作。就像再聰明的大腦,沒有了記憶也無法進行思考(摘自《匯編語言》)。
那存儲器是怎么存儲的呢?存儲器的容量怎么表示的呢?
存儲器被劃分為多個存儲單元,并從零開始編號。比如一個存儲器有128個存儲單元,編號即為0~127。
為什么要編號呢,因為編號了就相當于有地址了,cpu可以進行尋址了(通過與之相連的地址總線)。
微型機一個存儲單元存1個字節(8bit,8個二進制位),所以,存儲器的容量就可以表示了,128個存儲單元即128字節容量。
cpu將系統中各類存儲器看作一個邏輯存儲器。?
內存地址空間的概念。內存地址空間總是大于物理內存(物理存儲器,真正的存儲器芯片所提供的存儲單元)的。
?
總結
- 上一篇: Verilgo实现的FPGA奇偶校验
- 下一篇: 群晖NAS入门(一)