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


151

我正在pdftoppm将用户提供的PDF转换为300DPI图像。这非常有效,除非用户提供的PDF页面尺寸很大。 pdftoppm将分配足够的内存以在内存中保存该大小的300DPI图像,对于100平方英寸的页面而言,其大小为100 * 300 * 100 * 300 *每个像素4个字节= 3.5GB。恶意用户可能只会给我一个笨拙的PDF并引起各种问题。

因此,我想对即将运行的子进程的内存使用量设置某种硬性限制-如果该进程尝试分配超过500MB的内存,则该进程就会死掉。那可能吗?

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


也许docker吧?
Sridhar Sarnobat '16

Answers:


58

ulimit存在一些问题。这是关于该主题的有用读物:限制Linux中程序的时间和内存消耗,这会导致超时工具,该工具使您可以按时间或内存消耗来笼罩进程(及其派生)。

超时工具需要Perl 5+和已/proc挂载的文件系统。之后,将工具复制到例如/usr/local/bin

curl https://raw.githubusercontent.com/pshved/timeout/master/timeout | \
  sudo tee /usr/local/bin/timeout && sudo chmod 755 /usr/local/bin/timeout

之后,您可以通过内存消耗来“限制”进程,如下所示:

timeout -m 500 pdftoppm Sample.pdf

或者,您可以使用-t <seconds>-x <hertz>通过时间或CPU限制分别限制进程。

该工具的工作方式是每秒检查多次,如果生成的进程没有超额使用其设置的边界。这意味着实际上存在一个很小的窗口,在此窗口中,进程可能会在超时通知并终止进程之前超额预订。

因此,更正确的方法可能涉及cgroup,但是即使您使用Docker或runC,建立起来也要涉及更多的工作,其中Docker或runC在cgroup周围提供了更加用户友好的抽象。


似乎现在为我工作(再次?),但这是Google缓存版本:webcache.googleusercontent.com/…–
kvz

我们可以将超时与任务集一起使用吗(我们需要限制内存和内核)?
ransh

7
应该注意的是,这个答案不是指coreutils同名的linux标准实用程序!因此,如果您的系统上的任何地方,某些软件包的脚本都有望timeout成为linux标准coreutils软件包,那么答案可能是危险的!我不知道此工具已被打包用于debian等发行版。
user1404316 '18

难道-t <seconds>约束杀死多少秒之后的过程?
xxx374562 '18

116

限制此情况的另一种方法是使用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

将创建一个名为的控制组myGroup,将在myGroup下运行的进程集限制为最多500 MB的物理内存和最多5000 MB的交换。要在控制组下运行流程:

cgexec -g memory:myGroup pdftoppm

请注意,在现代Ubuntu发行版中,此示例需要安装cgroup-bin软件包并进行编辑/etc/default/grub以更改GRUB_CMDLINE_LINUX_DEFAULT为:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

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


3
firejail程序还将允许您使用内存限制来启动进程(使用cgroups和名称空间限制的不仅仅是内存)。在我的系统上,我无需更改内核命令行即可运行!
Ned64

1
是否需要GRUB_CMDLINE_LINUX_DEFAULT修改以使设置持久化?我找到了另一种方法来使它在这里持久存在。
stason


在此答案中指出,在某些发行版(例如,Ubuntu)上,sg是cgcreate所必需的,以后的命令也是必需的,除非已将权限授予当前用户。这将使读者不必在其他地方(例如askubuntu.com/questions/345055)找到此信息。我建议对此进行编辑,但被拒绝。
stewbasic

77

如果您的进程没有产生更多消耗最多内存的子级,则可以使用setrlimit函数。为此,更常见的用户界面是使用ulimitShell命令:

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

这仅会限制并限制您正在调用的进程与其他进程共享的内存,以及已映射但未保留的内存(例如Java的大堆),从而限制了进程的“虚拟”内存。尽管如此,虚拟内存仍然是非常大的进程的最接近近似值,从而使上述错误无关紧要。

如果您的程序产生了子级,而正是这些子级分配了内存,那么它将变得更加复杂,您应该编写辅助脚本以在您的控制下运行进程。我在博客中写道,为什么以及如何


2
为什么setrlimit对更多的孩子来说更复杂?man setrlimit告诉我“通过fork(2)创建的子进程继承了其父级资源限制。在execve(2)中保留了资源限制”
akira

6
因为内核不会汇总所有子进程的虚拟机大小;如果这样做,反正还是会得到错误的答案。限制是每个进程,是虚拟地址空间,而不是内存使用率。内存使用情况很难衡量。
MarkR 2011年

1
如果我正确理解了这个问题,那么OP每个子进程(子进程)的限制是多少。
akira

@MarkR,无论如何,虚拟地址空间是所用内存的一个很好的近似值,尤其是在您运行不受虚拟机控制的程序(例如Java)的情况下。至少我不知道任何更好的指标。

2
只是想说声谢谢-这种ulimit方法帮助我firefox错误622816 -装入大量的图像可以‘冻结’Firefox或系统崩溃 ; 在USB引导(从RAM)引导下,这往往会冻结操作系统,需要重新启动;现在至少会firefox崩溃,从而使操作系统还活着……干杯!
sdaau 2013年

8

我正在使用以下脚本,效果很好。它通过使用cgroups 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的命令行工具

#!/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

1
call to cgmanager_create_sync failed: invalid request对于我尝试运行的每个过程limitmem 100M processname。我使用的是Xubuntu 16.04 LTS,该软件包已安装。
亚伦·弗兰克

Ups,我收到此错误消息:有$ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request 任何想法吗?
R Kiselev '18

@RKiselev cgmanager现在已弃用,甚至在Ubuntu 17.10中也不可用。它使用的systemd api在某些时候已更改,因此这可能是原因。我已经更新了脚本以使用cgroup-tools命令。
JanKanis

如果计算percent结果为零,则expr状态码为1,并且此脚本过早退出。建议将行更改为:percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref:unix.stackexchange.com/questions/63166/…
Willi Ballenthin

如果我超过限制,如何配置cgroup杀死进程?
d9ngle

7

除了daemontoolsMark Johnson建议的工具之外,您还可以考虑chpst在中找到的工具runit。Runit本身捆绑在中busybox,因此您可能已经安装了它。

手册页chpst显示了以下选项:

-m字节限制内存。将数据段,堆栈段,锁定的物理页以及每个进程的所有段的总数限制为每个字节字节。


3

我正在运行Ubuntu 18.04.2 LTS,JanKanis脚本对我不起作用,正如他所建议的那样。运行时limitmem 100M script限制了100MB RAM的无限交换。

limitmem 100M -s 100M script由于cgget -g "memory:$cgname"没有名为的参数,运行无声地失败memory.memsw.limit_in_bytes

所以我禁用了swap:

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

@sourcejedi添加了它:)
d9ngle

2
是的,我编辑了答案。要启用交换限制,您需要在系统上启用交换记帐。这样做的运行时开销很小,因此在Ubuntu上默认未启用。看到我的编辑。
JanKanis

2

在任何基于systemd的发行版上,您也可以通过systemd-run间接使用cgroup。例如,对于限制pdftoppm为500M RAM的情况,请使用:

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

注意:这会要求您输入密码,但该应用会以您的用户身份启动。不要让这使您误以为该命令需要sudo,因为这会使该命令在root下运行,这几乎不是您的意图。

如果您不想输入密码(毕竟,作为用户,您拥有自己的内存,为什么需要密码来限制密码),则可以使用--useroption,但是要使其正常工作,您需要启用cgroupsv2支持,该权利现在需要使用systemd.unified_cgroup_hierarchy内核参数启动

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.