2

I have this Bash script named as s in current directory:

#!/bin/bash
pipe_test() {
    ( set -m; (
        $1
    ); set +m ) | 
    (
        $2
    )
}
pipe_test "$1" "$2"

If I call e.g.

./s yes less

the script gets stopped. (Similar thing happens if I use any other pager I tried instead of less, i.e. more and most.) I can continue it by fg builtin, though.

I want to have job control (enabled by set -m) for the subshell to have a distinct process group ID for the processes of the subshell.

Information about my system:

$ bashbug
...
Machine: x86_64
OS: linux-gnu
Compiler: gcc
Compilation CFLAGS: -g -O2 -fdebug-prefix-map=/build/bash-cP61jF/bash-5.0=. -fstack-protector-strong -Wformat -Werror=format->
uname output: Linux jarnos-OptiPlex-745 5.4.0-29-generic #33-Ubuntu SMP Wed Apr 29 14:32:27 UTC 2020 x86_64 x86_64 x86_64 GNU>
Machine Type: x86_64-pc-linux-gnu

Bash Version: 5.0
Patch Level: 16
Release Status: release
$ less --version
less version: 551
3
  • @roaima oh, you are right, but I added a call.
    – jarno
    Commented May 18, 2020 at 19:31
  • It's probably related to current terminal process group ID mentioned in the documentation. I don't know the mechanics good enough to write a decent answer, but maybe this comment will allow you to advance your research. Commented May 18, 2020 at 19:45
  • Fortunately this can be worked around using named pipe.
    – jarno
    Commented May 18, 2020 at 23:35

2 Answers 2

1

The reason why that happens is because enabling job control (set -m) brings along not just process grouping, but also the machinery for handling "foreground" and "background" jobs. This "machinery" implies that each command run in turn while job control is enabled becomes the foreground process group.

Therefore, in short, when that sub-shell (the left part of your pipeline) enables job control it literally steals the terminal from the entire pipeline, which had it until then and which, in your example, includes the less process, thus making it become background and, as such, not allowed to use the terminal any more. It therefore gets stopped because less does keep accessing the terminal.

By issuing fg you give the terminal back to the entire pipeline, hence to less, and all ends well. Unless you run additional commands within the job-controlling sub-shell, because in such case each additional command would steal the terminal again.

One way around it is to simply run your job-controlled sub-sub-shell in background:

( set -m; (
        $1
    ) & set +m ) | 
    (
        $2
    )

You will have the command expressed by $1 run in its distinct process group as you wish, while the backgrounded mode prevents stealing the terminal, thus leaving it to the pipeline and hence to $2.

Naturally this requires that the command in $1 does not want to read the terminal itself, otherwise it will be the one to get stopped as soon as it attempts to do it.

Also, likewise to as I said above, any additional job-controlled sub-sub-shell you might like to add would require the same "backgrounding" treatment, all along until you set +m, otherwise each additional job-controlled sub-sub-shell would steal the terminal again.

That said, if all you need process grouping for is to kill processes, you might consider using pkill to target them. For instance pkill -P will send a signal to the processes whose parent is the indicated PID. This way you can target all children (but not grand-children) of your sub-process by just knowing the sub-process's PID.

7
  • The code seems to work fine, even better than you tell. Job control seems to be disabled within the sub-sub shell and I can add additional commands besides $1 there without less being stopped. Can you give an example of a command $1 that would read terminal and stop less?
    – jarno
    Commented May 22, 2020 at 7:48
  • As for killing grand-children, there is rkill but you may have to install the package providing it separately.
    – jarno
    Commented May 22, 2020 at 7:56
  • Yes, you can run additional commands within the backgrounded job-controlled sub-sub-shell, ie besides $1 or with a $1 expressing several commands which would all stay within the innermost parentheses. Those will all belong to the same process group, job control is implicitly disabled there. But should you run additional job-controlled sub-sub-shells without the &, those will steal the terminal again. Try (set -m; (seq 1 100; ls) & (echo bye bye; sleep 100); set +m) | less: the sub-sub-shell running seq and ls will not disturb less, but the one running echo and sleep will
    – LL3
    Commented May 22, 2020 at 9:53
  • It may be enough to run commands in job-controlled sub-shell to make it stop: (set -m; (seq 1 100; ls) & echo bye bye; sleep 0; set +m) | less.
    – jarno
    Commented May 22, 2020 at 10:57
  • Of course. I showed additional sub-sub-shells just to make the whole thing homogeneous. A side-note for clarity: the sub-shell is job-controll_ing_, not job-controll_ed_, because it's the one having the set -m. The sub-sub-shell is job-controlled. Unless you use set -m in there too, in which case it also becomes job-controll_ing_ and complicates things even more. Also, the set +m at the very end is useless. It would be useful to disable the stealing if there were additional commands after it. A small enough sleep does stop less, but it is relinquished as soon as sleep ends.
    – LL3
    Commented May 22, 2020 at 11:01
0

Removing the set -m solves the problem (what is that to do anyway?).

Three processes are stopped by the kernel via SIGTTOU:

  • the script process
  • a subshell
  • less

But not yes. Its process is put into a separate process group; probably by the set -m. So the kernel tries to hit all processes in that pipeline but misses one. This missing is not the reason for the "stopped" message, though.

Usually SIGTTOU is caused by a background process trying to write to the terminal. But that is not the only possible reason:

int SIGTTOU
This is similar to SIGTTIN, but is generated when a process in a background job attempts to write to the terminal or set its modes. Again, the default action is to stop the process. SIGTTOU is only generated for an attempt to write to the terminal if the TOSTOP output mode is set; see Output Modes.

See https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html

The last syscall before the is (by less):

ioctl(3, SNDCTL_TMR_STOP or TCSETSW, {B38400 opost isig -icanon -echo ...}) = ? ERESTARTSYS (To be restarted if SA_RESTART is set)

So my assessment is that for some strange reason (i.e. set -m) the pipeline is put in the background. There are several syscalls like

ioctl(255, TIOCSPGRP, [23715]

by different processes. The last one is by the subshell

ioctl(2, TIOCSPGRP, [23718]) = 0

making yes the foreground process group after making it the leader of its own process group (with no other members) by

setpgid(23718, 23718 <unfinished ...>
8
  • If you replace less by cat it will not stop.
    – jarno
    Commented May 20, 2020 at 12:34
  • "I want to have job control for the subshell to have a distinct process group ID for the processes of the subshell."
    – jarno
    Commented May 20, 2020 at 12:35
  • @jarno Because cat does not try to reconfigure the terminal. "to have a distinct process group ID for the processes of the subshell" just shifts the question: What for? The only "solution" I see is to delay the execution of set -m until less is done with the terminal initialization. But how to do that cleanly... Not even less --no-init works. Commented May 20, 2020 at 16:16
  • To be able to send a signal to all processes that are running due to the subshell (and only to them). I can do that if I use named pipe, and there is no such stopping then.
    – jarno
    Commented May 20, 2020 at 18:58
  • ^including the subshell itself.
    – jarno
    Commented May 20, 2020 at 20:13

You must log in to answer this question.

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