我该如何在C语言中捕获SIGINT信号并仅杀死前台进程?

4

我正在创建一个shell程序,我希望 Ctrl+C 可以杀掉前台进程但不会影响后台进程,例如: &sleep 50 是一个后台进程,如果我使用 Ctrl+C ,它将只杀死前台进程,而不影响后台进程。但是我真的想不出该怎么做,请大家帮帮忙:D

int main(void) {
  Command cmd;
  int n, forSignals;
  char **cmds;
  char **pipedCmds;
  signal(SIGINT, INThandler);
  while (!done) {
    char *line;
    line = readline("> ");
    if (!line) {
      done = 1;
    } else {
      stripwhite(line);
      if(*line) {
        add_history(line);
        n = parse(line, &cmd);
        PrintCommand(n, &cmd);
        cmds = getCmds(cmd.pgm);
        pipedCmds = getPipedCmds(cmd.pgm);
        executionDecider(line, cmds, pipedCmds, cmd);
      }
    }
    if(line) {
      free(line);
    }
  }
  return 0;
}

void  INThandler(int sig)
{
  signal(sig, SIG_IGN);
  kill(SIGINT, 0);
  printf("\n");
}

顺便提一下,当然还有实际执行程序的其余代码,如果需要展示,请告诉我,但我认为这是一个很好的可以最小化重现问题的例子。

编辑:非常重要,不知道我怎么会忘记了 :/ 但我需要它不要通过这种方式创建僵尸进程,它不应该留下任何僵尸进程。

编辑:请找到链接到完整项目代码的URL。 在那里可能更有意义: https://codedump.io/share/d8hrj40JdEqL/1/lshc---c-shell-program


我可能漏掉了什么,但我认为我们需要在这里看到更多。UNIX tty已经将Ctrl-C转换为针对前台进程组的SIGINT。除了正确地管理作业控制进程组,您不需要做任何事情。 - pilcrow
你能添加 Command 和其他缺失的函数吗? - S.S. Anne
3个回答

2
默认情况下,所有后台进程的标准输入都被禁用。您可以使用sudo ls -al /proc/<"pid">/fd来检查。在这种情况下,任何输入都无法从终端传输到后台进程。因此,即使按下ctrl+c也不会终止它。只有前台进程才会终止。
根据man页面: int kill(pid_t pid, int sig); 在这里,您正在执行kill(SIGINT,0)。 根据您的代码,第二个参数0表示挂起信号(SIGHUP),仅在shell被终止时才会出现。它将终止运行在该shell中的所有进程。更新kill函数调用。
您已经提到了"&sleep 50",这在Linux中是无效的。那么您使用的是哪个操作系统?

2
我认为你的核心问题是如何在进程组的标准信号语义上进行干预。 默认情况下,您fork()的所有进程都保留在所属的进程组中。系统调用setpgid()可以创建一个新的进程组,使新进程脱离组信号语义。
某些信号会传递给进程组。来自tty驱动程序的通知会广播到当前tty会话领导者所属的进程组。简单明了,对吧 :-?
因此,您要做的是在启动的子进程中使用setpgid()创建一个新的进程组。随后启动的任何进程都将继承其新的进程组;因此,它们无法返回到原始的根进程组[I think,这里地面不平]。
我编写了一个示例程序,创建了一组进程,这些进程只是挂起睡眠,直到它们收到SIG_TERM信号。每个进程都被放置在自己的进程组中(浪费资源,但代码更小)。当您在运行此程序时按Control-C时,它会宣布其SIG_INT,然后向其启动的所有进程发送SIG_TERM。当您按Control-D时,主进程退出。
如果您输入Control-C、Control-D,则可以确定此程序是否有效,并且应通过ps/pgrep发现不存在残留的子进程,并且输出屏幕显示正确配置的(NPROC)数量的进程接收到SIG_TERM(15)。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#ifndef NPROC
#define NPROC 7
#endif
static int children[NPROC];
static int nproc;

static void SigPrint(char *act, int signo) {
    char buf[100];
    int n;
    n = sprintf(buf, "[%d]:(%d) -> %s\n", getpid(), signo, act);
    write(1, buf, n);
}

static void SigDie(int signo) {
    SigPrint("Die", signo);
    _exit(1);
}

static void SigDisp(int signo) {
    SigPrint("Dispatch", signo);
    int i;
    for (i = 0; i < nproc; i++) {
        if (kill(children[i], SIGTERM) < 0) {
            perror("kill");
        }
    }
}

int main(void) {
    signal(SIGINT, SigDisp);
    signal(SIGTERM, SigDie);
    for (nproc = 0; nproc < NPROC; nproc++) {
        children[nproc] = fork();
        if (children[nproc] == 0) {
            signal(SIGINT, SIG_DFL);
            if (setpgid(0, 0) < 0) {
                perror("setpgid");
                _exit(1);
            }
            while (sleep(5)) {
            }
            _exit(0);
        } else if (children[nproc] < 0) {
            perror("fork");
            kill(getpid(), SIGINT);
            perror("kill");
            _exit(1); /*Just in case... */
        }
    }
    int c;
    while ((c = getchar()) != EOF) {
        putchar(c);
    }
    return 0;
}

当你运行这个代码时,你应该得到类似以下的输出:
^C[15141]:(2) -> Dispatch
[15142]:(15) -> Die
[15143]:(15) -> Die
[15146]:(15) -> Die
[15144]:(15) -> Die
[15145]:(15) -> Die
[15147]:(15) -> Die
[15148]:(15) -> Die

使用不同的值作为[pid]字段。

该程序展示了如何将子进程与父进程的通知分离。虽然这里的模型非常复杂,但可以做得更好。


请注意,sprintf()不被POSIX保证为异步信号安全。在Linux上它是不安全的,只是多线程安全,但在Solaris上它是异步信号安全的 - Andrew Henle
谢谢,这太棒了。本地化毁了一切。 - mevets

0

我假设你通过 kill -2 发送了Ctrl-C,以便你可以轻松中断后台进程。

你可以通过检查终端前台进程组轻松地发现你的进程是在前台还是后台运行:

#include <unistd.h>

int isForeground() 
{
    pid_t pid = tcgetpgrp(STDIN_FILENO);
    if(pid == -1 /* piped */ || pid == getpgrp() /* foreground */)
        return 1;
    /* otherwise background */
    return 0;
}

使用该函数来决定是否要使用signal()函数注册您的信号处理程序。
if (isForeground())
    // register my custom signal handler
    if (SIG_ERR == signal(SIGINT, mySignalHandler))
        printf("unable to catch SIGINT!\n");

或者在你的信号处理函数内部

void handler(int sig)
{
    // background processes shall be left unaffected
    if (0 == isForeground())
        return;

    // otherwise process the signal accordingly, e.g. cleaning things up
    // and using exit() to terminate
    ...
}

这在我的Linux系统上可以工作。请注意,signal()的语义可能因不同的Unix系统(BSD、SysV)而异,这可能会影响此代码的可移植性。


1
这不是本质上有风险吗? - EOF

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接