`ulimit -t` 在 shell 和操作系统之间完全不可移植?

`ulimit -t` entirely unportable between shells and OSs?

更新:这不再是一个问题,而是一个总结。好吧...

bash、dash 和 zsh 都带有内置命令 ulimit。每个都有一个选项 -t,它接受一个数字作为参数,可以理解为进程可能消耗的 CPU 时间(以秒为单位)。此后,他们将收到一个信号。这么多就清楚了。

不过还有很多不清楚的地方。我发现其中一些相当出乎意料。具体来说,您获得的行为取决于 shell 和底层操作系统。我创建了一个 table 来总结可变性的程度。我还包含了用于自动获取这些结果的脚本代码。最后一个测试需要 root 权限,如果你注释掉 test_shell_sudo $shell.

可以从 运行 中保留
|                                              | Darwin/zsh | Darwin/bash | FreeBSD/zsh | FreeBSD/bash | FreeBSD/dash | Linux/zsh  | Linux/bash  | Linux/dash  |
| ulimit -t sets                               | soft limit | both limits | soft limit  | both limits  | both limits  | soft limit | both limits | both limits |
| ulimit -t gets                               | soft limit | soft limit  | soft limit  | soft limit   | soft limit   | soft limit | soft limit  | soft limit  |
| Hard limits can be set below the soft limit  | yes        | no          | yes         | yes          | yes          | yes        | no          | no          |
| Soft limits can be set above the hard limit  | yes        | no          | yes         | no           | no           | yes        | no          | no          |
| Hard limits can be raised without privileges | yes        | no          | yes         | no           | no           | yes        | no          | no          |
| soft signal                                  | SIGXCPU    | SIGXCPU     | SIGXCPU     | SIGXCPU      | SIGXCPU      | SIGXCPU    | SIGXCPU     | SIGXCPU     |
| hard signal                                  | SIGXCPU    | SIGXCPU     | SIGKILL     | SIGKILL      | SIGKILL      | SIGKILL    | SIGKILL     | SIGKILL     |
| Number of SIGXCPUs sent                      | one        | one         | one         | one          | one          | multiple   | multiple    | multiple    |
| Raising soft beyond hard limit raises it     | yes        | impossible* | yes         | no           | no           | yes        | impossible* | impossible* |

* even as root
#!/usr/bin/env bash

get_sigcode() {
    /bin/kill -l |
        tr '\n[a-z]' ' [A-Z]' |
        awk -v name= '
            { for (i=1; i<=NF; ++i) if ($i == name) print i }'
}

create_runner() {
    cat > sig.c <<'EOF'
#include <stdlib.h>
#include <stdio.h>

int
main()
{
  int runs = 0;
  double x = 0.0;
  for (;;runs++) {
    x += (double)rand() / RAND_MAX;
    if (x >= 1e7) {
      printf("Took %d iterations to reach 1000.\n", runs);
      x = 0.0;
      runs = 0;
    }
  }
  return 0;
}
EOF
    cc sig.c -o sig
    rm -f sig.c
    echo Successfully compiled sig.c
}

create_counter() {
    cat > sigcnt.c <<'EOF'
#include <stdatomic.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

sig_atomic_t sig_received;
void handle_signal(int signum) {
  sig_received = signum;
}

int
main()
{
  signal(SIGXCPU, handle_signal);

  int sigxcpu_cnt = 0;
  time_t start, now;
  time(&start);

  int runs = 0;
  double x = 1;
  for (;;) {
    if (sig_received == SIGXCPU) {
      sigxcpu_cnt++;
      sig_received = 0;
    }
    time(&now);
    if (now - start > 5) {
      switch (sigxcpu_cnt) {
      case 0:
        fprintf(stderr, "none\n");
        exit(0);
      case 1:
        fprintf(stderr, "one\n");
        exit(0);
      default:
        fprintf(stderr, "multiple\n");
        exit(0);
      }
    }

    // Do something random that eats CPU (sleeping is not an option)
    x += (double)rand() / RAND_MAX;
    if (x >= 1e7) {
      printf("Took %d iterations to reach 1000.\n", runs);
      x = 0.0;
      runs = 0;
    }
  }
}
EOF
    cc sigcnt.c -o sigcnt
    rm -f sigcnt.c
    echo Successfully compiled sigcnt.c
}

echo_underscored() {
    out1=
    out2=''
    for ((i=0; i < ${#out1}; ++i)); do
        out2+='='
    done
    echo $out1
    echo $out2
}


test_shell() {
    shell=
    echo_underscored "Testing shell: $shell"

    f() {
        $shell -c 'ulimit -St 3; ulimit -t 2; ulimit -Ht; ulimit -St' | tr -d '\n'
    }
    case `f` in
        22)
            t_sets='both limits';;
        unlimited2)
            t_sets='soft limit';;
        *)
            echo UNEXPECTED;;
    esac
    echo "ulimit -t sets: ${t_sets}"

    f() {
        $shell -c 'ulimit -St 3; ulimit -Ht 4; ulimit -St 3; ulimit -t'
    }
    case `f` in
        3)
            t_gets='soft limit';;
        *)
            echo UNEXPECTED;;
    esac
    echo "ulimit -t gets: ${t_gets}"

    f() {
        $shell -c 'ulimit -St 2; ulimit -Ht 1' >/dev/null 2>&1 &&
            echo yes || echo no
    }
    ht_can_set_below_soft=`f`
    echo "Hard limits can be set below the soft limit: ${ht_can_set_below_soft}"

    f() {
        $shell -c 'ulimit -St 1; ulimit -Ht 2; ulimit -St 3' >/dev/null 2>&1 &&
            echo yes || echo no
    }
    st_can_set_above_hard=`f`
    echo "Soft limits can be set above the hard limit: ${st_can_set_above_hard}"

    f() {
        $shell -c 'ulimit -St 1; ulimit -Ht 2; ulimit -Ht 3' >/dev/null 2>&1 &&
            echo yes || echo no
    }
    hard_can_be_raised=`f`
    echo "Hard limits can be raised without privileges: ${hard_can_be_raised}"

    f() {
        $shell -c 'ulimit -St 1; ./sig' >/dev/null 2>&1
        echo $?
    }
    case $((`f` - 128)) in
        ${sigxcpu})
            soft_signal=SIGXCPU;;
        ${sigkill})
            soft_signal=SIGKILL;;
        *)
            echo UNEXPECTED;
    esac
    echo "soft signal: ${soft_signal}"

    f() {
        $shell -c 'ulimit -St 1; ulimit -Ht 1; ./sig' >/dev/null 2>&1
        echo $?
    }
    case $((`f` - 128)) in
        ${sigxcpu})
            hard_signal=SIGXCPU;;
        ${sigkill})
            hard_signal=SIGKILL;;
        *)
            echo UNEXPECTED;;
    esac
    echo "hard signal: ${hard_signal}"

    f() {
        $shell -c 'ulimit -St 1; ./sigcnt 2>&1 >/dev/null'
    }
    sigxcpus_sent=`f`
    echo "Number of SIGXCPUs sent: ${sigxcpus_sent}"
}

test_shell_sudo() {
    shell=
    echo_underscored "Testing shell with sudo: $shell"

    f() {
        sudo $shell -c 'ulimit -St 1; ulimit -Ht 1; ulimit -St 2 && ulimit -Ht' \
            2>/dev/null;
    }
    out=`f`; ret=$?;
    if [[ $ret == 0 ]]; then
        case $out in
            1)
                raising_soft_beyond_hard='no';;
            2)
                raising_soft_beyond_hard='yes';;
            *)
                echo UNEXPECTED;;
        esac
    else
        raising_soft_beyond_hard='impossible'
    fi
    echo "Raising soft beyond hard limit raises it: ${raising_soft_beyond_hard}"
}

main() {
    echo "Testing on platform: $(uname)"

    sigxcpu=$(get_sigcode XCPU)
    sigkill=$(get_sigcode KILL)
    echo Number of signal SIGXCPU: ${sigxcpu}
    echo Number of signal SIGKILL: ${sigkill}

    create_runner
    create_counter
    echo

    for shell in zsh bash dash; do
        which $shell >/dev/null || continue;
        test_shell $shell
        echo
    done

    for shell in zsh bash dash; do
        which $shell >/dev/null || continue;
        test_shell_sudo $shell
        echo
    done
}

main

corresponding gist 还带有更好的 table。

首先,这里是ulimits的绝对规则,包括shells在内的所有进程都被限制为:

  • 任何人都可以降低自己的硬限制。
  • 提高硬限制需要特殊权限。
  • 软限制可以上调和下调,只要它小于硬限制。

考虑到这一点:

  1. Should I be able to raise the limit set by an earlier call to ulimit again?

软限制,是的。硬限制,没有。

bash appears to think no whereas zsh thinks yes.

Bash 默认设置硬限制。 zsh默认设置软限制。

Zsh 记录了这一点,但 bash 没有。无论如何,strace 告诉所有:

$ strace -e setrlimit zsh -c 'ulimit -t 1'
setrlimit(RLIMIT_CPU, {rlim_cur=1, rlim_max=RLIM64_INFINITY}) = 0

$ strace -e setrlimit bash -c 'ulimit -t 1'
setrlimit(RLIMIT_CPU, {rlim_cur=1, rlim_max=1}) = 0
  1. What signals will I be sent?

如果您超过软 CPU 限制,您 will receive a SIGXCPU。 POSIX 中未定义之后发生的事情。根据其手册页,Linux 将每秒重新发送 SIGXCPU 直到达到硬限制,此时您将被 SIGKILL。

Do I get a grace period?

您可以通过设置软限制来选择您自己的宽限期。

警告:

zsh 上,设置硬限制而不同时设置软限制将导致限制应用于儿童而不是 shell:

zsh% ulimit -H -t 1
zsh% ( while true; do true; done )   # is a child, soon killed
zsh% while true; do true; done       # not a child, never dies

如果您同时设置两个限制,它们将应用于当前 shell,如 bash:

zsh% ulimit -SH -t 1
zsh% while true; do true; done       # will now die, just like bash

我不知道这背后的原理是什么。