···16161717## Install
18181919-Either add to PATH:
2020-2121-```sh
2222-export PATH=$(pwd):$PATH
2323-```
2424-2525-Or symlink
1919+No installation required - the script is in the repository root:
26202727-```sh
2828-sudo ln -s $(pwd)/gentle.sh /usr/local/bin
2121+```bash
2222+chmod +x gentle.sh
2923```
30243125## Usage
32263327```bash
3434-gentle.sh <command> [args...]
2828+./gentle.sh <command> [args...]
3529```
36303731## Resource Limits
···40344135### CPU Limits
42364343-- **CPU Affinity**: All CPUs except the first 2 (keeps cores 0-1 free for system)
4444-- **CPU Scheduling Policy**: `batch` - lower priority than normal processes
4545- - Available lower option: `idle` - only runs when nothing else needs CPU
3737+- **CPU Affinity**: All CPUs except first 2 (keeps cores 0-1 free for system), specified as range `2-15`
3838+- **CPU Scheduling**: `SCHED_BATCH` via `chrt -b 0` - batch scheduling for non-interactive workloads
4639- **CPU Weight**: `1` - minimum weight (1-10000 range)
4740- **CPU Quota Period**: `300ms` - period for quota measurement
4841- **Nice Level**: `19` - lowest nice priority
49425043### I/O Limits
51445252-- **IO Weight**: `1` - minimum weight (1-10000 range)
5353-- **IO Scheduling Class**: `best-effort` with priority `7` - lowest best-effort priority
5454- - Available lower option: `idle` - lowest possible IO priority
5555-- **IO Device Latency**: `infinity` applied to all detected block devices - no strict latency enforcement on any drive
4545+- **IO Weight**: `1` (default 1) - minimum weight (1-10000 range), set post-launch via cgroup
4646+- **IO Priority Class**: `idle` - lowest IO priority class, set post-launch via cgroup
4747+- **IO Device Latency**: `60s` applied to all detected block devices
56485749### Memory Limits
5850···6456Build a large project without freezing your system:
65576658```bash
6767-gentle.sh make -j8
5959+./gentle.sh make -j8
6860```
69617062Run a compilation:
71637264```bash
7373-gentle.sh ./build.sh
6565+./gentle.sh ./build.sh
6666+```
6767+6868+Run with multiple arguments:
6969+7070+```bash
7171+./gentle.sh ninja -C build all
7272+```
7373+7474+Run a CMake configuration:
7575+7676+```bash
7777+./gentle.sh cmake -B build -DCMAKE_BUILD_TYPE=Release
7478```
75797680## Notes
77817878-- The script uses `systemd-run --scope --user`, which requires systemd user sessions
8282+- The script uses `systemd-run --scope --user`, which runs commands in the foreground
8383+- CPU scheduling policy is set to `SCHED_BATCH` via `chrt -b 0` wrapper
8484+- IO properties (`io.weight` and `io.prio.class`) are set post-launch via direct cgroup writes
7985- Block devices are auto-detected using `lsblk` and IO latency targets are set for all drives
8080-- The process runs in the foreground (not detached as a service)
8686+- Temporary files use `XDG_RUNTIME_DIR` (or `$HOME/.tmp` as fallback) for user-isolated storage
8187- All resource limits are applied to the process and its children
+96
cgroup-user-controllers.sh
···11+#!/bin/bash
22+33+# Enable cgroup controllers at necessary levels in cgroup hierarchy
44+55+# Check if running with elevated privileges
66+if [ "$(id -u)" = "0" ]; then
77+ USE_SUDO=""
88+else
99+ USE_SUDO="sudo"
1010+fi
1111+1212+# Generic function to write to a file with privilege escalation
1313+# Arguments: file_path, value, description
1414+# Output: changed: $PATH (BEFORE -> AFTER) / failed: $PATH (BEFORE -> WANTED)
1515+write_to_file() {
1616+ local file=$1
1717+ local value=$2
1818+ local desc=${3:-$file}
1919+2020+ if [ ! -f "$file" ]; then
2121+ echo " failed: $file (not found)"
2222+ return 1
2323+ fi
2424+2525+ local before=$(cat "$file")
2626+ if [ "$before" = "$value" ]; then
2727+ return 0
2828+ fi
2929+3030+ # Try user write first (only if user-writable)
3131+ if [ -w "$file" ]; then
3232+ if echo "$value" > "$file" 2>/dev/null; then
3333+ local after=$(cat "$file")
3434+ echo " changed: $file ($before -> $after)"
3535+ return 0
3636+ fi
3737+ fi
3838+3939+ # Fall back to sudo for non-writable files
4040+ if ! echo "$value" | $USE_SUDO tee "$file" >/dev/null; then
4141+ echo " failed: $file ($before -> $value)"
4242+ return 1
4343+ fi
4444+4545+ local after=$(cat "$file")
4646+ echo " changed: $file ($before -> $after) [$USE_SUDO]"
4747+ return 0
4848+}
4949+5050+# Reusable function to check and enable a cgroup controller
5151+# Arguments: controller_name, cgroup_path
5252+# Output format: already-done / changed / failed
5353+enable_cgroup_controller() {
5454+ local controller=$1
5555+ local path=$2
5656+ local subtree_file="$path/cgroup.subtree_control"
5757+5858+ if cat "$subtree_file" | grep -q "\\b${controller}\\b"; then
5959+ return 0
6060+ fi
6161+6262+ if ! write_to_file "$subtree_file" "+${controller}" "Enable $controller at $path"; then
6363+ return 1
6464+ fi
6565+6666+ return 0
6767+}
6868+6969+# Set cgroup value if different from current
7070+# Arguments: file_path, value
7171+set_cgroup_value() {
7272+ local file=$1
7373+ local value=$2
7474+7575+ write_to_file "$file" "$value"
7676+}
7777+7878+ USER_UID=$(id -u)
7979+USER_SLICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice"
8080+USER_SERVICE="$USER_SLICE/user@${UID}.service"
8181+BACKGROUND_SLICE="$USER_SERVICE/background.slice"
8282+8383+# Controllers needed at each level of cgroup hierarchy
8484+CONTROLLERS=("cpuset" "cpu" "memory" "pids" "io")
8585+8686+# Enable controllers at each level of the hierarchy
8787+for controller in "${CONTROLLERS[@]}"; do
8888+ enable_cgroup_controller "$controller" "/sys/fs/cgroup/user.slice"
8989+ enable_cgroup_controller "$controller" "$USER_SLICE"
9090+ enable_cgroup_controller "$controller" "$USER_SERVICE"
9191+ enable_cgroup_controller "$controller" "$BACKGROUND_SLICE"
9292+done
9393+9494+# Set IO scheduling defaults at background.slice level
9595+set_cgroup_value "$BACKGROUND_SLICE/io.weight" "default 1"
9696+set_cgroup_value "$BACKGROUND_SLICE/io.prio.class" "idle"
+162-17
gentle.sh
···2233# Run a command gently with limited CPU, IO, and nice values using systemd-run
4455+#=== Preflight Checks / Config ===#
66+77+# Preflight check: verify required cgroup controllers are enabled at given path
88+check_controllers_at_path() {
99+ local path=$1
1010+ local subtree_file="$path/cgroup.subtree_control"
1111+1212+ if [ ! -f "$subtree_file" ]; then
1313+ echo " $path: no cgroup.subtree_control file"
1414+ return 1
1515+ fi
1616+1717+ local controllers=$(cat "$subtree_file")
1818+ local needed=("cpuset" "cpu" "memory" "io")
1919+ local missing=()
2020+2121+ for controller in "${needed[@]}"; do
2222+ if ! echo "$controllers" | grep -q "\\b${controller}\\b"; then
2323+ missing+=("$controller")
2424+ fi
2525+ done
2626+2727+ if [ ${#missing[@]} -eq 0 ]; then
2828+ echo " $path: ok"
2929+ return 0
3030+ fi
3131+3232+ echo " $path: missing controllers: ${missing[*]}"
3333+ return 1
3434+}
3535+3636+# Default slice to use
3737+DEFAULT_SLICE=background
3838+3939+# Set up user cgroup paths
4040+USER_UID=$(id -u)
4141+USER_SLICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice"
4242+USER_SERVICE="$USER_SLICE/user@${UID}.service"
4343+4444+# Determine slice to use (empty string = systemd default, otherwise slice name)
4545+# Special value "false" becomes empty string
4646+if [ "$USE_SLICE" = "false" ]; then
4747+ USE_SLICE=""
4848+fi
4949+5050+# Default to DEFAULT_SLICE only if USE_SLICE is unset (not just empty)
5151+# But only if the slice actually exists and has controllers
5252+if [ -z "${USE_SLICE+x}" ]; then
5353+ if [ -f "$USER_SERVICE/$DEFAULT_SLICE.slice/cgroup.subtree_control" ]; then
5454+ USE_SLICE="$DEFAULT_SLICE"
5555+ else
5656+ USE_SLICE=""
5757+ fi
5858+fi
5959+6060+if [ -n "$USE_SLICE" ]; then
6161+ echo "Using slice: $USE_SLICE.slice"
6262+else
6363+ echo "Using slice: (systemd default)"
6464+fi
6565+6666+echo "Checking cgroup controllers..."
6767+6868+MISSING=0
6969+check_controllers_at_path "/sys/fs/cgroup/user.slice" || MISSING=1
7070+check_controllers_at_path "$USER_SLICE" || MISSING=1
7171+check_controllers_at_path "$USER_SERVICE" || MISSING=1
7272+7373+# Only check the slice if USE_SLICE is set
7474+if [ -n "$USE_SLICE" ]; then
7575+ CHECK_SLICE="$USER_SERVICE/$USE_SLICE.slice"
7676+ check_controllers_at_path "$CHECK_SLICE" || MISSING=1
7777+fi
7878+7979+if [ $MISSING -eq 1 ]; then
8080+ echo ""
8181+ echo "Some cgroup controllers are missing!"
8282+ echo "Run './cgroup-user-io.sh' with sudo to enable missing controllers"
8383+ echo ""
8484+fi
8585+8686+#=== Command Execution Setup ===#
8787+588# Calculate CPU affinity (all CPUs except first 2)
689CPU_COUNT=$(nproc)
77-CPU_AFFINITY="2-$(($CPU_COUNT - 1))"
9090+CPU_AFFINITY="2-$((CPU_COUNT - 1))"
891992# Get all base block devices to set IO latency target on all drives
1010-BLOCK_DEVICES=$(lsblk -d -n -o NAME | grep -E "^(nvme|sd)")
9393+mapfile -t BLOCK_DEVICES < <(lsblk -d -n -o NAME | grep -E "^(nvme|sd)")
11941212-# Build systemd-run command
1313-# Note: IOSchedulingClass=idle is even lower priority than best-effort
1414-# Note: CPUSchedulingPolicy=idle is even lower priority than batch
1515-RUN_CMD=(
9595+# Build systemd-run command with proper quoting
9696+CMD=(
1697 systemd-run
1798 --scope
1899 --user
19100 --nice=19
2020- --io-weight=1
2121- --cpu-affinity="$CPU_AFFINITY"
101101+ --property=IOWeight=1
102102+ --property="AllowedCPUs=$CPU_AFFINITY"
22103 --property=CPUWeight=1
23104 --property=MemoryMax=80%
24105 --property=TasksMax=infinity
25106 --property=CPUQuotaPeriodSec=300ms
2626- --property=IOSchedulingClass=best-effort
2727- --property=IOSchedulingPriority=7
2828- --property=CPUSchedulingPolicy=batch
29107)
30108109109+# Add --slice only if USE_SLICE is set
110110+if [ -n "$USE_SLICE" ]; then
111111+ CMD+=(--slice="$USE_SLICE.slice")
112112+fi
113113+31114# Add IODeviceLatencyTargetSec for each block device
3232-for device in $BLOCK_DEVICES; do
3333- RUN_CMD+=("--property=IODeviceLatencyTargetSec=/dev/$device 20s")
115115+for device in "${BLOCK_DEVICES[@]}"; do
116116+ CMD+=(--property="IODeviceLatencyTargetSec=/dev/$device 60s")
34117done
351183636-# Add the command to run
3737-RUN_CMD+=("$@")
119119+# Add command separator, chrt for batch scheduling, and command to run
120120+CMD+=(--)
121121+CMD+=(chrt -b 0)
122122+CMD+=("$@")
123123+124124+# Debug: print command if DEBUG env var is set
125125+if [ -n "$DEBUG" ]; then
126126+ echo "Running command:"
127127+ echo "USE_SLICE='$USE_SLICE'"
128128+ for arg in "${CMD[@]}"; do
129129+ echo " $arg"
130130+ done
131131+ echo ""
132132+fi
133133+134134+# Create temporary file to capture systemd-run stderr (scope unit name)
135135+TMPDIR="${XDG_RUNTIME_DIR:-$HOME/.tmp}"
136136+mkdir -p "$TMPDIR"
137137+OUTPUT_FILE=$(mktemp --tmpdir="$TMPDIR")
138138+139139+# Launch systemd-run, tee stderr to file and also to terminal
140140+"${CMD[@]}" 2> >(tee "$OUTPUT_FILE" >&2)
141141+EXIT_CODE=$?
142142+143143+# Read output and extract scope unit name from stderr
144144+RUN_OUTPUT=$(cat "$OUTPUT_FILE")
145145+SCOPE_UNIT=$(echo "$RUN_OUTPUT" | head -1 | awk '{print $4}' | tr -d ';')
146146+147147+# Set post-launch cgroup settings
148148+if [ -n "$SCOPE_UNIT" ]; then
149149+ USER_UID=$(id -u)
150150+ USER_SERVICE="/sys/fs/cgroup/user.slice/user-${USER_UID}.slice/user@${UID}.service"
381513939-# Execute
4040-"${RUN_CMD[@]}"
152152+ # Determine cgroup path based on slice name
153153+ if [ -z "$USE_SLICE" ]; then
154154+ CGROUP_PATH="$USER_SERVICE/${SCOPE_UNIT}"
155155+ else
156156+ CGROUP_PATH="$USER_SERVICE/$USE_SLICE.slice/${SCOPE_UNIT}"
157157+ fi
158158+159159+ # Generic function to set cgroup properties
160160+ set_cgroup_property() {
161161+ local property=$1
162162+ local value=$2
163163+ local description=${3:-$property}
164164+165165+ if [ -d "$CGROUP_PATH" ] && [ -f "$CGROUP_PATH/$property" ]; then
166166+ echo "$value" >"$CGROUP_PATH/$property" 2>/dev/null &&
167167+ ACTUAL=$(cat "$CGROUP_PATH/$property") &&
168168+ echo "Set $description at $CGROUP_PATH (actual: $ACTUAL)"
169169+ fi
170170+ }
171171+172172+ # Set IO properties
173173+ set_cgroup_property "io.weight" "default 1" "io.weight to minimum"
174174+ set_cgroup_property "io.prio.class" "idle" "io.prio.class to idle"
175175+176176+ # Set CPU scheduling to batch via chrt (done pre-launch via CMD wrapping)
177177+ # Note: cpuset and scheduling policies are set via chrt -b 0 in command wrapper
178178+ # Post-launch cgroup does not support direct scheduling policy changes
179179+fi
180180+181181+# Clean up temporary file
182182+rm -f "$OUTPUT_FILE"
183183+184184+# Exit with the command's exit code
185185+exit $EXIT_CODE