Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems.. It's 100% free, no registration required.

Given a shell process (e.g. sh) and it's child process (e.g. cat), how to simulate the behavior of CTRL+C using the shell's process ID?


This is what I've tried:

Running sh and then cat:

[user@host ~]$ sh
sh-4.3$ cat
test
test

Sending SIGINT to cat from another terminal:

[user@host ~]$ kill -SIGINT $PID_OF_CAT

cat received the signal and terminated (as expected).

Sending the signal to the parent process does not seem to work. Why is the signal not propagated to cat when sent to it's parent process sh?

This does not work:

[user@host ~]$ kill -SIGINT $PID_OF_SH
share|improve this question
1  
The shell has a way to ignore SIGINT signals not sent from the keyboard or terminal. –  konsolebox Aug 11 at 18:33

4 Answers 4

up vote 7 down vote accepted

How CTRL+C works

The first thing is to understand how CTRL+C works.

When you press CTRL+C, your terminal emulator sends an ETX character (end-of-text / 0x03).
The terminal is configured such that when it receives this character, it sends a SIGINT to the foreground process group of the terminal. This configuration can be viewed by doing stty and looking at intr = ^C;. The POSIX specification says that when INTR is received, it should send a SIGINT to the foreground process group of that terminal.

What is the foreground process group?

So, now the question is, how do you determine what the foreground process group is? The foreground process group is simply the group of processes which will receive any signals generated by the keyboard (SIGTSTOP, SIGINT, etc).

Simplest way to determine the process group ID is to use ps:

ps ax -O tpgid

The second column will the process group ID.

How do I send a signal to the process group?

Now that we know what the process group ID is, we need to simulate the POSIX behavior of sending a signal to the entire group.

This can be done with kill by putting a - in front of the group ID.
For example, if your process group ID is 1234, you would use:

kill -INT -1234

 


Simulate CTRL+C using the terminal number.

So the above covers how to simulate CTRL+C as a manual process. But what if you know the TTY number, and you want to simulate CTRL+C for that terminal?

This becomes very easy.

Lets assume $tty is the terminal you want to target (you can get this by running tty | sed 's#^/dev/##' in the terminal).

kill -INT -$(ps h -t $tty -o tpgid | uniq)

This will send a SIGINT to whatever the foreground process group of $tty is.

share|improve this answer

As vinc17 says, there’s no reason for this to happen.  When you type a signal-generating key sequence (e.g., Ctrl+C), the signal is sent to all processes that are attached to (associated with) the terminal.  There is no such mechanism for signals generated by kill.

However, a command like

kill -SIGINT -12345

will send the signal to all processes in process group 12345; see kill(1) and kill(2).  Children of a shell are typically in the shell’s process group (at least, if they’re not asynchronous), so sending the signal to the negative of the PID of the shell may do what you want.


Oops

As vinc17 points out, this doesn’t work for interactive shells.  Here’s an alternative that might work:

kill -SIGINT -$(echo $(ps -pPID_of_shell o tpgid=))

ps -pPID_of_shell gets process information on the shell.  o tpgid= tells ps to output only the terminal process group ID, with no header.  If this is less than 10000, ps will display it with leading space(s); the $(echo …) is a quick trick to strip off leading (and trailing) spaces.

I did get this to work in cursory testing on a Debian machine.

share|improve this answer
1  
This doesn't work when the process is started in an interactive shell (which is what the OP is using). I don't have a reference for this behavior, though. –  vinc17 Aug 11 at 19:19

There's no reason to propagate the SIGINT to the child. Moreover the system() POSIX specification says: "The system() function shall ignore the SIGINT and SIGQUIT signals, and shall block the SIGCHLD signal, while waiting for the command to terminate."

If the shell propagated the received SIGINT, e.g. following a real Ctrl-C, this would mean that the child process would receive the SIGINT signal twice, which may have unwanted behavior.

share|improve this answer
    
The shell doesn't have to implement this with system(). But you're right, if it catches the signal (obviously it does) then there's no reason to propagate it downward. –  goldilocks Aug 11 at 18:51
    
@goldilocks I've completed my answer, perhaps giving a better reason. Note that the shell cannot know whether the child has already received the signal, hence the problem. –  vinc17 Aug 11 at 19:07

The question contains its own answer. Sending the SIGINT to the cat process with kill is a perfect simulation of what happens when you press ^C.

To be more precise, the interrupt character (^C by default) sends SIGINT to every process in the terminal's foreground process group. If instead of cat you were running a more complicated command involving multiple processes, you'd have to kill the process group to achieve the same effect as ^C.

When you run any external command without the & background operator, the shell creates a new process group for the command and notifies the terminal that this process group is now in the foreground. The shell is still in its own process group, which is no longer in the foreground. Then the shell waits for the command to exit.

That's where you seem to have become the victim by a common misconception: the idea that the shell is doing something to facilitate the interaction between its child process(es) and the terminal. That's just not true. Once it has done the setup work (process creation, terminal mode setting, creation of pipes and redirection of other file descriptors, and executing the target program) the shell just waits. What you type into cat isn't going through the shell, whether it's normal input or a signal-generating special character like ^C. The cat process has direct access to the terminal through its own file descriptors, and the terminal has the ability to send signals directly to the cat process because it's the foreground process group. The shell has gotten out of the way.

After the cat process dies, the shell will be notified, because it's the parent of the cat process. Then the shell becomes active and puts itself in the foreground again.

Here is an exercise to increase your understanding.

At the shell prompt in a new terminal, run this command:

exec cat

The exec keyword causes the shell to execute cat without creating a child process. The shell is replaced by cat. The PID that formerly belonged to the shell is now the PID of cat. Verify this with ps in a different terminal. Type some random lines and see that cat repeats them back to you, proving that it's still behaving normally in spite of not having a shell process as a parent. What will happen when you press ^C now?

Answer:

SIGINT is delivered to the cat process, which dies. Because it was the only process on the terminal, the session ends, just as if you'd said "exit" at a shell prompt. In effect cat was your shell for a while.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.