I encountered this problem when doing shell lab from the book CSAPP, the lab ask you to implement your own version of shell with some specification,one of them is
Typing ctrl-c (ctrl-z) should cause a SIGINT (SIGTSTP) signal to be
sent to the current foreground job, as well as any descendents of that
job (e.g., any child processes that it forked). If there is no
foreground job, then the signal should have no effect.
so you should complete one of the given functions called sigint_handler which supposed to catch SIGINT signal and send it along to the foreground job. below is a piece of code I find online(the code passed the correctness check)
void sigint_handler(int sig)
{
int olderrno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
kill(-pid, sig);
errno = olderrno;
return;
}
what I don't understand is if SIGINT is sent using kill,then the descendents of foreground job will also use this handler to catch SIGINT signal right? so it's kind of a recursive call to me.so how does this actually work? thanks for helping me.
handler is installed in the main function
signal(SIGINT, sigint_handler); /* ctrl-c */
and fgpid return PID of current foreground job, 0 if no such job
Once a child process calls execve(), the child's (usually short-lived) initial address space is freed/released and replaced with space for the specified executable image; now the child no longer has a copy or access to the parent's data or text, like signal handlers.
Now consider a process-group associated with the control-terminal (tty). When a user types a CTRL-C (or CTRL-\ or CTRL-Z), the tty driver posts a signal to 1+ processes as members of the associated process-group. The result of delivering a signal would be the system default action unless a process established a different signal disposition (signal(), sigaction(), or related).
The posted code excerpt indicates a relayed event: user types a CTRL-C, tty driver posts a SIGINT to the shell, shell's handler looks for a foreground job, calls kill() with a negative pid to post a signal to members of that process-group.
For related info see these man pages:
man setpgrp
man tty_ioctl (symbols: TIOCSCTTY, TIOCGPGRP, TIOCSPGRP)
alternates:
man tcgetpgrp tcsetpgrp
Related
I am writing a shell in C and I am trying to add signal handling. In the shell, fork() is called and the child process executes a shell command. The child process is put into its own process group. This way, if Ctrl-C is pressed when a child process is in the foreground, it closes all of the processes that share the same process group id. The shell executes the commands as expected.
The problem is the signals. When, for example, I execute "sleep 5", and then I press Ctrl-C for SIGINT, the "shell>" prompt comes up as expected but the process is still running in the background. If I quickly run "ps" after I press Ctrl-C, the sleep call is still there. Then after the 5 seconds are up and I run "ps" again, it's gone. The same thing happens when I press Ctrl-Z (SIGTSTP). With SIGTSTP, the process goes to the background, as expected, but it doesn't pause execution. It keeps running until it's finished.
Why are these processes being sent to the background like this and continuing to run?
Here is the gist of my code...
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int status;
void sig_handler_parent()
{
printf("\n");
}
void sig_handler_sigchild(int signum)
{
waitpid(-1, &status, WNOHANG);
}
int main()
{
signal(SIGCHLD, sig_handler_sigchild);
signal(SIGINT, sig_handler_parent);
signal(SIGQUIT, sig_handler_parent);
signal(SIGTERM, sig_handler_parent);
signal(SIGCONT, sig_handler_parent);
signal(SIGTSTP, sig_handler_parent);
while (1)
{
printf("shell> ");
// GET COMMAND INPUT HERE
pid = fork();
if (pid == 0)
{
setpgid(getpid(), getpid());
execvp(cmd[0], cmd);
printf("%s: unknown command\n", cmd[0]);
exit(1);
}
else
waitpid(0, &status, WUNTRACED);
}
return 0;
}
p.s. I have already tried setting all of the signal handlers to SIG_DFL before the exec command.
The code you provide does not compile, and an attempt to fix it shows
that you omitted a lot. I am only guessing.
In order to bring you forward, I'll point out a number of facts that
you might have misunderstood. Together with a couple of documentation
links, I hope this is helpful.
Error Handling
First: please make a habit of handling errors, especially when you
know there's something that you don't understand. For example, the
parent (your shell) waits until the child terminates,
waitpid(0, &status, WUNTRACED);
You say,
When, for example, I execute "sleep 5", and then I press Ctrl-C for
SIGINT, the "shell>" prompt comes up as expected but the process is
still running in the background.
What actually happens is that once you press Ctrl-C, the parent (not the
child; see below for why) receives SIGINT (the kernel's terminal
subsystem handles keyboard input, sees that someone holds "Ctrl" and
"C" at the same time, and concludes that all processes with that
controlling terminal must be sent SIGINT).
Change the parent branch to,
int error = waitpid(0, &status, WUNTRACED);
if (error != 0)
perror("waitpid");
With this, you'd see perror() print something like:
waitpid: interrupted system call
You want SIGINT to go to the child, so something must be wrong.
Signal Handlers, fork(), and exec()
Next, what happens to your signal handlers across fork() and
exec()?
The signal overview man
page states,
A child created via fork(2) inherits a copy of its parent's signal
dispositions. During an execve(2), the dispositions of handled
signals are reset to the default; the dispositions of ignored
signals are left unchanged.
So, ideally, what this means is that:
The parent (shell) sees SIGINT, as observed above, and prints
"interrupted system call".
The child's signal handlers are reset back to their defaults. For SIGINT,
this means to terminate.
You do not fiddle with the controlling terminal, so the child
inherits the controlling terminal of the parent. This means that
SIGINT is delivered to both parent and child. Given that the
child's SIGINT behavior is to terminate, I'd bet that no process is
left running.
Except when you use setpgid() to create a new process group.
Process Groups, Sessions, and Controlling Terminal
Someone once called me a UNIX greybeard. While this is true form a
visual point of view, I must reject that compliment because I rarely
hang around in one of the darkest corners of UNIX - the terminal
subsystem. Shell writers have to understand that too though.
In this context, it's the "NOTES" section of the setpgid() man
page. I suggest
you read that, especially where it says,
At any time, one (and only one) of the process groups in the session
can be the foreground process group for the terminal; (...)
The shell (bash maybe) from which you start your shell program has
done so for the foreground invocation of your program, and marked that
as "foreground process group". Effectively this means, "Please, dear
terminal, whenever someone presses Ctrl-C, send a SIGINT to all
processes in that group. I (your parent) just sit and wait (waitpid()) until all is over, and will take control again then.".
You create a process group for the child, but don't tell the terminal
about it. What you want is to
Detach the parent from the terminal.
Set the child process group as the foregroud process group of the terminal.
Wait for the child (you already do).
Regain terminal foreground.
Further down in the "NOTES" section of said man page, they give links
to how that is done. Follow those, read thoroughly, try out things,
and make sure you handle errors. In most cases, such errors are signs
of misunderstanding. And, in most cases, such errors are fixed by
re-reading the documentation.
Are you sure that your child process is actually receiving the signals from your tty? I believe you need to make a call to tcsetpgrp to actually tell the controlling terminal to send signals to the process group of your child process.
For example, after you call fork, and before exec, try this from within your child.
tcsetpgrp(STDIN_FILENO, getpid())
Here is the man page for tcsetpgrp(3)
I'm writing a basic unix shell in C and I would like to catch Cntrl-C signals in the shell and pass them to foreground processes only, but not to background processes. The shell itself should keep running (and it does), and the Background processes should ignore a Cntrl-C and only be killed by a kill signal sent specifically to them, perhaps via a command-line "kill pid". Both foreground and background processes, however, should trigger the handler with SIGCHLD. Right now, however, the shell catches the Cntrl-C signal, and seems to correctly identify that there is no foreground process to pass the signal to, but the background process still dies.
I tried setting the group id of the background process to something else, and that solves the problem, but it creates a new problem. When I do that, my signal handler no longer catches the signal when the background process completes.
So far, I've looked at the man pages for SIGINT, I've read about 20 SO answers, I've tried setting the group id of the child to something different than the parent (solves the problem, but now child can no longer send SIGCHLD to parent), and I've checked that childid != foreground process and foregroundProcess == 0 when I'm running a background process. But still the background process gets killed. Any ideas?
I think my problem is somewhere in my signal handler, but not really sure:
in main:
struct sigaction sa;
sa.sa_handler = &handleSignal; /*passing function ref. to handler */
sa.sa_flags = SA_RESTART;
sigfillset(&sa.sa_mask); /*block all other signals while handling sigs */
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
sigaction(SIGCHLD, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
handleSignal looks like this:
void handleSignal(int signal){
int childid;
switch (signal) {
/*if the signal came from a child*/
case SIGCHLD:
/*get the child's id and status*/
childid = waitpid(-1,&childStatus,0);
/*No action for foreground processes that exit w/status 0 */
/*otherwise show pid & showStatus */
if ((childid != foregroundProcess)){
printf("pid %i:",childid);
showStatus(childStatus);
fflush(stdout);
}
break;
/* if signal came from somewhere else, pass it to foreground child */
/* if one exists. */
default:
printf("Caught signal: %i and passing it", signal);
printf(" to child w/pid: %i\n\n:", foregroundProcess);
fflush(stdout);
/*If there is a child, send signal to it. */
if (foregroundProcess){
printf("trying to kill foreground.\n");
fflush(stdout);
kill(foregroundProcess, signal);
}
}
}
Found an answer to my own question. I had already tried changing the group id of the background child process using setpid(0,0); and this worked, but created a different problem. After that call, I was no longer catching SIGCHLD signals from the child in the parent. This is because once the process group of the child is changed, it is essentially no longer connected to the parent for signalling purposes. This solved the problem of the background processes catching Cntrl-C (SIGINT) signals from the parent (undesired behavior), but prevented the background process from signalling the parent when complete. Solved one problem only to create another.
Instead, the solution was to detect whether a child was about to be created as a foreground or a background process, and if background, tell it to ignore the SIGINT signal: signal(SIGINT, SIG_IGN);
Since it's probably the first process on the tty, the kernel will send the signal to all its children when it sends a kernel-initiated SIGINT, SIGHUP, or SIGQUIT. See Terminate sudo python script when the terminal closes for some more details on that, and ideas for tracing/debugging what's happening to make sure you have it right.
An alternative to starting bg child processes out ignoring SIGINT (and SIGQUIT, and maybe SIGHUP?) is to avoid having the kernel deliver those signals to the shell.
I forget, but I think the kernel only delivers SIGINT to the current foreground process. Your shell won't get it if cat or something is running in the foreground. So you only have to worry about what happens while the shell is in the foreground. (I'm only about 80% sure of this, though, and this idea is useless if the shell (and thus all its children) always receive the SIGINT.)
If you disable the tty's signal-sending when the user is editing a command prompt, you can avoid having the shell receive SIGINT. Probably the best way is to do the equivalent of stty -isig, using tcsetattr(3)
// disable interrupts
struct termios term_settings;
tcgetattr(fd, &term_settings); // TODO: check errors
term_settings.c_lflag &= ~ISIG; // clear the interactive signals bit
tcsetattr(fd, TCSANOW, &term_settings);
// do the reverse (settings |= ISIG) after forking, before exec
If you strace that, you'll just see ioctl system calls, because that's the system call the termios library functions are implemented on top of.
I guess this leaves a tiny time window between a child process ending, and wait() returning and the shell disabling terminal interrupts.
IIRC, I read something about it being tricky for bash to keep track of when to swap the terminal between raw (for line editing) and cooked (for commands to be run), but I think that's only because of job control. (^z / fg)
I am trying to implement a shell program and I want the shell program to ignore SIG_INT(ctrl + c). But in my program the child also ignores the SIG_INT signal, which it should not because exec should take the child process to another program and that program should handle the SIG_INT signal by default. What should I do so that the child process terminates when ctrl + c is pressed.
Newly edited: after I put signal(certain_signal, SIG_DFL) in the block my child process, my code works fine. But I am still confused about how this work. Does this mean that signals as well as signal disposition can both propagate through execute command?
int main(void){
signal(SIG_INT, SIG_IGN);
int result = fork();
if(result == 0){
//child:
//exec some programs
}
else{
waitpid(result);
//do something
}
}
I believe you have misunderstood slightly how exec modifies signal dispositions. In the Linux exec man page, for example(1), it states that (my emphasis):
All process attributes are preserved during an execve(), except the following:
The dispositions of any signals that are being caught are reset to the default (signal(7)).
<Lots of other irrelevant stuff, in the context of this question>
Signals that are being caught are not the same as signals that are being ignored, as evidenced by the signal man page:
Using these system calls, a process can elect one of the following behaviors to occur on delivery of the signal:
perform the default action;
ignore the signal; or
catch the signal with a signal handler, a programmer-defined function that is automatically invoked when the signal is delivered.
That actually makes some sense because, while ignoring a signal can propagate through the exec call, a signal handler cannot - the function that is meant to handle the signal has been replaced by the exec call, so attempting to call it would most likely be catastrophic.
You can see this behaviour of inheriting the "ignore" disposition by compiling the following two programs, qqp.c:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int main (void) {
signal (SIGINT, SIG_IGN);
puts("Parent start");
if (fork() == 0)
execl ("./qqc", 0);
wait(0);
sleep (1);
puts("Parent end");
return 0;
}
and qqc.c:
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main (void) {
//signal (SIGINT, SIG_DFL);
puts("Child start");
sleep (60);
puts("Child end");
return 0;
}
Note that you could also change the disposition in the first code sample, between the fork and the exec. This would be preferable in cases where you don't actually control what the second code sample will do (such as if you're calling an executable you didn't compile).
Running qqp, neither the parent nor child will exit prematurely no matter how many times you press CTRL-C. But, uncomment out the line that reverts to default behaviour and you can break out of the child easily.
So, if you want your child to revert to the default behaviour, you need to do that in the child itself, with something like:
signal (SIG_INT, SIG_DFL);
(1) POSIX has a little more detail on what happens:
Signals set to the default action (SIG_DFL) in the calling process image shall be set to the default action in the new process image. Except for SIGCHLD, signals set to be ignored (SIG_IGN) by the calling process image shall be set to be ignored by the new process image. Signals set to be caught by the calling process image shall be set to the default action in the new process image (see <signal.h>). If the SIGCHLD signal is set to be ignored by the calling process image, it is unspecified whether the SIGCHLD signal is set to be ignored or to the default action in the new process image.
And, just on your edit that my proposed solution works, but that it raises another question for you:
Does this mean that signals as well as signal disposition can both propagate through execute command?
Signals themselves do not propagate through the exec call, a signal is actually the "interrupt" being generated. That's different to a signal handler (code to handle a signal) or the signal disposition (what to do when the signal occurs). As shown above, dispositions may survive the exec call but handlers cannot. Signals also do not.
What you're seeing when you press CTRL-C and multiple processes are affected has nothing to do with inheriting signals across the exec boundary, it's more to do with the terminal stuff.
A signal delivered to an individual process will not affect any of its child processes. However, pressing CTRL-C does not send a signal to an individual process. The POSIX terminal interface has a concept of controlling terminals and process groups:
Each process also is a member of a process group. Each terminal device records a process group that is termed its foreground process group. The process groups control terminal access and signal delivery. Signals generated at the terminal are sent to all processes that are members of the terminal's foreground process group.
I am writing a shell, now it comes to control the child process.
When I use signal (SIGTERM, SIG_DFL); in the child process,
the signal SIGINT is generated by Ctrl + C, and that signal terminates whole the OS shell.
how can I just terminate the process e.g “cat” only, but not whole shell??
Should I use somethings like:
void sig_handler(int sig) {
if(sig ==SIGINT)
{
kill(pid);
}
}
Really thanks a slot.
Your question is rather vague. Can you be more clear on what you want to achieve?
I think you should be using signal(SIGTERM, sig_handler) instead of SIG_DFL which is the default action taken. Since, you have a signal handler, you call it instead of predefined functions like SIG_INT or SIG_DFL. The code inside your function looks fine. As long as you know the pid, you can do a kill(pid).
In the exec'd child, the SIGINT (and SIGQUIT) handlers will be SIG_DFL if they were set to a handler in the parent shell, and that's most likely correct. (You can't inherit a non-default signal handler across an exec, of course, because the function usually doesn't even exist in the exec'd process.)
Setting a handler for SIGTERM won't affect the response to SIGINT, or vice versa.
Your shell shouldn't need to deliver signals to its children.
I am writing a mini-shell(no, not for school :P; for my own enjoyment) and most of the basic functionality is now done but I am stuck when trying to handle SIGTSTP.
Supposedly, when a user presses Ctrl+Z, SIGTSTP should be sent to the Foreground process of the shell if it exists, and Shell should continue normally.
After creating each process(if it's a Foreground process), the following code waits:
if(waitpid(pid, &processReturnStatus, WUNTRACED)>0){//wait stopped too
if(WIFEXITED(processReturnStatus) || WIFSIGNALED(processReturnStatus))
removeFromJobList(pid);
}
And I am handling the signal as follows:
void sigtstpHandler(int signum)
{
signum++;//Just to remove gcc's warning
pid_t pid = findForegroundProcessID();
if(pid > -1){
kill(-pid, SIGTSTP);//Sending to the whole group
}
}
What happens is that when I press Ctrl+Z, the child process does get suspended indeed(using ps -all to view the state of the processes) but my shell hangs at waitpid it never returns even though I passed WUNTRACED flag which as far as I understood is supposed to make waitpid return when the process is stopped too.
So what could I have possible done wrong? or did I understand waitpid's behavior incorrectly?
Notes:
-findForegroundProcessID() returns the right pid; I double checked that.
-I am changing each process's group when right after I fork
-Handling Ctrl+C is working just fine
-If I use another terminal to send SIGCONT after my shell hangs, the child process resumes its work and the shell reaps it eventually.
-I am catching SIGTSTP which as far as I read(and tested) can be caught.
-I tried using waitid instead of waitpid just in case, problem persisted.
EDIT:
void sigchldHandler(int signum)
{
signum++;//Just to remove the warning
pid_t pid;
while((pid = waitpid(-1, &processReturnStatus, 0)) > 0){
removeFromJobList(pid);
}
if(errno != ECHILD)
unixError("kill error");
}
My SIGCHLD handler.
SIGCHLD is delivered for stopped children. The waitpid() call in the signal handler - which doesn't specify WUNTRACED - blocks forever.
You should probably not have the removeFromJobList() processing in two different places. If I had to guess, it sounds like it touches global data structures, and doesn't belong in a signal handler.
Waitpid is not returning because you are not not setting a sigchld handler (which I sent you earlier). You have child processess that are not getting reaped. Furthermore, waitpid needs to be in a while loop, not an if (also sent you that).
The only signal you are supposed to catch is SIGCHLD. The reason being is that if your processes are forked properly, the kernel will send that signal to the foreground process and it will terminate it or stop it or do whatever the signal is properly.
When process groups are not set correctly, signals will get sent to the wrong process. One way to test that is by running a foreground process and hitting Ctrl-Z. If your entire shell exists, then Ctrl-Z signal is getting sent to the entire shell. This means you did not set the new process in a new process group and gave it a terminal.
Now here's what you need to do if your Ctrl-Z signal is stopping your entire shell. Once you fork a process, in the child process:
- Set the process in its own group using setpgid.
- Give it a sane terminal by blocking SIGTTOU and then giving it the terminal using tcsetpgrp.
In the parent:
- Also set its child process using setpgid. This is because you have no idea if the child or the parent will execute first, so this avoids a race condition. It doesn't hurt to set it twice.