Reputation: 720
I am writing a simple shell in C.
However, I found that my program cannot properly handle the Ctrl+Z signal. My program looks like this:
#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();
}
}
I have ignored above signals in the main process.
When I start cat(1)
and then hit Ctrl+Z, the next line of input will still be captured by the cat(1)
program rather than my main process. It means that my main process will do nothing but if I wake up the cat(1)
program, it will output what I typed immediately. All things go back to normal after this.
I can't figure out how to resolve this. I am still not sure if I have stated it clearly.
Upvotes: 4
Views: 7953
Reputation: 21213
Interesting. Even though this is tagged Linux, I'll go out on a limb and say that you are running this on OS X.
When compiled on Linux, the problem is not there, but on Mac it happens exactly as you described. It looks like a bug in OS X: because both the shell process and cat(1)
are on the same process group (since you don't explicitly change group membership), it seems like OS X makes the mistake of feeding the next input line to the fgets(3)
call that is asleep in the cat(1)
process, so you end up losing that line of input from the shell process (because it is consumed by the sleeping cat(1)
).
The reason this doesn't happen with bash is because bash supports job control, and as such processes are put in separate process groups (in particular, bash chooses the first process of a process pipeline as the process group leader). So when you do the same thing on bash, each invocation of cat(1)
ends up putting it in a separate process group (and then the shell controls which process group is in the foreground with tcsetpgrp(3)
). So, at any time, it is clear which process group has control over terminal input; the moment you suspend cat(1)
in bash, the foreground process group is changed to bash again and input is read successfully.
If you do the same as bash in your shell, it will work in Linux, OS/X, and basically any other UNIX variant (and it is how other shells do it too).
In fact, if you want your shell to have job support, you'll have to do this sooner or later (learn about process groups, sessions, tcsetpgrp(3)
, setpgid(2)
, etc.).
So, in short, do the right thing if you want job support and wrap the forked process in a new process group:
#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();
}
}
(Although, admittedly, it is unfortunate that OS X does such a poor job in this situation - you really shouldn't have to do this).
The changes are just inside the process-specific code: both the child and the parent call setpgid(2)
to make sure that the newborn process is indeed in a single process group before either the parent of the process itself assumes that this is already true (this pattern is recommended in Advanced Programming in the UNIX Environment); the tcsetpgrp(3)
call must be invoked by the parent.
Of course, this is far from complete, you then need to code the necessary functions to bring a job back to the foreground, list jobs, etc. But the code above works with your test scenario nonetheless.
Nitpick: you should be using sigaction(2)
instead of the deprecated, unreliable and platform-dependent signal(3)
, but it's a minor issue here.
Upvotes: 1