How Does a Session Get a Controlling Terminal?
When a new session is created with setsid(), it has no controlling terminal. A controlling terminal is acquired later when the session leader opens a terminal device for the first time.
Under Linux (System V behaviour):
- The session leader opens a terminal file (e.g.,
/dev/pts/1). - If that terminal is not already the controlling terminal for another session, it becomes the controlling terminal for this session.
- If the session leader specifies
O_NOCTTYwhen callingopen(), the terminal is not made the controlling terminal. - A terminal can only be the controlling terminal for one session at a time.
ioctl(fd, TIOCSCTTY) to set the controlling terminal. Linux supports both — the automatic (System V) way and the explicit ioctl way.The Controlling Process
As a consequence of opening the controlling terminal, the session leader simultaneously becomes the controlling process for that terminal.
The controlling process is special for one reason: if a terminal disconnect occurs (modem hangup, terminal window closed), the kernel sends SIGHUP to the controlling process to inform it. The default action of SIGHUP is to terminate the process.
The controlling terminal and controlling process are inherited across fork() and preserved across exec().
Accessing the Controlling Terminal: /dev/tty
/dev/tty is a special device file that always refers to the controlling terminal of the calling process. It is useful when standard input/output is redirected and you need to ensure you are talking directly to the terminal.
Example: getpass() (the function that reads a password without echoing) opens /dev/tty to ensure it always reads from the terminal, even if stdin is redirected.
If the process has no controlling terminal, opening /dev/tty fails with errno == ENXIO.
Removing the Controlling Terminal: TIOCNOTTY
A process can detach itself from its controlling terminal using:
ioctl(fd, TIOCNOTTY);
where fd is a file descriptor for the controlling terminal. After this call, opening /dev/tty will fail.
If the calling process is the controlling process, three additional things happen:
- All processes in the session lose their association with the controlling terminal.
- The terminal becomes free — another session leader can acquire it.
- SIGHUP (and SIGCONT) are sent to all members of the foreground process group.
Getting the Controlling Terminal Path: ctermid()
ctermid() returns a string containing the pathname of the controlling terminal. On Linux this is always /dev/tty, but the function exists for portability to non-UNIX systems.
#include <stdio.h>
/* If ttyname is not NULL: must be at least L_ctermid bytes; path is copied there.
* If ttyname is NULL: returns pointer to a static buffer (not reentrant). */
char *ctermid(char *ttyname);
| Mechanism | What it does | Notes |
|---|---|---|
open(terminal, 0) |
Automatically acquires controlling terminal | System V; Linux default |
open(terminal, O_NOCTTY) |
Opens terminal WITHOUT making it controlling | POSIX standard flag |
ioctl(fd, TIOCSCTTY) |
Explicitly sets controlling terminal | BSD style; also on Linux |
ioctl(fd, TIOCNOTTY) |
Removes controlling terminal association | Not in SUSv3 but widespread |
tcgetsid(fd) |
Gets session ID of terminal’s session | SUSv3; uses TIOCGSID ioctl |
ctermid() |
Returns pathname of controlling terminal | Always “/dev/tty” on Linux |
Code Example 1 — Opening /dev/tty to Directly Access the Terminal
/* open_dev_tty.c
* Shows how to open /dev/tty to communicate with the terminal
* even when stdin/stdout are redirected.
* Compile: gcc -o open_dev_tty open_dev_tty.c
* Test redirect: ./open_dev_tty < /dev/null > /dev/null
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
int main(void)
{
/* stdin and stdout might be redirected — but /dev/tty is always
* the controlling terminal */
int tty_fd = open("/dev/tty", O_RDWR);
if (tty_fd == -1) {
fprintf(stderr, "open /dev/tty failed: %s\n", strerror(errno));
if (errno == ENXIO)
fprintf(stderr, " → Process has NO controlling terminal\n");
return 1;
}
/* Write a message directly to the terminal */
const char *msg = "Hello from /dev/tty! (bypasses stdout redirection)\n";
write(tty_fd, msg, strlen(msg));
/* Read a line directly from the terminal */
write(tty_fd, "Enter your name: ", 17);
char buf[64] = {0};
ssize_t n = read(tty_fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
/* Remove newline */
if (buf[n-1] == '\n') buf[n-1] = '\0';
char reply[128];
snprintf(reply, sizeof(reply), "Hello, %s!\n", buf);
write(tty_fd, reply, strlen(reply));
}
close(tty_fd);
return 0;
}
/* Run with redirected stdin/stdout:
* ./open_dev_tty < /dev/null > /dev/null
* The message and prompt still appear on the terminal!
*/
Code Example 2 — O_NOCTTY and tcgetsid()
/* noctty_demo.c
* Demonstrates O_NOCTTY flag — opening a terminal without acquiring
* it as the controlling terminal.
* Compile: gcc -o noctty_demo noctty_demo.c
*/
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>
#include <errno.h>
int main(void)
{
printf("My SID = %ld\n", (long)getsid(0));
printf("My PGID = %ld\n\n", (long)getpgrp());
/* --- Open a terminal WITH O_NOCTTY --- */
/* On a typical system, /dev/tty is the current controlling terminal;
* to test with a different terminal, replace with e.g. /dev/pts/2 */
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
if (fd == -1) {
fprintf(stderr, "open /dev/tty: %s\n", strerror(errno));
return 1;
}
printf("Opened /dev/tty with O_NOCTTY\n");
/* tcgetsid: returns session ID associated with the terminal */
pid_t term_sid = tcgetsid(fd);
if (term_sid == -1) {
printf("tcgetsid: %s\n", strerror(errno));
} else {
printf("Terminal's session ID: %ld\n", (long)term_sid);
printf("Our session ID : %ld\n", (long)getsid(0));
printf("Same session? %s\n",
(term_sid == getsid(0)) ? "YES" : "NO");
}
/* ctermid: get pathname of controlling terminal */
char ttyname[L_ctermid];
if (ctermid(ttyname) != NULL)
printf("ctermid() says controlling terminal is: %s\n", ttyname);
else
printf("ctermid() could not determine controlling terminal\n");
close(fd);
return 0;
}
/*
* Expected output:
* My SID = 1200
* My PGID = 5000
*
* Opened /dev/tty with O_NOCTTY
* Terminal's session ID: 1200
* Our session ID : 1200
* Same session? YES
* ctermid() says controlling terminal is: /dev/tty
*/
