Tworzenie pliku wymiany – czyli jak sobie radzić z tymczasowym niedoborem pamięci

Tworzenie pliku przestrzeni wymiany

Jednym z kamieni milowych w rozwoju systemów operacyjnych była wirtualizacja pamięci. Dzięki niej procesy w systemie „widzą” pamięć jako ciągły obszar, który jest dla nich dostępny. Naturalnym rozwinięciem możliwości wirtualizacji pamięci jest przetrzymywanie stron pamięci nie tylko w pamięci operacyjnej (RAM), ale także zrzucanie i wczytywanie jej z urządzeń blokowych (np. dyski). W dzisiejszym artykule przyjrzymy się tworzeniu pliku wymiany, który będzie się znajdował na jednym z dysków naszego systemu.

Jednym z problemów, na który mogą natknąć się deweloperzy podczas budowania „ciężkich” programów, takich jak np. .NET czy serwery baz danych, jest wyczerpanie się pamięci. W logach pierścieniowego buforu jądra (ang. kernel ring buffer) pojawia się wtedy następujący log w formacie:

[CZAS] Out of memory: Killed process PID (NAZWA_PROCESU) # dalsze informacje o pamięci
[CZAS] oom_reaper: reaped process PID (NAZWA_PROCESU), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Przykładowy log prezentuje się następująco:

[227711.146660] Out of memory: Killed process 842395 (cc1plus) total-vm:3291904kB, anon-rss:2984768kB, file-rss:0kB, shmem-rss:0kB, UID:1001 pgtables:768kB oom_score_adj:0
[227711.168569] oom_reaper: reaped process 842395 (cc1plus), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

Zjawisko to powiązane jest z reguły ze zrównolegleniem budowania, gdy przykładowo na 16 rdzeni logicznych (ang. logical CPU) przypada 32GiB ramu, co może okazać się niewystarczającą ilością pamięci operacyjnej. Na dodatek kupując mocniejszą maszynę łatwiej jest pokusić się o zwiększenie wolumenu zadań, jakie może ona wykonywać. Można tego dokonać np. poprzez ustawienie większej ilości jednostek wykonawczych, zwiększenie maksymalnego obciążenia lub innej wartości w rozwiązaniach przeprowadzających automatyczne budowanie projektu. W obecnych czasach przestrzeń wymiany (ang. swap) jest rzadko wykorzystywana, a stare zasady mówiące o jej rozmiarze zostały wyparte na rzecz jej całkowitego porzucenia. W tym artykule zakładamy jednak, że system, na którym działamy, nie ma pamięci swap na osobnej partycji lub ma jej za mało.

W takich specyficznych przypadkach pomocny może okazać się plik swap, którego możemy użyć tymczasowo lub zamontować na stale. Proces ten opisany jest poniżej.

Tworzenie obszaru wymiany pamięci w pliku

Tworzenie pliku swap polega na:

  1. Utworzeniu pliku wypełnionego zerami.
  2. Nadaniu plikowi bezpiecznych uprawnień.
  3. Użyciu komendy mkswap w celu stworzenia odpowiednich, zarządzalnych struktur dla jądra.
  4. Dodaniu pliku tymczasowego swap do aktywnych/używanych przestrzeni wymiany.
  5. Zamontowaniu na stałe pliku swap, jeżeli istnieje taka potrzeba lub usunięcie go, gdy przestanie być potrzebny.

Wbrew pozorom punkt pierwszy nie jest taki oczywisty. W przykładzie man 8 mkswap do utworzenia pliku swap została użyta komenda fallocate. Jej składnia jest bardziej przyjazna od składni pradawnego programu dd. Posiada jednak jedną zasadniczą wadę – może nie zadziałać poprawnie na systemach plików ext4, xfs lub btrfs – czyli tych najpopularniejszych 🙂 Wynika to z mechanizmów takich jak prealokacje i CoW (Copy-on-Write). Należy więc zmusić system plików do rzeczywistej alokacji, gdyż jądro Linuksa będzie używać bezpośredniego dostępu do pliku, zamiast wykorzystywać abstrakcję oferowaną przez VFS (ang. Virtual File System).

# Tworzenie pliku myswapfile o rozmiarze 1GiB
sudo dd if=/dev/zero of=/myswapfile bs=1M count=1024

Plik swap powinien móc być odczytywany i zapisywany tylko przez użytkownika root. Jego odczyt przez innych użytkowników może poskutkować wyciekiem danych, które zostały zapisane w przestrzeni wymiany. Dlatego należy na nim wykonać:

sudo chown 600 /swapfile

Trzeci krok polega na stworzeniu odpowiedniej struktury wykorzystywanej przez jądro do zarządzania plikiem lub partycją swap. Można porównać plik przed i po użyciu.

[[email protected] ~]$ xxd -l 100 /swapfile 
0000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000010: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000020: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
0000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
(...)

Zainteresowani mogą zobaczyć, że nie występują w nim inne informacje od zera:

[[email protected] ~]$ xxd  /swapfile | fgrep -v '................'

Następnie wykonujemy komendę sudo mkswap /swapfile i przy pomocy fgrep (lub grep -F) filtrujemy jego zawartość.

[[email protected] ~]$ sudo mkswap  /swapfile
[[email protected] ~]$ xxd  /swapfile | fgrep -v '................'
0000400: 0100 0000 ffff 0300 0000 0000 37c4 1595  ............7...
0000410: cb34 4a42 8b23 1c82 4a8e 10bc 0000 0000  .4JB.#..J.......
0000ff0: 0000 0000 0000 5357 4150 5350 4143 4532  ......SWAPSPACE2

Struktura stworzona na potrzeby jądra nie jest imponująca. W tym miejscu występuje jednak pewna ciekawostka. mkswap nie nadpisuje pierwszego bloku danych, ponieważ mogą się tam znajdować informacje takie jak bootloader (w starszych komputerach uruchamianie systemu było wieloetapowe i wykorzystywało MBR [ang. Master Boot Record]) czy tabela partycji. Stąd dopiero po przesunięciu (8192 bitów -> 1024 bajty -> 1KiB [000400 to 1024 w systemie szesnastkowym]) pojawiają się dane. Jest to zresztą zgodne z unią swap_header znajdującą się w kodzie źródłowym Linuksa. Unia to struktura w C, która w zależności od kontekstu może być różnie interpretowana tj. zawierać różne dane.

// https://github.com/torvalds/linux/blob/master/include/linux/swap.h
union swap_header {
	struct {
		char reserved[PAGE_SIZE - 10];
		char magic[10];			/* SWAP-SPACE or SWAPSPACE2 */
	} magic;
	struct {
		char		bootbits[1024];	/* Space for disklabel etc. */
		__u32		version;
		__u32		last_page;
		__u32		nr_badpages;
		unsigned char	sws_uuid[16];
		unsigned char	sws_volume[16];
		__u32		padding[117];
		__u32		badpages[1];
	} info;
};

Dalszym krokiem jest użycie komendy swapon. Po sprawdzeniu uprawnień wykonuje ona wywołanie systemowe o tej samej nazwie. Tutaj pojawia się kolejna ciekawostka – przyjmuje się, że Linux może posiadać 32 przestrzenie wymiany. Jednak zgodnie z man 2 swapon:

There is an upper limit on the number of swap files that may be used, defined by the kernel constant MAX_SWAPFILES. Before kernel 2.4.10, MAX_SWAPFILES has the value 8; since kernel 2.4.10, it has the value 32. Since kernel 2.6.18, the limit is decreased by 2 (thus: 30) if the kernel is built with the CONFIG_MIGRATION option (which reserves two swap table entries for the page migration features of mbind(2) and migrate_pages(2)). Since kernel 2.6.32, the limit is further decreased by 1 if the kernel is built with the CONFIG_MEMORY_FAILURE option.

Co po przełożeniu na język polski oznacza:

Istnieje górny limit plików swap, które mogą być użyte, jest on zdefiniowany przez zmienną kernela MAX_SWAPFILES. Przed jądrem 2.4.10, MAX_SWAPFILES miał wartość 8; od kernela 2.4.10 jego wartość to 32. Od wersji jądra 2.6.18, limit został zmniejszony o 2 (więc: 30) jeśli jądro zostało zbudowane z opcją CONFIG_MIGRATION (opcja ta rezerwuje dwa wpisy w tabeli plików swap dla migracji stron mbind(2) i migrate_pages(2)). Od jądra 2.6.32 limit został zmniejszony o 1 dla jąder zbudowanych z opcją CONFIG_MEMORY_FAILURE.

W rzeczywistości dokumentacja zawiera błąd. Nie uwzględnia bowiem opcji kompilacji jądra CONFIG_DEVICE_PRIVATE, która dla jąder od 4.14 do 5.13 odejmuje 2, a dla jądra od 5.14 – odejmuje 4 możliwe do użycia przestrzenie wymiany swap. Oznacza to, że dla jąder Enterprise linuksowych będzie to 27 lub 25, jeżeli korzystamy z nowszego jądra (np. kernel-ml). Spostrzeżenie to wraz z łatką zostało przez nas zgłoszone na listę mailingową plików man jądra Linuksa: https://marc.info/?l=linux-man&m=164245800929084&w=2. Po przepisaniu zostało ono zaakceptowane, dzięki czemu najnowsza wersja dokumentacji jądra zawiera informacje dotyczące wpływu CONFIG_DEVICE_PRIVATE na maksymalną ilość plików/partycji swap.

By sprawdzić, ile plików lub partycji swap może zostać zamontowanych w systemie, można użyć następującego skryptu:

Wracając do głównego tematu – aktywowanie pliku swap jest proste i wymaga jedynie komendy:

sudo swapon /sciezka/do/pliku

Po skończonej pracy z plikiem swap możemy go wyłączyć z przestrzeni wymiany jądra i skasować:

sudo swapoff /sciezka/do/pliku
sudo rm /sciezka/do/pliku

lub wpisać do tabeli systemów plików /etc/fstab, by był zawsze używany:

echo "/sciezka/do/pliku    none    swap    defaults    0    0" | sudo tee -a /etc/fstab

Finalny skrypt

# swap file location
SWAP_FILE=/swapfile
# swap size in MiB
SWAP_FILE_SIZE=4096
sudo dd if=/dev/zero of=$SWAP_FILE bs=1M count=$SWAP_FILE_SIZE
sudo chmod 600 $SWAP_FILE
sudo mkswap $SWAP_FILE
sudo swapon $SWAP_FILE
# Uncomment line below to add swapfile to fstab
# echo "$SWAP_FILE    none    swap    defaults    0    0" | sudo tee -a /etc/fstab

Podsumowanie

Mimo iż tytuł artykułu mógłby sugerować administratorom Linux raczej mało zaawansowany temat, to weszliśmy zdecydowanie w jego głąb. Począwszy od wytłumaczenia powodów użycia komendy dd zamiast fallocate, poprzez sprawdzenie, jak wygląda struktura swap na dysku, zajrzenie w źródła jądra systemu, weryfikację użytego offset (przesunięcia), aż po znalezienie błędu w dokumentacji i napisanie łatki do plików man jądra Linuksa.