You are mixing a lot of things,
standard input,output,error file descriptors (fds) are not the same thing as the terminal fd.
When you open a terminal and enter a command, by default those 3 fds (stdin (0),stdout(1),stderr(2)) "point" to the terminal, but that can be changed by redirecting any of those fds (for example "ls non_existing_file 2>err" will redirect the error messages to the file "err" and you will not see it on the terminal).
Now i would like to address your "open /dev/tty" line, that actually points to your TERMINAL, to demonstrate the difference, check out this sample program.
To put how piping works simply:
When you pipe two commands (without any "<" or ">"), the first command will take its input from the keyboard (if it requires reading input) and will redirect its output to be taken as input by the second command, that means that the "standard" input of your process "b" is no longer the terminal, but the output of the process "a".
Now to finally answer your question you need to know 2 more things:
1) When you launch a process into the foreground, the shell will give that process control of terminal and it will wait until the process is finished to take back control of the terminal, and any keyboard-generated signals will be sent to that controlling process during its execution.
2) When you launch a piped command (cmd1 | cmd2), the shell will first put those 2 process in the same "process group" (If you want to know more, read about job control).
So what happens with your "a | b" is that these 2 process are in the same process group, so any keyboard-generated signals will be sent to BOTH those processes.
because keyboard-generated signals are sent to a whole process group and NOT a single process.
I hope this answers your question, don't hesitate to ask question if anything is not clear