我在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)
程序,它将输出我立即键入的内容。此后一切都恢复正常。
我无法弄清楚如何解决这个问题。我仍然不确定我是否已经说清楚了。
答案 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)
,但这只是一个小问题。