限制单个 Linux 进程的内存使用

限制单个 Linux 进程的内存使用

我正在pdftoppm将用户提供的 PDF 转换为 300DPI 图像。这非常有效,除非用户提供的 PDF 页面尺寸非常大。 pdftoppm将分配足够的内存来在内存中保存该大小的 300DPI 图像,对于 100 英寸见方的页面来说,该大小为 100*300 * 100*300 * 每像素 4 字节 = 3.5GB。恶意用户可能只给我一个愚蠢的大 PDF 并导致各种问题。

所以我想做的是对我即将运行的子进程的内存使用设置某种硬性限制——如果它尝试分配超过 500MB 的内存,就让该进程终止。那可能吗?

我不认为 ulimit 可以用于此目的,但是是否有一个等效的单进程?

答案1

限制这种情况的另一种方法是使用 Linux 的控制组。如果您想限制一个进程(或一组进程)的物理内存分配与虚拟内存不同,这尤其有用。例如:

cgcreate -g memory:myGroup
echo 500M > /sys/fs/cgroup/memory/myGroup/memory.limit_in_bytes
echo 5G > /sys/fs/cgroup/memory/myGroup/memory.memsw.limit_in_bytes

将创建一个名为 的控制组,限制在最多 500 MB 物理内存myGroup下运行的进程集,以及最多 5000 MB 物理内存和交换内存以及myGroupmemory.limit_in_bytesmemory.memsw.limit_in_bytes下运行。有关这些选项的更多信息可以在此处找到:https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/resource_management_guide/sec-memory

要在控制组下运行进程:

cgexec -g memory:myGroup pdftoppm

请注意,在现代 Ubuntu 发行版上,此示例需要安装cgroup-tools包(以前cgroup-bin):

sudo apt install cgroup-tools

并编辑/etc/default/grub更改GRUB_CMDLINE_LINUX_DEFAULT为:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

然后运行sudo update-grub并重新启动以使用新的内核启动参数进行启动。

答案2

在任何基于 systemd 的发行版上,您还可以通过以下方式间接使用 cgroup系统运行。例如,对于限制为 500M RAM 的情况pdftoppm,从 cgroupsv2 开始,您可以简单地执行以下操作:

systemd-run --scope -p MemoryMax=500M --user pdftoppm

此前,这需要使用systemd.unified_cgroup_hierarchy内核参数启动,但从 Ubuntu 22.04 cgroup-tools 2.0-2 开始测试,情况似乎不再如此,该命令只是在不对内核参数进行任何更改的情况下工作,并且systemd.unified_cgroup_hierarchy未设置。

在 cgroupsv2 之前,您不能使用--user, 而是运行:

systemd-run --scope -p MemoryMax=500M pdftoppm

但如果没有--user,即使应用程序以您的用户身份启动,每次都会要求您输入密码。不要让这个欺骗您认为该命令需要sudo,因为这会导致该命令在 root 下运行,而这几乎不是您的本意。

答案3

如果您的进程没有产生更多消耗最多内存的子进程,您可以使用setrlimit功能。更常见的用户界面是使用ulimitshell 命令:

$ ulimit -Sv 500000     # Set ~500 mb limit
$ pdftoppm ...

这只会限制进程的“虚拟”内存,同时考虑并限制被调用的进程与其他进程共享的内存,以及映射但未保留的内存(例如,Java 的大堆)。尽管如此,虚拟内存对于变得非常大的进程来说是最接近的近似值,使得上述错误变得微不足道。

如果您的程序产生子程序,并且由它们分配内存,那么它会变得更加复杂,您应该编写辅助脚本来在您的控制下运行进程。我在我的博客中写道,为什么如何

答案4

自 2022 年/Ubuntu 22.04 起,以下脚本已过时。 Ubuntu 22.04 不再默认挂载 cgroups v1,systemd-run 现在支持所需的一切。运行具有硬内存限制的程序的命令是

systemd-run --user --scope -p MemoryMax=<memorylimit> \
  -p MemorySwapMax=<swaplimit> <command>
  • 请注意,内存和交换区有单独的限制,这与memory.memsw.*cgroups v1 中的控制文件不同,后者控制内存 + 交换区的使用总量。到目前为止,我还没有找到一种方法来设置内存+交换组合的限制。

  • 还有一个MemoryHigh参数不如 严格MemoryMax。它不会终止进程,但会开始限制它们并积极交换内存。

下面的脚本可以修改为在 cgroups v2 上运行西罗·桑蒂利的回答,但systemd-run现在已经做了一切必要的事情,没有必要了。我有一个与我原来的答案类似的脚本,可以systemd-run新答案


原答案:

我正在使用下面的脚本,效果很好。它通过cgmanager. 更新:它现在使用来自 的命令cgroup-tools命名该脚本limitmem并将其放入您的 $PATH 中,您可以像limitmem 100M bash.这将限制内存和交换的使用。要仅限制内存,请删除带有 的行memory.memsw.limit_in_bytes

编辑:在默认的 Linux 安装中,这仅限制内存使用,而不限制交换使用。要启用交换使用限制,您需要在 Linux 系统上启用交换记帐。通过设置/添加swapaccount=1来做到这一点/etc/default/grub,使它看起来像

GRUB_CMDLINE_LINUX="swapaccount=1"

然后运行sudo update-grub并重新启动。

cgroup-tools免责声明:如果将来也出现故障,我不会感到惊讶。正确的解决方案是使用 systemd api 进行 cgroup 管理,但该 atm 没有命令行工具

编辑 (2021):到目前为止,这个脚本仍然有效,但它违背了 Linux 使用单个程序管理 cgroup 的建议。现在这个程序通常是systemd。不幸的是,systemd 有许多限制,使得很难用 systemd 调用替换此脚本。该systemd-run --user命令应允许用户运行具有资源限制的程序,但 cgroups v1 不支持此功能。 (每个人都使用 cgroups v1,因为除了最新版本之外,docker 还无法在 cgroupsv2 上运行。)通过 root 访问权限(此脚本也需要),应该可以用来systemd-run创建正确的 systemd 支持的 cgroup,然后手动在正确的 cgroup 中设置内存和交换属性,但这仍有待实现。也可以看看这个错误评论对于上下文,以及这里这里获取相关文档。

根据 @Mikko 的评论,在 systemd 中使用这样的脚本存在 systemd 丢失会话中进程跟踪的风险。我没有注意到这样的问题,但我主要在单用户计算机上使用这个脚本。

#!/bin/sh

# This script uses commands from the cgroup-tools package. The cgroup-tools commands access the cgroup filesystem directly which is against the (new-ish) kernel's requirement that cgroups are managed by a single entity (which usually will be systemd). Additionally there is a v2 cgroup api in development which will probably replace the existing api at some point. So expect this script to break in the future. The correct way forward would be to use systemd's apis to create the cgroups, but afaik systemd currently (feb 2018) only exposes dbus apis for which there are no command line tools yet, and I didn't feel like writing those.

# strict mode: error if commands fail or if unset variables are used
set -eu

if [ "$#" -lt 2 ]
then
    echo Usage: `basename $0` "<limit> <command>..."
    echo or: `basename $0` "<memlimit> -s <swaplimit> <command>..."
    exit 1
fi

cgname="limitmem_$$"

# parse command line args and find limits

limit="$1"
swaplimit="$limit"
shift

if [ "$1" = "-s" ]
then
    shift
    swaplimit="$1"
    shift
fi

if [ "$1" = -- ]
then
    shift
fi

if [ "$limit" = "$swaplimit" ]
then
    memsw=0
    echo "limiting memory to $limit (cgroup $cgname) for command $@" >&2
else
    memsw=1
    echo "limiting memory to $limit and total virtual memory to $swaplimit (cgroup $cgname) for command $@" >&2
fi

# create cgroup
sudo cgcreate -g "memory:$cgname"
sudo cgset -r memory.limit_in_bytes="$limit" "$cgname"
bytes_limit=`cgget -g "memory:$cgname" | grep memory.limit_in_bytes | cut -d\  -f2`

# try also limiting swap usage, but this fails if the system has no swap
if sudo cgset -r memory.memsw.limit_in_bytes="$swaplimit" "$cgname"
then
    bytes_swap_limit=`cgget -g "memory:$cgname" | grep memory.memsw.limit_in_bytes | cut -d\  -f2`
else
    echo "failed to limit swap"
    memsw=0
fi

# create a waiting sudo'd process that will delete the cgroup once we're done. This prevents the user needing to enter their password to sudo again after the main command exists, which may take longer than sudo's timeout.
tmpdir=${XDG_RUNTIME_DIR:-$TMPDIR}
tmpdir=${tmpdir:-/tmp}
fifo="$tmpdir/limitmem_$$_cgroup_closer"
mkfifo --mode=u=rw,go= "$fifo"
sudo -b sh -c "head -c1 '$fifo' >/dev/null ; cgdelete -g 'memory:$cgname'"

# spawn subshell to run in the cgroup. If the command fails we still want to remove the cgroup so unset '-e'.
set +e
(
set -e
# move subshell into cgroup
sudo cgclassify -g "memory:$cgname" --sticky `sh -c 'echo $PPID'`  # $$ returns the main shell's pid, not this subshell's.
exec "$@"
)

# grab exit code 
exitcode=$?

set -e

# show memory usage summary

peak_mem=`cgget -g "memory:$cgname" | grep memory.max_usage_in_bytes | cut -d\  -f2`
failcount=`cgget -g "memory:$cgname" | grep memory.failcnt | cut -d\  -f2`
percent=`expr "$peak_mem" / \( "$bytes_limit" / 100 \)`

echo "peak memory used: $peak_mem ($percent%); exceeded limit $failcount times" >&2

if [ "$memsw" = 1 ]
then
    peak_swap=`cgget -g "memory:$cgname" | grep memory.memsw.max_usage_in_bytes | cut -d\  -f2`
    swap_failcount=`cgget -g "memory:$cgname" |grep memory.memsw.failcnt | cut -d\  -f2`
    swap_percent=`expr "$peak_swap" / \( "$bytes_swap_limit" / 100 \)`

    echo "peak virtual memory used: $peak_swap ($swap_percent%); exceeded limit $swap_failcount times" >&2
fi

# remove cgroup by sending a byte through the pipe
echo 1 > "$fifo"
rm "$fifo"

exit $exitcode

相关内容