Search code examples
bashshellpipesignalspipeline

Ignore HUP signal in Bash script with pipe commands


I have the following script which monitors the /tmp directory indefinitely and if there are any operations with files in this directory, then file name is read by while loop and first a character in file name is replaced with b character and this modified file name is logged to test.log file:

#!/bin/bash

trap ':' HUP
trap 'kill $(jobs -p)' EXIT

/usr/local/bin/inotifywait -q -m /tmp --format %f | 
  while IFS= read -r filename; do
    echo "$filename" | sed 's/a/b/' > test.log
  done

This is simplified version of the actual script. I also have a Sys-V type init script for the script above and as I would like to stay LSB compliant, my init script has force-reload(Causes the configuration to be reloaded if the service supports this. Otherwise, the service is restarted.) option which sends the HUP signal to script. Now before executing the force-reload, which executes killproc -HUP test.sh, the output of pstree is following:

# pstree -Ap 4424
test.sh(4424)-+-inotifywait(4425)
              `-test.sh(4426)
# 

After executing the strace killproc -HUP test.sh the child shell is terminated:

# pstree -Ap 4424
test.sh(4424)---inotifywait(4425)
# 

According to strace, killproc sent SIGHUP to processes 4424 and 4426, but only the latter was terminated.

What is the point of this child-shell with PID 4426 in my example, i.e why is it created in the first place? In addition, is there a way to ignore HUP signal?


Solution

  • Pipeline commands are run in a subshell

    The first part of your question is explained by the mechanism through which a shell (in this case Bash) runs commands in a pipeline.

    A pipe is a FIFO (first in, first out) one-way inter-process communication (IPC) channel: it allows bytes to be written at one end (the write-only end) and read from the other (read-only end) without needing to read from or write to a physical filesystem.

    A pipeline allows two different commands to communicate with each other through an anonymous or unnamed (i.e., has no entry in the filesystem) pipe.

    When a simple command is executed by a shell, the command is run in a child process of the shell. If no job control is used, control of the terminal is regained by the shell when the child process terminates.

    When two commands are run in a pipeline, both commands in the pipeline are executed as two separate child processes which run concurrently.

    In Unix systems, pipes are created using the pipe(2) system call, which creates a new pipe and returns a pair of file descriptors with one referring to the read end and the other to the write end of the pipe.

    With Bash on a GNU/Linux system, the clone(2) system call is used to create the sub-processes. This allows the child process to share the table of file descriptors with its parent process so that both child sub-processes inherit the file descriptor of the anonymous pipe so that one can read to it and the other can write to it.

    In your case, the inotifywait command gets a PID of 4425 and writes to the write-only end of the pipe by connecting its stdout to the file descriptor of the write end.

    At the same time, the right hand side of the pipe command gets the PID, 4426 and its stdin file descriptor is set to that of the read-only end of the pipe. Since the subshell for the right hand side of the pipe isn’t an external command, the name to represent the child process is the same as that of its parent, test.sh.

    For more info, see man 7 pipe and the following links:

    Signal handling

    It took me ages (a couple of hours of research, in fact) to figure out why the trap for the SIGHUP signal wasn’t being ignored.

    All my research indicated that child process created by a clone(2) system call should also be able to share the table of signal handlers of the parent process.

    The Bash man page also states that

    Command substitution, commands grouped with parentheses, and asynchronous commands are invoked in a subshell environment that is a duplicate of the shell environment, except that traps caught by the shell are reset to the values that the shell inherited from its parent at invocation.

    It later states that

    Signals ignored upon entry to the shell cannot be trapped or reset. Trapped signals that are not being ignored are reset to their original values in a subshell or subshell environment when one is created.

    This indicates that subshells do not inherit signal handlers that are not ignored. As I understood it, your trap ':' HUP line meant that the SIGHUP signal was (effectively) being ignored (since the : builtin does nothing except return success) – and should in turn be ignored by the pipeline’s subshell.

    However, I eventually came across the description of the trap builtin in the Bash man page which defines what Bash means by ignore:

    If arg is the null string the signal specified by each sigspec is ignored by the shell and by the commands it invokes.

    Simply changing the trap command to trap '' HUP ensures that the SIGHUP signal is ignored, for the script itself – and any subshells.