Rework fork/exec strategy

cmd_exec_process is used whenever sway is meant to execute a child
process on behalf of the user, and had a lot of complexity.

In order to avoid having to wait on the user's process, a double-fork
was used, which in turn required us to wait on the outer process. In
order to track the child PID for launcher purposes, a pipe was used to
transmit the PID back to sway.

This resulted in sway blocking for 5-6 ms per exec on my system, which
is quite significant. The error handling was also quite lacking - the
read loop did not handle errors at all for example.

Instead, teach sway to handle SIGCHLD and do away with the double-fork.
This in turn allows us to get rid of the pipe as we can record the
child's PID directly. This reduces the time we block to just 1.5 ms on
my system. We'd be able to get down to just 150 µs if we could use
posix_spawn(3), but posix_spawn(3) cannot reset NOFILE. clone(2) or
vfork(2) would be alternatives, but that presents portability issues.

This change is replicated for swaybar, swaybg and swaynag handling,
which had similar albeit less complicated implementations.
This commit is contained in:
Kenny Levinsen 2025-02-11 12:41:59 +01:00
parent d093c2e358
commit 3480bc6678
5 changed files with 73 additions and 140 deletions

View file

@ -25,16 +25,6 @@ struct cmd_results *cmd_exec_validate(int argc, char **argv) {
return error;
}
static void export_xdga_token(struct launcher_ctx *ctx) {
const char *token = launcher_ctx_get_token_name(ctx);
setenv("XDG_ACTIVATION_TOKEN", token, 1);
}
static void export_startup_id(struct launcher_ctx *ctx) {
const char *token = launcher_ctx_get_token_name(ctx);
setenv("DESKTOP_STARTUP_ID", token, 1);
}
struct cmd_results *cmd_exec_process(int argc, char **argv) {
struct cmd_results *error = NULL;
char *cmd = NULL;
@ -56,67 +46,42 @@ struct cmd_results *cmd_exec_process(int argc, char **argv) {
sway_log(SWAY_DEBUG, "Executing %s", cmd);
int fd[2];
if (pipe(fd) != 0) {
sway_log(SWAY_ERROR, "Unable to create pipe for fork");
}
pid_t pid, child;
struct launcher_ctx *ctx = launcher_ctx_create_internal();
// Fork process
if ((pid = fork()) == 0) {
// Fork child process again
pid_t child = fork();
if (child == 0) {
restore_nofile_limit();
setsid();
sigset_t set;
sigemptyset(&set);
sigprocmask(SIG_SETMASK, &set, NULL);
signal(SIGPIPE, SIG_DFL);
close(fd[0]);
if ((child = fork()) == 0) {
close(fd[1]);
if (ctx) {
export_xdga_token(ctx);
const char *token = launcher_ctx_get_token_name(ctx);
setenv("XDG_ACTIVATION_TOKEN", token, 1);
if (!no_startup_id) {
setenv("DESKTOP_STARTUP_ID", token, 1);
}
if (ctx && !no_startup_id) {
export_startup_id(ctx);
}
execlp("sh", "sh", "-c", cmd, (void*)NULL);
sway_log_errno(SWAY_ERROR, "execlp failed");
_exit(1);
}
ssize_t s = 0;
while ((size_t)s < sizeof(pid_t)) {
s += write(fd[1], ((uint8_t *)&child) + s, sizeof(pid_t) - s);
}
close(fd[1]);
sway_log_errno(SWAY_ERROR, "execve failed");
_exit(0); // Close child process
} else if (pid < 0) {
} else if (child < 0) {
launcher_ctx_destroy(ctx);
free(cmd);
close(fd[0]);
close(fd[1]);
return cmd_results_new(CMD_FAILURE, "fork() failed");
}
free(cmd);
close(fd[1]); // close write
ssize_t s = 0;
while ((size_t)s < sizeof(pid_t)) {
s += read(fd[0], ((uint8_t *)&child) + s, sizeof(pid_t) - s);
}
close(fd[0]);
// cleanup child process
waitpid(pid, NULL, 0);
if (child > 0) {
sway_log(SWAY_DEBUG, "Child process created with pid %d", child);
if (ctx != NULL) {
sway_log(SWAY_DEBUG, "Recording workspace for process %d", child);
ctx->pid = child;
}
} else {
launcher_ctx_destroy(ctx);
return cmd_results_new(CMD_FAILURE, "Second fork() failed");
}
free(cmd);
return cmd_results_new(CMD_SUCCESS, NULL);
}

View file

@ -220,12 +220,6 @@ static void invoke_swaybar(struct bar_config *bar) {
signal(SIGPIPE, SIG_DFL);
restore_nofile_limit();
pid = fork();
if (pid < 0) {
sway_log_errno(SWAY_ERROR, "fork failed");
_exit(EXIT_FAILURE);
} else if (pid == 0) {
if (!sway_set_cloexec(sockets[1], false)) {
_exit(EXIT_FAILURE);
}
@ -242,19 +236,12 @@ static void invoke_swaybar(struct bar_config *bar) {
execvp(cmd[0], cmd);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
if (close(sockets[1]) != 0) {
sway_log_errno(SWAY_ERROR, "close failed");
return;
}
if (waitpid(pid, NULL, 0) < 0) {
sway_log_errno(SWAY_ERROR, "waitpid failed");
return;
}
sway_log(SWAY_DEBUG, "Spawned swaybar %s", bar->id);
}

View file

@ -1061,12 +1061,6 @@ static bool _spawn_swaybg(char **command) {
return false;
} else if (pid == 0) {
restore_nofile_limit();
pid = fork();
if (pid < 0) {
sway_log_errno(SWAY_ERROR, "fork failed");
_exit(EXIT_FAILURE);
} else if (pid == 0) {
if (!sway_set_cloexec(sockets[1], false)) {
_exit(EXIT_FAILURE);
}
@ -1082,20 +1076,12 @@ static bool _spawn_swaybg(char **command) {
command[0]);
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
if (close(sockets[1]) != 0) {
sway_log_errno(SWAY_ERROR, "close failed");
return false;
}
int fork_status = 0;
if (waitpid(pid, &fork_status, 0) < 0) {
sway_log_errno(SWAY_ERROR, "waitpid failed");
return false;
}
return WIFEXITED(fork_status) && WEXITSTATUS(fork_status) == EXIT_SUCCESS;
return true;
}
bool spawn_swaybg(void) {

View file

@ -48,6 +48,13 @@ void sig_handler(int signal) {
sway_terminate(EXIT_SUCCESS);
}
void sigchld_handler(int signal) {
pid_t pid;
do {
pid = waitpid(-1, NULL, WNOHANG);
} while (pid > 0);
}
void run_as_ipc_client(char *command, char *socket_path) {
int socketfd = ipc_open_socket(socket_path);
uint32_t len = strlen(command);
@ -325,6 +332,7 @@ int main(int argc, char **argv) {
// handle SIGTERM signals
signal(SIGTERM, sig_handler);
signal(SIGINT, sig_handler);
signal(SIGCHLD, sigchld_handler);
// prevent ipc from crashing sway
signal(SIGPIPE, SIG_IGN);

View file

@ -64,12 +64,6 @@ bool swaynag_spawn(const char *swaynag_command,
goto failed;
} else if (pid == 0) {
restore_nofile_limit();
pid = fork();
if (pid < 0) {
sway_log_errno(SWAY_ERROR, "fork failed");
_exit(EXIT_FAILURE);
} else if (pid == 0) {
if (!sway_set_cloexec(sockets[1], false)) {
_exit(EXIT_FAILURE);
}
@ -92,8 +86,6 @@ bool swaynag_spawn(const char *swaynag_command,
sway_log_errno(SWAY_ERROR, "execlp failed");
_exit(EXIT_FAILURE);
}
_exit(EXIT_SUCCESS);
}
if (swaynag->detailed) {
if (close(swaynag->fd[0]) != 0) {
@ -107,11 +99,6 @@ bool swaynag_spawn(const char *swaynag_command,
return false;
}
if (waitpid(pid, NULL, 0) < 0) {
sway_log_errno(SWAY_ERROR, "waitpid failed");
return false;
}
return true;
failed: