Last active
March 7, 2024 00:04
-
-
Save TBog/9b8b8dc4d77f535c3ebf7bbdc9389cfe to your computer and use it in GitHub Desktop.
Revisions
-
TBog revised this gist
Apr 28, 2021 . 1 changed file with 1 addition and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,5 @@ # Arch linux zram https://github.com/Nefelim4ag/systemd-swap ## Changes - only one zram device -
TBog created this gist
Apr 28, 2021 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,11 @@ # Arch linux zram ## 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 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,61 @@ ################################################################################ # 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 contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,38 @@ # 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 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,913 @@ #!/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()