限制进程组的CPU时间

时间:2017-08-07 20:27:47

标签: linux time limit ulimit cgroups

有没有办法限制进程组中的绝对CPU时间(以CPU秒为单位)?

my-process看起来是一个不错的选择,但如果cpu.cfs_period_us分叉,那么进程组中的每个进程都会获得自己的限制。整个过程组可以每隔9秒钟使用任意一段时间。

similar question上接受的答案是使用cgroup但不解释如何。但是,还有其他答案(Limit total CPU usage with cgroups)说这在cgroup中是不可能的,并且只能限制相对cpu的使用(例如,每1秒0.2秒)。

Liran Funaro建议长时间使用ulimithttps://stackoverflow.com/a/43660834/892961),但配额的参数最多可以为1秒。因此,即使是很长一段时间,我也不知道如何设置10秒或1小时的CPU时间限制。

如果.和cgroups无法做到这一点,还有其他办法吗?

3 个答案:

答案 0 :(得分:2)

你可以用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是最先进的方法。

设置固定的cpu份额:

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秒后程序将被停止,无论如何,我们可以解析用户时间(或系统或实时)来评估它的性能。

答案 1 :(得分:1)

这不是直接回答问题,而是指有关OP实际需求的讨论。

如果您的竞争者忽略了除CPU时间以外的所有内容,则可能存在根本缺陷。例如,可以简单地将结果缓存到主存储设备中。由于不计算存储访问时间,因此它可能具有最少的CPU周期,但实际性能较差。 完美的作法是简单地通过Internet将数据发送到另一台计算机,该计算机计算任务然后返回答案。这将以零周期完成任务。 您实际上是想测量“实时”时间,并将此过程分配给系统中最高优先级(或实际上秘密地运行)。

在检查学生的作业时,我们只是使用了一个不切实际的时间限制(例如,应将10分钟的程序设置为5分钟),然后在未及时完成的​​情况下终止该过程,并导致提交失败。

如果您想选择一个获胜者,则只需多次重新运行最佳竞争对手以确保其结果的有效性。

答案 2 :(得分:0)

我找到了一个适合我的解决方案。它仍然远非完美(在使用之前阅读警告)。我对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