Ограничить процессорное время группы процессов

Есть ли способ ограничить абсолютное время ЦП (в секундах ЦП), затрачиваемое в группе процессов?

ulimit -t 10; ./my-process выглядит как хороший вариант, но если my-process разветвляется, то каждый процесс в группе процессов получает свое ограничение. Вся группа процессов может использовать произвольное количество времени, разветвляясь каждые 9 секунд.

Принятый ответ на похожий вопрос заключается в использовании cgroups, но не объясняет как. Однако есть и другие ответы (Ограничить общее использование ЦП с помощью cgroups) говоря, что это невозможно в cgroups и может быть ограничено только относительное использование процессора (например, 0,2 секунды из каждой 1 секунды).

Лиран Фунаро предложил использовать длительный период для cpu.cfs_period_us (https://stackoverflow.com/a/43660834/892961), но параметр для квоты может быть не более 1 секунды. Поэтому даже с большим периодом я не вижу, как установить ограничение времени ЦП в 10 секунд или час.

Если ulimit и контрольные группы не могут этого сделать, есть ли другой способ?


person Flogo    schedule 07.08.2017    source источник


Ответы (3)


вы можете сделать это с cgroups. Делайте как root:

# Create cgroup
cgcreate -g cpu:/limited

# set shares (cpu limit)
cgset -r cpu.shares=256 limited

# run your program
cgexec -g cpu:limited /my/hungry/program

В качестве альтернативы вы можете использовать программу cpulimit, которая может периодически замораживать ваш код. cgroups - самый продвинутый метод.

установить фиксированную долю процессора:

cgcreate -g cpu:/fixedlimit
# allow fix 25% cpu usage (1 cpu)
cgset -r cpu.cfs_quota_us=25000,cpu.cfs_period_us=100000 fixedlimit
cgexec -g cpu:fixedlimit /my/hungry/program

Оказалось, что цель состоит в том, чтобы ограничить время выполнения определенными секундами при его измерении. После установки желаемых ограничений cgroup (чтобы получить честную песочницу) вы можете достичь этой цели, выполнив:

((time -p timeout 20 cgexec -g cpu:fixedlimit /program/to/test ) 2>&1) | grep user

Через 20 секунд программа будет остановлена, несмотря ни на что, и мы сможем проанализировать пользовательское время (или системное, или реальное время), чтобы оценить ее производительность.

person goteguru    schedule 07.08.2017
comment
Согласно документации shares является относительной мерой. Поэтому, если я хочу ограничить свою программу 10 секундами ЦП, установка cpu.shares=10 не уничтожит ее через 10 секунд. Это только гарантирует, что моя программа получит 10% процессорного времени программы с cpu.shares=100. Я неправильно это понимаю? - person Flogo; 07.08.2017
comment
Конечно. Я ориентировался на часть «как» :) Конечно, вы можете использовать пару cpu.cfs_quota_us + cpu.cfs_period_us, если это лучше соответствует вашим потребностям. - person goteguru; 08.08.2017
comment
Если это не то, что вам нужно, не могли бы вы немного уточнить свою основную цель? Чего бы вы хотели достичь? - person goteguru; 08.08.2017
comment
Я хотел бы иметь абсолютный лимит времени ЦП, так как при запуске этой программы в течение 10 секунд, а не относительный, например, дать этой программе 50% доступного времени ЦП. Основная цель - оценить соревнование по программированию. У вашей программы есть 10 секунд на ЦП, чтобы решить эту проблему, вы можете сделать это вовремя? Я мог бы запускать каждую программу дольше (скажем, 20 секунд времени настенных часов) и затем измерять/проверять, но это кажется очень косвенным и может привести к проблемам, когда процесс прерывается более чем на 10 секунд, чтобы дождаться занятого жесткого диска. - person Flogo; 08.08.2017
comment
А, теперь я вижу. Но зачем вам использовать время настенных часов? просто измерьте задачу с помощью time и проверьте время пользователя. Если это меньше 10 секунд, она сделала это. Но я не думаю, что это честный тест. Очень зависит от железа. Возможно, вы захотите оценить некоторые циклы измерения vm. - person goteguru; 08.08.2017
comment
Суть ограничения по времени заключается в остановке задач, которые выполняются слишком долго. time дает измерение только после завершения программы. Программы могут реально работать в течение нескольких лет процессора в этом соревновании, если мы не остановим их в какой-то момент. Или ошибка может привести к тому, что программа войдет в бесконечный цикл, и она никогда не остановится. Что касается справедливости: на самом деле вопрос не в том, хороши ли правила конкурса в том виде, в каком они есть, но я думаю, что они достаточно справедливы. Все работает на одном и том же оборудовании, и мы сравниваем результаты только друг с другом, а не с результатами экспериментов на другом оборудовании. - person Flogo; 08.08.2017
comment
Оберните их в тайм-аут тогда. Вот так: time timeout 20 /program/to/test код в любом случае будет уничтожен через 20 секунд, и вы сможете измерить время пользователя (время пользователя будет исключать задержку диска, поэтому ваше измерение будет справедливым на том же оборудовании, что и предполагалось). - person goteguru; 08.08.2017
comment
@Flogo Пожалуйста, укажите в ответе ваши фактические потребности (например, конкуренция). Таким образом, мы могли бы дать лучший ответ, который соответствует вашим потребностям (см. мой ответ ниже). - person Liran Funaro; 02.01.2019

Это не дает прямого ответа на вопрос, но относится к обсуждению фактической необходимости ОП.

Если ваши конкуренты игнорируют все, кроме процессорного времени, это может быть фундаментально ошибочным. Можно просто, например, кэшировать результаты на первичном запоминающем устройстве. Поскольку вы не считаете время доступа к хранилищу, оно может иметь наименьшее количество циклов ЦП, но худшую реальную производительность. Совершенным преступлением было бы просто отправить данные через Интернет на другой компьютер, который вычисляет задачу и возвращает ответ. Это завершит задачу с тем, что кажется нулевым циклом. На самом деле вы хотите измерить «реальное» время и дать этому процессу наивысший приоритет в вашей системе (или фактически запустить его в уединении).

При проверке домашних заданий студентов мы просто использовали нереалистичный лимит времени (например, 5 минут на то, что должно быть 10-секундной программой), затем убивали процесс, если он не завершился вовремя, и провалили эту отправку.

Если вы хотите выбрать победителя, просто повторно запустите лучших участников несколько раз, чтобы убедиться в достоверности их результатов.

person Liran Funaro    schedule 02.01.2019
comment
Спасибо за ответ. Конкурс уже завершен, так что это на всякий случай, если кто-то найдет это с похожей проблемой. Мы ограничили количество и размер файлов, записываемых программами, и запретили доступ в интернет. У нас также был более высокий лимит времени настенных часов (см. мой ответ), и мы рассмотрели случаи, когда прогон достиг предела настенных часов, а не ограничения времени процессора. Хотя в нашем случае ограничения были жестче, потому что запуск всех конкурентов занимал около недели на кластере из 400 ЦП, поэтому мы не могли позволить себе 30-кратное увеличение лимита (5 минут вместо 10 с) или повторный запуск нескольких конкурентов. - person Flogo; 03.01.2019

Я нашел решение, которое работает для меня. Он все еще далек от совершенства (прочитайте предостережения перед его использованием). Я новичок в написании сценариев bash, поэтому любые комментарии по этому поводу приветствуются.

#!/bin/bash
#
# This script tries to limit the CPU time of a process group similar to
# ulimit but counting the time spent in spawned processes against the
# limit. It works by creating a temporary cgroup to run the process in
# and checking on the used CPU time of that process group. Instead of
# polling in regular intervals, the monitoring process assumes that no
# time is lost to I/O (i.e., wall clock time = CPU time) and checks in
# after the time limit. It then updates its assumption by comparing the
# actual CPU usage to the time limit and waiting again. This is repeated
# until the CPU usage exceeds its limit or the monitored process
# terminates. Once the main process terminates, all remaining processes
# in the temporary cgroup are killed.
#
# NOTE: this script still has some major limitations.
# 1) The monitored process can exceed the limit by up to one second
#    since every iteration of the monitoring process takes at least that
#    long. It can exceed the limit by an additional second by ignoring
#    the SIGXCPU signal sent when hitting the (soft) limit but this is
#    configurable below.
# 2) It assumes there is only one CPU core. On a system with n cores
#    waiting for t seconds gives the process n*t seconds on the CPU.
#    This could be fixed by figuring out how many CPUs the process is
#    allowed to use (using the cpuset cgroup) and dividing the remaining
#    time by that. Since sleep has a resolution of 1 second, this would
#    still introduce an error of up to n seconds.


set -e

if [ "$#" -lt 2 ]; then
    echo "Usage: $(basename "$0") TIME_LIMIT_IN_S COMMAND [ ARG ... ]"
    exit 1
fi
TIME_LIMIT=$1
shift

# To simulate a hard time limit, set KILL_WAIT to 0. If KILL_WAIT is
# non-zero, TIME_LIMIT is the soft limit and TIME_LIMIT + KILL_WAIT is
# the hard limit.
KILL_WAIT=1

# Update as necessary. The script needs permissions to create cgroups
# in the cpuacct hierarchy in a subgroup "timelimit". To create it use:
#   sudo cgcreate -a $USER -t $USER -g cpuacct:timelimit
CGROUPS_ROOT=/sys/fs/cgroup
LOCAL_CPUACCT_GROUP=timelimit/timelimited_$$
LOCAL_CGROUP_TASKS=$CGROUPS_ROOT/cpuacct/$LOCAL_CPUACCT_GROUP/tasks

kill_monitored_cgroup() {
    SIGNAL=$1
    kill -$SIGNAL $(cat $LOCAL_CGROUP_TASKS) 2> /dev/null
}

get_cpu_usage() {
    cgget -nv -r cpuacct.usage $LOCAL_CPUACCT_GROUP
}

# Create a cgroup to measure the CPU time of the monitored process.
cgcreate -a $USER -t $USER -g cpuacct:$LOCAL_CPUACCT_GROUP


# Start the monitored process. In case it fails, we still have to clean
# up, so we disable exiting on errors.
set +e
(
    set -e
    # In case the process doesn't fork a ulimit is more exact. If the
    # process forks, the ulimit still applies to each child process.
    ulimit -t $(($TIME_LIMIT + $KILL_WAIT))
    ulimit -S -t $TIME_LIMIT
    cgexec -g cpuacct:$LOCAL_CPUACCT_GROUP --sticky $@
)&
MONITORED_PID=$!

# Start the monitoring process
(
    REMAINING_TIME=$TIME_LIMIT
    while [ "$REMAINING_TIME" -gt "0" ]; do
        # Wait $REMAINING_TIME seconds for the monitored process to
        # terminate. On a single CPU the CPU time cannot exceed the
        # wall clock time. It might be less, though. In that case, we
        # will go through the loop again.
        sleep $REMAINING_TIME
        CPU_USAGE=$(get_cpu_usage)
        REMAINING_TIME=$(($TIME_LIMIT - $CPU_USAGE / 1000000000))
    done

    # Time limit exceeded. Kill the monitored cgroup.
    if  [ "$KILL_WAIT" -gt "0" ]; then
        kill_monitored_cgroup XCPU
        sleep $KILL_WAIT
    fi
    kill_monitored_cgroup KILL
)&
MONITOR_PID=$!

# Wait for the monitored job to exit (either on its own or because it
# was killed by the monitor).
wait $MONITORED_PID
EXIT_CODE=$?

# Kill all remaining tasks in the monitored cgroup and the monitor.
kill_monitored_cgroup KILL
kill -KILL $MONITOR_PID 2> /dev/null
wait $MONITOR_PID 2>/dev/null

# Report actual CPU usage.
set -e
CPU_USAGE=$(get_cpu_usage)
echo "Total CPU usage: $(($CPU_USAGE / 1000000))ms"

# Clean up and exit with the return code of the monitored process.
cgdelete cpuacct:$LOCAL_CPUACCT_GROUP
exit $EXIT_CODE
person Flogo    schedule 10.08.2017