【Linux】Linux终端技术解析
Linux 终端系统是操作系统中最基础也是最复杂的组件之一,它不仅是用户与系统交互的桥梁,更是进程管理、会话控制的核心。理解终端的工作原理,对于深入掌握 Linux 系统运行机制、开发系统级应用以及进行系统维护都至关重要。
1. 基本概念
1.1 什么是终端?
终端(Terminal)是用户与计算机系统进行交互的接口,它允许用户输入命令并查看输出。在 Linux 系统中,终端是一个字符设备,用于处理字符流的输入输出。
可能有人会觉得终端就是下面这个样子:
其实它只是现代计算机中终端的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 关键特性
-
一个会话只能有一个控制终端 -
一个会话可以有多个进程组 -
一个进程组中的所有进程共享同一个控制终端 -
前台进程组可以接收终端的输入和信号
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
-
与用户的距离
-
终端是用户直接交互的界面 -
Shell 运行在终端内部 -
其他进程由 Shell 创建和管理
-
-
控制关系
-
终端控制 Shell 的输入输出 -
Shell 控制其他进程的创建和执行 -
终端是 Shell 的"容器" -
Shell 是进程的"管理者"
-
-
交互层次
用户 <-> 终端 <-> Shell <-> 其他进程-
用户直接与终端交互 -
终端将用户输入传递给 Shell -
Shell 解析命令并控制其他进程 -
进程的输出通过 Shell 和终端返回给用户
-
-
实际应用中的体现
-
终端提供界面和原始输入输出 -
Shell 提供命令解释和执行环境 -
用户通过终端输入命令 -
Shell 在终端中运行并处理命令 -
其他进程在 Shell 的控制下执行
-
2.3.3 交互关系
graph LR
A[用户] --> B[终端设备]
B --> C[Shell进程]
C --> D[其他进程]
D --> B
B --> A
2.3.4 工作流程
-
用户通过终端输入命令 -
终端将输入传递给 Shell -
Shell 解析命令并创建相应的进程 -
进程通过终端输出结果 -
终端将输出显示给用户
2.3.5 示例讲解
在Ubuntu系统中右键打开终端时,终端和Shell便被系统创建出来,此时的系统状态可以用下图描述:
当用户输入命令 ls 并回车,此时的系统状态为:
可以看到Shell所在的进程组,被切换成了后台进程组,ls命令被放在了一个新建的进程组中,并且成为了当前进程组,负责和外界交互。
当用户输入 ls | grep 命令时,表示启动两个进程,这两个进程使用管道符号 | 连接,此时的系统状态为:
当用户输入 ls & 命令时,表示启动一个后台进程,此时的系统状态为:
通过上图我们也可以很清晰地看出标准输入,标准输出,标准错误输出所扮演的角色,它们是进程和终端交互的接口,而且是一种文件接形式的接口,这也是Linux上一切皆文件思想的体现。
3. 终端类型
Linux 系统中的终端设备主要分为以下几类:
-
控制台终端(Console):
/dev/console-
系统启动时的主控制台 -
内核消息输出 -
系统紧急情况下的交互界面 -
单用户模式下的系统维护
-
-
虚拟终端(Virtual Terminal):
/dev/ttyN(N 为数字)-
多用户登录界面 -
图形界面下的终端模拟器 -
系统管理员维护操作 -
多任务并行处理
-
-
伪终端(Pseudo Terminal):
/dev/pts/N-
SSH 远程登录 -
图形界面下的终端模拟器(如 xterm、gnome-terminal) -
远程桌面会话 -
容器和虚拟机的终端访问
-
-
串行终端(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 控制字符类型
-
特殊控制字符:
-
Ctrl+C (SIGINT):中断进程 -
Ctrl+Z (SIGTSTP):暂停进程 -
Ctrl+\ (SIGQUIT):强制退出进程 -
Ctrl+D (EOF):文件结束符
-
-
行编辑字符:
-
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 核心步骤
-
创建子进程
pid_t pid = fork();
if (pid > 0) exit(0); // 父进程退出
-
创建新会话
setsid(); // 创建新会话,脱离终端控制
-
改变工作目录
chdir("/"); // 切换到根目录
-
关闭标准输入输出(STDIN、STDOUT、STDERR)
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
主要目的是:
-
防止守护进程与终端交互 -
避免占用这些文件描述符 -
确保守护进程完全脱离终端控制
-
设置权限掩码
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标志的主要用途:
-
防止打开的设备成为控制终端 -
在打开终端设备(比如说串口设备)时使用 -
确保进程不会意外获得控制终端,比如守护进程就不需要控制终端
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 常见使用场景
-
串口通信程序:需要操作串口设备,但不需要处理终端信号 -
终端模拟器:需要模拟终端行为,但不想成为控制终端 -
设备控制:需要控制终端设备,但不想处理终端相关信号 -
使用O_NOCTTY可以防止非会话首进程意外获得控制终端
7. 总结
日常工作中我们需要频繁和终端打交道,如果不明白终端的原理,将很难进行一些比较深入的工作。终端作为进程与外界交互的桥梁,在 Linux 系统中扮演着重要角色。通过理解进程、进程组、会话与终端的交互机制,可以更好地开发和管理终端相关的应用程序。