4

Could I trap a 5-minute signal inside my script? I imagine something like this,

function dosomething {
   echo "It's been 5 minutes."
}

trap dosomething SIGNAL-EVERY-5-MINUTES

while true
do
    sleep 10
done

Note that this example doesn't have sense. It's just to ask if it's possible to trap something every x time.


EDIT: Based on @vinc17 answer, I've noticed that sleep command from the main shell isn't interrupted by USR1 signal, and that's what I want to do.

#!/bin/bash
time=10

(
  set -e
  while true
  do
    sleep 30
    # Since this is run as a subshell (instead of an external command),
    # the parent pid is $$, not $PPID.
    kill -USR1 $$
  done
) &

finish() {
   kill $!
   echo "Closing."
   exit 0
}

changetime() {
   echo "Signal USR1 received."
   time=$(( $RANDOM % 8 + 1))
}

trap changetime USR1
trap finish SIGINT

while true
do
  echo "before sleep"
  sleep $time
  echo "after sleep"
done

outputs,

before sleep
Signal USR1 received.
after sleep
before sleep
Signal USR1 received.
after sleep
before sleep
Signal USR1 received.
Closing.

EDIT2: I've edited the above example, resulting in the same output, but adds one more difficulty: The sleep time is changed by the USR1 trap function. So now, every 30 seconds a random time between 1 and 8 is chosen. SO, I need to kill sleep from the main script when signal changes its time. I insist on that it has no sense, but I need to know if it is possible.

4
  • The problem is that if you attempt to kill the sleep, there is a high risk to kill another command in the script. The easiest solution is to do a shorter sleep run in a loop, e.g.: for i in $(seq 10); do sleep 1; done
    – vinc17
    Commented Sep 18, 2014 at 7:37
  • Alternatively, instead of sleep 10, you can do ./do_sleep; rm -f some_file where do_sleep is a script that does echo $$ > some_file; exec sleep 10, so that the code that sends the signal can also kill the sleep by reading its pid in the created file. Not tested.
    – vinc17
    Commented Sep 18, 2014 at 8:09
  • I'll try that last one. I think it'll work although it is isn't very clean, considering I'm having a script just to do sleep. The script I'm working on has a long sleep inside the main script, which should be interruped by the signal. Moreover, inside the trap associated with USR1 I need to change a global variable, which I guess I commented before, but forgot to include it on the example above. Commented Sep 18, 2014 at 15:38
  • Just edited again, adding that factor. Commented Sep 18, 2014 at 16:07

3 Answers 3

5

You can run a process that will send a signal (e.g. SIGALRM) to the shell script every x time, and use a trap for this signal. This process could be a script doing something like:

set -e
while true
do
  sleep 300
  kill -ALRM $PPID
done

if started by the main shell script.

The main shell script should kill this process when it is no longer needed, and/or the process should terminate when the pid no longer exists (however there's a race condition on that).

(EDITED) Note: If your main shell script uses the sleep command, it may badly interact with the ALRM signal if sleep is a builtin and sleep(3) is implemented with alarm(2). The POSIX description of the sleep shell utility also says in its rationale: "The exit status is allowed to be zero when sleep is interrupted by the SIGALRM signal because most implementations of this utility rely on the arrival of that signal to notify them that the requested finishing time has been successfully attained. Such implementations thus do not distinguish this situation from the successful completion case. Other implementations are allowed to catch the signal and go back to sleep until the requested time expires or to provide the normal signal termination procedures." To avoid potential issues with some implementations, you can use SIGUSR1 or SIGUSR2 instead of SIGALRM.

Here's an example using a subshell. To make the behavior easier to see, I've replaced the 5-minute period (sleep 300) by a 5-second period (sleep 5).

#!/bin/sh

{
  set -e
  while true
  do
    sleep 5
    # Since this is run as a subshell (instead of an external command),
    # the parent pid is $$, not $PPID.
    kill -USR1 $$
  done
} &

trap 'echo "Signal USR1 received."' USR1

while true
do
  date
  sleep 1
done

The script can be interrupted with Ctrl-C. It doesn't kill the subshell when this happens, but if the pid isn't reused, the subshell terminates automatically after no more than the period (here, 5 seconds) because the kill command fails (kill: No such process).

12
  • thanks! Just one thing. I don't want to kill the process, just send the signal to execute the trap function, is it possible? Commented Sep 17, 2014 at 23:48
  • @redraw If you define the trap, the signal won't kill the shell script. But what I meant is that before the shell script terminates, it should kill the process that sends the signals, otherwise its pid may be reused and the new process might receive this signal and be killed. But if the shell script never terminates, this is no longer a problem.
    – vinc17
    Commented Sep 17, 2014 at 23:57
  • I got a little confused. Would you just edit your answer and add a simple script that recieves the trap? My intention is to run a main script which traps a signal. Another script would be sending that signal every 5 minutes. Commented Sep 18, 2014 at 0:03
  • @redraw I've just added an example: it contains the script that receives the signal, and the script (started in background as a subshell) that sends the signal (here every 5 seconds so that you don't wait too much to see how it behaves).
    – vinc17
    Commented Sep 18, 2014 at 1:21
  • 1
    @jarno I've edited the note, which was implicitly assuming that sleep was implemented (via sleep(3)) with alarm(2). IIRC, this was often the case in the past (20 years ago or more?). I've added references to the alarm(2) Linux man page and to POSIX. Also, this mattered only when sleep is a builtin (I can see that this is the case with ksh93, though sleep isn't interrupted by SIGALRM under Linux); I don't know whether bash was ever affected, but this is a good thing to warn against this potential problem for users of other shells.
    – vinc17
    Commented Apr 24, 2021 at 12:18
2

Yes. The easiest way is to do it pretty much exactly as you originally describe in your question. It's just that part of that something you do should be spawning another child process to send your process a signal 5 minutes in the future. Taking a page from @vinc17's book, I'll shorten the span to 5 seconds also.

trap 'ME=$$
     : $( (sleep 5
     ps -p $ME >/dev/null 2>&1 && { 
        echo hey >/dev/tty
        kill -USR1 $ME
     })&)
' USR1; kill -USR1 $$

That way you don't have some long-running process just asking to be zombied. Instead you have a new, single-purposed counter that gets a fresh reference to the parent's PID at every interval. I also incorporate a little check just to make sure the child does not attempt to do its one job in the event its parent has died since it detached.

Run that and every 5 seconds you should get:

hey
hey
hey
hey
hey
hey
hey
hey
hey
hey

This example will also likely hang the terminal in the meantime, else it will do nothing at all if you try to background the whole process. This is due to its hold on /dev/tty - choose a file instead if you mean run the whole thing in the background, else do a direct stty call and re-exec the tty when you want it.

2
  • Would you put your code in place using my example? I got confused because you call the subshell inside the trap command, and I was doing that from my main script and let the trap command do something else. Commented Sep 18, 2014 at 15:44
  • As far as I understand it, the point of this is that the work happens inside the trap.
    – tripleee
    Commented Aug 26, 2021 at 8:17
0

Replace sleep $time by sleep $time & wait $! so the USR1 trap will run when expected. See answer to another question or SIGNALS chapter of bash manual page for explanation.

In my system ALRM signal seems to work as well as USR1.

You must log in to answer this question.

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