Skip to content

Instantly share code, notes, and snippets.

@TBog
Last active March 7, 2024 00:04
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save TBog/9b8b8dc4d77f535c3ebf7bbdc9389cfe to your computer and use it in GitHub Desktop.
Save TBog/9b8b8dc4d77f535c3ebf7bbdc9389cfe to your computer and use it in GitHub Desktop.
Chage Garuda Arch linux distribution zram configuration

Arch linux zram

https://github.com/Nefelim4ag/systemd-swap

Changes

  • only one zram device
  • predict zram size based on compression algorithm
  • set mem_limit for zram

Paths

  • /usr/bin/systemd-swap
  • /etc/systemd/swap.conf
  • /usr/share/systemd-swap/swap-default.conf
################################################################################
# Defaults are optimized for general usage
################################################################################
################################################################################
# You can override any settings with files in:
# /etc/systemd/swap.conf.d/*.conf
################################################################################
################################################################################
# Zswap
#
# Kernel >= 3.11
# Zswap create compress cache between swap and memory to reduce IO
# https://www.kernel.org/doc/Documentation/vm/zswap.txt
zswap_enabled=1
zswap_compressor=zstd # lzo lz4 zstd lzo-rle lz4hc
zswap_max_pool_percent=25 # 1-99
zswap_zpool=z3fold # zbud z3fold (note z3fold requires kernel 4.8+)
################################################################################
# ZRam
#
# Kernel >= 3.15
# Zram compression streams count for additional information see:
# https://www.kernel.org/doc/Documentation/blockdev/zram.txt
zram_enabled=0
zram_size=$(( RAM_SIZE / 4 )) # This is 1/4 of ram size by default.
zram_count=${NCPU} # Device count
zram_streams=${NCPU} # Compress streams
zram_alg=zstd # See $zswap_compressor
zram_prio=32767 # 1 - 32767
################################################################################
# Swap File Chunked
# Allocate swap files dynamically
# For btrfs fallback to swapfile + loop will be used
# ex. Min swap size 256M, Max 32*256M
swapfc_enabled=0
swapfc_force_use_loop=0 # Force usage of swapfile + loop
swapfc_frequency=1 # How often to check free swap space in seconds
swapfc_chunk_size=256M # Size of swap chunk
swapfc_max_count=32 # Note: 32 is a kernel maximum
swapfc_min_count=0 # Minimum amount of chunks to preallocate
swapfc_free_ram_perc=35 # Add first chunk if free ram < 35%
swapfc_free_swap_perc=15 # Add new chunk if free swap < 15%
swapfc_remove_free_swap_perc=55 # Remove chunk if free swap > 55% && chunk count > 2
swapfc_priority=50 # Priority of swapfiles (decreasing by one for each swapfile).
swapfc_path=/var/lib/systemd-swap/swapfc/
# Only for swapfile + loop
swapfc_nocow=1 # Disable CoW on swapfile
swapfc_directio=1 # Use directio for loop dev
swapfc_force_preallocated=0 # Will preallocate created files
################################################################################
# Swap devices
# Find and auto swapon all available swap devices
swapd_auto_swapon=1
swapd_prio=1024
# This file is part of systemd-swap.
#
# Entries in this file show the systemd-swap defaults as
# specified in /usr/share/systemd-swap/swap-default.conf
# You can change settings by editing this file.
# Defaults can be restored by simply deleting this file.
#
# See swap.conf(5) and /usr/share/systemd-swap/swap-default.conf for details.
zswap_enabled=0
#zswap_compressor=zstd
#zswap_max_pool_percent=25
#zswap_zpool=z3fold
zram_enabled=1
zram_size=$(( RAM_SIZE / 3 ))
zram_count=1
zram_streams=${NCPU}
zram_alg=lzo-rle
zram_prio=75
swapfc_enabled=0
#swapfc_force_use_loop=0
#swapfc_frequency=1
#swapfc_chunk_size=256M
#swapfc_max_count=32
#swapfc_min_count=0
#swapfc_free_ram_perc=35
#swapfc_free_swap_perc=15
#swapfc_remove_free_swap_perc=55
#swapfc_priority=50
#swapfc_path=/var/lib/systemd-swap/swapfc/
#swapfc_nocow=1
#swapfc_directio=1
#swapfc_force_preallocated=0
swapd_auto_swapon=0
#swapd_prio=1024
#!/usr/bin/python3 -u
# Copyright 2020, Timofey Titovets and the systemd-swap contributors
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
import argparse
import glob
import os
import pickle
import re
import shutil
import signal
import stat
import subprocess
import sys
import threading
import time
import types
from typing import * # pylint: disable=unused-wildcard-import
import systemd.daemon
import sysv_ipc
def get_mem_stats(fields: List[str]) -> Dict[str, int]:
stats = {}
with open("/proc/meminfo") as meminfo:
for line in meminfo:
items = line.split()
key = items[0][:-1]
if items[2] == "kB" and key in fields:
fields.remove(key)
stats[key] = int(items[1]) * 1024
if len(fields) == 0:
break
assert len(fields) == 0
return stats
# Global variables.
# NCPU and RAM_SIZE are referenced inside of `swap-default.conf`.
NCPU = os.cpu_count() or 1
RUN_SYSD = "/run/systemd"
ETC_SYSD = "/etc/systemd"
VEN_SYSD = "/usr/lib/systemd"
DEF_CONFIG = "/usr/share/systemd-swap/swap-default.conf"
ETC_CONFIG = f"{ETC_SYSD}/swap.conf"
RAM_SIZE = get_mem_stats(["MemTotal"])["MemTotal"]
PAGE_SIZE = int(
subprocess.run(
["getconf", "PAGESIZE"], check=True, text=True, stdout=subprocess.PIPE
).stdout
)
WORK_DIR = "/run/systemd/swap"
LOCK_STARTED = f"{WORK_DIR}/.started"
ZSWAP_M = "/sys/module/zswap"
ZSWAP_M_P = "/sys/module/zswap/parameters"
KMAJOR, KMINOR = [int(v) for v in os.uname().release.split(".")[0:2]]
IS_DEBUG = False
sigterm_event = threading.Event()
class Config:
def __init__(self):
os.environ["NCPU"] = str(NCPU)
os.environ["RAM_SIZE"] = str(RAM_SIZE)
self.config = {}
# Load default values.
if os.path.isfile(DEF_CONFIG):
try:
self.config.update(Config.parse_config(DEF_CONFIG))
except:
error(f"Error loading {DEF_CONFIG}")
# Config precedence follows systemd scheme:
# etc > run > lib for all fragments > /etc/systemd/swap.conf
if os.path.isfile(ETC_CONFIG):
try:
self.config.update(Config.parse_config(ETC_CONFIG))
except:
warn(f"Could not load {DEF_CONFIG}")
config_files = {}
for path in [VEN_SYSD, RUN_SYSD, ETC_SYSD]:
path += "/swap.conf.d"
for file_path in glob.glob(f"{path}/*.conf"):
if not os.access(file_path, os.R_OK) or os.path.isdir(file_path):
if os.path.isfile(file_path):
warn(f"Permission denied reading: {file_path}")
continue
config_files[os.path.basename(file_path)] = file_path
debug(f"Found {file_path}")
debug(f"Selected configuration artifacts: {list(config_files.values())}")
# Sort lexicographically.
config_files = dict(sorted(config_files.items()))
for config_file in config_files.values():
info(f"Load: {config_file}")
self.config.update(Config.parse_config(config_file))
def get(self, key: str, as_type: Type = str) -> as_type:
if as_type is bool:
return self.config[key].lower() in ["yes", "y", "1", "true"]
return as_type(self.config[key])
@staticmethod
def parse_config(file: str) -> Dict[str, str]:
config = {}
lines = None
with open(file) as f:
lines = f.read().splitlines()
for line in lines:
line = line.strip()
if line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
config[key] = subprocess.run(
[f"echo {value}"],
shell=True,
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.rstrip()
return config
class DestroyInfo:
pickle_path = f"{WORK_DIR}/destroy_info.pickle"
def __init__(
self, zswap_parameters: Dict[str, str], zram_already_set: Optional[bool]
):
self.zswap_parameters = zswap_parameters
self.zram_already_set = zram_already_set
def get_zswap_parameters(self) -> Dict[str, str]:
return self.zswap_parameters
def get_zram_already_set(self) -> bool:
return self.zram_already_set
def save(self) -> None:
with open(self.pickle_path, "wb") as f:
pickle.dump(self, f)
@classmethod
def load(cls) -> Optional[cls]:
try:
with open(cls.pickle_path, "rb") as f:
return pickle.load(f)
except:
return None
class SwapFc:
def __init__(self, config: Config, sem: sysv_ipc.Semaphore):
self.assign_config(config)
self.sem = sem
# Validate swapfc_frequency due to possible issues caused if set incorrectly.
if not 1 <= self.swapfc_frequency <= 24 * 60 * 60:
warn(
"swapfc_frequency must be in range of 1..86400: "
f"{self.swapfc_frequency} - set to 1"
)
self.swapfc_frequency = 1
self.polling_rate = self.swapfc_frequency
systemd.daemon.notify("STATUS=Monitoring memory status...")
# Create parent directories for swapfc_path.
makedirs(os.path.dirname(self.swapfc_path))
self.fs_type, subvolume = self.get_fs_type()
if self.fs_type == "btrfs":
if not subvolume:
subprocess.run(
["btrfs", "subvolume", "create", self.swapfc_path],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
else:
makedirs(self.swapfc_path)
self.chunk_size = int(
subprocess.run(
["numfmt", "--to=none", "--from=iec", self.swapfc_chunk_size],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout
)
self.block_size = os.statvfs(self.swapfc_path).f_bsize
if self.fs_type == "btrfs":
# If btrfs supports regular swap files (kernel version 5+), force disable
# COW to avoid data corruption. If it doesn't, use the old swap-through-loop
# workaround.
if KMAJOR >= 5:
self.swapfc_nocow = True
else:
self.swapfc_force_use_loop = True
if not 1 <= self.swapfc_max_count <= 32:
warn("swapfc_max_count must be in range 1..32, reset to 1")
self.swapfc_max_count = 1
makedirs(f"{WORK_DIR}/swapfc")
self.allocated = 0
for _ in range(self.swapfc_min_count):
self.create_swapfile("swapFC: allocate chunk: ")
def run(self) -> None:
systemd.daemon.notify("READY=1")
if self.allocated == 0:
memory_usage = round(
RAM_SIZE * (100 - self.swapfc_free_ram_perc) / (1024 * 1024 * 100)
)
info(
f"swapFC: on-demand swap activation at >{memory_usage} MiB memory usage"
)
signal.signal(signal.SIGTERM, sigterm_handler)
while True:
self.sem.release()
sigterm_event.wait(self.polling_rate)
if sigterm_event.is_set():
break
try:
self.sem.acquire(0)
except sysv_ipc.BusyError:
break
if self.allocated == 0:
curr_free_ram_perc = self.get_free_ram_perc()
if curr_free_ram_perc < self.swapfc_free_ram_perc:
self.create_swapfile(
f"swapFC: free ram: {curr_free_ram_perc} < "
f"{self.swapfc_free_ram_perc} - allocate chunk: "
)
continue
curr_free_swap_perc = self.get_free_swap_perc()
if (
curr_free_swap_perc < self.swapfc_free_swap_perc
and self.allocated < self.swapfc_max_count
):
self.create_swapfile(
f"swapFC: free swap: {curr_free_swap_perc} < "
f"{self.swapfc_free_swap_perc} - allocate chunk: "
)
continue
if self.allocated <= max(self.swapfc_min_count, 2):
continue
if curr_free_swap_perc > self.swapfc_remove_free_swap_perc:
self.destroy_swapfile(
f"swapFC: free swap: {curr_free_swap_perc} > "
f"{self.swapfc_remove_free_swap_perc} - free up chunk: "
+ str(self.allocated)
)
def get_fs_type(self) -> Tuple[str, bool]:
subvolume = False
path = None
if os.path.isdir(self.swapfc_path):
path = self.swapfc_path
elif os.path.isdir(os.path.dirname(self.swapfc_path)):
path = os.path.dirname(self.swapfc_path)
else:
error("swapfc_path is invalid")
output = subprocess.run(
["df", path, "--output=fstype"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout
fs_type = output.splitlines()[1]
if fs_type == "-":
ret_code = subprocess.run(
["btrfs", "subvolume", "show", path],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
if ret_code == 0:
fs_type = "btrfs"
if path == self.swapfc_path:
subvolume = True
else:
error("swapfc_path is located on an unknown filesystem")
return fs_type, subvolume
def assign_config(self, config: Config) -> None:
yn = lambda x: config.get(x, bool)
self.swapfc_chunk_size = config.get("swapfc_chunk_size")
self.swapfc_directio = yn("swapfc_directio")
self.swapfc_force_preallocated = yn("swapfc_force_preallocated")
self.swapfc_force_use_loop = yn("swapfc_force_use_loop")
self.swapfc_free_ram_perc = config.get("swapfc_free_ram_perc", int)
self.swapfc_free_swap_perc = config.get("swapfc_free_swap_perc", int)
self.swapfc_frequency = config.get("swapfc_frequency", int)
self.swapfc_max_count = config.get("swapfc_max_count", int)
self.swapfc_min_count = config.get("swapfc_min_count", int)
self.swapfc_nocow = yn("swapfc_nocow")
self.swapfc_path = config.get("swapfc_path").rstrip("/")
self.swapfc_priority = config.get("swapfc_priority", int)
self.swapfc_remove_free_swap_perc = config.get(
"swapfc_remove_free_swap_perc", int
)
def create_swapfile(self, msg: str) -> None:
if not self.has_enough_space(self.swapfc_path):
warn("swapFC: ENOSPC")
# Prevent spamming the journal.
self.double_polling_rate()
systemd.daemon.notify("STATUS=Not enough space for allocating chunk")
return
# In case we have adjusted the polling rate, reset it.
self.reset_polling_rate()
systemd.daemon.notify("STATUS=Allocating swap file...")
self.allocated += 1
info(f"{msg} {self.allocated}")
swapfile = self.prepare_swapfile(
os.path.join(self.swapfc_path, str(self.allocated))
)
subprocess.run(
["mkswap", "-L", f"SWAP_{self.fs_type}_{self.allocated}", swapfile],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
options = "discard" if not self.swapfc_force_preallocated else None
unit_name = gen_swap_unit(
what=swapfile,
priority=self.swapfc_priority,
options=options,
tag=f"swapfc_{self.allocated}",
)
self.swapfc_priority -= 1
subprocess.run(["systemctl", "daemon-reload"], check=True)
subprocess.run(["systemctl", "start", unit_name], check=True)
mode = os.stat(swapfile).st_mode
if stat.S_ISBLK(mode):
subprocess.run(["losetup", "-d", swapfile])
systemd.daemon.notify("STATUS=Monitoring memory status...")
def has_enough_space(self, path: str) -> bool:
# Check free space to avoid problems on swap IO + ENOSPC.
free_blocks = os.statvfs(path).f_bavail
free_bytes = free_blocks * self.block_size
# Also try leaving some free space.
free_bytes -= self.chunk_size
return free_bytes >= self.chunk_size
def double_polling_rate(self) -> None:
new_rate = self.polling_rate * 2
# Do not double, interval is long enough.
if new_rate > 86400 or new_rate > self.swapfc_frequency * 1000:
return
self.polling_rate = new_rate
warn(f"swapFC: polling rate doubled to {self.polling_rate}s")
def reset_polling_rate(self) -> None:
if self.polling_rate > self.swapfc_frequency:
self.polling_rate = self.swapfc_frequency
info(f"swapFC: polling rate reset to {self.polling_rate}s")
def prepare_swapfile(self, path: str) -> str:
# Delete file if it already exists.
force_remove(path)
os.mknod(path)
if self.fs_type == "btrfs" and self.swapfc_nocow:
subprocess.run(["chattr", "+C", path], check=True)
zeros = b"\x00" * 1024 * 1024
with open(path, "wb") as swapfile:
for _ in range(round(self.chunk_size / (1024 * 1024))):
swapfile.write(zeros)
swapfile.flush()
return path if not self.swapfc_force_use_loop else self.losetup_w(path)
def losetup_w(self, path: str) -> str:
directio = "on" if self.swapfc_directio else "off"
file = subprocess.run(
["losetup", "-f", "--show", f"--direct-io={directio}", path],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.rstrip()
# Loop uses a file descriptor - if the file still exists, but does not have a
# path like O_TMPFILE. When loop detaches a file, the file will be deleted.
os.remove(path)
return file
def destroy_swapfile(self, msg: str) -> None:
systemd.daemon.notify("STATUS=Deallocating swap file...")
info(msg)
for unit_path in find_swap_units():
content = None
with open(unit_path) as f:
content = f.read()
if f"swapfc_{self.allocated}" in content:
dev = get_what_from_swap_unit(unit_path)
unit_name = os.path.basename(unit_path)
ret_code = subprocess.run(["systemctl", "stop", unit_name]).returncode
if ret_code != 0:
subprocess.run(["swapoff", dev], check=True)
force_remove(unit_path, verbose=True)
if os.path.isfile(dev):
force_remove(dev)
break
self.allocated -= 1
systemd.daemon.notify("STATUS=Monitoring memory status...")
@staticmethod
def get_free_ram_perc() -> int:
ram_stats = get_mem_stats(["MemTotal", "MemFree"])
return round((ram_stats["MemFree"] * 100) / ram_stats["MemTotal"])
@staticmethod
def get_free_swap_perc() -> int:
swap_stats = get_mem_stats(["SwapTotal", "SwapFree"])
# Minimum for total is 1 to prevent divide by zero.
return round((swap_stats["SwapFree"] * 100) / max(swap_stats["SwapTotal"], 1))
def debug(msg: str) -> None:
if IS_DEBUG:
print("DEBUG:", msg, file=sys.stderr)
def info(msg: str) -> None:
print("INFO:", msg)
def warn(msg: str) -> None:
print("WARN:", msg, file=sys.stderr)
def error(msg: str) -> NoReturn:
print("ERRO:", msg, file=sys.stderr)
sys.exit(1)
def force_remove(file: str, verbose: bool = False) -> None:
try:
os.remove(file)
if verbose:
info(f"Removed {file}")
except OSError:
if verbose:
warn(f"Cannot remove {file}")
def relative_symlink(target: str, link_name: str) -> None:
if os.path.lexists(link_name):
force_remove(link_name)
os.symlink(os.path.relpath(target, os.path.dirname(link_name)), link_name)
def write(data: str, file: str) -> None:
with open(file, "w") as f:
f.write(data)
def read(file: str) -> str:
with open(file) as f:
return f.read()
def am_i_root(exit_on_error: bool = True) -> bool:
if os.getuid() == 0:
return True
if exit_on_error:
error("Script must be run as root!")
else:
return False
def find_swap_units() -> List[str]:
swap_units = []
for path in ["/run/systemd/system", "/run/systemd/generator"]:
for file_path in glob.glob(f"{path}/**/*.swap", recursive=True):
if os.path.isfile(file_path) and not os.path.islink(file_path):
swap_units.append(file_path)
return swap_units
def get_what_from_swap_unit(file: str) -> str:
with open(file) as file:
for line in file.read().splitlines():
if line.startswith("What="):
return line[len("What=") :]
def gen_swap_unit(
what: str, tag: str, priority: Optional[int] = None, options: Optional[str] = None
) -> str:
what = os.path.realpath(what)
# Assume it's a file by default.
_type = "File"
mode = os.stat(what).st_mode
if stat.S_ISBLK(mode):
_type = "Block/Partition"
if "loop" in what:
_type = "File"
unit_name = subprocess.run(
["systemd-escape", "-p", "--suffix=swap", what],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.rstrip()
unit_path = f"{RUN_SYSD}/system/{unit_name}"
content = (
"[Unit]\n"
f"Description=Swap {_type}\n"
"Documentation=https://github.com/Nefelim4ag/systemd-swap\n"
"\n"
"# Generated by systemd-swap\n"
f"# Tag={tag}\n"
"\n"
"[Swap]\n"
f"What={what}\n"
"TimeoutSec=1h\n"
)
if priority:
content += f"Priority={priority}\n"
if options:
content += f"Options={options}\n"
write(content, unit_path)
relative_symlink(unit_path, f"{RUN_SYSD}/system/swap.target.wants/{unit_name}")
if _type == "File":
relative_symlink(
unit_path, f"{RUN_SYSD}/system/local-fs.target.wants/{unit_name}"
)
return unit_name
def swapoff(unit_path: str, subsystem: str) -> None:
dev = get_what_from_swap_unit(unit_path)
info(f"{subsystem}: swapoff {dev}")
subprocess.run(["swapoff", dev])
force_remove(unit_path, verbose=True)
if subsystem == "swapFC":
if os.path.isfile(dev):
force_remove(dev, verbose=True)
elif subsystem == "Zram":
subprocess.run(["zramctl", "-r", dev])
def makedirs(path: str) -> None:
os.makedirs(path, exist_ok=True)
def sigterm_handler(signum: int, frame: Optional[types.FrameType]) -> None:
sigterm_event.set()
def get_sem_id() -> int:
sysv_id = sysv_ipc.ftok(__file__, 1, silence_warning=True)
debug(f"ftok() returned this ID: {sysv_id}")
return sysv_id
def init_directories() -> None:
makedirs(WORK_DIR)
makedirs(f"{RUN_SYSD}/system/local-fs.target.wants")
makedirs(f"{RUN_SYSD}/system/swap.target.wants")
def start() -> None:
am_i_root()
# Clean up in case a previous instance did not exit cleanly.
stop(on_init=True)
init_directories()
sem = None
try:
# Semaphore guarding against running more than one instance and signalling if
# cleanup can start.
sem = sysv_ipc.Semaphore(get_sem_id(), flags=sysv_ipc.IPC_CREX)
except sysv_ipc.ExistentialError:
error(f"{sys.argv[0]} already started")
config = Config()
yn = lambda x: config.get(x, bool)
if yn("zram_enabled") and (
yn("zswap_enabled") or yn("swapfc_enabled") or yn("swapd_auto_swapon")
):
warn(
"Combining zram with zswap/swapfc/swapd_auto_swapon can lead to LRU "
"inversion and is strongly recommended against"
)
zswap_parameters = {}
if yn("zswap_enabled"):
systemd.daemon.notify("STATUS=Setting up Zswap...")
if not os.path.isdir(ZSWAP_M):
error("Zswap - not supported on current kernel")
info("Zswap: backup current configuration: start")
makedirs(f"{WORK_DIR}/zswap")
for file in os.listdir(ZSWAP_M_P):
file_path = os.path.join(ZSWAP_M_P, file)
zswap_parameters[file_path] = read(file_path)
info("Zswap: backup current configuration: complete")
info("Zswap: set new parameters: start")
info(
f'Zswap: Enable: {config.get("zswap_enabled")}, Comp: '
f'{config.get("zswap_compressor")}, Max pool %: '
f'{config.get("zswap_max_pool_percent")}, Zpool: '
f'{config.get("zswap_zpool")}'
)
write(config.get("zswap_enabled"), f"{ZSWAP_M_P}/enabled")
write(config.get("zswap_compressor"), f"{ZSWAP_M_P}/compressor")
write(config.get("zswap_max_pool_percent"), f"{ZSWAP_M_P}/max_pool_percent")
write(config.get("zswap_zpool"), f"{ZSWAP_M_P}/zpool")
info("Zswap: set new parameters: complete")
zram_already_set = None
if yn("zram_enabled"):
systemd.daemon.notify("STATUS=Setting up Zram...")
info("Zram: check availability")
if not os.path.isdir("/sys/module/zram"):
zram_already_set = False
info("Zram: not part of kernel, trying to find zram module")
ret_code = subprocess.run(["modprobe", "-n", "zram"]).returncode
if ret_code != 0:
error("Zram: can't find zram module!")
zram_initialized = False
for _ in range(10):
ret_code = subprocess.run(["modprobe", "zram"]).returncode
if ret_code == 0:
zram_initialized = True
info("Zram: module successfully loaded")
break
time.sleep(1)
if not zram_initialized:
error("Zram: can't load zram module")
else:
zram_already_set = True
info("Zram: module already loaded")
subprocess.run(["systemctl", "daemon-reload"], check=True)
if config.get("zram_alg").startswith("lzo") or "zstd" == config.get("zram_alg"):
compression_factor = 3
elif "lz4" == config.get("zram_alg"):
compression_factor = 2.5
else:
compression_factor = 2
zram_size = round(config.get("zram_size", int) / config.get("zram_count", int))
for _ in range(config.get("zram_count", int)):
info("Zram: trying to initialize free device")
# zramctl is an external program -> return path to first free device.
output = subprocess.run(
[
"zramctl",
"-f",
"-a",
config.get("zram_alg"),
"-t",
config.get("zram_streams"),
"-s",
str(zram_size),
],
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
).stdout.rstrip()
zram_dev = None
if "failed to reset: Device or resource busy" in output:
time.sleep(1)
elif "zramctl: no free zram device found" in output:
warn("Zram: zramctl can't find free device")
info("Zram: using workaround hook for hot add")
if not os.path.isfile("/sys/class/zram-control/hot_add"):
error(
"Zram: this kernel does not support hot adding zram devices, "
"please use a 4.2+ kernel or see 'modinfo zram´ and create a "
"modprobe rule"
)
new_zram = read("/sys/class/zram-control/hot_add").rstrip()
info(f"Zram: success: new device /dev/zram{new_zram}")
elif "/dev/zram" in output:
mode = os.stat(output).st_mode
if not stat.S_ISBLK(mode):
continue
zram_dev = output
else:
error(f"Zram: unexpected output from zramctl: {output}")
mode = os.stat(zram_dev).st_mode
if stat.S_ISBLK(mode):
new_zram = zram_dev[zram_dev.rfind('/')+1:];
mem_limit = round(zram_size / compression_factor)
write(str(mem_limit), f"/sys/block/{new_zram}/mem_limit")
info(f"Zram: initialized: {zram_dev} size: {zram_size/1024/1024/1024:.2f}GiB limit: {mem_limit/1024/1024/1024:.2f}GiB")
ret_code = subprocess.run(
["mkswap", zram_dev],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode
if ret_code == 0:
unit_name = gen_swap_unit(
what=zram_dev,
options="discard",
priority=config.get("zram_prio"),
tag="zram",
)
subprocess.run(["systemctl", "daemon-reload"], check=True)
subprocess.run(["systemctl", "start", unit_name], check=True)
else:
warn("Zram: can't get free zram device")
systemd.daemon.notify("STATUS=Zram setup finished")
info("Writing destroy info...")
DestroyInfo(zswap_parameters, zram_already_set).save()
if yn("swapd_auto_swapon"):
systemd.daemon.notify("STATUS=Activating swap units...")
info("swapD: pick up devices from systemd-gpt-auto-generator")
for unit_path in find_swap_units():
if "systemd-gpt-auto-generator" in read(unit_path):
dev = get_what_from_swap_unit(unit_path)
subprocess.run(["swapoff", dev], check=True)
force_remove(unit_path, verbose=True)
info("swapD: searching swap devices")
makedirs(f"{WORK_DIR}/swapd")
swapd_prio = config.get("swapd_prio", int)
# blkid returns 2 if nothing was found.
devices = subprocess.run(
["blkid", "-t", "TYPE=swap", "-o", "device"],
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
for device in devices:
if "zram" in device or "loop" in device:
continue
used_devices = subprocess.run(
["swapon", "--show=NAME", "--noheadings"],
check=True,
text=True,
stdout=subprocess.PIPE,
).stdout.splitlines()
for used_device in used_devices:
if device == used_device:
device = None
if device is None:
continue
mode = os.stat(device).st_mode
if not stat.S_ISBLK(mode):
continue
unit_name = gen_swap_unit(
what=device, options="discard", priority=swapd_prio, tag="swapd"
)
subprocess.run(["systemctl", "daemon-reload"], check=True)
ret_code = subprocess.run(["systemctl", "start", unit_name]).returncode
if ret_code != 0:
continue
info(f"swapD: enabled device: {device}")
swapd_prio -= 1
systemd.daemon.notify("STATUS=Swap unit activation finished")
if yn("swapfc_enabled"):
swap_fc = SwapFc(config, sem)
swap_fc.run()
else:
systemd.daemon.notify("READY=1")
# Done setting up. Allow cleanup to take place.
sem.release()
def stop(on_init: bool = False) -> None:
am_i_root()
config = Config()
sem = None
sem_id = get_sem_id()
try:
sem = sysv_ipc.Semaphore(sem_id)
if not on_init:
try:
sem.acquire(60)
except sysv_ipc.BusyError:
warn("Could not acquire semaphore, commencing stop action anyway...")
systemd.daemon.notify("STOPPING=1")
except sysv_ipc.ExistentialError:
# Prevent systemd-swap from starting/stopping while cleaning up.
sem = sysv_ipc.Semaphore(sem_id, flags=sysv_ipc.IPC_CREX)
if not on_init:
warn(f"{sys.argv[0]} might not be running")
destroy_info = DestroyInfo.load()
swap_units = find_swap_units()
for unit_path in filter(lambda u: "swapd" in read(u), swap_units):
swapoff(unit_path, "swapD")
for unit_path in filter(lambda u: "swapfc" in read(u), swap_units):
swapoff(unit_path, "swapFC")
for unit_path in filter(lambda u: "zram" in read(u), swap_units):
swapoff(unit_path, "Zram")
if destroy_info:
if destroy_info.zram_already_set == False:
info("Zram: unloading kernel module...")
subprocess.run(["modprobe", "-r", "zram"])
if os.path.isdir(f"{WORK_DIR}/zswap"):
info("Zswap: restore configuration: start")
for zswap_parameter, value in destroy_info.zswap_parameters.items():
write(value, zswap_parameter)
info("Zswap: restore configuration: complete")
info("Removing working directory...")
shutil.rmtree(WORK_DIR, ignore_errors=True)
swapfc_path = config.get("swapfc_path")
info(f"Removing files in {swapfc_path}...")
try:
for file in os.listdir(swapfc_path):
force_remove(os.path.join(swapfc_path, file), verbose=True)
except OSError:
pass
sem.remove()
def status() -> None:
if not am_i_root(exit_on_error=False):
warn("Not root! Some output might be missing.")
swap_stats = get_mem_stats(["SwapTotal", "SwapFree"])
swap_used = swap_stats["SwapTotal"] - swap_stats["SwapFree"]
try:
if os.path.isdir("/sys/module/zswap"):
used_bytes = int(read("/sys/kernel/debug/zswap/pool_total_size"))
used_pages = used_bytes / PAGE_SIZE
stored_pages = int(read("/sys/kernel/debug/zswap/stored_pages"))
stored_bytes = stored_pages * PAGE_SIZE
ratio = 0
if stored_pages > 0:
ratio = used_pages * 100 / stored_pages
zswap_info = ""
for file in sorted(os.listdir("/sys/module/zswap/parameters")):
zswap_info += (
f'. {file} {read(f"/sys/module/zswap/parameters/{file}")}\n'
)
subprocess.run(["column", "-t"], input=zswap_info, text=True)
zswap_info = ""
for file in sorted(os.listdir("/sys/kernel/debug/zswap")):
zswap_info += f'. . {file} {read(f"/sys/kernel/debug/zswap/{file}")}\n'
zswap_info += f". . compress_ratio {round(ratio)}%\n"
if swap_used > 0:
zswap_info += (
f". . zswap_store/swap_store {stored_bytes}/{swap_used} "
f"{round(stored_bytes * 100 / swap_used)}%\n"
)
print("Zswap:")
subprocess.run(["column", "-t"], input=zswap_info, text=True)
except:
warn("Zswap info inaccesible")
zramctl = subprocess.run(
["zramctl"], check=True, text=True, stdout=subprocess.PIPE
).stdout
if "[SWAP]" in zramctl: # pylint: disable=unsupported-membership-test
zramctl = zramctl.splitlines()
zram_info = ""
for line in zramctl:
if line.startswith("NAME") or "[SWAP]" in line:
if line.endswith("MOUNTPOINT"):
line = line[: -len("MOUNTPOINT")]
elif line.endswith("[SWAP]"):
line = line[: -len("[SWAP]")]
zram_info += f". {line}\n"
print("Zram:")
subprocess.run(["column -t | uniq"], input=zram_info, text=True, shell=True)
if os.path.isdir(f"{WORK_DIR}/swapd"):
swapon = subprocess.run(
["swapon", "--raw"], check=True, text=True, stdout=subprocess.PIPE
).stdout.splitlines()
swapd_info = ""
for line in swapon:
if not re.search("zram|file|loop", line): # pylint: disable=no-member
swapd_info += f". {line}\n"
print("swapD:")
subprocess.run(["column", "-t"], input=swapd_info, text=True)
if os.path.isdir(f"{WORK_DIR}/swapfc"):
swapon = subprocess.run(
["swapon", "--raw"], check=True, text=True, stdout=subprocess.PIPE
).stdout.splitlines()
swapfc_info = ""
for line in swapon:
if re.search("NAME|file|loop", line): # pylint: disable=no-member
swapfc_info += f". {line}\n"
print("swapFC:")
subprocess.run(["column", "-t"], input=swapfc_info, text=True)
def compression() -> None:
proc_crypto = None
with open("/proc/crypto") as f:
proc_crypto = f.read()
matches = re.finditer( # pylint: disable=no-member
r"name\s*:\s*(\S*).*?type\s*:\s*(\S*)",
proc_crypto,
re.DOTALL, # pylint: disable=no-member
)
print("Found loaded compression algorithms: ", end="")
first = True
for match in matches:
algo, _type = match.groups()
if _type == "compression":
if first:
first = False
else:
print(", ", end="")
print(algo, end="")
print()
def main() -> None:
argparser = argparse.ArgumentParser()
argparser.add_argument(
"command",
choices=["start", "stop", "status", "compression"],
default="status",
nargs="?",
help="`start' the daemon, `stop' it, show some swap `status' info, or display "
"the loaded `compression' algorithms",
)
args = argparser.parse_args()
if args.command == "start":
start()
elif args.command == "stop":
stop()
elif args.command == "status":
status()
elif args.command == "compression":
compression()
else:
raise RuntimeError
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment