天天看點

深剖 Linux 程序位址空間

目錄

  • ​​傳統藝能😎​​
  • ​​内建指令🤔​​
  • ​​位址空間🤔​​
  • ​​虛拟位址🤔​​
  • ​​程序位址空間の意義🤔​​
  • ​​頁表🤔​​
  • ​​mm_struct 🤔​​
  • ​​程式變程序🤔​​
  • ​​寫時拷貝🤔​​
  • ​​fork 的雙傳回值🤔​​
  • ​​虛拟位址空間意義🤔​​
  • ​​保護記憶體😎​​
  • ​​功能子產品解耦😎​​
  • ​​統一視角😎​​

傳統藝能😎

深剖 Linux 程式位址空間

内建指令🤔

假如我們 kill 掉 bash 程序,我們指令行中的程序就會嗝屁,是以指令行中啟動的程序,父程序全部都是 bash 程序。

我們知道環境變量具有全局屬性,這個全局屬性的依據就是環境變量會被子程序繼承下去,也就是說隻要 bash 一開始就 export 了環境變量, bash 在開枝散葉後的所有子程序的環境變量都會随 bash 份子,但是本地變量不會繼承。

那麼問題來了,比如我定義一個本地變量,執行一個指令:

str =      

結果是 hello 毫無疑問,但是不是說子程序不繼承本地變量嗎?這裡 echo 是系統指令,那麼它本質上也是一個子程序,他是如何處理的本地變量呢?

其實 Linux 下大部分是通過子程序執行,但是還有一部分由 bash 自己執行,他會調用自己對應的函數來完成,這種指令我們稱之為内建指令,比如 cd 指令,說白了這種指令系統可信度非常高,bash 決定自己直接執行。

位址空間🤔

我們很早就有說過位址空間的分布,就像我們說 32 位系統下指針大小是 4 位元組,因為 4 正好是 32 個比特位,剛好可以表示全 0 到全 1 ,但是位址空間不是以前的非黑即白:

深剖 Linux 程式位址空間

上圖是位址空間區域圖,之前我們接觸的是除開核心的使用者空間,現在我們依舊研究這裡,這裡的共享區可能比較陌生先暫且不談。

你是否曾經覺得程式位址空間就是記憶體?不要入了C語言的坑,C語言是沒辦法講清楚的,實際上而這倆并不等同,程序位址空間屬于作業系統上的概念。

虛拟位址🤔

父子程序讀取同一個變量因為位址一樣,但後續沒有做修改時,父子程序讀取的内容卻不一樣,在 C/C++ 中使用的位址絕對不是實體位址,他叫虛拟位址,又稱為線性位址。作業系統不會讓我直接看到實體位址,他怕你修改壞壞,但是記憶體是一個硬體他并不能阻止你去通路,但是他會被動的進行讀取和寫入。

程序位址空間の意義🤔

每一個程序啟動的時候,都會讓作業系統給他建立一個位址空間,這個空間就是程序位址空間。

那麼作業系統就一定會對這些位址空間進行管理,就會有像管理程序一樣産生一個程序位址的結構體: mm_ struct ,他和 PCB 一樣是核心的一個資料結構。

深剖 Linux 程式位址空間

我們說過程序具有獨立性,多線程運作時,為了獨享各種資源期間互不幹擾,那麼他的資料和代碼是獨立的,資料結構也是獨立的,這是如何維護的呢?

我們可以類比作業系統是一個億萬富翁,而這些程序就像他的私生子,他對每個程序都畫了個大餅:程序位址空間!他為了維護程序的獨立性,各自不察覺和接觸其他程序,讓程序覺得 “ 我就是獨苗 ” 。是以,程序位址空間的最大意義就是維護程序獨立性。

所謂的位址空間其實就是 OS 通過提供一個軟體視角,認為自己回獨占系統的所有資源(記憶體)。

頁表🤔

在程序的 PCB 裡面,task_struct 會存在一個指針指向另一個描述程序位址空間的結構體 mm_struct,其實他最後抽象出來就是我們的位址空間區域。

當磁盤中資料傳輸到實體記憶體時,我們需要将虛拟位址空間和實體記憶體之間建立映射關系,程式加載到記憶體變成程序後,作業系統會給每一個程序建構一個頁表,位址空間區域其實存的是對應的虛拟位址,虛拟位址傳過來在頁表中轉換成實體位址然後才能找到實體記憶體位址。

深剖 Linux 程式位址空間

mm_struct 🤔

閣下是否聽聞傳說中的三八線?三八線就是對領地進行劃分,我們可以用結構體抽象出來:

struct 38-line
{
   int start;
   int end;
}
struct 38-line people-A = {0,50};
struct 38-line people-B = {50,100};      

如果範圍需要調整可以在 end 或者 strat 加上特定調整值即可。由此每個區域範圍,都是可以有對應編号的,再進行分區,将每個區域以連結清單形式組織起來,就可以抽象出 mm_struct 結構:

struct mm_struct
{
   long init_start;//全局變量
   long init_end;

   long uninit_start;//未初始化變量
   long uninit_end;
   
   long heap_start;//堆區
   long heap_end;

   long stack_start;//棧區
   long stack_end;
   ……
}      

程式變程序🤔

程式被編譯出來但是沒有被加載出來,在程式内部他有沒有位址?是有的;那麼程式内部有沒有區域呢?也是有的。

程序在這個區域的位址不是絕對位址而是相對位址,比如我要跑 100 m,但是我的起點定在起跑線前面 10 m,那麼我結束就應該在 110 m 處,是以我程式内部的位址和記憶體中的位址是沒有關系的!

磁盤中的代碼每一部分在什麼區域是已經确定好了的,位址編好加載到記憶體,起始偏移量從 0 開始由 0000~FFFF 進行編址,磁盤中的虛拟位址轉換記憶體中是不變的,至于加載到了那個地方就由頁表進行映射。但是虛拟位址空間不僅僅是作業系統在考慮,還有編譯器。

寫時拷貝🤔

當父子程序其中有人發生修改時,就會發生寫時拷貝去建構新的實體記憶體,實質虛拟位址是不發生變化的,是改變頁表右側的映射關系,也就能解釋内容不一樣但是位址一樣的情況,通過頁表将父子程序以寫時拷貝方式進行分離。

不管是寫入還是回收,寫時拷貝都保證了程序的獨立性原則。

fork 的雙傳回值🤔

fork 的 pid_t id,這個變量是屬于父程序棧空間中定義的變量, fork 内部 return 會執行兩次,return的本質是通過寄存器将傳回值寫入到接收變量中。當 id = fork() 時,誰先傳回誰就要發生寫時拷貝,是以同一個變量會有不同值,本質是因為大家的虛拟位址是一樣的,但是實體位址是不一樣的!

虛拟位址空間意義🤔

保護記憶體😎

如果沒有虛拟程序空間,系統會直接加載 PCB ,然後将對應的資料與代碼加載到記憶體中, CPU 執行程序時是沒有問題的,但是一旦有 bug ,存在野指針或者越界,我們 CPU 再進行通路不但會影響自己還會影響其他代碼,是以裸記憶體通路這種方法是不安全的!虛拟位址空間相當于一層軟硬體層,可以稽核轉化過程,可以直接攔截非法通路。

功能子產品解耦😎

當我們要 malloc 一段空間的話,位址空間就會幫我們先答應請求,去堆區向上移動出一段空間(這個向上移動其實就是我們 mm_struct 裡面 start 和 end 加上一個特定值進行整體移動即可)但是此時他還沒有去申請,我們會覺得他已經申請好了,其實是等你真正跑到需要開辟空間時,他會開到你需要的空間,此時空間中可能占有其他程序,系統會挪開其他程序來為你立刻開出這塊空間,最大好處就是提高運作效率,你不用就給别人用,你要用我就給你。

記憶體管理壓根不管你你要多大空間,他隻負責開好空間,建立映射等你去通路就行了,這就實作了功能子產品的解耦。

統一視角😎

繼續閱讀