Try   HackMD

Peilin Ye's blog

Understanding Control-C

arch: x86_64
Bash: 5.1, commit 9439ce094c9a ("Bash-5.1 patch 16: fix interpretation of multiple instances of ! in [[ conditional commands")
Linux: 5.18-rc5, commit 9c095bd0d4c4 ("Merge branch 'hns3-next'")
QEMU: 7.0.50, commit 2d20a57453f6 ("Merge tag 'pull-fixes-for-7.1-200422-1' of https://github.com/stsquad/qemu into staging")

ypl@home:~$ sudo make isntall
make: *** No rule to make target 'isntall'.  Stop.
ypl@home:~$ sudo make isntal^C
ypl@home:~$ 
ypl@home:~$ aargh
-bash: aargh: command not found

This post briefly describes what happens in Linux when you press Control-C.

Customizing Your ^C

but why not have some fun first? Ever wondered where does that ^C string come from? Tired of it? Apply this to your Bash:

diff --git a/lib/readline/signals.c b/lib/readline/signals.c
index f9174ab8a014..93b4b637122a 100644
--- a/lib/readline/signals.c
+++ b/lib/readline/signals.c
@@ -765,7 +765,7 @@ rl_echo_signal_char (int sig)

   if (CTRL_CHAR (c) || c == RUBOUT)
     {
-      cstr[0] = '^';
+      cstr[0] = '%';
       cstr[1] = CTRL_CHAR (c) ? UNCTRL (c) : '?';
       cstr[cslen = 2] = '\0';
     }
ypl@home:~/bash$ ^C
ypl@home:~/bash$ ./bash
ypl@home:~/bash$ %C
ypl@home:~/bash$ 

Yay! You just customized your ^C!

8250 UART Serial Driver

I'm using qemu-system-x86_64 (-nographic) because it's easier. It seems that, whenever I press Control-C, my guest Linux's 8250 UART serial driver receives an interrupt, so let's start there!

Feeling adventurous? Start from common_interrupt(), or even QEMU instead!

drivers/tty/serial/8250/8250_core.c:serial8250_interrupt()   /* port->handle_irq() */
                          8250_port.c:serial8250_default_handle_irq()
                                       :serial8250_handle_irq()
                                         :serial8250_rx_chars()

Let's take a closer look at serial8250_rx_chars():

unsigned char serial8250_rx_chars(struct uart_8250_port *up, unsigned char lsr)
{
	struct uart_port *port = &up->port;
	int max_count = 256;

	do {
		serial8250_read_char(up, lsr);
		if (--max_count == 0)
			break;
		lsr = serial_in(up, UART_LSR);
	} while (lsr & (UART_LSR_DR | UART_LSR_BI));

	tty_flip_buffer_push(&port->state->port);
	return lsr;
}

Here,

  1. serial8250_read_char() pushes this Control-C to the TTY flip buffer by calling uart_insert_char();
  2. tty_flip_buffer_push() then queues a work to push this TTY flip buffer to the line discipline (LDISC).

TTY Core

Next, we traverse a few TTY core functions before getting to the line discipline:

drivers/tty/tty_buffers.c:tty_flip_buffer_push()   	/* queue_work() */
...
                         :flush_to_ldisc()
                           :receive_buf() 		/* port->client_ops->receive_buf() */
                   tty_port.c:tty_port_default_receive_buf()
                   tty_buffer.c:tty_ldisc_receive_buf() /* ld->ops->receive_buf2() */

We will be dealing with N_TTY, the default line discipline. See tty_ldisc_init():

int tty_ldisc_init(struct tty_struct *tty)
{
	struct tty_ldisc *ld = tty_ldisc_get(tty, N_TTY);  /* default to N_TTY */

	if (IS_ERR(ld))
		return PTR_ERR(ld);
	tty->ldisc = ld;
	return 0;
}

N_TTY

drivers/tty/n_tty.c:n_tty_receive_buf2()
                     :n_tty_receive_buf_common()
                       :__receive_buf()
                         :n_tty_receive_buf_standard()
                           :n_tty_receive_char_special()
                             :n_tty_receive_signal_char()
                               :isig()
                                 :__isig()

In n_tty_receive_buf_standard(), N_TTY realizes that Control-C is a control character (ASCII 3, the End-of-Text character) and handles it specially. Look for ldata->char_map.

Eventually, as specified in POSIX 1003.1 Section 7.1.1.9, Special Characters:

It generates a SIGINT signal that is sent to all processes in the foreground process group for which the terminal is the controlling terminal.

__isig() sends a SIGINT to all processes in the foreground process group using kill_pgrp():

static void __isig(int sig, struct tty_struct *tty)
{
	struct pid *tty_pgrp = tty_get_pgrp(tty);
	if (tty_pgrp) {
		kill_pgrp(tty_pgrp, sig, 1);
		put_pid(tty_pgrp);
	}
}

Signal Handling

Briefly,

kernel/signal.c:kill_pgrp()
                 :__kill_pgrp_info()
                   :group_send_sig_info()
                     :do_send_sig_info()
                       :send_signal()
                         :__send_signal()
     include/linux/signal.h:sigaddset()

Let's say I pressed Control-C to stop yes. __send_signal() adds a pending SIGINT for yes to be handled later. For example, when yes returns from a system call:

arch/x86/entry/common.c:syscall_exit_to_user_mode()
    kernel/entry/common.c:__syscall_exit_to_user_mode_work()
                           :exit_to_user_mode_prepare()
                             :exit_to_user_mode_loop()
       arch/x86/kernel/signal.c:arch_do_signal_or_restart()
                  kernel/signal.c:get_signal()
                      kernel/exit.c:do_group_exit()
                                     :do_exit()
                    kernel/sched/core.c:do_task_dead()

get_signal() calls do_group_exit() to terminate yes, since that's the default action for SIGINT, see POSIX 1003.1 Table 3-1, Required Signals. In this case, get_signal() never returns.

On the other hand, if the process registered a handler for SIGINT, get_signal() returns to arch_do_signal_or_restart() instead:

void arch_do_signal_or_restart(struct pt_regs *regs, bool has_signal)
{
	struct ksignal ksig;

	if (has_signal && get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}
...

handle_signal() will "actually deliver the signal" to user space. This is true for Bash, for example.

That's it! "Whee!"

Appendix A: Signal Handling in GNU Readline

Interestingly, when I press Control-C while Bash is reading from stdin, the bash process actually receives SIGINT twice. See my printk() output:

[   17.337664] [bash, 0xffff96428638ba00] arch_do_signal_or_restart(): Whee! delivering SIGINT.
[   17.348565] [bash, 0xffff96428638ba00] arch_do_signal_or_restart(): Whee! delivering SIGINT.

Bash uses the GNU Readline library to read from stdin. When I press Control-C, GNU Readline catches the first SIGINT with its own handler (rl_signal_handler(), I think), performs some special processing, reinstalls Bash's SIGINT handler, then sends a second SIGINT to the bash process.

Conceptually, it's GNU Readline "forwarding" a SIGINT to Bash, but it happens within the same bash process.