Re: Kill sends signal to wrong process group with job control enabled
Ben Ashton <[email protected]>
| Newsgroups | gmane.comp.shells.bash.bugs |
|---|---|
| Message-ID | <[email protected]> |
On 01/03/2026 08:47, Robert Elz wrote: > Date: Sun, 1 Mar 2026 06:33:40 -0800 > From: Ben Ashton <[email protected]> > Message-ID: <[email protected]> > > | It's unrelated, but I'm curious what systems don't make the PGID the > | same as the PID of the leader? I thought that was a POSIX thing. > > No. It is related to how pipelines are forked in the shell, To make > a new process group: > > | The FreeBSD syscall manual for "setsid" states: "the setsid() system call > | returns the value of the process group ID of the new process group, > | which is the same� as�the� process� ID of the calling process." > > that is absolutely correct. So it all depends how the processes > of a pipeline are forked (for anything other than a pipeline it makes > no difference, the shell forks once, that becomes the process group leader, > and its pid is $!). > > It used to be the case once that the shell would fork once for pipelines > as well, and then that process would be both $! and the process group leader, > and because of posix rules, it would also become the rightmost process of > the pipeline, forking children for the other processes for the rest of the > pipe. But while not impossible, it is difficult, and defeats some > optimisations, to implement things that way on a system supporting the > pipefail option, which is required by POSIX now. > > So, instead the parent shell (the one which is going to set $!) forks all > of the children, and adds each of them to the new process group. To do > that it needs to know the process group ID as each child is created (there's > no sane way to send the info to a process later, only by the memory copy from > the fork()). So the shell forks once, that child creates a new process > group, the parent knows what that is, as it is the pid of the child it just > created (which fork()) returns. Then it forks processes for the rest of > the pipeline, those processes know the pgrp id, and join that pgrp > immediately after starting after the fork(). > > Now it all depends which order the shell creates the pipeline, for which > there are 2 rational choices, left to right, or right to left; which is > simpler depends upon the data structure the shell's parser created when > it parsed the pipeline. If the pipeline is created right to left, then > the pgrp id is the rightmost process (the way it used to be in times past), > and that also becomes $!. If the pipeline is created left to right, then > the pgrp is is the pid of the leftmost process, but $! still needs to be the > pid of the rightmost process, hence $! and the pgrp ID are different. > > kre > > ps: the NetBSD shell has a builtin command which converts between pids and > pgrp ids of children of the shell, so scripts can easily do whatever it is > they really want to achieve, and the shell doesn't need to attempt to guess > what the script is intending (-$! is certainly not guaranteed to be the > pgrp ID of anything, and if it is, it isn't necessarily what you want, so > when a shell sees that, it has to attempt to intuit what was really meant). > > Sorry, no markup survived my cut&paste from an xterm into my MUA in the > following: > > jobid [-g|-j|-p] [job] > With no flags, print the process identifiers of the processes in > the job. If the job argument is omitted, the current job is used. > Any of the ways to select a job may be used for job, including the > '%' forms, or the process id of the job leader ('$!' if the job > was created in the background.) > > If one of the flags is given, then instead of the list of process > identifiers, the jobid command prints: > > -g the process group, if one was created for this job, or > nothing otherwise (the job is in the same process group as > the shell.) > > -j the job identifier (using '%n' notation, where n is a > number) is printed. > > -p only the process id of the process group leader is printed. > > These flags are mutually exclusive. > > jobid exits with status 2 if there is an argument error, status 1, > if with -g the job had no separate process group, or with -p there > is no process group leader (should not happen), and otherwise > exits with status 0. > > So, "jobid -g $!" produces the process group ID from the $! value. > > Just for completeness, jobs without process group ids of their own > are ones created when job control is disabled - which is the default > for scripts. > > > Thank you for the thorough explanation. Given the issue you describe with pipelines, I can see why there are scenarios where the shell might want to translate $! so that you can kill the process group of a backgrounded job irrespective of the order in which the pipeline is composed. The issue is that within the subshell, backgrounded commands don't automatically get their own process groups (despite job control being enabled outside of the subshell), however that translation still occurs. Inside the subshell running "sleep 5 &" will just create a child process that belongs to the process group of the subshell. So that translation will result in me killing the process group of the subshell. Surely we only want that translation to occur when the shell created a new process group for the command? Thanks to your explanation I was able to simplify the reproduction quite a lot. Here is what happens with bash and various other shells: ben@work-laptop:~$ dash -c 'set -m; (setsid sleep 5 & pid=$!; sleep 2; kill -TERM -$pid; echo here)' here ben@work-laptop:~$ ash -c 'set -m; (setsid sleep 5 & pid=$!; sleep 2; kill -TERM -$pid; echo here)' here ben@work-laptop:~$ zsh -c 'set -m; (setsid sleep 5 & pid=$!; sleep 2; kill -TERM -$pid; echo here)' here ben@work-laptop:~$ bash -c 'set -m; (setsid sleep 5 & pid=$!; sleep 2; kill -TERM -$pid; echo here)' Terminated As you can see, bash is the only one that translates $! to the PGID of the subshell. I confirmed with strace that the other shells ARE killing the correct process group. The reason I previously stated that it doesn't matter how I obtain the PGID, is because it's not like the shell is performing any complex logic to figure out that the number was derived from $!. I could have used some external tool, or read /proc/PID/stat to determine the process group that I wanted to kill. The problem is that the PID effectively becomes a magic number that will be translated without my knowledge.