Chcę utworzyć program, który będzie symulował sytuację braku pamięci (OOM) na serwerze Unix. Stworzyłem ten super prosty pożeracz pamięci:
#include <stdio.h>
#include <stdlib.h>
unsigned long long memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
void *memory = NULL;
int eat_kilobyte()
{
memory = realloc(memory, (eaten_memory * 1024) + 1024);
if (memory == NULL)
{
// realloc failed here - we probably can't allocate more memory for whatever reason
return 1;
}
else
{
eaten_memory++;
return 0;
}
}
int main(int argc, char **argv)
{
printf("I will try to eat %i kb of ram\n", memory_to_eat);
int megabyte = 0;
while (memory_to_eat > 0)
{
memory_to_eat--;
if (eat_kilobyte())
{
printf("Failed to allocate more memory! Stucked at %i kb :(\n", eaten_memory);
return 200;
}
if (megabyte++ >= 1024)
{
printf("Eaten 1 MB of ram\n");
megabyte = 0;
}
}
printf("Successfully eaten requested memory!\n");
free(memory);
return 0;
}
Zjada tyle pamięci, ile zdefiniowano, w memory_to_eat
której teraz jest dokładnie 50 GB pamięci RAM. Alokuje pamięć o 1 MB i drukuje dokładnie punkt, w którym nie może przydzielić więcej, dzięki czemu wiem, jaką maksymalną wartość udało mu się zjeść.
Problem w tym, że to działa. Nawet w systemie z 1 GB pamięci fizycznej.
Kiedy sprawdzam górę, widzę, że proces zjada 50 GB pamięci wirtualnej i tylko mniej niż 1 MB pamięci rezydentnej. Czy istnieje sposób na stworzenie pożeracza pamięci, który naprawdę ją pochłania?
Specyfikacje systemu: jądro Linuksa 3.16 ( Debian ) najprawdopodobniej z włączonym overcommitem (nie wiem jak to sprawdzić) bez wymiany i zwirtualizowane.
źródło
sysctl -w vm.overcommit_memory=2
jako root; zobacz mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Pamiętaj, że może to mieć inne konsekwencje; w szczególności bardzo duże programy (np. przeglądarka internetowa) mogą nie uruchamiać programów pomocniczych (np. czytnik PDF).Odpowiedzi:
Kiedy twoja
malloc()
implementacja żąda pamięci od jądra systemu (przez wywołanie systemowesbrk()
lubmmap()
), jądro tylko zauważa, że zażądałeś pamięci i gdzie ma ona zostać umieszczona w twojej przestrzeni adresowej. W rzeczywistości nie mapuje jeszcze tych stron .Gdy proces następnie uzyskuje dostęp do pamięci w nowym regionie, sprzęt rozpoznaje błąd segmentacji i ostrzega jądro o stanie. Następnie jądro wyszukuje stronę w swoich własnych strukturach danych i stwierdza, że powinieneś mieć tam stronę zerową, więc odwzorowuje stronę zerową (prawdopodobnie najpierw wykluczając stronę z pamięci podręcznej stron) i wraca z przerwania. Twój proces nie zdaje sobie sprawy, że coś takiego się wydarzyło, operacja jądra jest całkowicie przezroczysta (z wyjątkiem krótkiego opóźnienia, w którym jądro wykonuje swoją pracę).
Ta optymalizacja umożliwia bardzo szybkie zwrócenie wywołania systemowego i, co najważniejsze, pozwala uniknąć przydzielenia zasobów do procesu podczas mapowania. Pozwala to procesom rezerwować dość duże bufory, których nigdy nie potrzebują w normalnych okolicznościach, bez obawy o pochłonięcie zbyt dużej ilości pamięci.
Tak więc, jeśli chcesz zaprogramować pożeracza pamięci, absolutnie musisz coś zrobić z przydzieloną pamięcią. W tym celu wystarczy dodać jedną linię do kodu:
Zauważ, że w zupełności wystarczy napisać do pojedynczego bajtu na każdej stronie (która zawiera 4096 bajtów na X86). Dzieje się tak, ponieważ cała alokacja pamięci z jądra do procesu odbywa się na poziomie szczegółowości strony pamięci, co z kolei jest spowodowane sprzętem, który nie pozwala na stronicowanie z mniejszą szczegółowością.
źródło
mmap
iMAP_POPULATE
(chociaż na stronie podręcznika jest napisane, że " MAP_POPULATE jest obsługiwane tylko dla prywatnych mapowań od Linuksa 2.6.23 ").mlockall(MCL_FUTURE)
. zadzwoń . (Wymaga to roota, ponieważulimit -l
ma tylko 64 kB dla kont użytkowników w domyślnej instalacji Debiana / Ubuntu.) Właśnie wypróbowałem to na Linuksie 3.19 z domyślnym sysctlvm/overcommit_memory = 0
, a zablokowane strony zużywają swap / fizyczną pamięć RAM.malloc()
powróci jedno z jego wywołańnull
. To oczywiście wada tego podejścia do zarządzania pamięcią. Jednak to już istnienie mapowań kopiowania przy zapisie (pomyśl o bibliotekach dynamicznych ifork()
) sprawia, że jądro nie może wiedzieć, ile pamięci będzie faktycznie potrzebne. Tak więc, gdyby nie przeciążał pamięci, zabrakłoby pamięci możliwej do mapowania na długo przed faktycznym wykorzystaniem całej pamięci fizycznej.SIGSEGV
sygnał powinien zostać dostarczony do procesu.Wszystkie strony wirtualne rozpoczynają się od kopiowania przy zapisie mapowanych na tę samą zerowaną stronę fizyczną. Aby wykorzystać strony fizyczne, możesz je zabrudzić, pisząc coś na każdej stronie wirtualnej.
Jeśli pracujesz jako root, możesz użyć
mlock(2)
lubmlockall(2)
zmusić jądro do łączenia stron, gdy są przydzielone, bez konieczności ich brudzenia. (zwykli użytkownicyulimit -l
inni niż root mają tylko 64 kB.)Ulepszona wersja kodu, która robi to, czego chciał OP:
To również rozwiązuje niezgodność łańcuchów formatu printf z typami memory_to_eat i eaten_memory, używając
%zi
do drukowaniasize_t
liczb całkowitych. Rozmiar pamięci do zjedzenia w kiB może być opcjonalnie określony jako argument wiersza poleceń.Nieuporządkowany projekt wykorzystujący zmienne globalne i zwiększający się o 1 000 zamiast 4 000 stron pozostaje niezmieniony.
źródło
Przeprowadzana jest tutaj sensowna optymalizacja. Środowisko wykonawcze nie pobiera pamięci, dopóki jej nie użyjesz.
Prosty
memcpy
wystarczy, aby obejść tę optymalizację. (Może się okazać, żecalloc
nadal optymalizuje alokację pamięci do momentu użycia).źródło
malloc
przydziela na granice strony, co wydaje mi się całkiem prawdopodobne.calloc
wykorzystuje mmap (MAP_ANONYMOUS) dając zerowane strony, więc nie powiela pracy jądra zerowania stron.Nie jestem pewien co do tego, ale jedyne wyjaśnienie, jakie mogę sobie wyobrazić, to fakt, że Linux jest systemem operacyjnym typu „kopiuj przy zapisie”. Kiedy ktoś wywołuje,fork
oba procesy wskazują na tę samą pamięć fizyczną. Pamięć jest kopiowana tylko wtedy, gdy jeden proces faktycznie ZAPISUJE do pamięci.Myślę, że tutaj faktyczna pamięć fizyczna jest przydzielana tylko wtedy, gdy próbuje się coś do niej zapisać. Wywołanie
sbrk
lubmmap
może tylko zaktualizować księgę pamięci jądra. Rzeczywista pamięć RAM może zostać przydzielona tylko wtedy, gdy faktycznie próbujemy uzyskać dostęp do pamięci.źródło
fork
nie ma z tym nic wspólnego. Zobaczysz to samo zachowanie, jeśli uruchomisz Linuksa z tym programem jako/sbin/init
. (tj. PID 1, pierwszy proces w trybie użytkownika). Miałeś jednak dobry ogólny pomysł z kopiowaniem przy zapisie: dopóki ich nie zabrudzisz, wszystkie nowo przydzielone strony są mapowane w trybie kopiowania przy zapisie na tę samą zerowaną stronę.