[翻譯] 內存 - 第一部分:內存類型

原文地址: https://techtalk.intersec.com/2013/08/memory-part-1-managing-memory/ 程序員

# 介紹

在Intersec.com咱們選擇C語言。由於它能讓咱們徹底控制想要作的事。而且能達到一個至關高的性能。對於許多人來講,性能就是儘量的少用CPU指令。然而,現代的硬件是如此複雜而不能僅僅考慮CPU。算法必須考慮內存,CPU,磁盤和網絡I/O等等。這些每一項都給算法增長了開銷。爲了同時保證算法的性能和可靠性咱們必須正確的理解他們中的每一項。 算法


CPU對性能的影響(由此產生了算法複雜度)已經被很好的認識到。磁盤和網絡的延遲也是如此。可是內存彷佛不多被深刻的理解。根據從咱們的客戶那裏獲得的經驗來看,即便是普遍使用的工具,例如top,對於大部分系統管理員來講都是神祕的。 網絡


這篇文章是五篇關於內存的一系列文章中的第一篇。咱們將會講解內存的定義,它是如何管理的,如何解讀工具的輸出等。這個系列的文章將面向開發人員和管理員都感興趣的主題。儘管大部分的規則都適用於大部分的現代操做系統,咱們也會特別談到Linux和C語言相關的內容。 工具


咱們不是第一個寫關於內存的文章。實際上,咱們想推薦一篇很棒的文章,由Ulricht Drepper寫的:[每個程序員應該知道的關於內存的知識]http://www.akkadia.org/drepper/cpumemory.pdf 性能


這是第一篇文章,咱們會講解內存的定義。假定你至少知道地址和進程這些基本的知識。也會涉及到系統調用和用戶態及內核態的區別。不過你只須要知道你的進程(用戶態)運行在內核之上,內核負責跟硬件交互,系統調用讓你的進程跟內核交談,以請求更多的資源。你能夠閱讀各個系統調用各自的手冊來了解更多的細節。 spa


# 虛擬內存

現代操做系統中,每一個進程存活在它本身的內存分配空間中。操做系統提供了硬件抽象層併爲每一個進程建立一個虛擬地址空間,而不是直接把內存地址映射到硬件地址。這個物理地址到虛擬地址的映射是由CPU完成的,而CPU是使用了內核爲每一個進程維護的一張轉換表(每次內核改變了運行在一個特定CPU核上的進程,它也要改變那個CPU的轉換表)。 操作系統


使用虛擬內存有幾個目的。第一,進程隔離。一個進程在用戶空間只能以虛擬內存來表示內存地址空間。所以它只能訪問被映射到虛擬地址空間的數據,而不能訪問其餘進程的內存(除了聲明爲共享的內存)。 設計


第二個目的是對硬件抽象。內核能夠自由的改變物理地址到虛擬地址的映射。也能夠不映射特定的虛擬地址直到它被真正的用到。此外,在內存長時間沒有被使用,或者系統內存不足時能夠把內存交換到磁盤。整體上給了內核很大的自由,惟一的限制是程序在讀取內存時實際上會看到以前寫過的數據。 orm


第三個目的是能夠給實際上不在物理內存(RAM)中的東西分配地址。這是mmap和映射文件背後的原理。你能夠把虛擬地址分配給一個文件,而後能夠像訪問內存buffer同樣來訪問它。這是很是有用的抽象,能夠保持代碼的簡單行。因爲64位系統有巨大的虛擬空間,只要你願意,你能夠映射你的整個硬盤到虛擬內存。 進程


第四個目的是共享。因爲內核知道各個運行進程在虛擬空間中的映射狀況,它能夠避免把數據重複加載到內存,使得使用相同資源的不一樣進程的虛擬地址指向相同的物理內存(即便每一個進程實際的虛擬地址不同)。內存共享使得內核可使用寫時拷貝(COW):當兩個進程使用相同的數據,但其中一個進程修改了數據而另外一個進程不容許看到改變時,內核會複製一份數據。更多時候,操做系統有能力檢測到多個地址空間的內存相等,自動把它們映射到相同的物理內存(把他們標識爲COW)。在Linux上這也稱爲KSM(內核相同頁合併Kerneal SamePage Merging)。


## fork()

關於COW最爲人們所知的就是fork()。 在類UNIX系統上,fork()是一個系統調用,它經過複製當前進程來建立一個新的進程。當fork()返回時,兩個進程在同一個點繼續執行。這兩個進程有着相同的打開文件,和相同的內存。感謝COW,fork()不會複製內存。只有數據被父進程或子進程修改時,數據纔會在內存中複製。因爲fork()大部分的使用場景是馬上跟一個exec()調用,使得整個虛擬內存地址空間失效,COW機制避免了對父進程內存的無用的拷貝。


另外一個附帶的好處是,fork()以很小的代價建立了一個進程(私有)內存的快照。若是你想在進程的內存上執行一些操做而不想冒被本身修改的風險,同時不想增長開銷很大且容易出錯的鎖機制,那麼僅僅fork,完成你的工做,而後把結果返回到父進程(經過返回碼,文件,共享內存,管道等等)


若是你的計算足夠快這個機制會很是好的工做。所以一個大塊的內存在父進程和子進程直接共享。這樣使你的代碼簡單。複雜性隱藏在內核的虛擬內存代碼中,而不是你的代碼。


## 頁

虛擬內存被分爲pages(頁)。頁的大小由CPU決定,一般是4KB。這意味着在內核中內存管理是以頁爲單位進行的。當你請求新的內存,內核會給你一個或多個頁。統一,當你釋放內存時,會釋放一個或多個頁。每一個更精細的內存分配 API(例如,malloc)都在用戶空間實現。


對每一個分配的頁,內核記錄了一組權限:可讀,可寫和/或可執行(注意不是全部的組合均可行)。這些權限在映射內存時設置,也可使用mprotect()系統調用在以後設置。沒有分配的也沒法訪問。當你嘗試對一個頁執行禁止的操做(例如,讀取沒有讀權限的頁),你會觸發(在Linux上)一個段錯誤(Segmentation Fault)。從側面來講,你會看到,段錯誤的粒度也是頁,你可能會執行一個超出地址的訪問,可是沒有引起段錯誤。


# 內存類型

虛擬內存空間內分配的內存不都是同樣的。咱們從兩個緯度來區分:第一個緯度是內存是私有的(private, 某個進程特有的)或共享的。第二個緯度是內存是不是文件備份的。非文件備份也叫作匿名。這兩個緯度把內存分爲四個類型:

## 私有內存

私有內存,正如其名,是指進程專用的內存。你在程序中用到的內存大部分都是私有內存。

因爲私有內存的變化對其餘進程是不可見的,它是寫時拷貝的。反作用是,即便內存是私有的,幾個進程也可能用一樣的物理內存來存放數據,特別是二進制執行文件和動態共享庫。一個常見的誤解是KDE佔用了不少物理內存,由於每個進程都加載了Qt和KDE庫。實際上,感謝COW機制,全部的進程都用徹底相同的物理內存來存放這些庫的只讀部分。

對於基於文件的私有內存來講,被進程修改的部分不會被寫回到對應的文件中。可是對文件的修改,進程能夠看到,也能夠不看到。

## 共享內存

共享內存被設計爲用於進程間通訊。它只能用明確的請求來建立,好比mmap或shm*系統調用。當一個進程修改共享內存時,全部映射一樣內存的進程都可以看到修改。

對基於文件的共享內存來講,全部映射同一文件的進程都會看到文件的變化,由於這些變化會經過文件自己來傳遞。(注,也就是說,進程對這類內存的改變,會回寫到文件中,這跟私有內存是不一樣的)

## 匿名內存

匿名內存徹底在物理內存中。可是內核在這塊內存被寫入以前,不會給它映射到實際的物理內存地址。所以,匿名內存在真正使用前,不會給內核帶來任何負擔。這使得進程能夠在虛擬地址空間「預留」不少的內存,而佔用實際的物理內存。所以內核容許你分配比實際內存更大的內存。這個行爲一般稱爲內存over-commit(或者memory overcommitment)。

## 基於文件和交換分區

當一個內存映射是基於文件的,數據從磁盤載入。大多數時間,載入是按需的。可是你能夠給內核一些提示,讓它提早預讀取。當你使用一個特別的訪問模式(一般是線性訪問)時,這會讓你的程序反應迅速。爲了不使用太多的物理內存,你能夠告訴內核,你再也不關心物理內存中的頁,可是不要解除映射關係。全部這些均可以經過系統調用madvise()實現

當系統缺乏物理內存時,內核會嘗試把一些數據從物理內存移動到磁盤上。若是內存是基於文件且共享的,這很容易。由於數據的源是文件,只要把數據從物理內存移除就行。下次要再使用這部分數據時,再從文件加載。

內核也可能選擇從物理內存移除匿名/私有內存數據。這種狀況下,數據會被寫到磁盤上一個特別的地方。這被稱爲交換出去。在Linux上,swap(交換分區)一般被存放在一個特別的分區。在其餘系統,它是一個特別的文件。而後它能夠像基於文件的內存同樣工做:當它被訪問時,再從磁盤加載到物理內存。

由於使用了虛擬地址空間,頁被換進換出對於進程是徹底透明的。除了磁盤I/O帶來的延遲。

相關文章
相關標籤/搜索