Thursday, 10 January 2008

Dealing with SIGINT in spawned processes

I'm writing a Linux command line application that has the ability to spawn processes of the users' choosing when they want it to. My application waits for the process to finish and then continues. But this raised a problem: If the launched process took a while to run and the user presses Ctrl-C then not only does the spawned process get killed so does my process! In this regard I'd prefer to work much more like a shell and regain control after the spawned process has terminated.

In order to solve these problems I was forced to revisit stuff that I'd read about long ago but not fully understood the implications of at the time. Thanks are due to Zefram for pointing me in the right direction.

Both processes die because they are in the same process group. When the user hits Ctrl-C a SIGINT signal is sent to all processes in the active process group. The signal is not sent to the shell that started my application because the shell arranged for me to be in a new process group (by a means not dissimilar to that below).

Process groups have a group leader - in fact it is the process ID of the group leader that is used as the process group ID.

So, step one is to make sure that the spawned process runs in its own process group (which will also contain any processes it starts unless it takes specific action to the contrary). This is done by calling setpgid(2).

But unfortunately that is insufficient. When pressing Ctrl-C the SIGINT is still set to the process group that contains my application; therefore I exit leaving the spawned process still running.

In order to explain this properly I need to briefly mention sessions. For the purposes of this explanation you can think of a session as representing a terminal. Each session can have a number of process groups. One of these process groups will be the foreground process group and there may be background process groups. The above behaviour resulted because although I'd placed the spawned process in a different process group that process group was in the background (rather like running it from a shell in the background with &.)

I needed to resolve this problem by moving the spawned process group to the foreground. This can be done with tcsetpgrp(3) but it's not quite as simple as that. By default background processes that try to write to the terminal will be sent the SIGTTOU signal. The default action for this signal is to stop the process (just as it is when you hit Ctrl-Z to suspend a process). tcsetpgrp counts as terminal output so my newly created child process just stopped as soon as I called it. In order to stop this happening I needed to arrange to ignore that signal for the duration of the call.

After the spawned process is complete I needed to put my process group back into the foreground again. Again I had to protect myself against being stopped by SIGTTOU.

The following program shows all this at work. The error handling is not too hot.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

int execfgvp(const char *file, char const * const argv[])
{
pid_t child_pid = fork();
if (child_pid == 0) // We're the child
{
// Create a process group for us
if (setpgid(0, 0) < 0)
exit(126); // Failed to setpgrp

// Become the active process group
signal(SIGTTOU, SIG_IGN);
tcsetpgrp(0, getpid());
signal(SIGTTOU, SIG_DFL);

execvp(file, (char * const *)argv);

// Failed to spawn process
exit(127);
}
else if (child_pid > 0) // We're the parent
{
int status;
if (waitpid(child_pid, &status, 0) < 0)
return -1; // Failed to wait. Pass errno on.

// Make us the foreground process group again.
signal(SIGTTOU, SIG_IGN);
tcsetpgrp(0, getpid());
signal(SIGTTOU, SIG_DFL);

if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
else
return -1; // Fork failed. Pass errno on.
}

int main()
{
const char *argv[] = { "ping", "localhost", NULL };
if (execfgvp(argv[0], argv) < 0)
{
fprintf(stderr, "Failed to start process: %m\n");
return 1;
}

printf("Process finished. Returned to foreground.\n");
printf("Press a key to exit.\n");
getchar();

return 0;
}

See also:
Edit: 2008/01/11 Fixed angle brackets in source code.

No comments: