mercredi 15 juin 2016

`ulimit -t` can often simply be ignored / useless unportable mechanism?

Update: This is becoming less of a question and more of a summary. Oh well...

bash, dash, and zsh all come with a builtin command ulimit. Each has an option -t which accepts a number as an argument, to be understood as CPU time in seconds that processes may consume. Thereafter, they will be sent a signal. So much is clear.

There's a lot that is not clear, though. And I find some of it rather unexpected. In particular, what behaviour you get depends both on the shell and the underlying operating system. I've created a table that summarises the extent of the variability. I'm also including the code for a script that I used to automatically obtain those results. The last test needs root privileges and can be kept from running if you comment out 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=$1 '
            { 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, "nonen");
        exit(0);
      case 1:
        fprintf(stderr, "onen");
        exit(0);
      default:
        fprintf(stderr, "multiplen");
        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=$1
    out2=''
    for ((i=0; i < ${#out1}; ++i)); do
        out2+='='
    done
    echo $out1
    echo $out2
}


test_shell() {
    shell=$1
    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=$1
    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

Aucun commentaire:

Enregistrer un commentaire