De-prioritizing a systemd-run job
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

preflight checks. user controller fixer. after-start tweaks

turns out systemd-run --scope disables a ton of functionality, and turns out theresignificant cgroup setup
required

rektide e75c8a40 4f33ebfd

+285 -38
+27 -21
README.md
··· 16 16 17 17 ## Install 18 18 19 - Either add to PATH: 20 - 21 - ```sh 22 - export PATH=$(pwd):$PATH 23 - ``` 24 - 25 - Or symlink 19 + No installation required - the script is in the repository root: 26 20 27 - ```sh 28 - sudo ln -s $(pwd)/gentle.sh /usr/local/bin 21 + ```bash 22 + chmod +x gentle.sh 29 23 ``` 30 24 31 25 ## Usage 32 26 33 27 ```bash 34 - gentle.sh <command> [args...] 28 + ./gentle.sh <command> [args...] 35 29 ``` 36 30 37 31 ## Resource Limits ··· 40 34 41 35 ### CPU Limits 42 36 43 - - **CPU Affinity**: All CPUs except the first 2 (keeps cores 0-1 free for system) 44 - - **CPU Scheduling Policy**: `batch` - lower priority than normal processes 45 - - Available lower option: `idle` - only runs when nothing else needs CPU 37 + - **CPU Affinity**: All CPUs except first 2 (keeps cores 0-1 free for system), specified as range `2-15` 38 + - **CPU Scheduling**: `SCHED_BATCH` via `chrt -b 0` - batch scheduling for non-interactive workloads 46 39 - **CPU Weight**: `1` - minimum weight (1-10000 range) 47 40 - **CPU Quota Period**: `300ms` - period for quota measurement 48 41 - **Nice Level**: `19` - lowest nice priority 49 42 50 43 ### I/O Limits 51 44 52 - - **IO Weight**: `1` - minimum weight (1-10000 range) 53 - - **IO Scheduling Class**: `best-effort` with priority `7` - lowest best-effort priority 54 - - Available lower option: `idle` - lowest possible IO priority 55 - - **IO Device Latency**: `infinity` applied to all detected block devices - no strict latency enforcement on any drive 45 + - **IO Weight**: `1` (default 1) - minimum weight (1-10000 range), set post-launch via cgroup 46 + - **IO Priority Class**: `idle` - lowest IO priority class, set post-launch via cgroup 47 + - **IO Device Latency**: `60s` applied to all detected block devices 56 48 57 49 ### Memory Limits 58 50 ··· 64 56 Build a large project without freezing your system: 65 57 66 58 ```bash 67 - gentle.sh make -j8 59 + ./gentle.sh make -j8 68 60 ``` 69 61 70 62 Run a compilation: 71 63 72 64 ```bash 73 - gentle.sh ./build.sh 65 + ./gentle.sh ./build.sh 66 + ``` 67 + 68 + Run with multiple arguments: 69 + 70 + ```bash 71 + ./gentle.sh ninja -C build all 72 + ``` 73 + 74 + Run a CMake configuration: 75 + 76 + ```bash 77 + ./gentle.sh cmake -B build -DCMAKE_BUILD_TYPE=Release 74 78 ``` 75 79 76 80 ## Notes 77 81 78 - - The script uses `systemd-run --scope --user`, which requires systemd user sessions 82 + - The script uses `systemd-run --scope --user`, which runs commands in the foreground 83 + - CPU scheduling policy is set to `SCHED_BATCH` via `chrt -b 0` wrapper 84 + - IO properties (`io.weight` and `io.prio.class`) are set post-launch via direct cgroup writes 79 85 - Block devices are auto-detected using `lsblk` and IO latency targets are set for all drives 80 - - The process runs in the foreground (not detached as a service) 86 + - Temporary files use `XDG_RUNTIME_DIR` (or `$HOME/.tmp` as fallback) for user-isolated storage 81 87 - All resource limits are applied to the process and its children
+96
cgroup-user-controllers.sh
··· 1 + #!/bin/bash 2 + 3 + # Enable cgroup controllers at necessary levels in cgroup hierarchy 4 + 5 + # Check if running with elevated privileges 6 + if [ "$(id -u)" = "0" ]; then 7 + USE_SUDO="" 8 + else 9 + USE_SUDO="sudo" 10 + fi 11 + 12 + # Generic function to write to a file with privilege escalation 13 + # Arguments: file_path, value, description 14 + # Output: changed: $PATH (BEFORE -> AFTER) / failed: $PATH (BEFORE -> WANTED) 15 + write_to_file() { 16 + local file=$1 17 + local value=$2 18 + local desc=${3:-$file} 19 + 20 + if [ ! -f "$file" ]; then 21 + echo " failed: $file (not found)" 22 + return 1 23 + fi 24 + 25 + local before=$(cat "$file") 26 + if [ "$before" = "$value" ]; then 27 + return 0 28 + fi 29 + 30 + # Try user write first (only if user-writable) 31 + if [ -w "$file" ]; then 32 + if echo "$value" > "$file" 2>/dev/null; then 33 + local after=$(cat "$file") 34 + echo " changed: $file ($before -> $after)" 35 + return 0 36 + fi 37 + fi 38 + 39 + # Fall back to sudo for non-writable files 40 + if ! echo "$value" | $USE_SUDO tee "$file" >/dev/null; then 41 + echo " failed: $file ($before -> $value)" 42 + return 1 43 + fi 44 + 45 + local after=$(cat "$file") 46 + echo " changed: $file ($before -> $after) [$USE_SUDO]" 47 + return 0 48 + } 49 + 50 + # Reusable function to check and enable a cgroup controller 51 + # Arguments: controller_name, cgroup_path 52 + # Output format: already-done / changed / failed 53 + enable_cgroup_controller() { 54 + local controller=$1 55 + local path=$2 56 + local subtree_file="$path/cgroup.subtree_control" 57 + 58 + if cat "$subtree_file" | grep -q "\\b${controller}\\b"; then 59 + return 0 60 + fi 61 + 62 + if ! write_to_file "$subtree_file" "+${controller}" "Enable $controller at $path"; then 63 + return 1 64 + fi 65 + 66 + return 0 67 + } 68 + 69 + # Set cgroup value if different from current 70 + # Arguments: file_path, value 71 + set_cgroup_value() { 72 + local file=$1 73 + local value=$2 74 + 75 + write_to_file "$file" "$value" 76 + } 77 + 78 + USER_UID=$(id -u) 79 + USER_SLICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice" 80 + USER_SERVICE="$USER_SLICE/user@${UID}.service" 81 + BACKGROUND_SLICE="$USER_SERVICE/background.slice" 82 + 83 + # Controllers needed at each level of cgroup hierarchy 84 + CONTROLLERS=("cpuset" "cpu" "memory" "pids" "io") 85 + 86 + # Enable controllers at each level of the hierarchy 87 + for controller in "${CONTROLLERS[@]}"; do 88 + enable_cgroup_controller "$controller" "/sys/fs/cgroup/user.slice" 89 + enable_cgroup_controller "$controller" "$USER_SLICE" 90 + enable_cgroup_controller "$controller" "$USER_SERVICE" 91 + enable_cgroup_controller "$controller" "$BACKGROUND_SLICE" 92 + done 93 + 94 + # Set IO scheduling defaults at background.slice level 95 + set_cgroup_value "$BACKGROUND_SLICE/io.weight" "default 1" 96 + set_cgroup_value "$BACKGROUND_SLICE/io.prio.class" "idle"
+162 -17
gentle.sh
··· 2 2 3 3 # Run a command gently with limited CPU, IO, and nice values using systemd-run 4 4 5 + #=== Preflight Checks / Config ===# 6 + 7 + # Preflight check: verify required cgroup controllers are enabled at given path 8 + check_controllers_at_path() { 9 + local path=$1 10 + local subtree_file="$path/cgroup.subtree_control" 11 + 12 + if [ ! -f "$subtree_file" ]; then 13 + echo " $path: no cgroup.subtree_control file" 14 + return 1 15 + fi 16 + 17 + local controllers=$(cat "$subtree_file") 18 + local needed=("cpuset" "cpu" "memory" "io") 19 + local missing=() 20 + 21 + for controller in "${needed[@]}"; do 22 + if ! echo "$controllers" | grep -q "\\b${controller}\\b"; then 23 + missing+=("$controller") 24 + fi 25 + done 26 + 27 + if [ ${#missing[@]} -eq 0 ]; then 28 + echo " $path: ok" 29 + return 0 30 + fi 31 + 32 + echo " $path: missing controllers: ${missing[*]}" 33 + return 1 34 + } 35 + 36 + # Default slice to use 37 + DEFAULT_SLICE=background 38 + 39 + # Set up user cgroup paths 40 + USER_UID=$(id -u) 41 + USER_SLICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice" 42 + USER_SERVICE="$USER_SLICE/user@${UID}.service" 43 + 44 + # Determine slice to use (empty string = systemd default, otherwise slice name) 45 + # Special value "false" becomes empty string 46 + if [ "$USE_SLICE" = "false" ]; then 47 + USE_SLICE="" 48 + fi 49 + 50 + # Default to DEFAULT_SLICE only if USE_SLICE is unset (not just empty) 51 + # But only if the slice actually exists and has controllers 52 + if [ -z "${USE_SLICE+x}" ]; then 53 + if [ -f "$USER_SERVICE/$DEFAULT_SLICE.slice/cgroup.subtree_control" ]; then 54 + USE_SLICE="$DEFAULT_SLICE" 55 + else 56 + USE_SLICE="" 57 + fi 58 + fi 59 + 60 + if [ -n "$USE_SLICE" ]; then 61 + echo "Using slice: $USE_SLICE.slice" 62 + else 63 + echo "Using slice: (systemd default)" 64 + fi 65 + 66 + echo "Checking cgroup controllers..." 67 + 68 + MISSING=0 69 + check_controllers_at_path "/sys/fs/cgroup/user.slice" || MISSING=1 70 + check_controllers_at_path "$USER_SLICE" || MISSING=1 71 + check_controllers_at_path "$USER_SERVICE" || MISSING=1 72 + 73 + # Only check the slice if USE_SLICE is set 74 + if [ -n "$USE_SLICE" ]; then 75 + CHECK_SLICE="$USER_SERVICE/$USE_SLICE.slice" 76 + check_controllers_at_path "$CHECK_SLICE" || MISSING=1 77 + fi 78 + 79 + if [ $MISSING -eq 1 ]; then 80 + echo "" 81 + echo "Some cgroup controllers are missing!" 82 + echo "Run './cgroup-user-io.sh' with sudo to enable missing controllers" 83 + echo "" 84 + fi 85 + 86 + #=== Command Execution Setup ===# 87 + 5 88 # Calculate CPU affinity (all CPUs except first 2) 6 89 CPU_COUNT=$(nproc) 7 - CPU_AFFINITY="2-$(($CPU_COUNT - 1))" 90 + CPU_AFFINITY="2-$((CPU_COUNT - 1))" 8 91 9 92 # Get all base block devices to set IO latency target on all drives 10 - BLOCK_DEVICES=$(lsblk -d -n -o NAME | grep -E "^(nvme|sd)") 93 + mapfile -t BLOCK_DEVICES < <(lsblk -d -n -o NAME | grep -E "^(nvme|sd)") 11 94 12 - # Build systemd-run command 13 - # Note: IOSchedulingClass=idle is even lower priority than best-effort 14 - # Note: CPUSchedulingPolicy=idle is even lower priority than batch 15 - RUN_CMD=( 95 + # Build systemd-run command with proper quoting 96 + CMD=( 16 97 systemd-run 17 98 --scope 18 99 --user 19 100 --nice=19 20 - --io-weight=1 21 - --cpu-affinity="$CPU_AFFINITY" 101 + --property=IOWeight=1 102 + --property="AllowedCPUs=$CPU_AFFINITY" 22 103 --property=CPUWeight=1 23 104 --property=MemoryMax=80% 24 105 --property=TasksMax=infinity 25 106 --property=CPUQuotaPeriodSec=300ms 26 - --property=IOSchedulingClass=best-effort 27 - --property=IOSchedulingPriority=7 28 - --property=CPUSchedulingPolicy=batch 29 107 ) 30 108 109 + # Add --slice only if USE_SLICE is set 110 + if [ -n "$USE_SLICE" ]; then 111 + CMD+=(--slice="$USE_SLICE.slice") 112 + fi 113 + 31 114 # Add IODeviceLatencyTargetSec for each block device 32 - for device in $BLOCK_DEVICES; do 33 - RUN_CMD+=("--property=IODeviceLatencyTargetSec=/dev/$device 20s") 115 + for device in "${BLOCK_DEVICES[@]}"; do 116 + CMD+=(--property="IODeviceLatencyTargetSec=/dev/$device 60s") 34 117 done 35 118 36 - # Add the command to run 37 - RUN_CMD+=("$@") 119 + # Add command separator, chrt for batch scheduling, and command to run 120 + CMD+=(--) 121 + CMD+=(chrt -b 0) 122 + CMD+=("$@") 123 + 124 + # Debug: print command if DEBUG env var is set 125 + if [ -n "$DEBUG" ]; then 126 + echo "Running command:" 127 + echo "USE_SLICE='$USE_SLICE'" 128 + for arg in "${CMD[@]}"; do 129 + echo " $arg" 130 + done 131 + echo "" 132 + fi 133 + 134 + # Create temporary file to capture systemd-run stderr (scope unit name) 135 + TMPDIR="${XDG_RUNTIME_DIR:-$HOME/.tmp}" 136 + mkdir -p "$TMPDIR" 137 + OUTPUT_FILE=$(mktemp --tmpdir="$TMPDIR") 138 + 139 + # Launch systemd-run, tee stderr to file and also to terminal 140 + "${CMD[@]}" 2> >(tee "$OUTPUT_FILE" >&2) 141 + EXIT_CODE=$? 142 + 143 + # Read output and extract scope unit name from stderr 144 + RUN_OUTPUT=$(cat "$OUTPUT_FILE") 145 + SCOPE_UNIT=$(echo "$RUN_OUTPUT" | head -1 | awk '{print $4}' | tr -d ';') 146 + 147 + # Set post-launch cgroup settings 148 + if [ -n "$SCOPE_UNIT" ]; then 149 + USER_UID=$(id -u) 150 + USER_SERVICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice/user@${UID}.service" 38 151 39 - # Execute 40 - "${RUN_CMD[@]}" 152 + # Determine cgroup path based on slice name 153 + if [ -z "$USE_SLICE" ]; then 154 + CGROUP_PATH="$USER_SERVICE/${SCOPE_UNIT}" 155 + else 156 + CGROUP_PATH="$USER_SERVICE/$USE_SLICE.slice/${SCOPE_UNIT}" 157 + fi 158 + 159 + # Generic function to set cgroup properties 160 + set_cgroup_property() { 161 + local property=$1 162 + local value=$2 163 + local description=${3:-$property} 164 + 165 + if [ -d "$CGROUP_PATH" ] && [ -f "$CGROUP_PATH/$property" ]; then 166 + echo "$value" >"$CGROUP_PATH/$property" 2>/dev/null && 167 + ACTUAL=$(cat "$CGROUP_PATH/$property") && 168 + echo "Set $description at $CGROUP_PATH (actual: $ACTUAL)" 169 + fi 170 + } 171 + 172 + # Set IO properties 173 + set_cgroup_property "io.weight" "default 1" "io.weight to minimum" 174 + set_cgroup_property "io.prio.class" "idle" "io.prio.class to idle" 175 + 176 + # Set CPU scheduling to batch via chrt (done pre-launch via CMD wrapping) 177 + # Note: cpuset and scheduling policies are set via chrt -b 0 in command wrapper 178 + # Post-launch cgroup does not support direct scheduling policy changes 179 + fi 180 + 181 + # Clean up temporary file 182 + rm -f "$OUTPUT_FILE" 183 + 184 + # Exit with the command's exit code 185 + exit $EXIT_CODE