【问题标题】:Ctrl+Z Signal handling in CC 中的 Ctrl+Z 信号处理
【发布时间】:2016-01-08 23:19:23
【问题描述】:

我正在用 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) 程序,它将立即输出我输入的内容。此后一切恢复正常。

我不知道如何解决这个问题。我还不确定我是否说清楚了。

【问题讨论】:

  • 这很不清楚:下一行输入仍然会转到 cat 而不是我的 main...
  • 让我举个例子。猫程序暂停后,我输入“猫”,应该启动另一只猫但没有任何反应。如果我唤醒之前的cat程序,它会立即输出“cat”。
  • 你能发布完整的代码、预期的行为和你得到的行为吗?
  • @FilipeGonçalves 我已经发布了完整的代码。输入:“cat”“ctrl-d”“cat”,预期行为:一只猫开始然后暂停,一只新猫开始,现在行为:一只猫开始然后暂停。
  • 吹毛求疵:没有Ctrl-Z 信号之类的东西。 Ctrl-Z只是一个普通的键盘组合,让终端设备驱动发送SIGTSTP到前台进程组;但它可以作为终端设置的一部分进行更改(请参阅tcsetattr(3))。

标签: c macos signals


【解决方案1】:

有趣。即使它被标记为 Linux,我还是会说你在 OS X 上运行它。

在 Linux 上编译时,问题不存在,但在 Mac 上,它完全按照您的描述发生。它看起来像 OS X 中的一个错误:因为 shell 进程和 cat(1) 都在同一个进程组上(因为你没有显式地更改组成员身份),看起来 OS X 错误地输入了下一个输入行到在cat(1) 进程中休眠的fgets(3) 调用,因此您最终会丢失来自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),但这是一个小问题。

【讨论】:

  • 非常感谢您的帮助!
  • @vincentzhou 很高兴我能帮上忙。祝你的项目好运:)
  • 您的代码运行良好。但是,我在我的程序中添加了管道和作业控制功能,这使事情变得相当复杂。对于命令“A|B|C”,我的主进程将在循环中分叉子进程,然后等待它们。就我而言,单行命令的所有进程都应该属于一个组,我将组 ID 设置为第一个家伙。对于 tcsetgrp,我不确定应该将 stdout 绑定到哪个进程。目前看来之前的问题已经解决了,但是当我唤醒一个挂起的程序时,出现错误“stdin: Interrupted system call”。
  • @vincentzhou 是的,管道中的所有进程都应该共享同一个组。您应该将进程组 ID 传递给tcsetpgrp(3);这与组长的 PID 相同(您提到的是管道中的第一个进程)。关于中断的系统调用错误:您需要开始使用sigaction(2) 并在struct sigactionsa_flags 字段中设置SA_RESTART 标志(有关更多信息,请参阅man 7 signal。),或显式测试errno == EINTR在您的代码中并再次调用 read 函数。如果这一切对你来说看起来很复杂,我建议发布另一个问题。
  • 其实是cat程序输出了错误信息,我无法让cat再次读取。我尝试了第一种方法,但不起作用。但是osx的内置终端好像也有这个bug。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-11
  • 2016-07-16
  • 2011-09-23
  • 2011-01-29
相关资源
最近更新 更多