Why Linux Always Output "^C" Upon Pressing of Ctrl+C

Why Linux always output ^C upon pressing of Ctrl+C?

It is the terminal (driver) that intercepts the ^C and translates it to a signal sent to the attached process (which is the shell) stty intr ^B would instruct the terminal driver to intercept a ^B instead. It is also the terminal driver that echoes the ^C back to the terminal.

The shell is just a process that sits at the other end of the line, and receives it's stdin from your terminal via the terminal driver (such as /dev/ttyX), and it's stdout (and stderr) are also attached to the same tty.

Note that (if echoing is enabled) the terminal sends the keystrokes to both the process (group) and back to the terminal. The stty command is just wrapper around the ioctl()s for the tty driver for the processes "controlling" tty.

UPDATE: to demonstrate that the shell is not involved, I created the following small program. It should be executed by its parent shell via exec ./a.out (it appears an interactive shell will fork a daughter shell, anyway) The program sets the key that generates the SIGINTR to ^B, switches echo off, and than waits for input from stdin.

#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>

int thesignum = 0;
void handler(int signum);

void handler(int signum)
{ thesignum = signum;}

#define THE_KEY 2 /* ^B */

int main(void)
{
int rc;
struct termios mytermios;

rc = tcgetattr(0 , &mytermios);
printf("tcgetattr=%d\n", rc );

mytermios.c_cc[VINTR] = THE_KEY; /* set intr to ^B */
mytermios.c_lflag &= ~ECHO ; /* Dont echo */
rc = tcsetattr(0 , TCSANOW, &mytermios);
printf("tcsetattr(intr,%d) =%d\n", THE_KEY, rc );

printf("Setting handler()\n" );
signal(SIGINT, handler);

printf("entering pause()\n... type something followed by ^%c\n", '@'+THE_KEY );
rc = pause();
printf("Rc=%d: %d(%s), signum=%d\n", rc, errno , strerror(errno), thesignum );

// mytermios.c_cc[VINTR] = 3; /* reset intr to ^C */
mytermios.c_lflag |= ECHO ; /* Do echo */
rc = tcsetattr(0 , TCSANOW, &mytermios);
printf("tcsetattr(intr,%d) =%d\n", THE_KEY, rc );

return 0;
}

intr.sh:

#!/bin/sh
echo $$
exec ./a.out
echo I am back.

Ctrl + C interrupt event handling in Linux

When you press Ctr + C, the operating system sends a signal to the process. There are many signals and one of them is SIGINT. The SIGINT ("program interrupt") is one of the Termination Signals.

There are a few more kinds of Termination Signals, but the interesting thing about SIGINT is that it can be handled (caught) by your program. The default action of SIGINT is program termination. That is, if your program doesn't specifically handle this signal, when you press Ctr + C your program terminates as the default action.

To change the default action of a signal you have to register the signal to be caught. To register a signal in a C program (at least under POSIX systems) there are two functions

  1. signal(int signum, sighandler_t handler);
  2. sigaction(int signum, const struct sigaction *act,
    struct sigaction *oldact);.

These functions require the header signal.h to be included in your C code. I have provide a simple example of the signal function below with comments.

#include <stdio.h>
#include <stdlib.h>
#include <signal.h> // our new library
volatile sig_atomic_t flag = 0;
void my_function(int sig){ // can be called asynchronously
flag = 1; // set flag
}

int main(){
// Register signals
signal(SIGINT, my_function);
// ^ ^
// Which-Signal |-- which user defined function registered
while(1)
if(flag){ // my action when signal set it 1
printf("\n Signal caught!\n");
printf("\n default action it not termination!\n");
flag = 0;
}
return 0;
}

Note: you should only call safe/authorized functions in signal handler. For example avoid calling printf in signal handler.

You can compile this code with gcc and execute it from the shell. There is an infinite loop in the code and it will run until you send a SIGINT signal by pressing Ctr + C.

Sender pid is zero when receiving SIGINT via CTRL-C

No system calls get invoked, by any process, when SIGINT gets sent as a result of CTRL-C getting pressed. That's why the sender's PID is zero.

The signal gets sent from the kernel. Specifically, the TTY module. Each process is (optionally) tied to a terminal, the "controlling terminal". Each terminal has a defined key sequence that generates a SIGINT to all processes that are tied to the terminal. The default key sequence is, of course, CTRL-C.

You can think of PID 0 as the "kernel process", in this instance.

Reference:

  • The stty(1) manual page.

See if you can spot CTRL-C in stty's output:

$ stty -a
speed 38400 baud; rows 24; columns 80; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?;
swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; discard = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff
-iuclc ixany imaxbel iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
echoctl echoke -extproc
  • See also the "NOTES" section of the setpgid(2) manual page for more information on controlling terminals.

How does Ctrl-C terminate a child process?

Signals by default are handled by the kernel. Old Unix systems had 15 signals; now they have more. You can check </usr/include/signal.h> (or kill -l). CTRL+C is the signal with name SIGINT.

The default action for handling each signal is defined in the kernel too, and usually it terminates the process that received the signal.

All signals (but SIGKILL) can be handled by program.

And this is what the shell does:

  • When the shell running in interactive mode, it has a special signal handling for this mode.
  • When you run a program, for example find, the shell:

    • forks itself
    • and for the child set the default signal handling
    • replace the child with the given command (e.g. with find)
    • when you press CTRL+C, parent shell handle this signal but the child will receive it - with the default action - terminate. (the child can implement signal handling too)

You can trap signals in your shell script too...

And you can set signal handling for your interactive shell too, try enter this at the top of you ~/.profile. (Ensure than you're a already logged in and test it with another terminal - you can lock out yourself)

trap 'echo "Dont do this"' 2

Now, every time you press CTRL+C in your shell, it will print a message. Don't forget to remove the line!

If interested, you can check the plain old /bin/sh signal handling in the source code here.

At the above there were some misinformations in the comments (now deleted), so if someone interested here is a very nice link - how the signal handling works.

What is the difference between Ctrl-C and SIGINT?

^C sends a SIGINT to all the processes in the foreground process group. To do the equivalent with kill, you should send the signal to the process group (OS-level concept):

kill -SIGINT -<pid>

or to the job (shell-level concept, the pipeline ended with &):

kill -SIGINT %


Related Topics



Leave a reply



Submit