How to kill a process tree programmatically on Linux using C - c

I am trying to write a function that spawns a child process, lets it run for a certain amount of time and then kills it if it hasn't finished:
int sysExecTimeout(const char * exePath, int timeoutSec);
In the function, I use fork and execl to spawn the child, and, when it times out, I use kill(pid, SIGTERM) and kill(pid, SIGKILL) after 2 seconds, to ensure the child dies:
pid_t pid = fork();
if(pid == 0) {
execl("/bin/sh", "sh", "-c", exePath);
exit(127);
} else if(pid != -1) {
// timeout code
if(timeout) {
kill(pid, SIGTERM);
sleep(2);
kill(pid, SIGKILL);
}
}
I am using Linux, and it seems that when a parent process dies, the child is not killed automatically. So the two kill calls will just kill the /bin/sh process and leave the exePath command running, since it is a child process of /bin/sh.
I am trying to write the sysExecTimeout function such that it kills the entire process tree rooted at pid, where pid is the PID from pid = fork()
I need this because the exePath command will spawn other commands, which can also spawn other commands, which could get stuck and consume resources.
I do not have control over the exePath binaries/scripts that get executed, so I cannot write my own parent-dies-so-kill-the-children logic in them.
I tried using kill(0, SIGTERM), which almost did the job, except it also killed my own process :)
I am wondering if there is a flag I can turn on programatically in C that says "hey man, when I die, take all my children and kill them, and repeat recursively for their children" such that the entire process tree started from that program dies (assuming the PID/PPID chain can be followed).
I could use that flag here:
if(pid == 0) {
turnOnRecursiveDeathFlag();
system(exePath);
//execl("/bin/sh", "sh", "-c", exePath);
exit(127);
}
Is there a way to do that? I've been searching for a while, but all I can find are hacks using ps -ef, or modifying the children processes that you are running etc.

Use setpgid in the child to set its GPID equal to its own PID. The parent then can kill(-pid,...) to signal the entire group.
pid_t pid = fork();
if(pid == 0) {
setpgid(0, 0);
execl("/bin/sh", "sh", "-c", exePath);
exit(127);
} else if(pid == -1) {
// timeout code
if(timeout) {
kill(-pid, SIGTERM);
sleep(2);
kill(-pid, SIGKILL);
}
}
That should do it.
One more thing, when you spawn a shell, make sure it doesn't enable job control. Otherwise, it will create its own process groups. Your "/bin/sh -c" is fine.

Related

Fork() and execv() not running in parallel, why?

I'm trying to run a series of commands through execv() and forking a new process in C to run each one, and yet for some reason they aren't running in parallel. The following code is run for each process, with "full" being the filepath and "args" being the arguments. I know that the execv() part isn't the issue, it has to do with the way I'm forking and waiting.
int status;
pid_t pid = fork();
if (pid == 0) {
execv(full, args);
//perror("execv");
} else if (pid < 0) {
printf("%s\n", "Failed to fork");
status = -1;
} else {
if (waitpid(pid, &status, 0) != pid) {
status = -1;
return status;
}
}
When running this code, the forked commands simply run one after the other. I don't know how this could be happening.
If you don't want to wait for each child process, don't call waitpid immediately; as written, you fork a child, then immediately stop all processing in the parent until the child process exits, preventing you from forking any further children. If you want to launch multiple children without leaving zombie processes lying around (and possibly monitoring them all at some point to figure out their exit status), you can do one of:
Store off the pids from each fork in an array, and call waitpid on them one by one after you've launched all the processes you need to launch
Store a count of successfully launched child processes and call wait that many times to wait on them in whatever order they complete.
Ignore the SIGCHLD from the child processes entirely, for when you don't care when they exit, don't need to know their status, etc.

Kill sh -c and all its sub processes in c with linux system calls

I'm working on this status bar program that needs to fork and exec bash scripts using job control. It then pipes the output to a string and displays it. I'm currently implementing this timeout functionality that starts a timer and then kills the started process if they takes too long to exit.
The processes are started like this :
if (pipe2(pipefd, O_CLOEXEC) == -1)
return -1; int pid;
if ((pid = fork()) == -1)
return -1;
if (pid == 0) {
close(STDIN_FILENO);
close(STDERR_FILENO);
dup2(pipefd[1], STDOUT_FILENO);
setpgid(0, 0);
execl("/bin/sh", "sh", "-c", cmd, NULL);
_exit(127);
}
return pid;
And killed like this :
kill(cpid, SIGKILL);
However, this code won't kill sh's sub processes and that's a big problem. To add to that it creates a lot of defunct sh processes even though I'm using waitpid.
Kill the entire process group by using the negative of the process group leader:
kill(-cpid, SIGKILL);

Using setpgid in a mini-shell breaks interactive commands

I'm trying to write a mini-shell in C using this template. But whenever I try to use interactive commands such as less and vi, the shell gets stuck on waitpid (with WUNTRACED enabled these commands return immediately because they are stopped by a job control signal as indicated by ps) . Other commands that don't require input such as ls are fine. The root cause is setpgid, which seems to puts the forked child process (such as less and vi) into a different process group which no longer shares a terminal. The child process is therefore stopped by a job control signal. Deleting setpgid will make the mini-shell work again, but it can't be removed since the mini-shell needs to control its foreground processes as a group (e.g. if the foreground process P forks additional processes P1 and P2, the shell, upon receiving a SIGTSTP from the user, should stop P, P1, and P2. This can be conveniently done if P, P1, P2 are in the same process group whose pgid is the same as P's pid. We can just send SIGTSTP to the entire process group).
I have tried to use tcsetpgrp to fix my shell. Although it'll make commands such as vi functional again, the mini-shell will automatically exit upon completion of the forked child, presumably because the parent shell mistakenly views the completion of the forked child as also the completion of the mini-shell.
Is there a fix which will still allow me to keep setpgid?
// full code is in the provided link
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) { /* Child runs user job */
if (execve(argv[0], argv, environ) < 0) {
execvp(argv[0], argv);
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
// call wrapper function for error handling
// set process group id of child to the pid of child
Setpgid(pid, 0);
if (!bg) {
// foreground process, should wait for completion
// tcsetpgrp does make vi and less work,
// but their completion also terminates the mini-shell
// tcsetpgrp(STDERR_FILENO, pid);
int status;
if (waitpid(pid, &status, 0) < 0) {
unix_error("waitfg: waitpid error");
}
} else {
// background process
printf("%d %s", pid, cmdline);
}
}
The solution is to relinquish control of the tty to the other process group using tcsetpgrp, and when the child is done, take back the control of the tty using tcsetpgrp again. Note that tcsetpgrp sends SIGTTOU to its caller if the calling process belongs to a background process group, so SIGTTOU and SIGTTIN must be blocked.
// error handling is omitted for brevity
// these two signal functions can be anywhere before tcsetpgrp
// alternatively, they can be masked during tcsetpgrp
signal(SIGTTOU, SIG_IGN);
signal(SIGTTIN, SIG_IGN);
if (!builtin_command(argv)) {
if ((pid = Fork()) == 0) {
if (execve(argv[0], argv, environ) < 0) {
execvp(argv[0], argv);
printf("%s: Command not found.\n", argv[0]);
exit(0);
}
}
if (!bg) {
setpgid(pid, 0);
tcsetpgrp(STDERR_FILENO, pid);
int status;
if (waitpid(pid, &status, 0) < 0) {
unix_error("waitfg: waitpid error");
}
tcsetpgrp(STDERR_FILENO, getpgrp());
} else {
printf("%d %s", pid, cmdline);
}
}
This is a rather crude implementation. Consult Craig's comments for this question for where to find bash's implementation.

Setting process group prevents child process from starting

So I am trying to create a basic terminal, and my problem is this: if I use the following code to execute most "normal" terminal commands (e.g. ls, cat, etc), there are no issues. It sets the process group and works perfectly. However, when I try to execute a command such as vim ., it appears that the process immediately stops. If I remove the call to setpgrp(), the command works as expected (and executes with the pgid of the controlling terminal).
Here is my code:
pid_t normal_cmd(char **argv, int bg) {
int pid = fork(), status;
if(pid < 0)
unix_error("Could not fork child process");
else if(!pid) { //child (this is the problematic area)
setpgrp();
status = execvp(argv[0], argv);
if(status < 0)
unix_error("Could not exec child process");
}
else { //parent
if(!bg) {
addjob(jobs, pid, FG);
pause(); //waitpid() is in SIGCHLD handler
struct job_t *cj = getjobpid(jobs, pid);
if(cj && cj->state != ST)
deletejob(jobs, pid);
}
else
addjob(jobs, pid, BG);
}
return pid;
}
Any idea why changing the process group would cause vim to fail here?
vim will try to read from the controlling terminal, and because its process group is not the foreground process group, it will receive a SIGTTIN signal which suspends it. ls and other "normal" commands aren't suspended because they don't read from stdin (which is the terminal in this case).
The setpgrp() call has the effect of creating a new process group with the calling process as its leader -- and the new process group is not the foreground process group on the terminal until you make it so with tcsetpgrp() or ioctl(TIOCSPGRP).
You can read more about job control here. Especially this, which explains why a program in the background receives a SIGTTIN if it tries to read from the the tty, but not a SIGTTOU if it tries to write to it.

Background process is exiting faster than I can add its pid for management

I'm creating background processes in C using fork().
When I created one of these processes, I add its pid to an array so I can keep track of background processes.
pid = fork();
if(pid == -1)
{
printf("error: fork()\n");
}
else if(pid == 0)
{
execvp(*args, args);
exit(0);
}
else
{
// add process to tracking array
addBGroundProcess(pid, args[0]);
}
I have a handler for reaping zombies
void childHandler(int signum)
{
pid_t pid;
int status;
/* loop as long as there are children to process */
while (1) {
/* get zombie pids */
pid = waitpid(-1, &status, WNOHANG);
if (pid == -1)
{
if (errno == EINTR)
{
continue;
}
break;
}
else if (pid == 0)
{
break;
}
/* Remove this child from tracking array */
if (pid != mainPid)
cleanUpChild(pid);
}
}
When I create a background process, the handler is executing and attempting to clean up the child before I can even make the call to addBGroundProcess.
I'm using commands like emacs& which should not be exiting immediately.
What am I missing?
Thanks.
You're right, there is a race condition there. I suggest that you block the delivery of SIGCHLD using the sigprocmask function. When you have added the new PID to your data structure, unblock the signal again. When a signal is blocked, if that signal is received, the kernel remembers that it needs to deliver that signal, and when the signal is unblocked, it's delivered.
Here's what I mean, specifically:
sigset_t mask, prevmask;
//Initialize mask with just the SIGCHLD signal
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, &prevmask); /*block SIGCHLD, get previous mask*/
pid = fork();
if(pid == -1)
{
printf("error: fork()\n");
}
else if(pid == 0)
{
execvp(*args, args);
exit(0);
}
else
{
// add process to tracking array
addBGroundProcess(pid, args[0]);
// Unblock SIGCHLD again
sigprocmask(SIG_SETMASK, &prevmask, NULL);
}
Also, I think there's a possibility that execvp could be failing. (It's good to handle this in general, even if it's not happening in this case.) It depends exactly how it's implemented, but I don't think that you're allowed to put a & on the end of a command to get it to run in the background. Running emacs by itself is probably what you want in this case anyway, and putting & on the end of a command line is a feature provided by the shell.
Edit: I saw your comments about how you don't want emacs to run in the current terminal session. How do you want it to run, exactly - in a separate X11 window, perhaps? If so, there are other ways of achieving that.
A fairly easy way of handling execvp's failure is to do this:
execvp(*args, args);
perror("execvp failed");
_exit(127);
Your code just catches the exit of the child process it fork'ed, which is not to say that another process wasn't fork'ed by that child first. I'm guessing that emacs in your case is doing another fork() on itself for some reason, and then allowing the initial process to exit (that's a trick daemons will do).
The setsid() function might also be worth looking at, although without writing up some code myself to check it I'm not sure if that's relevant here.
You should not be using the shell with & to run background processes. If you do that, they come out as grandchildren which you cannot track and wait on. Instead you need to either mimic what the shell does to run background processes in your own code, or it would probably work just as well to close the terminal (or rather stdin/out/err) and open /dev/null in its place in the child processes so they don't try to write to the terminal or take control of it.

Resources