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
.
…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
!
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,
serial8250_read_char()
pushes this Control-C
to the TTY flip buffer by calling uart_insert_char()
;tty_flip_buffer_push()
then queues a work to push this TTY flip buffer to the line discipline (LDISC).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;
}
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);
}
}
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!"
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.