Limitar o uso de memória para um único processo Linux

152

Estou correndo pdftoppmpara converter um PDF fornecido pelo usuário em uma imagem 300DPI. Isso funciona muito bem, exceto se o usuário fornecer um PDF com um tamanho de página muito grande. pdftoppmalocará memória suficiente para armazenar uma imagem de 300 DPI desse tamanho na memória, que para uma página quadrada de 100 polegadas é de 100 * 300 * 100 * 300 * 4 bytes por pixel = 3,5 GB. Um usuário mal-intencionado pode me fornecer um PDF bobo e causar todos os tipos de problemas.

Então, o que eu gostaria de fazer é colocar algum tipo de limite rígido no uso da memória para um processo filho que estou prestes a executar - basta interromper o processo se tentar alocar mais do que, digamos, 500 MB de memória. Isso é possível?

Não acho que o ulimit possa ser usado para isso, mas existe um equivalente de um processo?

Ben Dilts
fonte

Respostas:

58

Há alguns problemas com o ulimit. Aqui está uma leitura útil sobre o tópico: Limitando o tempo e o consumo de memória de um programa no Linux , que levam à ferramenta de tempo limite , que permite armazenar um processo (e seus garfos) pelo tempo ou pelo consumo de memória.

A ferramenta de tempo limite requer o Perl 5+ e o /procsistema de arquivos montado. Depois disso, você copia a ferramenta para, por exemplo /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

Depois disso, você pode 'engaiolar' seu processo pelo consumo de memória, como na sua pergunta da seguinte maneira:

timeout -m 500 pdftoppm Sample.pdf

Como alternativa, você pode usar -t <seconds>e, -x <hertz>respectivamente, limitar o processo por tempo ou restrições de CPU.

A maneira como essa ferramenta funciona é verificando várias vezes por segundo se o processo gerado não ultrapassou demais os limites definidos. Isso significa que na verdade existe uma pequena janela em que um processo pode estar com excesso de inscrições antes que o tempo limite seja notado e acabe com o processo.

Portanto, uma abordagem mais correta provavelmente envolveria cgroups, mas isso é muito mais complicado de configurar, mesmo se você usasse o Docker ou o runC, que, entre outras coisas, oferecem uma abstração mais amigável ao cgroups.

kvz
fonte
Parece estar a trabalhar para mim agora (? Novo), mas aqui está a versão cache do Google: webcache.googleusercontent.com/...
KVZ
Podemos usar o tempo limite juntamente com o conjunto de tarefas (precisamos limitar a memória e os núcleos)?
ransh
7
Note-se que esta resposta não se refere ao coreutilsutilitário padrão linux com o mesmo nome! Portanto, a resposta é potencialmente perigosa se, em qualquer lugar do seu sistema, algum pacote tiver um script que espera timeoutser o coreutilspacote padrão do linux ! Não conheço esta ferramenta sendo empacotada para distribuições como o debian.
precisa saber é o seguinte
A -t <seconds>restrição mata o processo depois de tantos segundos?
xxx374562
116

Outra maneira de limitar isso é usar os grupos de controle do Linux. Isso é especialmente útil se você deseja limitar a alocação de memória física de um processo (ou grupo de processos) distintamente da memória virtual. Por exemplo:

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

criará um grupo de controle chamado myGroup, limitará o conjunto de processos executados no myGroup até 500 MB de memória física e até 5000 MB de swap. Para executar um processo no grupo de controle:

cgexec -g memory:myGroup pdftoppm

Observe que em uma distribuição moderna do Ubuntu, este exemplo requer a instalação do cgroup-binpacote e a edição /etc/default/grubpara mudar GRUB_CMDLINE_LINUX_DEFAULTpara:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

e, em seguida, executando sudo update-grube reiniciando para inicializar com os novos parâmetros de inicialização do kernel.

user65369
fonte
3
O firejailprograma também permitirá que você inicie um processo com limites de memória (usando cgroups e namespaces para limitar mais do que apenas memória). Nos meus sistemas, não precisei alterar a linha de comando do kernel para que isso funcionasse!
Ned64
1
Você precisa da GRUB_CMDLINE_LINUX_DEFAULTmodificação para tornar a configuração persistente? Eu encontrei outra maneira de torná-lo persistente aqui .
Stason
Seria útil notar nesta resposta que, em algumas distribuições (por exemplo, Ubuntu), o sudo é necessário para o cgcreate e também para os comandos posteriores, a menos que seja dada permissão ao usuário atual. Isso evitaria que o leitor tivesse que encontrar essas informações em outro lugar (por exemplo, askubuntu.com/questions/345055 ). Sugeri uma edição para esse efeito, mas ela foi rejeitada.
stewbasic 23/07
77

Se o seu processo não gerar mais filhos que consomem mais memória, você poderá usar a setrlimitfunção Interface de usuário mais comum para isso é usar o ulimitcomando do shell:

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

Isso limitará apenas a memória "virtual" do seu processo, levando em consideração - e limitando - a memória que o processo que está sendo chamado compartilha com outros processos e a memória mapeada, mas não reservada (por exemplo, a grande pilha de Java). Ainda assim, a memória virtual é a aproximação mais próxima para processos que crescem muito, tornando os erros mencionados insignificantes.

Se o seu programa gera filhos, e são eles que alocam memória, ele se torna mais complexo e você deve escrever scripts auxiliares para executar processos sob seu controle. Eu escrevi no meu blog, por que e como .

P Shved
fonte
2
por que é setrlimitmais complexo para mais crianças? man setrlimitme diz que "Um processo filho criado através de fork (2) herda seus limites de recursos pais limites de recursos são preservados através execve (2)."
akira
6
Porque o kernel não soma o tamanho da vm para todos os processos filhos; se o fizesse, a resposta seria errada de qualquer maneira. O limite é por processo e é espaço de endereço virtual, não uso de memória. O uso da memória é mais difícil de medir.
21811 MarkRe:
1
se eu entendi a pergunta corretamente, OP qual é o limite por subprocesso (filho) .. não no total.
Akira
De qualquer forma, o @MarkR, o espaço de endereço virtual é uma boa aproximação para a memória usada, especialmente se você executar um programa que não é controlado por uma máquina virtual (por exemplo, Java). Pelo menos não conheço nenhuma métrica melhor.
2
Só queria agradecer - essa ulimitabordagem me ajudou com firefoxo bug 622816 - Carregar uma imagem grande pode "congelar" o firefox ou travar o sistema ; que em uma inicialização USB (a partir da RAM) tende a congelar o sistema operacional, exigindo reinicialização forçada; agora pelo menos firefoxtrava, deixando o sistema operacional vivo ... Saúde!
Sdaau
8

Estou usando o script abaixo, o que funciona muito bem. Ele usa cgroups through cgmanager. Atualização: agora usa os comandos de cgroup-tools. Nomeie esse script limitmeme coloque-o no seu $ PATH e você poderá usá-lo como limitmem 100M bash. Isso limitará o uso de memória e troca. Para limitar apenas a memória, remova a linha com memory.memsw.limit_in_bytes.

edit: Nas instalações padrão do Linux, isso limita apenas o uso de memória, não o uso de swap. Para habilitar a limitação do uso de swap, você precisa habilitar a contabilidade de swap no seu sistema Linux. Fazer isso através da criação / adição swapaccount=1no /etc/default/grubmodo que parece algo como

GRUB_CMDLINE_LINUX="swapaccount=1"

Em seguida, execute sudo update-grube reinicie.

Disclaimer: Eu não ficaria surpreso se cgroup-toolstambém quebrar no futuro. A solução correta seria usar as APIs systemd para gerenciamento de cgroup, mas não há ferramentas de linha de comando para esse 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
JanKanis
fonte
1
call to cgmanager_create_sync failed: invalid requestpara cada processo que eu tento executar limitmem 100M processname. Estou no Xubuntu 16.04 LTS e esse pacote está instalado.
Aaron Franke
Ups, recebo esta mensagem de erro: $ limitmem 400M rstudio limiting memory to 400M (cgroup limitmem_24575) for command rstudio Error org.freedesktop.DBus.Error.InvalidArgs: invalid request alguma ideia?
R: Kiselev
O @RKiselev cgmanager está obsoleto e nem está disponível no Ubuntu 17.10. A API do systemd que ele usa foi alterada em algum momento, então essa é provavelmente a razão. Eu atualizei o script para usar os comandos cgroup-tools.
JanKanis
se o cálculo dos percentresultados for zero, o exprcódigo de status será 1 e esse script será encerrado prematuramente. recomendamos alterar a linha para: percent=$(( "$peak_mem" / $(( "$bytes_limit" / 100 )) ))(ref: unix.stackexchange.com/questions/63166/… )
Willi Ballenthin
como posso configurar o cgroup para matar meu processo se eu ultrapassar o limite?
d9ngle
7

Além das ferramentas daemontoolssugeridas por Mark Johnson, você também pode considerar chpstquais são encontradas em runit. O próprio Runit está incluído no pacote busybox, portanto você já deve ter o instalado.

A página do manualchpst mostra a opção:

-m bytes limitam a memória. Limite o segmento de dados, o segmento de pilha, as páginas físicas bloqueadas e o total de todos os segmentos por processo em bytes de bytes cada.

Oz123
fonte
3

Estou executando o Ubuntu 18.04.2 LTS e o script JanKanis não funciona para mim como ele sugere. A execução limitmem 100M scriptestá limitando 100 MB de RAM com troca ilimitada .

A execução limitmem 100M -s 100M scriptfalha silenciosamente, pois cgget -g "memory:$cgname"não possui nenhum parâmetro chamado memory.memsw.limit_in_bytes.

Então eu desabilitei o 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`
d9ngle
fonte
@sourcejedi adicionou :)
d9ngle
2
Certo, editei minha resposta. Para habilitar limites de troca, você precisa habilitar a contabilidade de troca no seu sistema. Existe uma pequena sobrecarga de tempo de execução para que ele não seja ativado por padrão no Ubuntu. Veja minha edição.
JanKanis
3

Em qualquer distribuição baseada em systemd, você também pode usar os cgroups indiretamente através do systemd-run. Por exemplo, para o seu caso de limitar pdftoppma 500M de RAM, use:

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

Nota: isso solicitará uma senha, mas o aplicativo será iniciado como seu usuário. Não permita que isso o iluda pensando que o comando precisa sudo, porque isso faria com que o comando fosse executado na raiz, o que dificilmente era sua intenção.

Se você não quiser digitar a senha (afinal, como usuário você possui sua memória, por que você precisaria de uma senha para limitá-la) , você pode usar a --useropção; no entanto, para isso funcionar, você precisará do suporte ao cgroupsv2 ativado, o que agora requer a inicialização com o systemd.unified_cgroup_hierarchyparâmetro kernel .

Olá anjo
fonte
Obrigado, fiz o meu dia
Geradlus_RU