简体   繁体   中英

C shell, signal caught by parent still goes to child process.

I'm writing a basic unix shell in C and I would like to catch Cntrl-C signals in the shell and pass them to foreground processes only, but not to background processes. The shell itself should keep running (and it does), and the Background processes should ignore a Cntrl-C and only be killed by a kill signal sent specifically to them, perhaps via a command-line "kill pid ". Both foreground and background processes, however, should trigger the handler with SIGCHLD. Right now, however, the shell catches the Cntrl-C signal, and seems to correctly identify that there is no foreground process to pass the signal to, but the background process still dies.

I tried setting the group id of the background process to something else, and that solves the problem, but it creates a new problem. When I do that, my signal handler no longer catches the signal when the background process completes.

So far, I've looked at the man pages for SIGINT, I've read about 20 SO answers, I've tried setting the group id of the child to something different than the parent (solves the problem, but now child can no longer send SIGCHLD to parent), and I've checked that childid != foreground process and foregroundProcess == 0 when I'm running a background process. But still the background process gets killed. Any ideas?

I think my problem is somewhere in my signal handler, but not really sure:

in main:

  struct sigaction sa;
  sa.sa_handler = &handleSignal; /*passing function ref. to handler */
  sa.sa_flags = SA_RESTART;  
  sigfillset(&sa.sa_mask); /*block all other signals while handling sigs */

  sigaction(SIGUSR1, &sa, NULL);
  sigaction(SIGINT, &sa, NULL);
  sigaction(SIGCHLD, &sa, NULL);
  sigaction(SIGTERM, &sa, NULL);

handleSignal looks like this:

 void handleSignal(int signal){
  int childid;
  switch (signal) { 
        /*if the signal came from a child*/
       case SIGCHLD:  
        /*get the child's id and status*/  
        childid = waitpid(-1,&childStatus,0);

        /*No action for foreground processes that exit w/status 0 */
        /*otherwise show pid & showStatus */
          if ((childid != foregroundProcess)){
            printf("pid %i:",childid);
            showStatus(childStatus);
            fflush(stdout);
          }
          break;

        /* if signal came from somewhere else, pass it to foreground child */
        /* if one exists. */
        default:
          printf("Caught signal: %i and passing it", signal);
          printf(" to child w/pid: %i\n\n:", foregroundProcess);
          fflush(stdout);

          /*If there is a child, send signal to it. */
          if (foregroundProcess){
            printf("trying to kill foreground.\n");
            fflush(stdout);
            kill(foregroundProcess, signal);
          }     
      }
    }

Found an answer to my own question. I had already tried changing the group id of the background child process using setpid(0,0); and this worked, but created a different problem. After that call, I was no longer catching SIGCHLD signals from the child in the parent. This is because once the process group of the child is changed, it is essentially no longer connected to the parent for signalling purposes. This solved the problem of the background processes catching Cntrl-C (SIGINT) signals from the parent (undesired behavior), but prevented the background process from signalling the parent when complete. Solved one problem only to create another.

Instead, the solution was to detect whether a child was about to be created as a foreground or a background process, and if background, tell it to ignore the SIGINT signal: signal(SIGINT, SIG_IGN);

Since it's probably the first process on the tty, the kernel will send the signal to all its children when it sends a kernel-initiated SIGINT, SIGHUP, or SIGQUIT. See Terminate sudo python script when the terminal closes for some more details on that, and ideas for tracing/debugging what's happening to make sure you have it right.


An alternative to starting bg child processes out ignoring SIGINT (and SIGQUIT, and maybe SIGHUP?) is to avoid having the kernel deliver those signals to the shell.

I forget, but I think the kernel only delivers SIGINT to the current foreground process. Your shell won't get it if cat or something is running in the foreground. So you only have to worry about what happens while the shell is in the foreground. (I'm only about 80% sure of this, though, and this idea is useless if the shell (and thus all its children) always receive the SIGINT.)

If you disable the tty's signal-sending when the user is editing a command prompt, you can avoid having the shell receive SIGINT. Probably the best way is to do the equivalent of stty -isig , using tcsetattr(3)

// disable interrupts
struct termios term_settings;
tcgetattr(fd, &term_settings);     // TODO: check errors
term_settings.c_lflag &= ~ISIG;   // clear the interactive signals bit
tcsetattr(fd, TCSANOW, &term_settings);

// do the reverse (settings |= ISIG) after forking, before exec

If you strace that, you'll just see ioctl system calls, because that's the system call the termios library functions are implemented on top of.

I guess this leaves a tiny time window between a child process ending, and wait() returning and the shell disabling terminal interrupts.

IIRC, I read something about it being tricky for bash to keep track of when to swap the terminal between raw (for line editing) and cooked (for commands to be run), but I think that's only because of job control. (^z / fg)


The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM