Bash – Ctrl-c kills shell when pipeline is used in input redirection subshell

bashpipeprocess-substitution

If I run a command like:

cat <(echo 1 | pv) | pv
cat <(echo 1 | pv) | less
cat <(echo 1 | pv) | cat

The command seems to run forever. Entering ^C (SIGINT) kills the entire shell instead of just the commands executed. Why is this the case?


Relevant output of ps xf from another shell on minimal case cat <(pv) | less:

Ss  /bin/bash
S+   \_ cat /dev/fd/XX
S    |   \_ /bin/bash
T    |       \_ pv
S+   \_ less

With these file descriptors open:

bash

0 -> /dev/pts/YY
1 -> /dev/pts/YY
2 -> /dev/pts/YY
255 -> /dev/pts/YY

cat /dev/fd/ZZ

0 -> /dev/pts/YY
1 -> pipe:[RRRRRRRR]
2 -> /dev/pts/YY
3 -> pipe:[QQQQQQQQ]
ZZ -> pipe:[QQQQQQQQ]

bash

0 -> /dev/pts/YY
1 -> pipe:[QQQQQQQQ]
2 -> /dev/pts/YY
255 -> /dev/pts/YY

pv

0 -> /dev/pts/YY
1 -> pipe:[QQQQQQQQ]
2 -> /dev/pts/YY

less

0 -> pipe:[RRRRRRRR]
1 -> /dev/pts/YY
2 -> /dev/pts/YY
3 -> /dev/tty

Using the original example, cat <(echo 1 | pv) | less (this also happens when echo is not a bash builtin but another program like dd if=/dev/zero bs=1 count=1:

Ss   /bin/bash
S+    \_ cat /dev/fd/63
S     |   \_ /bin/bash
T     |       \_ pv
S+    \_ less

bash

0 -> /dev/pts/18
1 -> /dev/pts/18
2 -> /dev/pts/18
255 -> /dev/pts/18

cat /dev/fd/63

0 -> /dev/pts/18
1 -> pipe:[36932796]
2 -> /dev/pts/18
3 -> pipe:[36929317]
63 -> pipe:[36929317]

bash

0 -> /dev/pts/18
1 -> pipe:[36929317]
2 -> /dev/pts/18
255 -> /dev/pts/18

pv

0 -> pipe:[36930391]
1 -> pipe:[36929317]
2 -> /dev/pts/18

less

0 -> pipe:[36932796]
1 -> /dev/pts/18
2 -> /dev/pts/18
3 -> /dev/tty

Best Answer

It happens because the <( process ) isn't properly job-controlled - it's just forked and forgot. That doesn't matter most of the time, because almost as soon as it is born that process is placed in a separate process group and backgrounded. For the instant that the shell requires to open input and output for that process, however, that is the tty's foreground process group, and, as such, vulnerable to SIGINT - unless it is otherwise trapped or ignored as your interactive shell generally does.

The thing is, though: you have a pipe deadlock there. When the parent attempts to open output for that process the pipe blocks. It never gets the chance to change the process group and all the rest because when you CTRL+C the foreground group is killed - it's sent SIGINT - and when the foreground group dies and the parent cannot resume control because it's still blocked on a pipe, the terminal sends a HUP because there's nobody home. kaboom

You need a writer first, then a reader for every pipe you open before you can open another.