Що може статися, якщо процес "знищений через низьку оперативну пам'ять"?
Іноді кажуть, що за замовчуванням Linux ніколи не відмовляє в запитах на отримання більшої кількості пам'яті з коду програми - наприклад malloc()
. 1 Це насправді не так; за замовчуванням використовується евристика, згідно з якою
Очевидне перевиконання адресного простору відмовляється. Використовується для типової системи. Це забезпечує серйозну помилку розподілу, дозволяючи перевиконати комісію для зменшення використання свопів.
З [linux_src]/Documentation/vm/overcommit-accounting
(усі цитати з дерева 3,11). Точно те, що вважається "серйозно диким виділенням", не є явним, тому нам доведеться пройти джерело, щоб визначити деталі. Ми також могли б використати експериментальний метод у виносці 2 (нижче), щоб спробувати отримати деяке евристичне відображення - виходячи з цього, моє первісне емпіричне спостереження полягає в тому, що при ідеальних обставинах (== система не працює), якщо ви не ' не матимете жодного свопу, вам буде дозволено виділити близько половини оперативної пам’яті, а якщо у вас є підміна, ви отримаєте приблизно половину своєї оперативної пам’яті плюс усю свопу. Це більше або менше на кожен процес (але зауважте, що ця межа є динамічною і може змінюватися через стан, див. Деякі спостереження у виносці 5).
Половина вашої оперативної пам’яті плюс своп - явно за замовчуванням для поля «Команда ліміту» в /proc/meminfo
. Ось що це означає - і зауважте, що насправді це не має нічого спільного з межею, щойно обговорюваною (з [src]/Documentation/filesystems/proc.txt
):
CommitLimit: Виходячи з коефіцієнта перевиконання ('vm.overcommit_ratio'), це загальний обсяг пам’яті, який зараз можна розподілити в системі. Цього ліміту дотримується лише в тому випадку, якщо включений суворий облік перевиконання (режим 2 у "vm.overcommit_memory"). CommLimit обчислюється за такою формулою: CommitLimit = ('vm.overcommit_ratio' * Physical RAM) + Swap Наприклад, у системі з 1G фізичної оперативної пам’яті та 7G свопу з 'vm.overcommit_ratio' 30, це дасть Комісія лінійки 7.3G.
У раніше цитованому документі з доповіддю з бухгалтерського обліку зазначається, що за замовчуванням vm.overcommit_ratio
дорівнює 50. Тож якщо sysctl vm.overcommit_memory=2
ви можете, то можете налаштувати vm.covercommit_ratio (with sysctl
) і побачити наслідки. 3 Режим за замовчуванням, коли CommitLimit
він не застосовується і відмовляються лише "явні перевиконання адресного простору", - це коли vm.overcommit_memory=0
.
У той час як стратегія за замовчуванням має евристичний обмеження на процес, що запобігає "серйозно дикому виділенню", вона залишає систему в цілому вільною, щоб отримати серйозну дикість, а також розподілити розумно. 4 Це означає, що в якийсь момент у неї може закінчитися пам'ять, і доведеться оголосити про банкрутство певного процесу через убивцю ООМ .
Що вбиває вбивцю ООМ? Це не обов'язково той процес, який вимагав пам'яті, коли його не було, оскільки це не обов'язково справді винен процес, і що ще важливіше, не обов'язково той, який найшвидше позбавить систему від проблеми, в якій знаходиться.
Це цитується звідси, що, ймовірно, цитує джерело 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: "Мета - [вбити] завдання, що споживає найбільше пам’яті, щоб уникнути наступних невдач уомі", і за наслідком №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
. Це абсолютно нешкідливо, оскільки процес не використовує жоден простір, який він вимагає - тобто, він ніколи не використовує жодної ОЗУ.
У системі 3,11 x86_64 із системою 4 ГБ та 6 ГБ свопу, я не вдався до ~ 7400000 кБ; число коливається, тому, можливо, стан є фактором. Це збігом близько до CommitLimit
дюйма /proc/meminfo
, але зміна цього з допомогою vm.overcommit_ratio
робить ніякої різниці. У 3.6.11 32-бітовій системі 448 Мб ARM з 64 МБ свопів, однак, я не виходить при ~ 230 МБ. Це цікаво, оскільки в першому випадку сума майже вдвічі перевищує об'єм оперативної пам’яті, тоді як у другому це приблизно 1/4, що - сильно означає, що обмін свопом є фактором. Це було підтверджено вимкненням swap у першій системі, коли поріг відмови знизився до ~ 1,95 ГБ, що дуже схоже відношення до маленької коробки 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;
}
Однак остерігайтеся, що мова йде не лише про кількість оперативної пам’яті та своп незалежно від використання - див. Виноску 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, ви можете бачити, як процеси чергуються їх більші та більші виділення (це простіше, якщо ви кинете під у вихід), поки один, очевидно, не стверджує достатньо, що інший не працює. Тоді переможець може безкоштовно схопити все до CommitLimit
мінуса Committed_AS
.
4 У цьому моменті варто згадати, якщо ви вже не розумієте віртуальну адресацію та підказка з вимогою, що в першу чергу можливе перевиконання зобов’язань - це те, що ядро виділяє для процесів користувальницької роботи - це зовсім не фізична пам'ять - це віртуальний адресний простір . Наприклад, якщо процес резервує 10 Мб для чогось, це викладається як послідовність (віртуальних) адрес, але ці адреси ще не відповідають фізичній пам'яті. Коли доступ до такої адреси, це призводить до помилки сторінкиа потім ядро намагається відобразити його на реальній пам'яті, щоб воно могло зберігати реальне значення. Процеси зазвичай резервують набагато більше віртуального простору, ніж вони фактично отримують доступ, що дозволяє ядру максимально ефективно використовувати ОЗУ. Однак фізична пам’ять все ще є кінцевим ресурсом, і коли все це було зіставлено у віртуальний адресний простір, потрібно звільнити деякий віртуальний адресний простір, щоб звільнити деяку оперативну пам'ять.
5 Перше попередження : Якщо ви спробуєте це vm.overcommit_memory=0
, спочатку збережіть свою роботу та закрийте будь-які критичні програми, оскільки система буде заморожена на ~ 90 секунд, і деякий процес загине!
Ідея полягає у тому, щоб запустити вилкову бомбу, яка вичерпується через 90 секунд, при цьому вилки виділяють простір, а деякі з них записують велику кількість даних в оперативну пам'ять, весь час звітуючи перед 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 forks" - не загальна кількість процесів, це кількість процесів у ланцюзі / гілці, що веде до цього.
Тепер спробуйте 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 ГБ - демонстрації того, що евристика для overcommit_memory = 0 буде залежить від стану. Скільки там процесів було? Переглядаючи кінець tmp.txt, ймовірно,> 100 000. Тепер, як насправді можна використовувати 1 Гб?
> 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 ГБ оперативної пам’яті та 6 ГБ свопу.
Перегляньте свої системні журнали після цього. Ви повинні побачити результати звітів про вбивство OOM (серед іншого); імовірно, це стосується oom_badness
.