The Terminal, Shells, and Sessions
Three things people lump together as "the command line" are separate components, and on a server the difference is operational. The terminal is a character device that moves bytes between you and a program. The shell (bash on Debian/Ubuntu for interactive logins, dash as /bin/sh) is an ordinary program that reads those bytes, parses commands, and runs them. The session is the kernel's bookkeeping that ties a login, its terminal, and the processes it spawned into one unit that can be signalled and torn down together.
Getting this wrong is how a long-running job dies the instant your SSH connection drops, or how a script that worked in your interactive shell behaves differently under cron. The terminal decides who receives SIGHUP when a connection closes; the shell type decides which startup files run and therefore what PATH and environment a process sees. Both are invisible until they bite.
Terminals, TTYs, and PTYs
A terminal is a kernel device, not an application. The historical hardware was a teletype, which is why the device class is still called a TTY. A physical Linux console — the screens you reach with Ctrl+Alt+F1 through F6 — is a real TTY backed by the kernel's virtual-console driver, exposed as /dev/tty1, /dev/tty2, and so on. These exist before any graphical session and are where you land when the network or the display manager is broken.
Everything else is a pseudo-terminal, a PTY: a kernel-emulated terminal with two ends. A terminal emulator or sshd holds the master side; your shell runs on the slave side at /dev/pts/0, /dev/pts/1, and so on. When you type in an SSH session, sshd writes your keystrokes to the master, the kernel's line discipline delivers them to the shell on the slave, and the shell's output flows back the same way. Run tty to see which device you are on; a job with no controlling terminal — most daemons, anything under cron — reports not a tty.
# Where am I, and what is on the other terminals? tty # e.g. /dev/pts/2 over SSH, /dev/tty1 on the console who # every logged-in session and its terminal ps -o pid,tty,cmd -t pts/2 # processes attached to one PTY
The Shell Is Just a Program
The shell reads command lines, expands globs and variables, sets up pipes and redirections, forks the requested programs, and reports their exit status. It is not part of the kernel and not part of the terminal — swap it and nothing else changes. The login shell for an account is the last field in /etc/passwd, edited with chsh -s /usr/bin/zsh and listed in /etc/shells. On Debian and Ubuntu the interactive default is bash, but /bin/sh is a symlink to dash, a smaller, faster, strictly POSIX shell with none of bash's arrays, [[ ]], or process substitution.
That split is a recurring server footgun. A script with #!/bin/sh runs under dash, so bashisms fail; the same file with #!/bin/bash runs under bash and works. Red Hat and Fedora point /bin/sh at bash in POSIX mode instead, which is why a script that passes on RHEL can break on Ubuntu without a single line changing. Pin the interpreter you actually tested against in the shebang rather than trusting the platform default.
Login, Interactive, and Non-Interactive Shells
A shell is one of three kinds, and the kind decides which startup files run. A login shell is what you get at console sign-in or the start of an SSH session; bash reads /etc/profile and then the first it finds of ~/.bash_profile, ~/.bash_login, or ~/.profile. An interactive non-login shell — a new terminal tab, or bash typed at a prompt — reads /etc/bash.bashrc and ~/.bashrc instead, and never touches the profile files. A non-interactive shell, the one that runs a script or a cron job, reads neither; it only honours the file named in $BASH_ENV, which is usually empty.
This is why exported variables and PATH edits in ~/.bashrc silently vanish under cron, and why putting environment setup only in ~/.bash_profile leaves your second terminal tab without it. The convention that fixes both: keep environment and PATH in the profile, keep aliases and prompt tweaks in ~/.bashrc, and have the profile source ~/.bashrc so a login shell gets everything. Treat cron as having no environment at all and set PATH explicitly in the job.
# Classify the current shell shopt -q login_shell && echo login || echo non-login case $- in *i*) echo interactive;; *) echo non-interactive;; esac
Sessions, Process Groups, and Job Control
When the shell starts it becomes a session leader and the session's controlling terminal is bound to it. Every pipeline you launch becomes a process group; one group at a time is the terminal's foreground group and receives your keystrokes and signals, while the rest run in the background. Ctrl+C sends SIGINT to the whole foreground group, Ctrl+Z sends SIGTSTP to suspend it, and the shell's job-control built-ins — jobs, fg, bg, kill %1 — move groups between those states. This is why Ctrl+C kills an entire pipeline, not just the last command.
The dangerous edge is the controlling terminal going away. When an SSH connection drops or you close the terminal, the kernel sends SIGHUP to the session leader, which forwards it to its jobs, and anything that does not catch it dies. A backgrounded job is not safe — it is still in the session. nohup cmd & detaches one command from the hangup and redirects its output to nohup.out; disown -h %1 removes an already-running job from the shell's hangup list. Neither gives you the terminal back, which is what tmux and screen are for.
Persistent and Detached Sessions
tmux and screen solve the hangup problem properly by inserting their own PTY between you and the shell. The server process holds the real session and keeps your shells alive; your client merely attaches to and detaches from it. Detach with Ctrl+b d, drop the SSH connection, reconnect tomorrow, run tmux attach, and the long migration or compile is exactly where you left it. For any multi-hour command over SSH, starting it inside tmux is the difference between a flaky network being an annoyance and being a lost job.
For services, do not lean on tmux at all — that is what the init system is for. A process started in a detached session still dies if the machine reboots and has no log integration, no restart policy, and no resource limits. Run daemons under systemd as units; systemd also tracks interactive logins as sessions through logind, which is why loginctl lists them and why KillUserProcesses can reap stray background jobs when a user logs out. Use tmux for your own interactive work, systemd for anything that must survive a reboot.
# A long job that survives a dropped SSH connection tmux new -s migrate # named session; Ctrl+b d to detach tmux attach -t migrate # reconnect later from anywhere # Inspect systemd-tracked login sessions loginctl list-sessions loginctl session-status 3
Terminal emulator — the application that draws characters and owns the master end of a PTY (GNOME Terminal, the SSH client, the kernel's virtual console). It transports bytes; it does not interpret commands. Reach for this layer when the problem is keys, colours, resize, or line wrapping.
Shell — the program reading from the terminal that parses and runs commands (bash, dash, zsh). Reach for this layer when the problem is PATH, startup files, quoting, globbing, or a script behaving differently than at the prompt.
TTY / PTY — the kernel device in between, with its line discipline handling echo, line editing, and the Ctrl+C/Ctrl+Z signal mappings. Reach for this layer when a program complains it is not a tty, when piped output buffers differently, or when stty settings are wrong.
- Backgrounding a long job with
&and assuming it is safe — it is still in the session, so theSIGHUPon disconnect kills it. Usetmux, ornohup/disownat minimum. - Putting
PATHand exports only in~/.bashrc, then finding them missing undercronand in non-interactive SSH commands, which never read it. - Writing a script with bashisms (
[[ ]], arrays, process substitution) under a#!/bin/shshebang — on Debian/Ubuntu that isdash, which rejects them, even though the same file runs on RHEL where/bin/shis bash. - Running production daemons inside a
tmuxwindow — they vanish on reboot and have no logging, restart policy, or resource limits. That is asystemdunit's job. - Editing environment in
~/.bash_profileonly, then wondering why a new terminal tab (an interactive non-login shell) does not have it because that file is never read there. - Expecting Ctrl+C to stop only the last command in a pipeline — it signals the whole foreground process group, so the entire pipeline dies.
- Hard-coding
/dev/pts/0or a specific TTY in scripts — pseudo-terminal numbers are assigned per connection and change every login.
- Start every multi-hour command over SSH inside
tmux new -s nameso a dropped connection is an annoyance, not a lost job. - Pin the interpreter you tested against in the shebang — write
#!/bin/bashwhen you use bash features, never rely on what/bin/shhappens to point at. - Keep environment and
PATHin~/.profile, keep aliases and prompt tweaks in~/.bashrc, and source~/.bashrcfrom the profile so login shells get both. - Set
PATHexplicitly inside everycronjob and systemd unit; treat their shells as having no inherited environment. - Run anything that must survive a reboot as a
systemdunit withRestart=and journald logging — not a detached terminal session. - Use
ttyandcase $- in *i*)guards in shell config so interactive-only settings never run in scripts and break non-interactive use. - Reach the physical console (Ctrl+Alt+F2,
/dev/tty2) to recover a box whose network or display manager is down — it exists independently of any PTY.
zsh as the default login shell since CatalinaBSD — the original job-control and TTY design Linux inherited; tcsh and sh defaults, same tmux/screen toolsKnowledge Check
You launch a 6-hour migration with ./migrate.sh & over SSH and the connection drops an hour in. What most likely happens?
- The job receives
SIGHUPand dies, because backgrounding does not remove it from the session whose controlling terminal just went away - It keeps running safely to completion — a backgrounded job is fully detached from the terminal and therefore immune to the disconnect
- It pauses on the drop and then resumes automatically from where it stopped the moment you reconnect over SSH
- It survives the disconnect only if you remembered to press Ctrl+Z to suspend it first
A script with #!/bin/sh uses [[ $x == y* ]]. It passes on RHEL but fails on Ubuntu. Why?
- On Debian/Ubuntu
/bin/shisdash, which lacks[[ ]], whereas on RHEL/bin/shis bash in POSIX mode and accepts it - Ubuntu ships an older build of bash that dropped support for the
[[ ]]test operator entirely - The kernel parses the
#!shebang line differently between the two distributions, so it picks a different interpreter - RHEL silently rewrites every
[[ ]]into[ ]at runtime before executing the test
PATH additions in your ~/.bashrc work at the prompt but are missing in a cron job. What explains it?
cronruns a non-interactive shell, which reads neither~/.bashrcnor the profile files, so its environment is nearly emptycronreads your~/.bashrclike any shell but then strips out thePATHentries it considers unsafe before it runs the scheduled job- The additions in
~/.bashrconly apply to the user's very first interactive login of the day cronreads~/.bash_profileinstead of~/.bashrc, and that is where the edit was never made
Why is a tmux session the wrong place to run a production database daemon, even though it survives SSH disconnects?
- It dies on reboot and has no restart policy, log integration, or resource limits — those are what a
systemdunit provides tmuxcaps the CPU available to each pane, so the database process would be silently throttled under loadtmuxcannot keep a process alive any longer than the original login session that first started the server- A daemon running inside a
tmuxpane loses its controlling PTY on disconnect and therefore can no longer open or hold listening sockets
You got correct