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
jarno
  • 620

2 Answers2

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.

LL3
  • 5,418
  • 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 May 22 '20 at 07:48
  • As for killing grand-children, there is rkill but you may have to install the package providing it separately. – jarno May 22 '20 at 07: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 May 22 '20 at 09: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 May 22 '20 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 May 22 '20 at 11:01
  • So now that the job_controlled sub-sub-shell is running in background, job control can not put any job in the foreground making pipeline flow as usual. I wish I would found some documentation about this. – jarno May 22 '20 at 11:23
  • You can put any job back to foreground at will. You can fg at some point in the job-controlling sub-shell to assign the terminal to any one of its background jobs, but of course will steal it from less. In the job-controlling sub-shell you can also wait for its job-controlled sub-sub-shells, so as to keep synchronous operation for the job-controlling sub-shell itself while not stealing the terminal. In general, for not too complicated set-ups you may be able to handle things up to some extent, but you need to synchronize everything carefully between the parts of the main pipeline. – LL3 May 22 '20 at 11:43
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 ...>
Hauke Laging
  • 90,279
  • If you replace less by cat it will not stop. – jarno May 20 '20 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 May 20 '20 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. – Hauke Laging May 20 '20 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 May 20 '20 at 18:58
  • ^including the subshell itself. – jarno May 20 '20 at 20:13
  • @jarno It might be a better idea to create a cgroup for the subshell. That would make it easy to signal all processes in the subshell. – Hauke Laging May 20 '20 at 21:56
  • Oh, is that something you can do in Bash shell? – jarno May 21 '20 at 12:08
  • @jarno I haven't used that in too long so I cannot give you a detailed description now but basically you create a new cgroup and put the subshell ID in it as soon as it is known. When sending signals you just read the cgroup member list and run kill individually. I am not aware of a "send signal to all members of cgroup x" command. – Hauke Laging May 21 '20 at 16:02