Что может произойти, если процесс "убит из-за нехватки оперативной памяти"?
Иногда говорят, что linux по умолчанию никогда не отклоняет запросы дополнительной памяти из кода приложения - например malloc()
. 1 На самом деле это не так; по умолчанию используется эвристика, посредством которой
Очевидные нарушения адресного пространства отклоняются. Используется для типичной системы. Это обеспечивает серьезное сбои при распределении, позволяя при избыточном коммите сократить использование подкачки.
От [linux_src]/Documentation/vm/overcommit-accounting
(все цитаты из дерева 3.11). Точно то, что считается «серьезно диким распределением», не указывается явно, поэтому нам нужно было бы просмотреть источник, чтобы определить детали. Мы также могли бы использовать экспериментальный метод в сноске 2 (ниже), чтобы попытаться получить некоторое отражение эвристики - исходя из этого, мое первоначальное эмпирическое наблюдение состоит в том, что при идеальных обстоятельствах (== система бездействует), если вы не Если у вас нет свопа, вам будет позволено выделить около половины вашей оперативной памяти, а если у вас есть своп, вы получите около половины вашей оперативной памяти плюс весь своп. Это более или менее на процесс (но обратите внимание, что этот предел является динамическим и может изменяться в зависимости от состояния, см. Некоторые наблюдения в сноске 5).
Половина вашей оперативной памяти плюс подкачка явно является значением по умолчанию для поля «CommitLimit» в /proc/meminfo
. Вот что это означает - и обратите внимание, что на самом деле это не имеет ничего общего с только что обсужденным пределом (из [src]/Documentation/filesystems/proc.txt
):
CommitLimit: На основе коэффициента сверхкоммитирования ('vm.overcommit_ratio') это общий объем памяти, доступной в настоящее время для выделения в системе. Это ограничение соблюдается только в том случае, если включен строгий учет overcommit (режим 2 в vm.overcommit_memory). CommitLimit рассчитывается по формуле CommitLimit 7.3G.
В ранее цитируемом документе о оверкомит-бухгалтерии указано, что по умолчанию установлено значение vm.overcommit_ratio
50. Так что если вы sysctl vm.overcommit_memory=2
, то можете настроить vm.covercommit_ratio (с sysctl
) и увидеть последствия. 3 Режим по умолчанию, когда CommitLimit
он не применяется и только «очевидные превышения адресного пространства отклоняются», это когда vm.overcommit_memory=0
.
В то время как стратегия по умолчанию имеет эвристический лимит для каждого процесса, предотвращающий «серьезно дикое распределение», она оставляет систему в целом свободной, чтобы получить серьезно дикое, мудрое распределение. 4 Это означает, что в какой-то момент он может исчерпать память и должен объявить о банкротстве некоторых процессов через убийцу 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
. Он абсолютно безвреден, так как процесс не использует пространство, которое он запрашивает, т. Е. Он никогда не использует ОЗУ.
В системе 3.11 x86_64 с системой 4 ГБ и 6 ГБ подкачки произошел сбой при ~ 7400000 кБ; число колеблется, поэтому, возможно, состояние является фактором. По совпадению это близко к CommitLimit
in /proc/meminfo
, но изменение через via vm.overcommit_ratio
не имеет никакого значения. Однако на 3.6.11 32-битной системе ARM 448 МБ с 64 МБ подкачки у меня не получается ~ 230 МБ. Это интересно, поскольку в первом случае объем почти вдвое превышает объем ОЗУ, тогда как во втором он составляет около 1/4, что является сильным следствием того, что объем подкачки является фактором. Это было подтверждено отключением свопинга в первой системе, когда порог отказа снизился до ~ 1,95 ГБ, что очень похоже на маленькую коробку ARM.
Но так ли это на процесс? Похоже, что Нижеприведенная короткая программа запрашивает определенный пользователем кусок памяти, и, если это удается, ждет, когда вы нажмете return - таким образом, вы можете попробовать несколько одновременных экземпляров:
#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, вы можете видеть, как процессы чередуют свое все большее и большее распределение (это проще, если вы добавляете pid в вывод), пока один, очевидно, не потребует достаточно, чтобы другой отказал. Тогда победитель может забрать все до 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 вилок» - это не общее количество процессов, а число процессов в цепочке / ветви, ведущих к этому.
Теперь попробуйте это с 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
.