Ctrl + Z C中的信号处理

时间:2015-10-11 12:17:38

标签: c macos signals

我在C中写了一个简单的shell。

但是,我发现我的程序无法正确处理 Ctrl + Z 信号。我的程序看起来像这样:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
void interpreter() {
char input[256];
int i;
char dir[PATH_MAX+1];
char *argv[256];
int argc = 0;
char *token;
if (getcwd(dir, PATH_MAX+1) == NULL) {
    //error occured
    exit(0);
}
printf("[shell:%s]$ ", dir);
fgets(input,256,stdin);
if (strlen(input) == 0) {
    exit(0);
}
input[strlen(input)-1] = 0;
if (strcmp(input,"") == 0) {
    return;
}
token = strtok(input, " ");
while(token && argc < 255) {
    argv[argc++] = token;
    token = strtok(NULL, " ");
}
argv[argc] = 0;
    pid_t forknum = fork();
    if (forknum != 0) {
        int status;
        waitpid(forknum, &status, WUNTRACED);
    } else {
        signal(SIGINT, SIG_DFL);
        signal(SIGTERM, SIG_DFL);
        signal(SIGQUIT, SIG_DFL);
        signal(SIGTSTP, SIG_DFL);
        setenv("PATH","/bin:/usr/bin:.",1);
        execvp(argv[0], argv);
        if (errno == ENOENT) {
            printf("%s: command not found\n", argv[0]);
        } else {
            printf("%s: unknown error\n", argv[0]);
        }
        exit(0);
    }
}

int main() {
signal(SIGINT, SIG_IGN);
signal(SIGTERM, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
while(1) {
    interpreter();
}
}

我在主要过程中忽略了上述信号。

当我开始cat(1)然后点击 Ctrl + Z 时,cat(1)程序仍将捕获下一行输入比我的主要过程。这意味着我的主进程什么都不做,但如果我唤醒cat(1)程序,它将输出我立即键入的内容。此后一切都恢复正常。

我无法弄清楚如何解决这个问题。我仍然不确定我是否已经说清楚了。

1 个答案:

答案 0 :(得分:1)

有趣。虽然这是标记的Linux,但我会说你在OS X上运行它。

在Linux上编译时,问题不存在,但在Mac上,它完全按照您的描述发生。它看起来像OS X中的一个错误:因为shell进程和cat(1)都在同一个进程组中(因为你没有明确地改变组成员身份),所以OS X似乎犯了错误的进程在fgets(3)进程中睡眠的cat(1)调用的下一个输入行,因此您最终会从shell进程中丢失该输入行(因为它被睡眠cat(1)消耗)。

bash没有发生这种情况的原因是因为bash支持作业控制,因此这些进程被放在不同的进程组中(特别是,bash选择进程管道的第一个进程作为进程组负责人) 。因此,当您在bash上执行相同操作时,cat(1)的每次调用最终都会将其放在一个单独的进程组中(然后shell控制哪个进程组位于前台tcsetpgrp(3))。因此,在任何时候,都清楚哪个进程组可以控制终端输入;在bash中挂起cat(1)的那一刻,前台进程组再次更改为bash并成功读取输入。

如果你在shell中执行与bash相同的操作,它将在Linux,OS / X以及基本上任何其他UNIX变体中运行(并且它也是其他shell也是如此)。

事实上,如果您希望shell获得工作支持,您迟早必须这样做(了解流程组,会话,tcsetpgrp(3)setpgid(2)等。 )。

因此,简而言之,如果您想要工作支持并将分叉流程包装在新流程组中,请做正确的事情:

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

void interpreter() {
    char input[256];
    char dir[PATH_MAX+1];
    char *argv[256];
    int argc = 0;
    char *token;
    if (getcwd(dir, PATH_MAX+1) == NULL) {
        //error occured
        exit(0);
    }
    printf("[shell:%s]$ ", dir);
    fgets(input,256,stdin);

    if (strlen(input) == 0) {
        exit(0);
    }
    input[strlen(input)-1] = 0;
    if (strcmp(input,"") == 0) {
        return;
    }
    token = strtok(input, " ");
    while(token && argc < 255) {
        argv[argc++] = token;
        token = strtok(NULL, " ");
    }
    argv[argc] = 0;
    pid_t forknum = fork();
    if (forknum != 0) {
        setpgid(forknum, forknum);
        signal(SIGTTOU, SIG_IGN);
        tcsetpgrp(STDIN_FILENO, forknum);
        tcsetpgrp(STDOUT_FILENO, forknum);
        int status;
        waitpid(forknum, &status, WUNTRACED);
        tcsetpgrp(STDOUT_FILENO, getpid());
        tcsetpgrp(STDIN_FILENO, getpid());
    } else {
        setpgid(0, getpid());
        signal(SIGINT, SIG_DFL);
        signal(SIGTERM, SIG_DFL);
        signal(SIGQUIT, SIG_DFL);
        signal(SIGTSTP, SIG_DFL);
        setenv("PATH","/bin:/usr/bin:.",1);
        execvp(argv[0], argv);
        if (errno == ENOENT) {
            printf("%s: command not found\n", argv[0]);
        } else {
            printf("%s: unknown error\n", argv[0]);
        }
        exit(0);
    }
}

int main() {
    signal(SIGINT, SIG_IGN);
    signal(SIGTERM, SIG_IGN);
    signal(SIGQUIT, SIG_IGN);
    signal(SIGTSTP, SIG_IGN);
    while(1) {
        interpreter();
    }
}

(尽管如此,不幸的是,OS X在这种情况下的表现非常糟糕 - 你真的不应该 来做这件事。)

更改只是在特定于流程的代码中:子和父调用setpgid(2)以确保新生成进程确实在单个进程组中,然后进程的父进程才假定这已经成立(在UNIX环境中高级编程中推荐使用此模式);必须由父母调用tcsetpgrp(3)调用。

当然,这还远未完成,您需要编写必要的功能以将作业恢复到前台,列出作业等。但上面的代码仍适用于您的测试场景。

Nitpick:您应该使用sigaction(2)而不是已弃用,不可靠和依赖于平台的signal(3),但这只是一个小问题。