1
votes

I am writing a shell in C and I am trying to add signal handling. In the shell, fork() is called and the child process executes a shell command. The child process is put into its own process group. This way, if Ctrl-C is pressed when a child process is in the foreground, it closes all of the processes that share the same process group id. The shell executes the commands as expected.

The problem is the signals. When, for example, I execute "sleep 5", and then I press Ctrl-C for SIGINT, the "shell>" prompt comes up as expected but the process is still running in the background. If I quickly run "ps" after I press Ctrl-C, the sleep call is still there. Then after the 5 seconds are up and I run "ps" again, it's gone. The same thing happens when I press Ctrl-Z (SIGTSTP). With SIGTSTP, the process goes to the background, as expected, but it doesn't pause execution. It keeps running until it's finished.

Why are these processes being sent to the background like this and continuing to run? Here is the gist of my code...

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

int status;

void sig_handler_parent()
{
    printf("\n");
}

void sig_handler_sigchild(int signum)
{
    waitpid(-1, &status, WNOHANG);
}

int main()
{
    signal(SIGCHLD, sig_handler_sigchild);
    signal(SIGINT, sig_handler_parent);
    signal(SIGQUIT, sig_handler_parent);
    signal(SIGTERM, sig_handler_parent);
    signal(SIGCONT, sig_handler_parent);
    signal(SIGTSTP, sig_handler_parent);

    while (1)
    {
        printf("shell> ");

        // GET COMMAND INPUT HERE

        pid = fork();

        if (pid == 0)
        {
            setpgid(getpid(), getpid());

            execvp(cmd[0], cmd);
            printf("%s: unknown command\n", cmd[0]);

            exit(1);
        }
        else
            waitpid(0, &status, WUNTRACED); 
    }

    return 0;
}

p.s. I have already tried setting all of the signal handlers to SIG_DFL before the exec command.

2
If the child is in it’s own process group, it won’t get signals destined for the parent’s process group, will it? - Jonathan Leffler

2 Answers

1
votes

Are you sure that your child process is actually receiving the signals from your tty? I believe you need to make a call to tcsetpgrp to actually tell the controlling terminal to send signals to the process group of your child process.

For example, after you call fork, and before exec, try this from within your child.

tcsetpgrp(STDIN_FILENO, getpid())

Here is the man page for tcsetpgrp(3)

0
votes

The code you provide does not compile, and an attempt to fix it shows that you omitted a lot. I am only guessing.

In order to bring you forward, I'll point out a number of facts that you might have misunderstood. Together with a couple of documentation links, I hope this is helpful.

Error Handling

First: please make a habit of handling errors, especially when you know there's something that you don't understand. For example, the parent (your shell) waits until the child terminates,

waitpid(0, &status, WUNTRACED); 

You say,

When, for example, I execute "sleep 5", and then I press Ctrl-C for SIGINT, the "shell>" prompt comes up as expected but the process is still running in the background.

What actually happens is that once you press Ctrl-C, the parent (not the child; see below for why) receives SIGINT (the kernel's terminal subsystem handles keyboard input, sees that someone holds "Ctrl" and "C" at the same time, and concludes that all processes with that controlling terminal must be sent SIGINT).

Change the parent branch to,

int error = waitpid(0, &status, WUNTRACED); 
if (error != 0)
    perror("waitpid");

With this, you'd see perror() print something like:

waitpid: interrupted system call

You want SIGINT to go to the child, so something must be wrong.

Signal Handlers, fork(), and exec()

Next, what happens to your signal handlers across fork() and exec()?

The signal overview man page states,

A child created via fork(2) inherits a copy of its parent's signal dispositions. During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.

So, ideally, what this means is that:

  1. The parent (shell) sees SIGINT, as observed above, and prints "interrupted system call".
  2. The child's signal handlers are reset back to their defaults. For SIGINT, this means to terminate.

You do not fiddle with the controlling terminal, so the child inherits the controlling terminal of the parent. This means that SIGINT is delivered to both parent and child. Given that the child's SIGINT behavior is to terminate, I'd bet that no process is left running.

Except when you use setpgid() to create a new process group.

Process Groups, Sessions, and Controlling Terminal

Someone once called me a UNIX greybeard. While this is true form a visual point of view, I must reject that compliment because I rarely hang around in one of the darkest corners of UNIX - the terminal subsystem. Shell writers have to understand that too though.

In this context, it's the "NOTES" section of the setpgid() man page. I suggest you read that, especially where it says,

At any time, one (and only one) of the process groups in the session can be the foreground process group for the terminal; (...)

The shell (bash maybe) from which you start your shell program has done so for the foreground invocation of your program, and marked that as "foreground process group". Effectively this means, "Please, dear terminal, whenever someone presses Ctrl-C, send a SIGINT to all processes in that group. I (your parent) just sit and wait (waitpid()) until all is over, and will take control again then.".

You create a process group for the child, but don't tell the terminal about it. What you want is to

  1. Detach the parent from the terminal.
  2. Set the child process group as the foregroud process group of the terminal.
  3. Wait for the child (you already do).
  4. Regain terminal foreground.

Further down in the "NOTES" section of said man page, they give links to how that is done. Follow those, read thoroughly, try out things, and make sure you handle errors. In most cases, such errors are signs of misunderstanding. And, in most cases, such errors are fixed by re-reading the documentation.