Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the double forking in start_application #5510

Merged
merged 1 commit into from
Jun 15, 2023

Conversation

kolayne
Copy link
Contributor

@kolayne kolayne commented May 17, 2023

Having just a single fork is beneficial, as it preserves the approprate parent information for the children of i3, which is useful in some scenarious e.g. when a child wants to do something on the wm's exit (possible via prctl(PR_SET_PDEATHSIG, ...)).

Moreover, this is a zero-cost benefit: i3 is already using libev with the default loop, which automatically reaps all the zombie children even when there is no corresponding event set.

Quote from the ev(3) man page (emphasis mine):

Libev grabs "SIGCHLD" as soon as the default event loop is initialised. This is necessary to guarantee proper behaviour even if the first child watcher is started after the child exits. The occurrence of "SIGCHLD" is recorded asynchronously, but child reaping is done synchronously as part of the event loop processing. Libev always reaps all children, even ones not watched.

Resolves #5506

Having just a single fork is beneficial, as it preserves the approprate
parent information for the children of i3, which is useful in some
scenarious e.g. when a child wants to do something on the wm's exit
(possible via `prctl(PR_SET_PDEATHSIG, ...)`).

Moreover, this is a zero-cost benefit: i3 is already using libev with
the default loop, which automatically reaps all the zombie children even
when there is no corresponding event set.

Resolves i3#5506
@orestisfl
Copy link
Member

So, this would be a backwards-incompatible change (even if the new behavior is an improvement). I wonder how this is going to affect users and if there are usecases where keeping programs alive after i3's exit is beneficial.

Also, will changing this mean programs get killed after a restart? Because that would absolutely break users' expectations of i3 and can cause lost work.

@kolayne
Copy link
Contributor Author

kolayne commented May 18, 2023

Programs won't get killed neither on restart nor on i3 exit, unless the child process explicitly requests that via prctl. The behavior only changes in case the child application is watching the parent (either directly via ppid (it will now return i3's pid for as long as it's alive, rather than the closest reaper's pid, as it would with a double fork) or indirectly e.g. via prctl by requesting the kernel to deliver a signal when the parent dies - and if an application launched by a window manager requests that, I'd argue it wants to watch the window manager, not the closest reaper or PID1).

The restart should not involve this at all, as i3 execs and it does not change the pid, but I did not test it, so I'll double check and come back with the description of scenarios and the changed behavior in a few hours.

@kolayne
Copy link
Contributor Author

kolayne commented May 18, 2023

TL;DR

is in the next comment

Clarification

To reiterate: the double fork() logic used in start_application (which this PR proposes to remove) has nothing to do with killing or not killing children: no signals are delivered in either way. It has do with reaping children (that is, collecting zombie processes after they exited or got killed). The double fork forces immediate reparenting of the child processes (child becomes an orphan and is reparented by the kernel to the closest alive subreaper ancestor, with PID1 being the global default), so i3 doesn't have to deal with reaping. However, it turned out to be unnecessary: the ev library will reap all the zombies anyway.

And the benefit of the patch is that the valid parent information is preserved. Children can rely on it however they want, for example, in my particular use case, automatically die when the wm exits (but the patch does not enforce this behavior in any way).

Behavior

For the tests I used simple programs of 3 types:

  • careless - does not care about its parents at all, just doing it's thing
  • parent_watcher - wants to do something parent-related, requests the parent pid from system for that
  • pdeath_waiter - wants to do something on the event of parent's death

Here are their source codes

careless.c
#include <stdio.h>
#include <unistd.h>

int main() {
    for(int i = 0; i < 6; ++i) {
        puts("I don't care who my parent is, I am running");
        fflush(stdout); 
        sleep(2);
    }   
    puts("Exiting gracefully");
}
parent_watcher.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

void print_cmd_name(pid_t pid) {
    char buf[512] = "/proc/";
    sprintf(buf + 6, "%ld", pid);
    size_t pos = strlen(buf);
    memcpy(buf + pos, "/comm", 6); 

    FILE *f = fopen(buf, "r");
    if(!f) {
        puts("couldn't get their executable name");
        return;
    }   

    fgets(buf, 512, f); 
    printf("which is %s", buf);
}

int main() {
    for(int i = 0; i < 6; ++i) {
        pid_t ppid = getppid();
        printf("My current parent is %ld, ", ppid);
        print_cmd_name(ppid);
        fflush(stdout);
        sleep(2);
    }   
    puts("Exiting gracefully");
}
pdeath_waiter.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>
#include <errno.h>
#include <assert.h>
    
void handle_hup(int _signal) {
    puts("Got a SIGHUP! Proceeding with the execution");
}   

int main() {
    prctl(PR_SET_PDEATHSIG, SIGHUP, 0, 0, 0);
    assert(!errno);
        
    struct sigaction sa;
    sa.sa_handler = handle_hup;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGHUP, &sa, NULL);
    assert(!errno);

    for(int i = 0; i < 6; ++i) {
        puts("I'm waiting for a notification");
        fflush(stdout);
        sleep(2);
    }   
    puts("Exiting gracefully");
}

I ran each of them on both original and the patched versions and checked their behavior on i3's restart and exit (the lines starting with <--- in the output are added by me).

Output with upstream i3

careless does not care: it keeps running no matter what.

I don't care who my parent is, I am running
I don't care who my parent is, I am running
<--- i3 restarted here.
I don't care who my parent is, I am running
I don't care who my parent is, I am running
<--- i3 exited here.
I don't care who my parent is, I am running
I don't care who my parent is, I am running
Exiting gracefully

parent_watcher gets reparented immediately. On my system there is no subreapers, so it gets adopted by PID1:

My current parent is 1, which is systemd
My current parent is 1, which is systemd
<--- i3 restarted here.
My current parent is 1, which is systemd
My current parent is 1, which is systemd
<--- i3 exited here.
My current parent is 1, which is systemd
My current parent is 1, which is systemd
Exiting gracefully

Just like parent_watcher, pdeath_waiter becomes an orphan very early (in fact, so early that it doesn't have enough time to call prctl by that time: the call takes place only after reparenting), so it waits for its (new) parent to exit (which won't happen in case it's reparanted to PID1, but very well might happen if there's a subreaper on the way, which may lead to unintended behavior):

I'm waiting for a notification
I'm waiting for a notification
<--- i3 restarted here
I'm waiting for a notification
I'm waiting for a notification
<--- i3 exited here
I'm waiting for a notification
I'm waiting for a notification
Exiting gracefully

Output with patched i3

For careless nothing changes, the behavior is exactly the same:

I don't care who my parent is, I am running
I don't care who my parent is, I am running
<--- i3 restarted here
I don't care who my parent is, I am running
I don't care who my parent is, I am running
<--- i3 exited here
I don't care who my parent is, I am running
I don't care who my parent is, I am running
Exiting gracefully

parent_watcher now correctly identifies i3 as its parent for as long as it's alive; after i3 exits, the child gets reparented in the same way as before:

My current parent is 12232, which is i3
My current parent is 12232, which is i3
<--- i3 restarted here
My current parent is 12232, which is i3
My current parent is 12232, which is i3
<--- i3 exited here
My current parent is 1, which is systemd
My current parent is 1, which is systemd
Exiting gracefully

pdeath_waiter now can do arbitrary actions when i3 exits!

I'm waiting for a notification
I'm waiting for a notification
<--- i3 restarted here
I'm waiting for a notification
I'm waiting for a notification
<--- i3 exited here
Got a SIGHUP! Proceeding with the execution
I'm waiting for a notification
I'm waiting for a notification
Exiting gracefully

@kolayne
Copy link
Contributor Author

kolayne commented May 18, 2023

TL;DR

  • i3 restart does not cause any effects at all (both before and after the patch)
  • i3 exit (whether graceful or abrupt) does not cause any effects on applications which don't request that explicitly (both before and after the patch)
  • Applications which do request something parent-related won't see i3 as their parent, as they are immediately reparented (and not necessarily to PID1, which makes the behavior less predictable), but with the patch they will remain i3's children for as long as it's alive

@kolayne
Copy link
Contributor Author

kolayne commented May 26, 2023

@orestisfl, is there any action needed from my side? Should I explicitly request someone's review or just wait?

@kolayne
Copy link
Contributor Author

kolayne commented Jun 7, 2023

It seems that the double fork logic was added in f4f4d78. So, @stapelberg, I would appreciate your comments

@orestisfl orestisfl requested a review from stapelberg June 7, 2023 07:26
@stapelberg
Copy link
Member

It seems that the double fork logic was added in f4f4d78. So, @stapelberg, I would appreciate your comments

No, that commit only moves the code around. It was originally added in 4589c26, way before we even used libev.

The change seems reasonable to me, or at least reasonable enough to try it out :)

@stapelberg stapelberg merged commit 3ae5f31 into i3:next Jun 15, 2023
3 checks passed
@kolayne kolayne deleted the remove_double_fork branch June 15, 2023 07:31
slyshot pushed a commit to slyshot/i3 that referenced this pull request Jun 19, 2023
Having just a single fork is beneficial, as it preserves the approprate
parent information for the children of i3, which is useful in some
scenarious e.g. when a child wants to do something on the wm's exit
(possible via `prctl(PR_SET_PDEATHSIG, ...)`).

Moreover, this is a zero-cost benefit: i3 is already using libev with
the default loop, which automatically reaps all the zombie children even
when there is no corresponding event set.

Resolves i3#5506
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

i3wm: use avoid double fork() when starting applications to preserve parent information
3 participants