Linux內核筆記--內存管理之用戶態進程內存分配


內核版本:linux-2.6.11


Linux在加載一個可執行程序的時候做了種種復雜的工作,內存分配是其中非常重要的一環,作為一個linux程序員必然會想要知道這個過程到底是怎么樣的,內核源碼會告訴你這一切。

線性區

一個可執行程序,是經過編譯器處理后的遵守一定規則的數據、符號表和指令序列的組合,當linux加載一個可執行程序的時候,會為其創建一個新的進程,其對應的進程描述符task_struct中會保存許多資源的描述符,其中的mm_struct就是這個進程的內存描述符,用來管理該進程擁有的所有內存。

一個進程擁有的內存是動態變化的,比如棧的擴充、堆的擴充、新的文件映射等等,出於這個原因,需要一個更細粒度的單位來實現內存的動態增加減少,這個單位叫線性地址區間,簡稱線性區,用vm_area_struct描述。

線性地址空間

線性地址空間是基於單個進程的,暫時拋開寫時復制機制不談,不同進程之間的線性地址空間是彼此隔離的,這是由linux的多級分頁機制實現。
一個進程擁有的線性地址空間的具體表示就是這個進程的內存描述符中存儲的線性區的集合。

死程序,活進程

現在,我們知道了,進程是通過增加和減少線性區來管理自己擁有的內存,並通過邏輯地址加上某一個線性區的基地址來進行尋址操作,那么ok,這兩點已經能夠保證一個四肢頭腦健全的進程正常運行,然而,一個可執行程序是存在硬盤上的,是一個死的東西,linux加載器需要把它變成活的,需要給她四肢給她頭腦,即把她的代碼、數據、棧、依賴庫全部放到內存中。
這個過程,從do_fork開始。

do_fork和寫時復制

Linux用do_fork來創建一個新的用戶態進程,寫時復制機制讓新的子進程在不進行寫操作的前提下會擁有父進程的所有頁框,相當於父子進程擁有相同的線性區,當子進程對線性區寫操作或者執行exec的時候,系統會將子進程的mm_struct重新初始化,

簡單說下寫時復制機制的實現:主要函數調用流程do_fork-->copy_process-->copy_mm-->dup_mmap-->copy_page_range,copy_page_range將父進程的多級頁表結構整個復制一遍,此時,父子進程擁有彼此分離的多級頁表結構,但在最后一級頁表中存放的相同的頁描述符,即子進程在進行寫操作之前依然跟父進程共享相同的頁,當子進程對某個共享頁進行寫操作時,系統會將執行流定向到do_wp_page,這個函數將復制一個新的頁來替換要寫的頁。因此一個新的進程在初始的時候跟父進程共享相同的地址空間,但經過一段時間后,父子進程的地址空間將變得真正隔離開來。

分配線性區

然而運行一個新的程序會干掉所有舊的內存空間,並為新進程重新分配新的線性地址空間,從sys_execve()即exec的系統調用例程開始,調用流程依次是sys_execve-->do_execve-->mm_alloc-->mm_init-->mm_alloc_pgd-->pgd_alloc。最后這個pgd_alloc為這個進程分配了一個新的頁全局目錄(第一級頁表)。
此時,該進程的線性地址空間依然為空,因為還未曾為其分配任何線性區。

sys_execve()會在最后會調用這個可執行程序對應格式的load_binary函數,這個函數完成了這種格式的可執行程序的加載,其中最主要的過程就是多次調用do_mmap為該進程分配一系列的線性區並存放不同的內容,分配順序是,棧段->代碼段->數據段->bss->依賴庫,堆是在運行過程中動態分配的,由內核中brk和mmap函數實現,C庫將其封裝成我們熟知的malloc函數。
線性區的分配簡單說就是掃描用戶態線性地址空間(32位系統下通常是從0x40000000開始的低3G的空間),查找一個足夠大的線性地址范圍。

經過以上的過程,新進程擁有了自己的線性地址空間,但是別忘了,系統從未給這個進程分配任何可用的物理頁,
僅僅只初始化了一個頁全局目錄,那么,當進程尋址的時候,MMU如何正確進行地址轉換呢。

分配頁框(填充頁表)

Linux順理成章的將新進程物理頁的分配放在了缺頁異常處理程序中,進程運行前期會頻繁通過缺頁異常來請求分頁,缺頁異常處理程序最終會調用伙伴系統的一個入口alloc_pages來分配新的頁框並為缺頁的線性地址填充頁表,一段時間后,該進程的運行環境就會被完全載入內存。

至此,死程序變活進程。

插一段:sys_execve()第一步是調用getname()函數,獲得程序名網上和一些書上說這個函數是用來得到一個新的頁框並從用戶空間拷貝程序名到這個頁框中,然而,2.6的源碼最終指向的一個函數是kmem_cache_alloc(cachep, flags),這個函數我在Linux內核筆記——內存管理之slab分配器里提到過,這是slab分配器的調用入口,所以從這里可以知道,getname其實是通過指定一個叫names_cachep的高速緩存描述符來分配一個這個類型的內存對象,這個names_cachep則是一個kmem_cache_t類型的指針,是一種高速緩存類型,所以這里說獲得一個新的頁框是欠妥的,實際上getname是獲得了一個names_cachep這種高速緩存里注冊的構造函數對應的一個指定的可用內存對象,然后再存入程序名到這個內存對象中。雖然這個對象可能就是一個普通頁框,這依賴於這個注冊的構造函數,
詳細解釋見Linux內核筆記——內存管理之slab分配器


PS: 個人理解,錯誤難免,望能指出,萬分感謝


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM