# Linux tty Devices — DragonOS dev 文档

备注

AI Translation Notice

This document was automatically translated by `hunyuan-turbos-latest` model, for reference only.

- Source document: kernel/device/tty.md

- Translation time: 2025-10-09 07:02:59

- Translation model: `hunyuan-turbos-latest`

Please report issues via Community Channel

# Linux tty Devices

`dev/tty` is a very special device file in Linux and other Unix-like systems. Essentially, it is an alias or shortcut pointing to the controlling terminal of the current process.

## 1. Core Concept: Terminal

In the early days of computing, users interacted with computers through physical devices called “terminals.” A typical physical terminal consisted of a keyboard for input and a screen (or printer) for output.

In modern Linux systems, physical terminals are uncommon, having been replaced by terminal emulators (Terminal Emulator) such as GNOME Terminal, Konsole, xterm, iTerm2, etc. These are software programs under graphical interfaces that simulate the behavior of physical terminals.

Additionally, there are consoles (Console), which are terminals directly connected to computer hardware, typically used when there is no graphical interface or when the graphical interface has crashed. In Linux, you can switch to a virtual console using `Ctrl + Alt + F1-F6`.

Regardless of the form, the system manages these terminal sessions through a driver subsystem called TTY, named after the early “Teletypewriter.”

## 2. TTY Device Files

In Linux, “everything is a file.” The system communicates with hardware devices through special files in the `/dev` directory. For terminals, there is also a series of device files, typically located in the `/dev/` directory, such as:

- /dev/ttyS0, /dev/ttyS1, …: Physical serial port devices.

- /dev/tty1, /dev/tty2, …: Virtual consoles.

- /dev/pts/0, /dev/pts/1, …: Pseudo-terminals. These are the most commonly used; when you open a terminal emulator window, the system creates a pseudo-terminal and assigns it a device file like `/dev/pts/0`.

Each terminal session (e.g., a terminal window you open) is associated with a specific TTY device file. You can use the `tty` command to view the device file corresponding to the current terminal:

Bash

    $ tty
    /dev/pts/0

## 3. The Role of `/dev/tty`: A Dynamic Link to the “Current” Terminal

Now let’s return to the main subject, `/dev/tty`.

Imagine you are writing a program that needs to interact directly with the user (read the user’s input or display information on the user’s screen), regardless of where the program ultimately runs.

- If the user runs your program on virtual console `tty2`, the program should read from and write to `/dev/tty2`.

- If the user runs it in a GNOME Terminal window, the program may need to read from and write to `/dev/pts/5`.

- If the user logs in remotely via `ssh`, the program needs to read from and write to another pseudo-terminal device.

Having the program determine which terminal it is running on would be very complex and unreliable.

`/dev/tty` exists to solve this problem. No matter what the specific device (e.g., `/dev/tty2` or `/dev/pts/5`) of a process’s “controlling terminal” is, `/dev/tty` is always a link pointing to this controlling terminal.

When a program opens the `/dev/tty` file, the kernel automatically redirects this file descriptor to the current process’s actual controlling terminal. Thus, program developers do not need to worry about the underlying specific TTY device; they only need to uniformly read from and write to `/dev/tty`, ensuring interaction with the current user.

In short, `/dev/tty` tells the program: “Send the information to the user who started you, no matter where they are.”

## 4. The Difference Between `/dev/tty` and Standard Input/Output/Error (stdin, stdout, stderr)

You might ask: This sounds similar to standard input (stdin) and standard output (stdout). What’s the difference?

In most cases, a process’s standard input, output, and error streams are by default connected to its controlling terminal. For example, when you run the `ls` command, its `stdout` is your terminal by default, so you can see the file list on the screen.

However, redirection (Redirection) changes this default behavior.

- `ls > files.txt`: The `stdout` of the `ls` command is redirected to the `files.txt` file instead of the terminal.

- `cat my_script.sh | bash`: The `stdin` of the `bash` process is redirected to a pipe (`|`), reading content from the output of the `cat` command instead of the keyboard.

In such cases, if the program still wants to forcibly interact with the user (for example, a script that requires the user to input a password), it can no longer rely on `stdin` or `stdout`, because they may have been redirected to a file or pipe, no longer the user’s screen and keyboard.

This is where `/dev/tty` comes into play.

Reading from and writing to `/dev/tty` bypasses the redirection of standard input/output and directly accesses the controlling terminal.

### Example:

Let’s look at a practical example. Suppose we have a script `ask_password.sh`:

Bash

    #!/bin/bash

    # 尝试从标准输入读取密码
    echo "Enter password (from stdin):"
    read password_stdin

    # 现在，强制从控制终端读取密码
    echo "Enter password again (from /dev/tty):"
    read password_tty < /dev/tty

    echo "Password from stdin: $password_stdin"
    echo "Password from tty: $password_tty"

Now, we run it normally:

Bash

    $ ./ask_password.sh
    Enter password (from stdin):
    my_secret_pass
    Enter password again (from /dev/tty):
    my_secret_pass
    Password from stdin: my_secret_pass
    Password from tty: my_secret_pass

There seems to be no difference. But now, let’s try running it with redirection:

Bash

    $ echo "password_from_file" | ./ask_password.sh
    Enter password (from stdin):
    Enter password again (from /dev/tty):
    my_real_secret  <-- 这里光标会停住，等待你从键盘输入
    Password from stdin: password_from_file
    Password from tty: my_real_secret

Analysis:

1. The first `read` command reads from `stdin`. Since we redirected the output of `echo` to the script’s `stdin` via a pipe, it reads “password_from_file”.

2. The second `read` command is explicitly redirected to read from `/dev/tty` (`< /dev/tty`). This operation bypasses the `stdin` pipe and directly accesses your keyboard and screen. Therefore, it will stop and wait for you to manually input the password.

This is the core value of `/dev/tty`: providing a reliable channel to interact with the user’s terminal, regardless of how standard streams are redirected. Programs like `ssh` and `sudo` that require secure password input use this mechanism internally.

## Summary

Feature | Description
--- | ---
Definition | A special device file that serves as an alias or shortcut for the current process’s controlling terminal.
Function | Provides a stable and reliable way for programs to interact with the user’s terminal that launched them.
Dynamism | It is not a specific device itself but a dynamically managed link by the kernel, pointing to the specific TTY device.
Difference from stdin/stdout | When standard input/output/error streams are redirected to files, pipes, or other processes, `/dev/tty` can still be used to directly access the user’s screen and keyboard.
Typical Uses | - Programs requiring user input of passwords or confirmations (e.g., `sudo`, `ssh`). - Scripts needing explicit user interaction, even when run via pipes or redirection.

## How is /dev/tty Typically Used in User Programs?

Well, the core goal of using `/dev/tty` in user programs is: to bypass potentially redirected standard input/output streams and forcibly interact directly with the user’s controlling terminal.

This is very common in the following scenarios:

1. Requesting Sensitive Information: Such as passwords, private key passwords, etc. Even if a script’s output is redirected to a log file, you do not want the password prompt and input process to be recorded.

2. Interactive Confirmation: In a script that may be automatically called, before performing dangerous operations (e.g., `rm -rf /`), it is necessary to force the user to manually confirm.

3. Diagnostics and Debugging: Printing debugging information to the user’s screen, even if the user has redirected the script’s standard output elsewhere.

4. Fullscreen or Cursor-Based Applications: Programs like `vim` and `top` need to directly control the terminal’s screen, colors, and cursor positions, and they interact directly with the TTY device.

### Summary

The pattern of using `/dev/tty` in user programs is very consistent:

1. Open the File: Open `/dev/tty` like a regular file, typically requiring read/write permissions (`r+`).

2. Error Handling: Check if the open operation was successful. If a process does not have a controlling terminal (e.g., a background daemon process started by `systemd`), opening `/dev/tty` will fail. The program needs to handle this situation properly.

3. Writing (Output): Use standard file writing functions (e.g., `fprintf`, `write`) to write data to the opened `/dev/tty` file descriptor, which will display prompt information on the user’s screen.

4. Reading (Input): Use standard file reading functions (e.g., `fgets`, `read`) to read data from `/dev/tty`, obtaining the user’s keyboard input.

5. Close the File: After completing the interaction, close the file descriptor.

## What is the Role of /dev/ptmx and Files Under /dev/pts/?

Well, let’s delve into the files under `/dev/ptmx` and `/dev/pts/`. These two components are the core of the pseudo-terminal (PTY) mechanism in modern Linux systems, crucial for terminal emulators and SSH remote login functions we use daily.

Simply put, they together create a “fake” terminal device, making programs (e.g., `bash`) think they are communicating with a physical terminal, when in fact they are interacting with software (e.g., GNOME Terminal or `sshd`).

This mechanism consists of two parts:

- Master Device: Represented by `/dev/ptmx`.

- Slave Device: Located in the `/dev/pts/` directory, such as `/dev/pts/0`, `/dev/pts/1`, etc.

Let’s delve deeper into their respective roles and how they work together.

### 1. The Concept of Pseudo-Terminals (PTY)

First, understand why pseudo-terminals are needed. In early Unix systems, users interacted with computers through physical serial ports (e.g., `/dev/ttyS0`) connected to physical terminals. Later, with the development of graphical interfaces and networks, we needed a way to simulate this hardware terminal at the software level.

Pseudo-terminals are this software simulation of terminals. They act like a pipe, with a “device” at each end:

- Master Side: Held and controlled by terminal emulators (e.g., xterm, GNOME Terminal) or remote login services (e.g., `sshd`).

- Slave Side: Provided for applications (e.g., `shell`, `vim`, `top`) to use. This slave device looks and behaves exactly like a true physical terminal device.

When you type on the keyboard in a terminal emulator, the terminal emulator program writes data from the master side; the kernel forwards this data to the slave side, and the `shell` program can read your input from the slave side. Conversely, when the `shell` program produces output (e.g., the result of `ls`), it writes data to the slave side; the kernel forwards it to the master side, and the terminal emulator reads this data and displays it in the window.

### 2. `/dev/ptmx`: The Pseudo-Terminal Master Multiplexer

`/dev/ptmx` is a special character device file, whose name is an abbreviation for “pseudo-terminal multiplexer.” You can think of it as a factory for creating pseudo-terminal master/slave device pairs.

Its core functions are:

1. Creating New PTY Pairs: When a program (e.g., a terminal emulator) needs a new pseudo-terminal, it opens the `/dev/ptmx` file.

2. Returning the Master Device File Descriptor: This `open` operation successfully returns a file descriptor. This file descriptor represents the master side of the newly created PTY pair.

3. Dynamically Creating the Slave Device: While opening `/dev/ptmx`, the kernel dynamically creates a corresponding slave device (Slave) file in the `/dev/pts/` directory, such as `/dev/pts/0`.

4. Providing a Control Interface: The program can perform `ioctl()` system calls on the file descriptor returned by `/dev/ptmx` to configure the PTY, such as obtaining the name of the slave device, unlocking the slave device, etc.

Key Point: You cannot directly perform extensive read/write operations on `/dev/ptmx`. Its primary purpose is to request and create a new PTY pair through `open()` calls. Subsequent read/write operations are performed through the file descriptor returned by `open()`. Each opening of `/dev/ptmx` creates a brand new, independent PTY master/slave device pair.

### 3. The `/dev/pts/` Directory and Its Files

`/dev/pts` is a special file system of type `devpts`. This directory is specifically used to store pseudo-terminal slave device files.

- Dynamically Created: The files in this directory (e.g., `/dev/pts/0`, `/dev/pts/1`, …) are not permanently present. They are dynamically created by the kernel when the corresponding PTY master device is created (i.e., when `/dev/ptmx` is opened).

- Role of Slave Devices: Each `/dev/pts/N` file acts as the slave side of a PTY pair. It is a standard TTY device, and applications (e.g., `bash`) can open it, read user input, and write program output just like any other terminal device.

- Assigned to Shell: After creating a PTY pair, the terminal emulator will `fork` a child process and redirect the standard input, output, and error of the child process to this newly created slave device (e.g., `/dev/pts/0`), then execute `bash` or another shell. Thus, `bash` “owns” this pseudo-terminal as its controlling terminal.

You can use the `tty` command to view the slave device associated with the current shell:

Bash

    $ tty
    /dev/pts/0

If you open a new terminal window and run `tty` in the new window, you might see:

Bash

    $ tty
    /dev/pts/1

### 4. Complete Creation Process

Let’s string the entire process together to see what happens in the background when you open a new terminal window:

1. Open ptmx: The GNOME Terminal program calls `open("/dev/ptmx", O_RDWR)`.

2. Create PTY Pair: The kernel receives the request and creates a new pseudo-terminal master/slave device pair.

3. Return Master Device FD: The `open` call returns a file descriptor (e.g., `fd=3`) to GNOME Terminal. This `fd` is the master side of the PTY.

4. Create Slave Device File: Simultaneously, the kernel creates a new slave device file in the `/dev/pts/` directory, such as `/dev/pts/5`.

5. Unlock and Authorize: The GNOME Terminal program performs a series of `ioctl` calls (e.g., `grantpt` and `unlockpt`) on the file descriptor of the master device to set the permissions and status of the slave device `/dev/pts/5`, making it usable.

6. Get Slave Device Name: The GNOME Terminal queries the name of the slave device associated with the master device of `fd=3` through an `ioctl` call to `ptsname`, obtaining the string “/dev/pts/5”.

7. Create Child Process: The GNOME Terminal calls `fork()` to create a child process.

8. Set Session and Redirect: In the child process:

   - A new session is created (`setsid()`), and `/dev/pts/5` is set as the controlling terminal of the session.

   - The standard input, output, and error (file descriptors 0, 1, 2) are closed.

   - `/dev/pts/5` is opened and duplicated to file descriptors 0, 1, 2. Now, the child process’s `stdin`, `stdout`, and `stderr` all point to this pseudo-terminal slave device.

9. Execute Shell: The child process calls `execve("/bin/bash", ...)` to start `bash`. `bash` inherits the already set file descriptors, naturally reading commands from `/dev/pts/5` and writing results to it.

10. Data Forwarding:

    - You type `ls` in the GNOME Terminal window.

    - The GNOME Terminal program reads the input from keyboard events and writes “ls\n” through the file descriptor of the master device `fd=3`.

    - The kernel forwards the data from the master side to the slave side `/dev/pts/5`.

    - `bash` reads “ls\n” from its standard input (`/dev/pts/5`) and executes the command.

    - The output of `ls` is written by `bash` to its standard output (`/dev/pts/5`).

    - The kernel forwards the data from the slave side to the master side.

    - The GNOME Terminal program reads the output result of `ls` through the file descriptor of the master device `fd=3` and renders it in the window.

### Summary

Component | Role | Function
--- | --- | ---
`/dev/ptmx` | Master Device Factory | As a single entry point, when opened, it creates a new pseudo-terminal master/slave device pair and returns the file descriptor representing the master device.
Files Under `/dev/pts/` | Slave Devices | These are dynamically created device files, each representing the slave device of a pseudo-terminal. They provide a standard TTY interface for applications (e.g., shells) to interact as if with a physical terminal.

### A Simple User Program Demonstrating the Use of /dev/ptmx

Well, here is a simple C language user program that clearly demonstrates how to use `/dev/ptmx` to create a pseudo-terminal and start a shell (`/bin/bash`) within this pseudo-terminal.

This program will act as the most basic “terminal emulator.” It handles the master side, while the `bash` shell runs on the slave side it creates.

#### Program Function Breakdown

1. Open `/dev/ptmx`: Obtain a file descriptor for the pseudo-terminal master device.

2. Initialize the Slave Device: Call `grantpt()` and `unlockpt()` to set the permissions and status of the slave device (slave PTY).

3. Get the Slave Device Name: Call `ptsname()` to obtain the path of the corresponding slave device (e.g., `/dev/pts/3`).

4. Create a Child Process: Use `fork()` to create a child process, which will run the shell.

5. Child Process Setup: In the child process, associate its session with the slave device, redirect standard input, output, and error to the slave device, and then execute `/bin/bash`.

6. Parent Process Communication: In the parent process, listen to input from the user’s current terminal and output from `bash`, transferring data back and forth between them to achieve interaction.

#### Source Code (`ptmx_demo.c`)

C

    #define _XOPEN_SOURCE 600 // Needed for grantpt, unlockpt, ptsname
    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/ioctl.h>
    #include <sys/select.h>
    #include <sys/wait.h>
    #include <termios.h> // Not strictly needed for the demo, but good practice
    #include <string.h>

    int main() {
        int master_fd;
        char *slave_name;
        pid_t pid;

        // 1. 打开 /dev/ptmx 来获取一个主设备文件描述符
        master_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
        if (master_fd < 0) {
            perror("Error opening /dev/ptmx");
            return 1;
        }
        printf("1. Master PTY opened with fd: %d\n", master_fd);

        // 2. 授权并解锁从设备
        if (grantpt(master_fd) != 0) {
            perror("Error calling grantpt");
            close(master_fd);
            return 1;
        }
        if (unlockpt(master_fd) != 0) {
            perror("Error calling unlockpt");
            close(master_fd);
            return 1;
        }
        printf("2. Slave PTY permissions granted and unlocked.\n");

        // 3. 获取从设备的名字
        slave_name = ptsname(master_fd);
        if (slave_name == NULL) {
            perror("Error calling ptsname");
            close(master_fd);
            return 1;
        }
        printf("3. Slave PTY name is: %s\n", slave_name);

        // 4. 创建子进程
        pid = fork();
        if (pid < 0) {
            perror("Error calling fork");
            close(master_fd);
            return 1;
        }

        // 5. 子进程的代码
        if (pid == 0) {
            int slave_fd;

            // 创建一个新的会话，使子进程成为会话领导者
            // 这是让从设备成为控制终端的关键步骤
            if (setsid() < 0) {
                perror("setsid failed");
                exit(1);
            }

            // 打开从设备
            slave_fd = open(slave_name, O_RDWR);
            if (slave_fd < 0) {
                perror("Error opening slave pty");
                exit(1);
            }

            // 将从设备设置为该进程的控制终端
            // TIOCSCTTY 是 "Set Controlling TTY" 的意思
            if (ioctl(slave_fd, TIOCSCTTY, NULL) < 0) {
                perror("ioctl TIOCSCTTY failed");
                exit(1);
            }

            // 将子进程的标准输入、输出、错误重定向到从设备
            dup2(slave_fd, STDIN_FILENO);   // fd 0
            dup2(slave_fd, STDOUT_FILENO);  // fd 1
            dup2(slave_fd, STDERR_FILENO);  // fd 2

            // 关闭不再需要的文件描述符
            close(master_fd); // 子进程不需要主设备
            close(slave_fd);  // 因为已经 dup2 了，这个原始的也可以关了

            // 执行一个新的 bash shell
            printf("--- Starting Bash Shell in Slave PTY ---\n\n");
            fflush(stdout);
            execlp("/bin/bash", "bash", NULL);

            // 如果 execlp 成功，下面的代码不会被执行
            perror("execlp failed");
            exit(1);
        }

        // 6. 父进程的代码
        printf("4. Forked child process with PID: %d\n", pid);
        printf("5. Parent process will now forward data between stdin and master PTY.\n");
        printf("--- You are now interacting with the new shell. Type 'exit' to quit. ---\n\n");

        // 父进程不需要从设备
        // close(slave_fd) in parent - it was never opened here

        char buffer[256];
        ssize_t nread;

        // 循环，直到子进程退出
        while (1) {
            fd_set read_fds;
            FD_ZERO(&read_fds);
            FD_SET(STDIN_FILENO, &read_fds); // 监听当前终端的输入
            FD_SET(master_fd, &read_fds);    // 监听主设备的输出 (来自子进程shell)

            // 使用 select 阻塞，直到有数据可读
            if (select(master_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
                perror("select failed");
                break;
            }

            // 检查是否是当前终端有输入
            if (FD_ISSET(STDIN_FILENO, &read_fds)) {
                nread = read(STDIN_FILENO, buffer, sizeof(buffer));
                if (nread > 0) {
                    // 将用户的输入写入主设备，数据会流向子进程的shell
                    write(master_fd, buffer, nread);
                } else {
                    break; // 读错误或EOF
                }
            }

            // 检查是否是主设备有输出
            if (FD_ISSET(master_fd, &read_fds)) {
                nread = read(master_fd, buffer, sizeof(buffer));
                if (nread > 0) {
                    // 将来自shell的输出写入当前终端的屏幕
                    write(STDOUT_FILENO, buffer, nread);
                } else {
                    // 读取到 0 或 -1，意味着子进程的另一端关闭了连接
                    // 通常是 shell 执行了 exit
                    break;
                }
            }
        }

        printf("\n--- Shell terminated. Parent process is shutting down. ---\n");
        close(master_fd);
        wait(NULL); // 等待子进程完全终止

        return 0;
    }

#### How to Compile and Run

1. Save the Code: Save the above code as `ptmx_demo.c`.

2. Compile: Compile using gcc.

   Bash

       gcc -o ptmx_demo ptmx_demo.c
       _en```_translated_label__

3. Run: Execute the generated executable file.

   Bash

   _en`_translated_label__ ./ptmx_demo _en`

#### Runtime Output and Interaction

When you run the program, you will see output similar to the following:

    1. Master PTY opened with fd: 3
    2. Slave PTY permissions granted and unlocked.
    3. Slave PTY name is: /dev/pts/2
    4. Forked child process with PID: 12345
    5. Parent process will now forward data between stdin and master PTY.
    --- You are now interacting with the new shell. Type 'exit' to quit. ---

    bash-5.1$

What Happened?

- Your `./ptmx_demo` program created a new pseudo-terminal `/dev/pts/2`.

- It started a new `bash` process, whose “world” is `/dev/pts/2`.

- Your `./ptmx_demo` program acts as an intermediary:

  - It reads keyboard input from your current terminal (e.g., `/dev/pts/1`).

  - Writes your input (e.g., `ls -l`) to the master device (`master_fd`).

  - The kernel forwards the data to the slave device `/dev/pts/2`.

  - `bash` reads `ls -l` from its standard input (`/dev/pts/2`) and executes it.

  - `ls`’s output is written by `bash` to its standard output (`/dev/pts/2`).

  - The kernel forwards the data to the master device.

  - Your `./ptmx_demo` program reads the result from the master device (`master_fd`) and prints it to its standard output, so you see the result of `ls -l` on the screen.

You can execute commands as in a normal shell:

    bash-5.1$ pwd
    /home/user/test
    bash-5.1$ whoami
    user
    bash-5.1$ ps -f
    UID          PID    PPID  C STIME TTY          TIME CMD
    user       12344   5678  0 10:30 pts/1    00:00:00 ./ptmx_demo
    user       12345  12344  0 10:30 pts/2    00:00:00 bash
    user       12350  12345  0 10:31 pts/2    00:00:00 ps -f

Note the output of the `ps` command! Our `ptmx_demo` program runs on `pts/1` (your original terminal), while the `bash` process it creates runs in a completely new terminal `pts/2`.

When you type `exit` and press Enter, the `bash` process terminates, causing the pseudo-terminal connection to close. The `read(master_fd, ...)` in the parent process returns 0, the loop breaks, and the program exits gracefully.

This example fully demonstrates the core use of `/dev/ptmx`: creating an isolated terminal session environment and allowing a program (parent process) to fully control the input and output of another program (child process). This is also the foundational working principle of all terminal emulators, `ssh` services, and `tmux`/`screen` tools.

Other Versions v: V0.4.0

Tags
  V0.1.0
  V0.1.1
  V0.1.10
  V0.1.2
  V0.1.3
  V0.1.4
  V0.1.5
  V0.1.6
  V0.1.7
  V0.1.8
  V0.1.9
  V0.2.0
  V0.3.0
  V0.4.0

Branches
  master