【Linux】Linux终端技术解析

撬动未来的支点 7月前 544

【Linux】Linux终端技术解析

Linux 终端系统是操作系统中最基础也是最复杂的组件之一,它不仅是用户与系统交互的桥梁,更是进程管理、会话控制的核心。理解终端的工作原理,对于深入掌握 Linux 系统运行机制、开发系统级应用以及进行系统维护都至关重要。

1. 基本概念

1.1 什么是终端?

终端(Terminal)是用户与计算机系统进行交互的接口,它允许用户输入命令并查看输出。在 Linux 系统中,终端是一个字符设备,用于处理字符流的输入输出。

可能有人会觉得终端就是下面这个样子:

Ubuntu中的终端UI 其实它只是现代计算机中终端的UI界面,在Linux中,终端实际上是一种进程与外部进行命令交互的机制的名称。终端是一种操作系统机制,而UI只是附加在终端机制上的图形界面,终端机制是主体,UI只是一种输出方式,如果没有显示器,终端一样可以工作。关于UI图形界面的地位,大家一定不要放得太高,它仅仅是一种现代化的交互方式。特别是学习Linux,Linux中的功能模块是与UI解耦的,独立的。可能从事较多的应用开发的同学,在设计软件时,首先想到的是先画一个界面原型,再设计功能,但是越往系统层面,越是功能机制大于图形界面。在学习底层技术时,这个思维方式一定要转换过来,这也是学习Linux一大思维技巧之一。

1.2 进程、进程组和会话

1.2.1 进程(Process)

程序执行的实例,拥有独立的地址空间和资源

1.2.2 进程组(Process Group)

进程组就像一队工人,方便系统统一管理多个关联进程:

组长的角色

  • 第一个启动的进程是组长(如 ls | grep 中的 ls),全组的「工号」(PGID)和组长相同。

统一行动

  • Ctrl+C 会终止全组(前台组),kill -信号 -组号 可批量操作后台组。

生命周期

  • 即使组长挂了(进程终止),只要组里还有人在干活(其他进程存活),组就继续存在。

进程组是「打包管理」,像公司统一指挥项目组,发通知、派任务都整组操作。

1.2.3 会话(Session)

进程组的集合,共享同一个会话ID(SID)

2. 进程、会话与终端的关系

2.1 层次结构

graph TD
    A[会话 Session] --> B[进程组A<br/>前台进程组]
    A --> D[控制终端]
    A --> C[进程组B<br/>后台进程组]
    B --> E[进程1]
    B --> F[进程2]
    C --> G[进程3]
    C --> H[进程4]

2.2 关键特性

  1. 一个会话只能有一个控制终端
  2. 一个会话可以有多个进程组
  3. 一个进程组中的所有进程共享同一个控制终端
  4. 前台进程组可以接收终端的输入和信号

2.3 Shell 与终端的关系

2.3.1 基本概念

  • 终端(Terminal):提供用户输入和程序输出的界面
  • Shell:命令解释器,处理用户输入的命令并执行相应的程序

2.3.2 控制关系与用户距离

graph TD
    A[用户] --> B[终端]
    B --> C[Shell]
    C --> D[其他进程]
    
    style A fill:#f9f,stroke:#333,stroke-width:2px
    style B fill:#bbf,stroke:#333,stroke-width:2px
    style C fill:#bfb,stroke:#333,stroke-width:2px
    style D fill:#fbb,stroke:#333,stroke-width:2px
  1. 与用户的距离

    • 终端是用户直接交互的界面
    • Shell 运行在终端内部
    • 其他进程由 Shell 创建和管理
  2. 控制关系

    • 终端控制 Shell 的输入输出
    • Shell 控制其他进程的创建和执行
    • 终端是 Shell 的"容器"
    • Shell 是进程的"管理者"
  3. 交互层次

    用户 <-> 终端 <-> Shell <-> 其他进程
    • 用户直接与终端交互
    • 终端将用户输入传递给 Shell
    • Shell 解析命令并控制其他进程
    • 进程的输出通过 Shell 和终端返回给用户
  4. 实际应用中的体现

    • 终端提供界面和原始输入输出
    • Shell 提供命令解释和执行环境
    • 用户通过终端输入命令
    • Shell 在终端中运行并处理命令
    • 其他进程在 Shell 的控制下执行

2.3.3 交互关系

graph LR
    A[用户] --> B[终端设备]
    B --> C[Shell进程]
    C --> D[其他进程]
    D --> B
    B --> A

2.3.4 工作流程

  1. 用户通过终端输入命令
  2. 终端将输入传递给 Shell
  3. Shell 解析命令并创建相应的进程
  4. 进程通过终端输出结果
  5. 终端将输出显示给用户

2.3.5 示例讲解

在Ubuntu系统中右键打开终端时,终端和Shell便被系统创建出来,此时的系统状态可以用下图描述:

会话进程组与控制终端架构图-初始状态
会话进程组与控制终端架构图-初始状态

当用户输入命令 ls 并回车,此时的系统状态为:

会话进程组与控制终端架构图-执行ls命令
会话进程组与控制终端架构图-执行ls命令

可以看到Shell所在的进程组,被切换成了后台进程组,ls命令被放在了一个新建的进程组中,并且成为了当前进程组,负责和外界交互。

当用户输入 ls | grep 命令时,表示启动两个进程,这两个进程使用管道符号 | 连接,此时的系统状态为:

会话进程组与控制终端架构图-执行ls和grep命令
会话进程组与控制终端架构图-执行ls和grep命令

当用户输入 ls & 命令时,表示启动一个后台进程,此时的系统状态为:

会话进程组与控制终端架构图-执行ls &命令

通过上图我们也可以很清晰地看出标准输入,标准输出,标准错误输出所扮演的角色,它们是进程和终端交互的接口,而且是一种文件接形式的接口,这也是Linux上一切皆文件思想的体现。

3. 终端类型

Linux 系统中的终端设备主要分为以下几类:

  1. 控制台终端(Console):/dev/console

    • 系统启动时的主控制台
    • 内核消息输出
    • 系统紧急情况下的交互界面
    • 单用户模式下的系统维护
  2. 虚拟终端(Virtual Terminal):/dev/ttyN(N 为数字)

    • 多用户登录界面
    • 图形界面下的终端模拟器
    • 系统管理员维护操作
    • 多任务并行处理
  3. 伪终端(Pseudo Terminal):/dev/pts/N

    • SSH 远程登录
    • 图形界面下的终端模拟器(如 xterm、gnome-terminal)
    • 远程桌面会话
    • 容器和虚拟机的终端访问
  4. 串行终端(Serial Terminal):/dev/ttyS*/dev/ttyAMA*

    • 嵌入式设备调试
    • 工业控制设备通信
    • 串口设备连接(如调制解调器)
    • 硬件设备固件更新

3.1 常见应用场景

graph TD
    A[终端类型选择] --> B{使用场景}
    B -->|系统管理| C[控制台终端]
    B -->|本地多任务| D[虚拟终端]
    B -->|远程访问| E[伪终端]
    B -->|硬件通信| F[串行终端]
    
    C --> C1[系统启动]
    C --> C2[内核调试]
    
    D --> D1[本地Shell]
    D --> D2[图形终端]
    
    E --> E1[SSH连接]
    E --> E2[远程桌面]
    
    F --> F1[设备调试]
    F --> F2[工业控制]

4. 控制字符处理机制

控制终端中的字符处理程序,被称作“行规程”,英文:Line Discipline,简单来说就是字符行式处理程序。行规程是用来处理终端字符的程序,它是终端设备驱动中的一个重要软件层,位于终端设备驱动和用户程序之间,负责处理终端输入输出的各种字符,对输入输出进行预处理。

4.1 控制字符类型

  1. 特殊控制字符:

    • Ctrl+C (SIGINT):中断进程
    • Ctrl+Z (SIGTSTP):暂停进程
    • Ctrl+\ (SIGQUIT):强制退出进程
    • Ctrl+D (EOF):文件结束符
  2. 行编辑字符:

    • Backspace:删除前一个字符
    • Ctrl+U:删除整行
    • Ctrl+W:删除前一个单词

4.2 控制字符处理流程

sequenceDiagram
    participant U as 用户
    participant T as 终端设备
    participant L as 行规程
    participant P as 进程组

    U->>T: 输入控制字符
    T->>L: 字符识别
    alt 特殊控制字符
        L->>P: 发送信号
        P->>P: 处理信号
    else 行编辑字符
        L->>L: 编辑缓冲区
        L->>T: 更新显示
    end

4.3 信号处理

// 信号处理函数示例
void signal_handler(int signo) {
    switch(signo) {
        case SIGINT:
            // 处理 Ctrl+C
            break;
        case SIGTSTP:
            // 处理 Ctrl+Z
            break;
        case SIGQUIT:
            // 处理 Ctrl+\
            break;
    }
}

// 注册信号处理函数
signal(SIGINT, signal_handler);
signal(SIGTSTP, signal_handler);
signal(SIGQUIT, signal_handler);

5. 内核中的实现

5.1 关键数据结构

// 进程结构体中的终端相关字段
struct task_struct {
    struct tty_struct *tty;        // 控制终端,这是实现关联的地方
    struct tty_ldisc *tty_ldisc;   // 行规程
    struct tty_port *tty_port;     // 终端端口
    pid_t pgrp;                    // 进程组ID
    pid_t session;                 // 会话ID
};

// 终端结构体
struct tty_struct {
    struct tty_driver *driver;     // 终端驱动
    struct tty_ldisc *ldisc;       // 行规程
    struct tty_port *port;         // 端口信息
    struct tty_operations *ops;     // 操作函数集
    struct pid *session;           // 关联的会话,这也是实现关联的地方
    struct pid *pgrp;              // 前台进程组,这也是实现关联的地方
};

6. 实际应用

6.1 实现守护进程

通过对会话和终端的精确控制,我们可以将一个程序实现为守护进程,也就是系统服务。

6.1.1 核心步骤

  1. 创建子进程
pid_t pid = fork();
if (pid > 0exit(0);  // 父进程退出
  1. 创建新会话
setsid();  // 创建新会话,脱离终端控制
  1. 改变工作目录
chdir("/");  // 切换到根目录
  1. 关闭标准输入输出(STDIN、STDOUT、STDERR)
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

主要目的是:

  • 防止守护进程与终端交互
  • 避免占用这些文件描述符
  • 确保守护进程完全脱离终端控制
  1. 设置权限掩码
umask(0);  // 设置文件权限掩码

6.1.2 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
    // 1. 创建子进程
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    }
    
    // 父进程退出
    if (pid > 0) {
        exit(0);
    }
    
    // 2. 创建新的会话
    if (setsid() < 0) {
        perror("setsid failed");
        exit(1);
    }
    
    // 3. 改变工作目录到根目录
    if (chdir("/") < 0) {
        perror("chdir failed");
        exit(1);
    }
    
    // 4. 关闭标准输入输出
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    
    // 5. 设置文件权限掩码
    umask(0);
    
    // 守护进程的主要工作
    while (1) {
        // 在这里添加守护进程的具体工作
        sleep(1);
    }
    
    return 0;
}

6.2 控制终端和O_NOCTTY标志

6.2.1 控制终端(Controlling Terminal)

  • 控制终端是进程组的一个特殊属性
  • 每个会话最多只能有一个控制终端
  • 控制终端用于处理终端输入输出和信号

6.2.2 O_NOCTTY标志的作用

int fd = open("/dev/tty", O_RDWR | O_NOCTTY);

O_NOCTTY标志的主要用途:

  1. 防止打开的设备成为控制终端
  2. 在打开终端设备(比如说串口设备)时使用
  3. 确保进程不会意外获得控制终端,比如守护进程就不需要控制终端

6.2.3 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

int main() {
    // 打开终端设备,但不让它成为控制终端,因为我们仅仅想要读写串口数据而已
    int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
    if (fd < 0) {
        perror("open failed");
        exit(1);
    }

    // 设置终端属性
    struct termios tty;
    tcgetattr(fd, &tty);
    
    // 修改终端属性
    tty.c_cflag &= ~PARENB;  // 禁用奇偶校验
    tty.c_cflag &= ~CSTOPB;  // 1个停止位
    tty.c_cflag &= ~CSIZE;   // 清除大小位
    tty.c_cflag |= CS8;      // 8位数据位
    
    // 应用新的终端属性
    tcsetattr(fd, TCSANOW, &tty);
    
    close(fd);
    return 0;
}

6.2.4 常见使用场景

  1. 串口通信程序:需要操作串口设备,但不需要处理终端信号
  2. 终端模拟器:需要模拟终端行为,但不想成为控制终端
  3. 设备控制:需要控制终端设备,但不想处理终端相关信号
  4. 使用O_NOCTTY可以防止非会话首进程意外获得控制终端

7. 总结

日常工作中我们需要频繁和终端打交道,如果不明白终端的原理,将很难进行一些比较深入的工作。终端作为进程与外界交互的桥梁,在 Linux 系统中扮演着重要角色。通过理解进程、进程组、会话与终端的交互机制,可以更好地开发和管理终端相关的应用程序。

最新回复 (0)
返回
发新帖