限制进程组的时间CPU

Limit CPU time of process group

有没有办法限制在进程组中花费的绝对 CPU 时间(在 CPU 秒内)?

ulimit -t 10; ./my-process 看起来是个不错的选择,但如果 my-process 分叉,则进程组中的每个进程都有自己的限制。整个进程组可以通过每 9 秒分叉一次来使用任意数量的时间。

similar question is to use cgroups but doesn't explain how. However, there are other answers () 上接受的答案说这在 cgroups 中是不可能的,只能限制相对 cpu 的使用(例如,每 1 秒中的 0.2 秒)。

Liran Funaro 建议使用较长的 cpu.cfs_period_us () 时间段,但配额的参数最多为 1 秒。因此,即使时间很长,我也看不出如何将 CPU 时间限制设置为 10 秒或一个小时。

如果ulimit和cgroups不能做到这一点,还有别的办法吗?

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

我找到了适合我的解决方案。它仍然远非完美(使用前请阅读注意事项)。我对 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 "[=10=]") TIME_LIMIT_IN_S COMMAND [ ARG ... ]"
    exit 1
fi
TIME_LIMIT=
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=
    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

这不是直接回答问题,而是参考OP实际需要的讨论。

如果您的比赛忽略了除 CPU 时间之外的所有内容,则它可能存在根本性缺陷。例如,可以简单地将结果缓存在主存储设备中。由于您不计算存储访问时间,它可能具有最少 CPU 个周期,但实际性能更差。 一个完美的犯罪是简单地通过互联网将数据发送到另一台计算机,计算任务然后 return 答案。这将以看似零循环的方式完成任务。 你实际上想要测量 "real" 时间并在你的系统中给这个进程最高的优先级(或者实际上 运行 秘密地设置它)。

在检查学生的作业时,我们简单地使用了一个不切实际的时间限制(例如,一个应该是 10 秒的程序是 5 分钟),然后如果它没有及时完成并且提交失败则终止进程。

如果您想选出获胜者,只需多次重新运行最好的参赛者,以确保他们结果的有效性。