blob: c902a400ce8a3e65c2e95ea5243eae40ffeac6e5 [file] [log] [blame]
#!/bin/sh
# Copyright 2016 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
# Set margin for low-memory notifier (for tab discarder)
# Configure and start swap if SWAP_ENABLE_FILE exists.
# SWAP_ENABLE_FILE may optionally contain the uncompressed swap size (in Mb).
# Otherwise it is set to 1.5 times total RAM.
PER_BOARD_OVERRIDE_DIR=/etc/swap
PER_DEVICE_OVERRIDE_DIR=/var/lib/swap
SWAP_SIZE_BOARD_OVERRIDE_FILE="${PER_BOARD_OVERRIDE_DIR}/swap_size_mb"
SWAP_ENABLE_FILE="${PER_DEVICE_OVERRIDE_DIR}/swap_enabled"
DISK_BASED_SWAP_FILE="/proc/sys/vm/disk_based_swap"
SWAP_DIR="/mnt/stateful_partition/unencrypted/swap"
MARGIN_MAX=20000 # MiB
MARGIN_CONVERSION=1
MARGIN_OVERRIDE_FILE="${PER_DEVICE_OVERRIDE_DIR}/lowmem_margin"
MARGIN_BOARD_OVERRIDE_FILE="${PER_BOARD_OVERRIDE_DIR}/lowmem_margin"
MARGIN_SPECIAL_FILE="/sys/kernel/mm/chromeos-low_mem/margin"
margin_default_generator() {
default_low_memory_margin "$(get_mem_total)" # MiB
}
MIN_FILELIST_MAX=1000 # MiB
MIN_FILELIST_CONVERSION=1024
MIN_FILELIST_OVERRIDE_FILE="${PER_DEVICE_OVERRIDE_DIR}/min_filelist_kbytes"
MIN_FILELIST_BOARD_OVERRIDE_FILE="${PER_BOARD_OVERRIDE_DIR}/min_filelist_kbytes"
MIN_FILELIST_SPECIAL_FILE="/proc/sys/vm/min_filelist_kbytes"
min_filelist_default_generator() {
# Check if ARC++ is running. But don't check if it's not installed.
if grep -q CHROMEOS_ARC_VERSION /etc/lsb-release && \
[ "$(initctl status arcpp-post-login-services)" = \
"arcpp-post-login-services start/running" ]; then
echo 400000 # KiB
else
echo 100000 # KiB
fi
}
EXTRA_FREE_MAX=20000 # MiB
EXTRA_FREE_CONVERSION=1024
EXTRA_FREE_OVERRIDE_FILE="${PER_DEVICE_OVERRIDE_DIR}/extra_free_kbytes"
EXTRA_FREE_BOARD_OVERRIDE_FILE="${PER_BOARD_OVERRIDE_DIR}/extra_free_kbytes"
EXTRA_FREE_SPECIAL_FILE="/proc/sys/vm/extra_free_kbytes"
extra_free_default_generator() {
echo 0
}
RAM_VS_SWAP_WEIGHT_MAX=256
RAM_VS_SWAP_WEIGHT_CONVERSION=1
RAM_VS_SWAP_WEIGHT_OVERRIDE_FILE="${PER_DEVICE_OVERRIDE_DIR}/ram_vs_swap_weight"
RAM_VS_SWAP_WEIGHT_BOARD_OVERRIDE_FILE="${PER_BOARD_OVERRIDE_DIR}/ram_vs_swap_weight"
RAM_VS_SWAP_WEIGHT_SPECIAL_FILE="/sys/kernel/mm/chromeos-low_mem/ram_vs_swap_weight"
ram_vs_swap_weight_default_generator() {
# Historically we've found that zram compresses approximately at this ratio
# which is why this is the current default.
echo 4
}
HIST_MIN=100
HIST_MAX=10000
HIST_BUCKETS=50
HIST_ARGS="${HIST_MIN} ${HIST_MAX} ${HIST_BUCKETS}"
# Upstart sets JOB, but we're not always called by upstart,
# so set it here too.
JOB="swap"
# Takes a string S and returns the value of ${S}.
expand_var() {
eval echo "\"\$$1\""
}
get_mem_total() {
# Extract second field of MemTotal entry in /proc/meminfo.
# NOTE: this could be done with "read", "case", and a function
# that sets ram=$2, for a savings of about 3ms on an Alex.
local mem_total
mem_total=$(awk '/MemTotal/ { print $2; }' /proc/meminfo)
if [ -z "${mem_total}" ]; then
logger -t "${JOB}" "could not get MemTotal"
exit 1
fi
echo "${mem_total}"
}
default_low_memory_margin() {
# compute fraction of total RAM used for low-mem margin. The fraction is
# given in bips. A "bip" or "basis point" is 1/100 of 1%.
local critical_margin
local moderate_margin
CRITICAL_MARGIN_BIPS=520 # 5.2% FREE
critical_margin=$(( $1 / 1024 * CRITICAL_MARGIN_BIPS / 10000 )) # MiB
MODERATE_MARGIN_BIPS=4000 # 40% FREE
moderate_margin=$(( $1 / 1024 * MODERATE_MARGIN_BIPS / 10000 )) # MiB
echo "${critical_margin} ${moderate_margin}"
}
# Gets the target value of a kernel memory manager parameter, whose name is
# passed in $1.
#
# Each parameter <P> has a default value, computed by <P>_default_generator.
# The default value can be overridden by a board-specific value contained
# in <P>_BOARD_OVERRIDE_FILE, or (with higher priority) a device-specific file,
# <P>_OVERRIDE_FILE which a user may have set, typically for the purpose of
# experimentation.
get_target_value() {
local PARAM="$(echo "$1" | tr '[a-z]' '[A-Z]')"
local value
local override_file="$(expand_var "${PARAM}_OVERRIDE_FILE")"
local board_override_file="$(expand_var "${PARAM}_BOARD_OVERRIDE_FILE")"
local default_generator="$1"_default_generator
if [ -e "${override_file}" ]; then
rm -f "${override_file}" 2> /dev/null
fi
# We no longer allow per-user overrides because they were causing issues
# where users were unaware of kernel changes requiring new parameters.
# Additionally, user overrides prevent finch based experimentation.
if [ -e "${board_override_file}" ]; then
value=$(cat "${board_override_file}")
else
value=$(${default_generator})
fi
echo "${value}"
}
# Sets the kernel value of a memory manager parameter, whose name is passed in
# $1, via a procfs or sysfs entry.
initialize_parameter() {
local value="$(get_target_value "$1")"
local PARAM="$(echo "$1" | tr '[:lower:]' '[:upper:]')"
local special_file="$(expand_var "${PARAM}_SPECIAL_FILE")"
# Older kernels don't support all parameters.
if [ ! -e "${special_file}" ]; then
return 0
fi
echo "${value}" > "${special_file}"
}
create_write_file() {
file="$1"
dir=$(dirname "${file}")
content="$2"
# Delete the file first in case its permissions have gotten ... weird.
rm -f "${file}"
mkdir -p -m 0755 "${dir}"
echo "${content}" > "${file}"
}
# TODO(semenzato): Remove this after R65
migrate_old_swap_setting() {
OLD_SWAP_ENABLE_FILE=/home/chronos/.swap_enabled
if [ -e "${OLD_SWAP_ENABLE_FILE}" ]; then
old_content="$(cat "${OLD_SWAP_ENABLE_FILE}")"
create_write_file "${SWAP_ENABLE_FILE}" "${old_content}"
rm -f "${OLD_SWAP_ENABLE_FILE}"
fi
}
disk_based_swap_supported() {
# Can be set in the ebuild.
local disk_based_swap_enabled=false
# Return true if kernel supports disk based swap.
if [ "${disk_based_swap_enabled}" = "true" ] &&
[ -e "${DISK_BASED_SWAP_FILE}" ]; then
echo 1 > "${DISK_BASED_SWAP_FILE}"
else
disk_based_swap_enabled=false
fi
${disk_based_swap_enabled}
}
swap_to_micron() {
local micron_swap=false
# Return true if micron dual namespace is configured.
if [ -b "/dev/nvme0n1" ] && [ -b "/dev/nvme0n2" ]; then
local dev_dir model
dev_dir="/sys/block/nvme0n1"
model="$(sed -E 's/[ \t]+$//' "${dev_dir}"/device/model)"
if [ "${model}" = "MTFDHBK256TDP" ] || [ "${model}" = "MTFDHBK128TDP" ]; then
micron_swap=true
fi
else
micron_swap=false
fi
${micron_swap}
}
die() {
message="$1"
logger -t "${JOB}" "${message}"
exit 1
}
start() {
local mem_total param
mem_total=$(get_mem_total)
# TODO(semenzato): Remove this after R65
migrate_old_swap_setting
for param in margin min_filelist extra_free; do
initialize_parameter "${param}"
done
# Allocate zram (compressed ram disk) for swap.
# SWAP_ENABLE_FILE contains the zram size in MB.
# Empty or missing SWAP_ENABLE_FILE means use default size.
# 0 size means do not enable zram.
# Calculations are in Kb to avoid 32 bit overflow.
local requested_size_mb size_kb
# For security, only read first few bytes of SWAP_ENABLE_FILE.
requested_size_mb="$(head -c 5 "${SWAP_ENABLE_FILE}")" || :
# If SWAP_ENABLE_FILE does not exist or is empty, try the board override.
if [ -z "${requested_size_mb}" ]; then
requested_size_mb=$(cat "${SWAP_SIZE_BOARD_OVERRIDE_FILE}") || :
fi
# If still empty, compute swap based on RAM size.
if [ -z "${requested_size_mb}" ]; then
# Default multiplier for zram size. (Shell math is integer only.)
local multiplier="3 / 2"
# On ARM32 / ARM64 CPUs graphics memory is not reclaimable, so use a smaller
# size.
if arch | grep -qiE "arm|aarch64"; then
multiplier="1"
fi
# The multiplier may be an expression, so it MUST use the $ expansion.
size_kb=$(( mem_total * ${multiplier} ))
elif [ "${requested_size_mb}" = "0" ]; then
metrics_client Platform.CompressedSwapSize 0 ${HIST_ARGS}
exit 0
else
size_kb=$(( requested_size_mb * 1024 ))
fi
local swap_device=
if disk_based_swap_supported && swap_to_micron; then
swap_device=/dev/nvme0n1
fi
if [ -z "${swap_device}" ]; then
# Load zram module. Ignore failure (it could be compiled in the kernel).
modprobe zram || logger -t "${JOB}" "modprobe zram failed (compiled?)"
logger -t "${JOB}" "setting zram size to ${size_kb} Kb"
# Approximate the kilobyte to byte conversion to avoid issues
# with 32-bit signed integer overflow.
echo "${size_kb}000" >/sys/block/zram0/disksize ||
logger -t "${JOB}" "failed to set zram size"
mkswap /dev/zram0 || logger -t "${JOB}" "mkswap /dev/zram0 failed"
# Swapon may fail because of races with other programs that inspect all
# block devices, so try several times.
local tries=0
while [ ${tries} -le 10 ]; do
swapon /dev/zram0 && break
: $(( tries += 1 ))
logger -t "${JOB}" "swapon /dev/zram0 failed, try ${tries}"
sleep 0.1
done
else
local table
table="0 $(blockdev --getsz "${swap_device}") crypt aes-xts-plain64 \
$(tr -dc 'A-F0-9' < /dev/urandom | fold -w 64 | head -n 1) \
0 ${swap_device} 0 2 allow_discards submit_from_crypt_cpus"
/sbin/dmsetup create enc-swap --table "${table}" ||
die "/sbin/dmsetup create enc-swap failed"
mkswap "/dev/mapper/enc-swap" ||
die "mkswap /dev/mapper/enc-swap failed"
swapon -d "/dev/mapper/enc-swap" ||
die "swapon /dev/mapper/enc-swap failed"
echo 1 > /sys/module/zswap/parameters/enabled
echo z3fold > /sys/module/zswap/parameters/zpool
echo 1 > /sys/kernel/mm/chromeos-low_mem/ram_vs_swap_weight
fi
local swaptotalkb
swaptotalkb=$(awk '/SwapTotal/ { print $2 }' /proc/meminfo)
metrics_client Platform.CompressedSwapSize \
$(( swaptotalkb / 1024 )) ${HIST_ARGS}
}
stop() {
logger -t "${JOB}" "turning off swap"
# This is safe to call even if no swap is turned on.
swapoff -av
if [ -b "/dev/zram0" ]; then
# When we start up, we try to configure zram0, but it doesn't like to
# be reconfigured on the fly. Reset it so we can changes its params.
echo 1 > /sys/block/zram0/reset || :
fi
}
status() {
# Show general swap info first.
cat /proc/swaps
# Show tunables.
printf "low-memory margin (MiB): "
cat "${MARGIN_SPECIAL_FILE}"
printf "min_filelist_kbytes (KiB): "
cat "${MIN_FILELIST_SPECIAL_FILE}"
printf "ram_vs_swap_weight: "
cat "${RAM_VS_SWAP_WEIGHT_SPECIAL_FILE}"
if [ -e "${EXTRA_FREE_SPECIAL_FILE}" ]; then
printf "extra_free_kbytes (KiB): "
cat "${EXTRA_FREE_SPECIAL_FILE}"
fi
if [ -b "/dev/zram0" ]; then
# Then spam various zram settings.
local dir="/sys/block/zram0"
printf '\ntop-level entries in %s:\n' "${dir}"
cd "${dir}"
grep -s '^' * || :
elif [ -e "/sys/kernel/debug/zswap" ]; then
local dir="/sys/kernel/debug/zswap"
printf '\ntop-level entries in %s:\n' "${dir}"
cd "${dir}"
grep -s '^' * || :
fi
}
enable() {
local size="$1"
# Sizes of 0 or -1 mean restore factory default.
# Don't confuse this with setting 0 in the file in the disable code path.
if [ "${size}" = "0" -o "${size}" = "-1" ]; then
size=""
logger -t "${JOB}" "enabling swap with default size"
elif [ "${size}" -lt 100 -o "${size}" -gt 20000 ]; then
echo "${JOB}: error: size ${size} is not between 100 and 20000" >&2
exit 1
else
logger -t "${JOB}" "enabling swap via config with size ${size}"
fi
create_write_file "${SWAP_ENABLE_FILE}" "${size}"
}
disable() {
logger -t "${JOB}" "disabling swap via config"
create_write_file "${SWAP_ENABLE_FILE}" "0"
}
set_parameter() {
local param="$1"
local value="$2"
case "${param}" in
margin|min_filelist|extra_free|ram_vs_swap_weight)
# We're good.
;;
*)
echo "invalid parameter ${param}" >&2
exit 1
;;
esac
local PARAM="$(echo "${param}" | tr '[:lower:]' '[:upper:]')"
local max="$(expand_var "${PARAM}_MAX")"
local special_file="$(expand_var "${PARAM}_SPECIAL_FILE")"
local conversion="$(expand_var "${PARAM}_CONVERSION")"
local default_generator=${param}_default_generator
# Don't try to set the parameter if the kernel doesn't support it.
if [ ! -e "${special_file}" ]; then
return 0
fi
if [ "${value}" -gt "${max}" ]; then
echo "${JOB}: invalid set ${param} to ${value} (MiB), max is ${max}" >&2
exit 1
fi
# We no longer allow per-user overrides because they were causing issues
# where users were unaware of kernel changes requiring new parameters.
# Additionally, user overrides prevent finch based experimentation.
local system_value
if [ "${value}" = "-1" ]; then
system_value=$(get_target_value "${param}")
else
# User units (always MiB) may differ from system units (sometimes KiB).
system_value=$(( value * conversion ))
fi
echo "${system_value}" > "${special_file}"
value=$(( system_value / conversion ))
logger "changed ${param} to ${value} MiB"
}
usage() {
cat <<EOF
Usage: $0 <start|stop|status|enable <size>|disable|
set_parameter <margin|min_filelist|extra_free|ram_vs_swap_weight> <value>>|
get_target_value <margin|min_filelist|extra_free|ram_vs_swap_weight> <value>>
Start or stop the use of the compressed swap file, or persistently set
various memory manager tunable parameters, or get their chosen values.
A value of -1 for "enable" or "set_parameter <p>" results in removing any local
persistent setting of the parameter, thus reverting to its factory default.
The start phase is normally invoked by init during boot, but we never run the
stop phase when shutting down (since there's no point). The stop phase is used
by developers via debugd to restart things on the fly.
Disabling changes the config, but doesn't actually turn on/off swap.
That will happen at the next reboot.
EOF
exit $1
}
main() {
set -e
if [ $# -lt 1 ]; then
usage 1
fi
# Make sure that the subcommand is one we know and that it has the right
# number of arguments.
local cmd="$1"
shift
case "${cmd}" in
start|stop|status|disable)
if [ $# -ne 0 ]; then
usage 1
fi
;;
enable|get_target_value)
if [ $# -ne 1 ]; then
usage 1
fi
;;
set_parameter)
if [ $# -ne 2 ]; then
usage 1
fi
;;
*)
usage 1
;;
esac
# Just call the func requested directly.
${cmd} "$@"
}
main "$@"