![Linux 會「耗盡 RAM」嗎?](https://rvso.com/image/50560/Linux%20%E6%9C%83%E3%80%8C%E8%80%97%E7%9B%A1%20RAM%E3%80%8D%E5%97%8E%EF%BC%9F.png)
我在網路上看到一些帖子,人們顯然在抱怨託管的 VPS 意外終止進程,因為它們使用了太多 RAM。
這怎麼可能?我認為所有現代作業系統都透過僅使用磁碟交換來處理實體 RAM 上的任何內容來提供「無限 RAM」。它是否正確?
如果進程“由於 RAM 不足而終止”,可能會發生什麼情況?
答案1
如果進程“由於 RAM 不足而終止”,可能會發生什麼情況?
有時有人說,Linux 預設從不拒絕應用程式程式碼對更多記憶體的請求——例如malloc()
。1 事實上這並非事實;預設使用啟發式
明顯的地址空間過量使用將被拒絕。用於典型系統。它確保嚴重的瘋狂分配失敗,同時允許過度使用以減少交換使用。
來自[linux_src]/Documentation/vm/overcommit-accounting
(所有引用均來自 3.11 樹)。到底什麼才算是「嚴重瘋狂的分配」並沒有明確說明,因此我們必須透過原始碼來確定細節。我們也可以使用腳註 2(如下)中的實驗方法來嘗試獲得啟發式的一些反映——基於此,我最初的經驗觀察是,在理想情況下(==系統空閒),如果你不這樣做如果沒有任何交換,您將被允許分配大約一半的RAM,如果您有交換,您將獲得大約一半的RAM 加上所有交換。也就是或多或少每個行程(但請注意這個限制是動態並可能因狀態而變化,請參閱註腳 5)中的一些觀察結果。
RAM 的一半加上交換區明確地預設為/proc/meminfo
.這就是它的含義 - 請注意它實際上與剛剛討論的限制無關(來自[src]/Documentation/filesystems/proc.txt
):
提交限制:基於過量使用比率('vm.overcommit_ratio'),這是目前可分配的記憶體總量在系統上。只有在啟用嚴格的過量使用記帳(「vm.overcommit_memory」中的模式 2)時,才會遵守此限制。 CommitLimit 使用以下公式計算: CommitLimit = ('vm.overcommit_ratio' * 實體 RAM) + Swap 例如,在具有 1G 實體 RAM 和 7G 交換空間且 'vm.overcommit_ratio' 為 30 的系統上,將產生CommitLimit 為 7.3G。
前面引用的 overcommit-accounting 文件指出預設值為vm.overcommit_ratio
50 sysctl vm.overcommit_memory=2
。3 預設模式,當不強制執行並且僅“拒絕明顯的地址空間過度使用”時,是when 。sysctl
CommitLimit
vm.overcommit_memory=0
雖然預設策略確實有一個啟發式的每個進程限制,可以防止“嚴重瘋狂的分配”,但它確實使整個系統可以自由地進行嚴重瘋狂的分配。4 這意味著在某些時候它可能會耗盡內存,並且必須通過OOM 殺手。
OOM 殺手會殺死什麼?不一定是在沒有記憶體的情況下請求記憶體的進程,因為這不一定是真正有罪的進程,更重要的是,不一定是最能讓系統擺脫當前問題的進程。
這是引用自這裡其中可能引用了 2.6.x 原始碼:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
這似乎是一個不錯的理由。然而,在沒有進行取證的情況下,#5(與#1 是多餘的)似乎很難實現,而#3 與#2 是多餘的。因此,考慮將其縮減為#2/3 和#4 可能是有意義的。
我查了一下最近的來源(3.11)並注意到此評論在此期間發生了變化:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
這對#2 來說更明確一些:“目標是[殺死]消耗最多內存的任務,以避免後續的 oom 失敗,”並暗示 #4 (「我們想要殺死最少量的進程(一))。
如果您想了解 OOM 殺手的實際情況,請參閱註腳 5。
1謝天謝地,吉爾斯擺脫了我的妄想,請參閱評論。
2這是一個簡單的 C 程式碼,它要求越來越大的記憶體區塊來確定何時請求更多記憶體會失敗:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
如果你不懂C,你可以編譯它gcc virtlimitcheck.c -o virtlimitcheck
,然後執行./virtlimitcheck
。它是完全無害的,因為該進程不使用它所要求的任何空間——即,它從未真正使用任何 RAM。
在具有 4 GB 系統和 6 GB 交換空間的 3.11 x86_64 系統上,我在 ~7400000 kB 處失敗;數字會波動,所以狀態可能是一個因素。這恰好與CommitLimit
in接近/proc/meminfo
,但修改此 viavm.overcommit_ratio
沒有任何區別。然而,在具有 64 MB 交換空間的 3.6.11 32 位元 ARM 448 MB 系統上,我在大約 230 MB 時失敗。這很有趣,因為在第一種情況下,該數量幾乎是 RAM 數量的兩倍,而在第二種情況下,大約是 RAM 數量的 1/4——強烈暗示交換量是一個因素。當故障閾值降至約 1.95 GB 時,關閉第一個系統上的交換區證實了這一點,與小 ARM 盒子的比率非常相似。
但這真的是每個進程嗎?看來是的。下面的簡短程式請求使用者定義的記憶體區塊,如果成功,則等待您按回車鍵 - 這樣您就可以嘗試多個同時實例:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
但請注意,無論使用如何,它並不嚴格涉及 RAM 和交換空間的數量 - 有關係統狀態影響的觀察,請參閱註腳 5。
3 CommitLimit
指的是允許的位址空間量系統當 vm.overcommit_memory = 2 時Committed_AS
。
一個可能有趣的實驗證明了這一點,即添加#include <unistd.h>
到 virtlimitcheck.c 的頂部(參見腳註 2),並在循環fork()
之前添加一個while()
。如果沒有一些繁瑣的同步,不能保證按照這裡描述的方式工作,但很有可能它會,YMMV:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
這是有道理的——詳細查看tmp.txt,您可以看到進程交替分配越來越大的分配(如果您將pid 放入輸出中,這會更容易),直到一個進程顯然已經聲明了足夠多的資源,導致另一個行程失敗。然後獲勝者可以自由地獲得CommitLimit
負數以內的所有東西Committed_AS
。
4值得一提的是,在這一點上,如果您還不了解虛擬尋址和需求分頁,那麼首先導致過度承諾的原因是內核分配給用戶態進程的根本不是實體記憶體——而是實體記憶體。虛擬位址空間。例如,如果一個進程為某項保留 10 MB,則會將其佈置為一系列(虛擬)位址,但這些位址尚未對應於實體記憶體。當訪問這樣的地址時,這會導致頁面錯誤然後內核嘗試將其映射到真實內存,以便它可以存儲真實值。進程通常保留比實際存取更多的虛擬空間,這使得核心能夠最有效地利用 RAM。然而,實體記憶體仍然是有限的資源,當所有實體記憶體都映射到虛擬位址空間時,必須消除一些虛擬位址空間以釋放一些 RAM。
5第一一個警告:如果您使用 嘗試此操作vm.overcommit_memory=0
,請確保先保存您的工作並關閉所有關鍵應用程序,因為系統將凍結約 90 秒,並且某些進程將終止!
這個想法是運行一個叉子炸彈90 秒後超時,分叉分配空間,其中一些將大量資料寫入 RAM,同時向 stderr 報告。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
編譯這個gcc forkbomb.c -o forkbomb
.首先,試試看sysctl vm.overcommit_memory=2
-你可能會得到類似的結果:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
在這種環境下,這種叉子炸彈走不了多遠。請注意,「說 N 個分支」中的數字不是進程總數,而是通往該分支的鏈/分支中的進程數。
現在嘗試一下vm.overcommit_memory=0
。如果將 stderr 重新導向到文件,則可以隨後進行一些粗略分析,例如:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
只有 15 個行程未能分配 1 GB - 顯示 overcommit_memory = 0 的啟發式是受國家影響。有多少個進程?查看 tmp.txt 的結尾,可能> 100,000。現在如何才能實際使用 1 GB 呢?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
八——這又是有道理的,因為當時我有大約 3 GB 的可用 RAM 和 6 GB 的交換空間。
執行此操作後查看系統日誌。您應該看到 OOM 殺手報告分數(除其他外);大概這與oom_badness
.
答案2
如果您只將 1G 資料載入到記憶體中,則不會發生這種情況。如果您加載更多怎麼辦?例如,我經常處理包含數百萬個機率的巨大文件,需要將其加載到 R 中。
在我的筆記型電腦上運行上述過程將導致一旦我的 8GB RAM 被填滿,它就會開始瘋狂地交換。反過來,這會減慢一切速度,因為從磁碟讀取比從 RAM 讀取慢得多。如果我的筆記型電腦記憶體為 2GB,可用空間只有 10GB,該怎麼辦?一旦進程佔用了所有 RAM,它也會填滿磁碟,因為它正在寫入交換,而我沒有更多的 RAM 和交換空間(人們傾向於將交換限製到專用分區而不是交換文件正是出於這個原因)。這就是 OOM 殺手發揮作用並開始殺死進程的地方。
因此,系統確實可能會耗盡記憶體。此外,大量交換的系統可能早在這種情況發生之前就變得不可用,這僅僅是因為交換導致 I/O 操作緩慢。人們通常希望盡可能避免交換。即使在配備快速 SSD 的高階伺服器上,效能也會明顯下降。在我的筆記型電腦上,它有一個經典的 7200RPM 驅動器,任何重大的交換基本上都會導致系統無法使用。交換的越多,速度就越慢。如果我不快速終止有問題的進程,一切都會掛起,直到 OOM 殺手介入。
答案3
當沒有更多的 RAM 時,進程不會被殺死,當它們被這樣欺騙時,它們會被殺死:
- Linux 核心通常允許進程分配(即保留)一定量的虛擬內存,該內存量大於實際可用內存(部分 RAM + 所有交換區域)
- 只要進程只存取它們保留的頁面的子集,一切就可以正常運作。
- 如果一段時間後,進程嘗試存取它擁有的頁面,但沒有更多頁面可用,則會發生記憶體不足的情況
- OOM 殺手選擇其中一個進程(不一定是請求新頁面的進程),然後終止它以恢復虛擬記憶體。
即使系統沒有主動交換,例如,如果交換區域充滿了休眠守護程序記憶體頁面,也可能會發生這種情況。
在不過度使用記憶體的作業系統上永遠不會發生這種情況。使用它們,不會殺死隨機進程,但在虛擬記憶體耗盡時請求虛擬記憶體的第一個進程會錯誤地傳回 malloc (或類似的)。因此,它有機會妥善處理這一情況。然而,在這些作業系統上,也可能會出現系統耗盡虛擬記憶體而仍有可用 RAM 的情況,這是相當令人困惑且通常會被誤解的情況。
答案4
只是從其他答案中添加另一個觀點,許多 VPS 在任何給定伺服器上託管多個虛擬機器。任何單一虛擬機器都將擁有指定數量的 RAM 供自己使用。許多提供者提供“突發 RAM”,他們可以使用超出指定數量的 RAM。這僅適用於短期使用,超出此延長時間的用戶可能會受到主機終止進程以減少 RAM 使用量的懲罰,這樣其他人就不會受到影響主機超載。