Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

MacOS - make child processes die with parent

860 views
Skip to first unread message

Mut...@dastardlyhq.com

unread,
Jan 21, 2022, 9:15:33 AM1/21/22
to
Making all child processes exit when the parent exits (without having to add
extra code into the child, eg getppid() polling) is done rather simply using

prctl(PR_SET_PDEATHSIG, SIGKILL)

with the proviso that there may be a race condition if the parent exits
immediately after the fork().

However I (or rather google) can't find a method of emulating that in MacOS and
I presume this applies to BSD also. Does anyone know a way?

Thanks for any help.

William Ahern

unread,
Jan 27, 2022, 6:30:16 PM1/27/22
to
You can use ptys, sessions, and process groups to emulate this behavior, and
without having to modify children. This relies on a *little* ambiguity in
the POSIX standard, but macOS and all other Unix-like systems I've tried
(Linux, various BSDs, Solaris, AIX) behave the same way.

The trick is that on "modem disconnect" SIGHUP is sent to all processes in
the process group associated with the controlling terminal (i.e. sent to the
foreground process group), *presuming* at least one process currently has an
open descriptor to the slave tty. POSIX only requires SIGHUP to be sent to
the controlling process--i.e. the session leader. See POSIX.1-2017 11.1.10
at https://pubs.opengroup.org/onlinepubs/9699919799/. But in practice SIGHUP
is sent to the foreground process group so long as the slave tty still has
an valid, open descriptor reference. (Slave tty can still exist without any
open descriptor references as that's how opening /dev/tty works, and how a
process can have a controlling terminal even if it dup'd over references at
stdin, stdout, stderr. But the historical SIGHUP seems to check for some
valid, open descriptor reference.)

The second aspect to understand is that when the controlling process (i.e.
session leader) of a pty pair exits, this is treated as a modem disconnect,
triggering SIGHUP. POSIX is ambiguous on this point. Section 11.1.3
says

"When a controlling process terminates, the controlling terminal is
dissociated from the current session, allowing it to be acquired by a new
session leader. Subsequent access to the terminal by other processes in
the earlier session may be denied, with attempts to access the terminal
treated as if a modem disconnect had been sensed."

To me that doesn't require SIGHUP to be sent unless/until a process performs
an "access" operation, but again in practice the behavior I've seen
everywhere I've tested is for SIGHUP to be delivered immediately upon exit
of the controlling process.

Lastly, the default signal handler for SIGHUP is termination. Processes
could change this, unlike SIGKILL. In practice very few command-line
utilities or simple processes will do this. One notable counter example
would be daemon services, where it's not uncommon to overload SIGHUP to mean
reload configuration. (OTOH, in the process in optional step #3, below, you
could *catch* SIGHUP and re-send SIGKILL to the process group.)

The sequence is pretty simple:

1) Invoke setsid from the parent process, which (if successful--not already a
leader) will become the leader of a new session and process group,
disassociated from any controlling terminal.

2) Create a new pty master/slave pair from the parent process:
a) Create a new master pty (mtty):
i) mtty = posix_openpt(O_RDWR|O_NOCTTY)
ii) grantpt(mtty)
iii) unlockpt(mtty)
b) Open slave pty (stty):
i) stty = open(ptsname(mtty), O_RDWR)
c) If TIOCSCTTY is defined, assume that we need to explicitly set slave
pty (stty) as controlling terminal of our session as POSIX leaves
unspecified how a controlling terminal is assigned (NB: theoretically
a system might require some other operation, but in practice most
systems assign controlling terminal by default upon opening unless
O_NOCTTY is specified, and most others use TIOCSCTTY):
i) ioctl(stty, TIOCSCTTY)

3) If you're worried about children closing slave tty references (e.g. if
stty is accidentally at stdin, stdout, or stderr, or if a process might
unconditionally close unknown descriptors using closefrom(2) or similar
pattern) then fork at least one child that will sleep indefinitely holding
an open reference.

And that's it. Now you can fork all you want, and whenever the original
parent (i.e. PID == setsid == getpgrp) exits, SIGHUP will be sent to all
processes that have that controlling terminal (i.e. every descendent that
hasn't called setsid or otherwise disassociated).

The above sequence is translated from a regression test I wrote:
https://github.com/wahern/lunix/blob/98877f3/regress/0-ctty-sighup.lua

There are almost certainly some caveats here that are unknown to me,
especially if we're trying to be portable across *all* systems that will
send SIGHUP to processes other than the controlling process. For example, I
have vague recollections that on some systems (AIX?) the master tty may also
need to have an open, valid descriptor reference at least in the controlling
process itself. But at least across macOS and Linux--systems where I
regularly test and use my software--I haven't hit such exceptions. I've
tested it on FreeBSD, NetBSD, OpenBSD, Solaris, and AIX, but only sample
programs. Frankly I'm still a little surprised it worked on AIX.
Unfortunately, I no longer have access to AIX since polarhome.com retired.

Geoff Clare

unread,
Jan 28, 2022, 9:11:06 AM1/28/22
to
William Ahern wrote:

> The second aspect to understand is that when the controlling process (i.e.
> session leader) of a pty pair exits, this is treated as a modem disconnect,
> triggering SIGHUP. POSIX is ambiguous on this point. Section 11.1.3
> says
>
> "When a controlling process terminates, the controlling terminal is
> dissociated from the current session, allowing it to be acquired by a new
> session leader. Subsequent access to the terminal by other processes in
> the earlier session may be denied, with attempts to access the terminal
> treated as if a modem disconnect had been sensed."
>
> To me that doesn't require SIGHUP to be sent unless/until a process performs
> an "access" operation, but again in practice the behavior I've seen
> everywhere I've tested is for SIGHUP to be delivered immediately upon exit
> of the controlling process.

Seems you missed these statements on the _Exit() page (under
"Consequences of Process Termination", which says it applies to all
forms of termination, not just exiting):

* If the process is a controlling process, the SIGHUP signal shall be
sent to each process in the foreground process group of the
controlling terminal belonging to the calling process.

* If the exit of the process causes a process group to become orphaned,
and if any member of the newly-orphaned process group is stopped,
then a SIGHUP signal followed by a SIGCONT signal shall be sent to
each process in the newly-orphaned process group.

--
Geoff Clare <net...@gclare.org.uk>

Mut...@dastardlyhq.com

unread,
Jan 28, 2022, 9:44:30 AM1/28/22
to
On Thu, 27 Jan 2022 15:28:15 -0800
William Ahern <wil...@25thandClement.com> wrote:
>Mut...@dastardlyhq.com wrote:
>The second aspect to understand is that when the controlling process (i.e.
>session leader) of a pty pair exits, this is treated as a modem disconnect,
>triggering SIGHUP. POSIX is ambiguous on this point. Section 11.1.3
>says

Hi, thanks for the long explanation. However in the following code compiled
and run on MacOS the signal handler is never called yet the child process
is simply reparented by init (launchd) and carries on running:

#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sighandler(int sig)
{
printf("pid %d got signal %d\n",getpid(),sig);
}


int main()
{
signal(SIGHUP,sighandler);

switch(fork())
{
case -1:
perror("fork()");
return 1;
case 0:
printf("child pid %d\n",getpid());
sleep(3);
puts("child exiting");
return 0;
}
printf("parent pid %d exiting\n",getpid());
return 0;
}



0 new messages