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

137

Estou executando pdftoppm para converter um PDF fornecido pelo usuário em uma imagem de 300 DPI. Isso funciona muito bem, exceto se o usuário fornecer um PDF com um tamanho de página muito grande. pdftoppm alocará 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 poderia me dar um PDF grande e 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 de memória para um processo filho que estou prestes a executar - apenas faça o processo morrer se ele tentar alocar mais do que, digamos, 500MB de memória. Isso é possível?

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

    
por Ben Dilts 13.02.2011 / 09:00

5 respostas

53

Existem alguns problemas com o ulimit. Aqui está uma leitura útil sobre o tema: Limitando o tempo e consumo de memória de um programa no Linux , que leva à ferramenta timeout , que permite bloquear um processo (e seus garfos) tempo ou consumo de memória.

A ferramenta de tempo limite requer o Perl 5+ e o sistema de arquivos /proc montado. Depois disso, você copia a ferramenta para, por exemplo, /usr/local/bin assim:

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 "bloquear" seu processo pelo consumo de memória, assim como na pergunta:

timeout -m 500 pdftoppm Sample.pdf

Como alternativa, você pode usar -t <seconds> e -x <hertz> para limitar, respectivamente, 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 tiver excedido os limites definidos. Isso significa que há, na verdade, uma pequena janela em que um processo poderia potencialmente ser superinscrito antes que o tempo limite seja percebido e elimine o processo.

Uma abordagem mais correta, portanto, provavelmente envolveria cgroups, mas isso é muito mais complicado de configurar, mesmo se você usasse o Docker ou o runC, o que, entre outras coisas, oferece uma abstração mais amigável ao usuário em relação aos cgroups. >     
por 30.11.2011 / 10:42
98

Outra maneira de limitar isso é usar os grupos de controle do Linux. Isso é especialmente útil se você quiser 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 $(( 500 * 1024 * 1024 )) > /sys/fs/cgroup/memory/myGroup/memory.limit_in_bytes
echo $(( 5000 * 1024 * 1024 )) > /sys/fs/cgroup/memory/myGroup/memory.memsw.limit_in_bytes

criará um grupo de controle chamado myGroup , limitará o conjunto de processos executados em myGroup até 500 MB de memória física e até 5000 MB de troca. 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 pacote cgroup-bin e a edição /etc/default/grub para alterar GRUB_CMDLINE_LINUX_DEFAULT para:

GRUB_CMDLINE_LINUX_DEFAULT="cgroup_enable=memory swapaccount=1"

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

    
por 16.04.2014 / 10:36
69

Se o seu processo não gerar mais filhos que consumam mais memória, você pode usar setrlimit função. Interface de usuário mais comum para isso é usar o comando ulimit do shell:

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

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

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

    
por 13.02.2011 / 09:11
6

Além das ferramentas de daemontools , sugeridas por Mark Johnson, você também pode considerar chpst , que é encontrado em runit . O próprio Runit é empacotado em busybox , então você pode já tê-lo instalado.

A página man de chpst mostra a opção:

-m bytes limit memory. Limit the data segment, stack segment, locked physical pages, and total of all segment per process to bytes bytes each.

    
por 09.07.2015 / 14:26
5

Estou usando o script abaixo, que funciona muito bem. Utiliza cgroups através de cgmanager . Actualização: agora usa os comandos de cgroup-tools . Nomeie este script limitmem e coloque-o no seu $ PATH e pode use-o como limitmem 100M bash . Isso limitará a memória e o uso de troca. Para limitar apenas a memória, remova a linha com memory.memsw.limit_in_bytes .

Isenção de responsabilidade: eu não ficaria surpreso se cgroup-tools também quebrar no futuro. A solução correta seria usar as APIs do systemd para o gerenciamento de cgroups, mas não há ferramentas de linha de comando para essa mensagem.

#!/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
    
por 26.04.2016 / 14:53