Điều gì có thể xảy ra nếu một quá trình "bị giết do RAM thấp"?
Đôi khi người ta nói rằng linux theo mặc định không bao giờ từ chối các yêu cầu để có thêm bộ nhớ từ mã ứng dụng - ví dụ malloc()
. 1 Điều này thực tế không đúng; mặc định sử dụng một heuristic theo đó
Rõ ràng là quá mức của không gian địa chỉ bị từ chối. Được sử dụng cho một hệ thống điển hình. Nó đảm bảo phân bổ hoang dã nghiêm trọng thất bại trong khi cho phép overcommit giảm sử dụng trao đổi.
Từ [linux_src]/Documentation/vm/overcommit-accounting
(tất cả các trích dẫn là từ cây 3.11). Chính xác những gì được coi là "phân bổ hoang dã nghiêm túc" không được làm rõ ràng, vì vậy chúng tôi sẽ phải thông qua nguồn để xác định chi tiết. Chúng tôi cũng có thể sử dụng phương pháp thử nghiệm trong chú thích 2 (bên dưới) để thử và nhận được một số phản ánh của heuristic - dựa trên đó, quan sát thực nghiệm ban đầu của tôi là trong các trường hợp lý tưởng (== hệ thống không hoạt động), nếu bạn không ' Không có bất kỳ trao đổi nào, bạn sẽ được phép phân bổ khoảng một nửa RAM của mình và nếu bạn có trao đổi, bạn sẽ nhận được khoảng một nửa RAM cộng với tất cả các trao đổi của bạn. Đó là nhiều hơn hoặc ít hơn cho mỗi quá trình (nhưng lưu ý giới hạn này là động và có thể thay đổi vì trạng thái, xem một số quan sát trong chú thích 5).
Một nửa RAM của bạn cộng với trao đổi rõ ràng là mặc định cho trường "CommitLimit" trong /proc/meminfo
. Đây là ý nghĩa của nó - và lưu ý rằng nó thực sự không liên quan gì đến giới hạn vừa thảo luận (từ [src]/Documentation/filesystems/proc.txt
):
CommitLimit: Dựa trên tỷ lệ overcommit ('vm.overcommit_ratio'), đây là tổng số lượng bộ nhớ hiện có sẵn để được phân bổ trên hệ thống. Giới hạn này chỉ được tuân thủ nếu kế toán vượt mức nghiêm ngặt được bật (chế độ 2 trong 'vm.overcommit_memory'). CommitLimit được tính theo công thức sau: CommitLimit = ('vm.overcommit_ratio' * RAM vật lý) + Hoán đổi Cam kết là 7.3G.
Tài liệu kế toán overcommit được trích dẫn trước đó nói rằng mặc định vm.overcommit_ratio
là 50. Vì vậy, nếu bạn sysctl vm.overcommit_memory=2
, bạn có thể điều chỉnh vm.covercommit_ratio (với sysctl
) và xem hậu quả. 3 Chế độ mặc định, khi CommitLimit
không được thi hành và chỉ "từ chối không gian địa chỉ rõ ràng bị từ chối", là khi nào vm.overcommit_memory=0
.
Mặc dù chiến lược mặc định có giới hạn theo quy trình heuristic ngăn chặn "phân bổ hoang dã nghiêm trọng", nhưng nó lại khiến hệ thống hoàn toàn tự do để có được sự phân bổ nghiêm túc, khôn ngoan. 4 Điều này có nghĩa là tại một thời điểm nào đó, nó có thể hết bộ nhớ và phải tuyên bố phá sản đối với một số quy trình thông qua kẻ giết người OOM .
Kẻ giết người OOM giết cái gì? Không nhất thiết là quá trình yêu cầu bộ nhớ khi không có, vì đó không nhất thiết là quá trình thực sự có tội, và quan trọng hơn, không nhất thiết là quá trình sẽ nhanh chóng đưa hệ thống ra khỏi vấn đề.
Điều này được trích dẫn từ đây có thể trích dẫn nguồn 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)
*/
Mà có vẻ như một lý do hợp lý. Tuy nhiên, không nhận được pháp y, # 5 (dự phòng số 1) có vẻ như là một triển khai bán hàng khó khăn và # 3 là dự phòng số 2. Vì vậy, có thể có ý nghĩa khi xem xét điều này giảm xuống # 2/3 và # 4.
Tôi đã xem qua một nguồn gần đây (3.11) và nhận thấy rằng nhận xét này đã thay đổi trong thời gian tạm thời:
/**
* 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.
*/
Đây là một chút rõ ràng hơn về # 2: "Mục tiêu là [giết] nhiệm vụ tiêu tốn nhiều bộ nhớ nhất để tránh những thất bại tiếp theo," và bằng hàm ý số 4 ( "chúng tôi muốn tiêu diệt số lượng quá trình tối thiểu ( một ) ) .
Nếu bạn muốn thấy kẻ giết người OOM hoạt động, xem chú thích 5.
1 Gilles ảo tưởng rất may đã loại bỏ tôi, xem bình luận.
2 Đây là một bit C đơn giản yêu cầu khối bộ nhớ ngày càng lớn để xác định khi nào yêu cầu thêm sẽ thất bại:
#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;
}
Nếu bạn không biết C, bạn có thể biên dịch cái này gcc virtlimitcheck.c -o virtlimitcheck
, rồi chạy ./virtlimitcheck
. Nó hoàn toàn vô hại, vì quá trình không sử dụng bất kỳ không gian nào mà nó yêu cầu - tức là, nó không bao giờ thực sự sử dụng bất kỳ RAM nào.
Trên hệ thống 3.11 x86_64 với hệ thống 4 GB và 6 GB trao đổi, tôi đã thất bại ở mức ~ 7400000 kB; số lượng dao động, vì vậy có lẽ nhà nước là một yếu tố. Điều này là trùng hợp ngẫu nhiên với CommitLimit
trong /proc/meminfo
, nhưng sửa đổi điều này thông qua vm.overcommit_ratio
không làm cho bất kỳ sự khác biệt. Trên hệ thống ARM 3.6 MB 32 bit 32.11 với 64 MB trao đổi, tuy nhiên, tôi thất bại ở mức ~ 230 MB. Điều này rất thú vị vì trong trường hợp đầu tiên, số lượng gần gấp đôi dung lượng RAM, trong khi trong lần thứ hai, nó chỉ bằng 1/4 - ngụ ý mạnh mẽ số lượng trao đổi là một yếu tố. Điều này đã được xác nhận bằng cách tắt trao đổi trên hệ thống đầu tiên, khi ngưỡng thất bại giảm xuống ~ 1,95 GB, một tỷ lệ rất giống với hộp ARM nhỏ.
Nhưng đây có thực sự là một quá trình? Nó xuất hiện để được. Chương trình ngắn dưới đây yêu cầu một đoạn bộ nhớ do người dùng xác định và nếu thành công, hãy đợi bạn quay trở lại - bằng cách này bạn có thể thử nhiều phiên bản đồng thời:
#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;
}
Tuy nhiên, hãy coi chừng rằng nó không nghiêm ngặt về dung lượng RAM và trao đổi bất kể sử dụng - xem chú thích 5 để biết các quan sát về tác động của trạng thái hệ thống.
3 CommitLimit
đề cập đến lượng không gian địa chỉ được phép cho hệ thống khi vm.overcommit_memory = 2. Có lẽ sau đó, số tiền bạn có thể phân bổ sẽ trừ đi những gì đã cam kết, rõ ràng là Committed_AS
trường.
Một thử nghiệm thú vị tiềm năng chứng minh điều này là thêm #include <unistd.h>
vào đầu của virtlimitcheck.c (xem chú thích 2) và một fork()
quyền trước while()
vòng lặp. Điều đó không được đảm bảo để hoạt động như được mô tả ở đây mà không có sự đồng bộ hóa tẻ nhạt, nhưng có một cơ hội tốt, 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.
Điều này có ý nghĩa - nhìn vào tmp.txt một cách chi tiết, bạn có thể thấy các quy trình thay thế phân bổ lớn hơn và lớn hơn của chúng (điều này sẽ dễ dàng hơn nếu bạn ném pid vào đầu ra) cho đến khi, một điều hiển nhiên là đã tuyên bố đủ rằng cái kia thất bại. Người chiến thắng sau đó được tự do lấy mọi thứ lên đến CommitLimit
âm Committed_AS
.
4 Điều đáng nói, tại thời điểm này, nếu bạn chưa hiểu địa chỉ ảo và phân trang theo yêu cầu, thì điều đầu tiên có thể làm cho sự cam kết có thể xảy ra là hạt nhân phân bổ cho các quy trình của người dùng hoàn toàn không phải là bộ nhớ vật lý - đó là không gian địa chỉ ảo . Ví dụ: nếu một quá trình dự trữ 10 MB cho một cái gì đó, thì đó là một chuỗi các địa chỉ (ảo), nhưng các địa chỉ đó chưa tương ứng với bộ nhớ vật lý. Khi một địa chỉ như vậy được truy cập, điều này dẫn đến lỗi trangvà sau đó kernel cố gắng ánh xạ nó vào bộ nhớ thực để nó có thể lưu trữ một giá trị thực. Các tiến trình thường dự trữ nhiều không gian ảo hơn so với thực tế chúng truy cập, điều này cho phép kernel sử dụng RAM hiệu quả nhất. Tuy nhiên, bộ nhớ vật lý vẫn là một tài nguyên hữu hạn và khi tất cả chúng đã được ánh xạ vào không gian địa chỉ ảo, một số không gian địa chỉ ảo phải được loại bỏ để giải phóng một số RAM.
5 Cảnh báo đầu tiên : Nếu bạn thử điều này với vm.overcommit_memory=0
, hãy đảm bảo bạn lưu công việc của mình trước và đóng mọi ứng dụng quan trọng, vì hệ thống sẽ bị đóng băng trong ~ 90 giây và một số quy trình sẽ chết!
Ý tưởng là chạy một quả bom ngã ba sau 90 giây, với các nhánh phân bổ không gian và một số trong số chúng ghi một lượng lớn dữ liệu vào RAM, tất cả trong khi báo cáo cho 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);
}
}
}
Biên dịch này gcc forkbomb.c -o forkbomb
. Trước tiên, hãy thử với sysctl vm.overcommit_memory=2
- bạn có thể sẽ nhận được một cái gì đó như:
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.
Trong môi trường này, loại bom ngã ba này không đi được xa lắm. Lưu ý rằng số trong "for N forks" không phải là tổng số quy trình, nó là số lượng quy trình trong chuỗi / nhánh dẫn đến quy trình đó.
Bây giờ hãy thử nó với vm.overcommit_memory=0
. Nếu bạn chuyển hướng stderr đến một tệp, bạn có thể thực hiện một số phân tích thô sau đó, ví dụ:
> 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!
Chỉ có 15 quá trình thất bại trong việc phân bổ 1 GB - chứng minh rằng các heuristic cho overcommit_memory = 0 là bị ảnh hưởng bởi nhà nước. Có bao nhiêu quá trình ở đó? Nhìn vào phần cuối của tmp.txt, có thể> 100.000. Bây giờ làm thế nào thực sự có thể sử dụng 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
Tám - một lần nữa có ý nghĩa, vì tại thời điểm đó tôi có ~ 3 GB RAM miễn phí và 6 GB trao đổi.
Có một cái nhìn vào nhật ký hệ thống của bạn sau khi bạn làm điều này. Bạn sẽ thấy điểm báo cáo sát thủ OOM (trong số những thứ khác); có lẽ điều này liên quan đến oom_badness
.