Why Linux always output “^C” upon pressing of Ctrl

2019-03-27 19:26发布

问题:

I have been studying signals in Linux. And I've done a test program to capture SIGINT.

#include <unistd.h>
#include <signal.h>
#include <iostream>
void signal_handler(int signal_no);
int main() {
  signal(SIGINT, signal_handler);
  for (int i = 0; i < 10; ++i) {
  std::cout << "I'm sleeping..." << std::endl;
  unsigned int one_ms = 1000;
  usleep(200* one_ms);
  }
  return 0;
}
void signal_handler(int signal_no) {
  if (signal_no == SIGINT)
    std::cout << "Oops, you pressed Ctrl+C!\n";
  return;
}

While the output looks like this:

I'm sleeping...
I'm sleeping...
^COops, you pressed Ctrl+C!
I'm sleeping...
I'm sleeping...
^COops, you pressed Ctrl+C!
I'm sleeping...
^COops, you pressed Ctrl+C!
I'm sleeping...
^COops, you pressed Ctrl+C!
I'm sleeping...
^COops, you pressed Ctrl+C!
I'm sleeping...
I'm sleeping...
I'm sleeping...

I understand that when pressing Ctrl+C, processes in foreground process group all receives a SIGINT(if no process chooses to ignore it).

So is it that the shell(bash) AND the the instance of the above program both received the signal? Where does the "^C" before each "Oops" come from?

The OS is CentOS, and the shell is bash.

回答1:

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.


回答2:

The shell echoes everything you type, so when you type ^C, that too gets echoed (and in your case intercepted by your signal handler). The command stty -echo may or may not be useful to you depending on your needs/constraints, see the man page for stty for more information.

Of course much more goes on at a lower level, anytime you communicate with a system via peripherals device drivers (such as the keyboard driver that you use to generate the ^C signal, and the terminal driver that displays everything) are involved. You can dig even deeper at the level of assembly/machine language, registers, lookup tables etc. If you want a more detailed, in-depth level of understanding the books below are a good place to start:

The Design of the Unix OS is a good reference for these sort of things. Two more classic references: Unix Programming Environment and Advanced Programming in the UNIX Environment

Nice summary here in this SO question How does Ctrl-C terminate a child process?

"when youre run a program, for example find, the shell:

  • the shell fork 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)"


回答3:

I did not find any of these answers satisfying because (on Linux) neither ksh nor tcsh exhibits this behavior while bash does. A bit of poking around revealed that the readline library (which bash uses) is the culprit (see https://unix.stackexchange.com/questions/333766/remove-c-when-ctrlc). The behavior is controlled with "echo-control-characters" in $HOME/.inputrc. Setting it to "off" will disable the behavior. The behavior of already-running processes is not changed by this setting.