7

Whilst writing this answer to another Q&A here, I noticed that the timing in the zsh solution that uses select() to sleep for a given number of centiseconds was off by an amount larger than what I was expecting.

It seems to be down to select() on my system whose timeout seems to have a consistent drift as if its seconds were worth 1.001 normal seconds.

$ strace -qqTe /select zsh -c 'zmodload zsh/zselect; zselect -t 6000'
pselect6(0, 0x7ffcee6a9f60, 0x7ffcee6a9fe0, 0x7ffcee6aa060, {tv_sec=60, tv_nsec=0}, NULL) = 0 (Timeout) <60.058930>
$ strace -qqTe /select zsh -c 'zmodload zsh/zselect; zselect -t 600'
pselect6(0, 0x7ffcf2042b70, 0x7ffcf2042bf0, 0x7ffcf2042c70, {tv_sec=6, tv_nsec=0}, NULL) = 0 (Timeout) <6.006212>
$ uname -rs
Linux 6.7.12-amd64

See how it takes 6.006 seconds with a 6 second timeout and 60.06 for a 60 second timeout.

I observe the same on different hardware, and Ubuntu 22.04 with Linux 6.5.0.

Same with perl instead of zsh:

$ strace -qqTe /select perl -e 'select undef,undef,undef,60'
pselect6(0, NULL, NULL, NULL, {tv_sec=60, tv_nsec=0}, NULL) = 0 (Timeout) <60.060218>

And the same with poll() instead of select():

$ strace -qqTe /poll perl -MIO::Poll -e 'IO::Poll->new()->poll(6)'
poll([], 0, 6000)                       = 0 (Timeout) <6.006215>
$ strace -qqTe /poll perl -MIO::Poll -e 'IO::Poll->new()->poll(60)'
poll([], 0, 60000)                      = 0 (Timeout) <60.060214>

That doesn't seem to apply to clock_nanosleep() or alarm().

Is that a known issue? Is that even possibly intentional?

0

1 Answer 1

9

This is intentional, although not documented as part of the kernel interface. select timeouts have some slack, currently¹ 0.1% of the timeout for regular tasks, with a cap of 100 msec (0.5% for “nice” tasks):

/*
 * Estimate expected accuracy in ns from a timeval.
 *
 * After quite a bit of churning around, we've settled on
 * a simple thing of taking 0.1% of the timeout as the
 * slack, with a cap of 100 msec.
 * "nice" tasks get a 0.5% slack instead.
 *
 * Consider this comment an open invitation to come up with even
 * better solutions..
 */

The slack is used when scheduling the timeout; it defines how much leeway the kernel has to move the wakeup back for power or performance reasons.

This slack isn’t quite the maximum delay you’ll see on select, since time is spent before the timer is set and after the task is woken up; but it should be close to the upper bound:

$ for i in {1..10}; do strace -qqTe /select zsh -c 'zmodload zsh/zselect; zselect -t 6000'; done
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.060082>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.058872>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.058342>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.038167>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.037595>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.059775>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.058236>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.060196>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.057481>
select(0, …, {tv_sec=60, tv_usec=0}) = 0 (Timeout) <60.059292>

If a task has a higher timer slack (/proc/pid/timerslack_ns) than the calculated select slack, that’s used instead:

$ for i in {1..10}; do strace -qqTe /select zsh -c 'echo 10000000 | sudo tee /proc/$$/timerslack_ns > /dev/null; zmodload zsh/zselect; zselect -t 600'; done 2>&1 | grep select
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.006330>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.006650>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.010218>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.007780>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.004928>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.006641>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.009273>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.010185>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.010173>
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.006746>

The only way to reduce the slack is to make the task real-time:

$ sudo strace -qqTe /select chrt 99 zsh -c 'zmodload zsh/zselect; zselect -t 600'
select(0, …, {tv_sec=6, tv_usec=0}) = 0 (Timeout) <6.000141>

¹ Since 2.6.28, in 2008…

6
  • 🤦D'oh. I see, you can change that to increase that slack (up to 100ms), not decrease it. Commented Jun 21 at 14:54
  • And indeed, after echo 100000000 > /proc/self/timerslack_ns, a select() with 1 second timeout can take up to 1.1 seconds. Commented Jun 21 at 14:56
  • 1
    You can increase the delay past 100ms, the 100ms cap doesn’t apply if the slack is set using timerslack_ns. Commented Jun 21 at 14:59
  • Indeed, I can make a select() with one second timeout last hours by raising that to a very high value. I wonder if that could cause security vulnerabilities, I see the value is preserved after executing a setuid/setgid executable. Commented Jun 21 at 15:05
  • 1
    @Olivier this isn’t a sleep call, it’s a select timeout; as such it’s a grace period, and usually if you ask for a given grace period you want it to be at least that (i.e. if you want to give something 5s extra to respond, it might matter if it’s less than 5s, but not if it’s more). You could specify a 0.999s (not 0.994) to get at most 1s with slack (0.999 × 1.001), but that will result in timeouts between 0.999s and 1s, not always 1s. Commented Jun 22 at 14:46

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .