Quick Answer

A terminal emulator and a shell are two separate programs connected through a kernel-provided pseudo-terminal (PTY): the emulator owns the PTY's master end for display and input, the shell runs attached to the slave end as just another process reading stdin. Every command you type triggers a fresh fork-then-exec cycle, and signals like Ctrl+C's SIGINT get delivered to an entire foreground process group at once, not to a single named program.

The Terminal and the Shell Are Two Completely Different Programs

Open a terminal app (Windows Terminal, iTerm2, GNOME Terminal, whatever) and type a command, and it feels like one continuous thing talking to you. It's actually two entirely separate programs with a narrow, well-defined boundary between them, and almost every confusing terminal behavior traces back to not knowing which side of that boundary something happens on.

The terminal emulator is the program that draws the window, renders text, handles your keyboard and mouse input, manages scrollback, tabs, and color themes. It has no idea what ls or git status mean — its only job is display and input/output plumbing. The shell (bash, zsh, PowerShell, fish) is a completely separate program that the terminal emulator starts up, and it's the shell that actually parses your command line, decides what program to run, and manages things like environment variables and your working directory. The terminal emulator and the shell communicate through a specific OS-provided mechanism built exactly for this split: the pseudo-terminal, or PTY.

The PTY: A Fake Serial Port Connecting Two Programs

Historically, a "terminal" was a physical device — a screen and keyboard wired to a mainframe over a serial line. Modern terminal emulators don't talk to real hardware, so the OS provides a pseudo-terminal (PTY) that behaves exactly like that old serial connection would, implemented entirely in software. A PTY always comes as a pair: a master end and a slave end. Whatever gets written to the master shows up as input on the slave, and whatever gets written to the slave (including a running program's normal output) shows up as readable data on the master.

When your terminal emulator starts up, it asks the kernel for a new PTY pair. It keeps the master end for itself — this is how it will send you keystrokes to the shell and receive text to display back. Then it forks a child process, and in that child, before running anything else, it wires the slave end of the PTY up as the child's stdin, stdout, and stderr. Only after that plumbing is in place does the child call exec to actually become the shell. From the shell's point of view, it's just a normal program reading from stdin and writing to stdout, with absolutely no awareness that there's a GUI window and a human on the other end — it's talking to a slave file descriptor that happens, through the PTY mechanism, to be connected to whatever the terminal emulator does with it.

What Actually Happens Between Pressing Enter and Seeing Output

Put the PTY split together with what the shell actually does, and pressing Enter after typing ls -la triggers a specific, mechanical sequence, not magic:

  1. The terminal emulator captures the keystrokes and writes the raw characters to the PTY master as you type, including the Enter key as a newline byte.
  2. The kernel's PTY layer delivers those bytes to the slave side, where the shell is blocked reading from its stdin. The shell has been accumulating characters into a line buffer and now sees the newline, signaling "a complete command line is ready."
  3. The shell parses that line: splits it into the command name and arguments, expands anything that needs expanding (globs like *.txt, environment variables, ~ for home directory), and figures out which actual program ls refers to by searching the directories listed in the PATH environment variable.
  4. The shell calls fork(), which creates a near-identical copy of the shell process itself. In that new child process, the shell calls one of the exec family of system calls, which replaces the child's entire program image with /bin/ls, keeping the same process ID and the same open file descriptors (which are still, ultimately, connected back through the PTY slave to the terminal).
  5. ls runs, writes its output to its stdout — which is the inherited PTY slave file descriptor — and exits. The kernel delivers that output through the PTY pair to the master, which the terminal emulator is continuously reading from, and the emulator renders those bytes as text in the window you're looking at.
  6. The shell, which was waiting (via wait()) for that child process to finish, wakes back up, prints a fresh prompt, and goes back to reading stdin for your next command.

Every single command you run is a brand new fork+exec cycle — the shell never "becomes" ls, it spawns a child that becomes ls while the shell itself keeps existing and waiting. This is exactly why cd has to be a built-in command implemented inside the shell itself rather than a separate program: if cd ran as a forked child process, it could only change the working directory of that short-lived child, which would vanish the instant the child exited, leaving the parent shell's own directory completely unaffected.

Ctrl+C, Background Jobs, and Process Groups

Job control is the layer that makes Ctrl+C, Ctrl+Z, and backgrounding commands with & work, and it depends on a concept one level above individual processes: the process group. When the shell launches a command (or a whole pipeline of commands connected with |), it puts all of the processes involved into a single process group, identified by a process group ID (PGID), and one of those groups is designated the current foreground process group for the terminal.

When you press Ctrl+C, the terminal driver (the kernel-level line-discipline code that watches for special control characters) doesn't go hunting for "the running program" by name — it sends a SIGINT signal directly to every process in the current foreground process group, all at once. That's why Ctrl+C correctly kills an entire pipeline like grep foo | sort | less in one keypress rather than only killing grep and leaving sort and less running: all three processes share the same PGID as members of that one pipeline, so the signal reaches all of them simultaneously. Ctrl+Z works the same way but sends SIGTSTP (suspend) instead of SIGINT (interrupt), which is what makes a suspended job resumable later with fg instead of being killed outright. Backgrounding a command with & simply skips making that process group the foreground one, so it keeps running without being attached to the terminal's keyboard-signal plumbing at all, and without blocking the shell from reading your next command.

Why Any of This Is Useful to Actually Know

This isn't just trivia — it explains real, otherwise-confusing behavior you've probably already hit. A GUI app launched from a shell script that doesn't background itself properly can hang the script's exit because the parent shell is still waiting for a child it forgot to detach from the process group. A program that ignores or re-handles SIGINT (some database clients and REPLs do this deliberately) won't quit on the first Ctrl+C, because it's choosing to catch the signal rather than die from it, not because Ctrl+C is broken. And remote tools like SSH, tmux, and screen are, at their core, just more PTY master ends — SSH's server side allocates a PTY on the remote machine exactly the same way a local terminal emulator does, forks a shell against its slave end, and tunnels the master end's bytes over the network connection, which is the entire reason a remote shell session feels indistinguishable from a local one: structurally, it is the same mechanism, just with the master and slave sitting on different machines.

Frequently Asked Questions

Is the terminal and the shell the same program? No. The terminal emulator handles the window, rendering, and keyboard input; the shell is a separate program the terminal starts that parses commands and runs other programs. They communicate through a kernel-provided pseudo-terminal (PTY).
Why does cd have to be a shell built-in instead of a separate program? Every command the shell runs executes in a forked child process, and a child process's working directory change dies with that child when it exits. If cd were a separate program, the directory change would vanish instantly instead of persisting in the shell you're typing into.
Why does Ctrl+C sometimes not stop a program? Ctrl+C sends SIGINT to the foreground process group, but a program can choose to catch that signal and handle it differently instead of terminating, which is common in database clients, REPLs, and programs doing cleanup before exit.
What is a PTY, in plain terms? A pseudo-terminal is a kernel-provided pair of connected virtual endpoints, a master and a slave, that behaves like the old physical serial-line terminal connection, but entirely in software, letting a GUI terminal emulator and a text-based shell communicate.
Why does an SSH session feel exactly like a local terminal? Because it structurally is the same mechanism. The SSH server allocates a PTY on the remote machine, starts a shell attached to its slave end just like a local terminal emulator would, and tunnels the master end's bytes over the network connection instead of to a local window.