什么是终端

能实现数据输入、输出的统称为终端

物理终端:传统显示器、键盘,通过 VGA、HDMI、USB 等连接。

软件终端(虚拟终端/伪终端):通过 TCP/IP 协议实现的终端,能模拟出来一个实现「数据输入、输出」的终端,比如:ssh、telnet …

这篇博客介绍的比较详细:「转」彻底理解Linux的各种终端类型以及概念

伪终端的连接过程

在linux中有物理终端,有虚拟终端(伪终端)。

sshd 服务会打开一个 [dev/ptmx],这个文件是一个伪终端主设备文件。

bin/bash 会打开一个 dev/pts(0,1…), 是一个伪终端从设备文件。

ptmx/ptsx 它们通过伪终端设备驱动程序模拟出输入和输出的功能。

bin/bash 进程就可以实现数据读取和数据写入。

ssh客户端 ——————> 远程服务器的一个进程 ——-> ssh客户端。

输入单元(ssh客户端)——–> /bin/bash进程————–> 输出单元(ssh客户端)。

bin/bash 启动之后,对终端数据的读取和写入就是通过 pts 实现的(能实现标准输入,标准输出)。

dev/pts0 0 1 2 标准输入,标准输出,标准错误。

通过 strace 命令查看sshd服务:

strace 前进程关系:

├─sshd,9408
│   └─sshd,2888142    
│       └─bash,2888170
│           └─pstree,2889779 -ap

strace 中:

$strace -f -s 65500 -o sshd.log -p 9408 # strace 查看系统调用,进行一个新的 SSH 连接

strace: Process 9408 attached
strace: Process 2888330 attached
strace: Process 2888331 attached
strace: Process 2888336 attached
strace: Process 2888342 attached
strace: Process 2888343 attached
strace: Process 2888344 attached
strace: Process 2888345 attached
strace: Process 2888346 attached
strace: Process 2888347 attached
strace: Process 2888348 attached
strace: Process 2888349 attached
strace: Process 2888350 attached
strace: Process 2888351 attached
strace: Process 2888352 attached
strace: Process 2888353 attached
strace: Process 2888354 attached
strace: Process 2888355 attached
strace: Process 2888356 attached
strace: Process 2888357 attached
strace: Process 2888358 attached # 新的SSH连接
strace: Process 2888359 attached
strace: Process 2888360 attached
strace: Process 2888361 attached
strace: Process 2888362 attached
strace: Process 2888363 attached
strace: Process 2888364 attached
strace: Process 2888365 attached
strace: Process 2888382 attached
^Cstrace: Process 9408 detached # 终止信号
strace: Process 2888330 detached
strace: Process 2888357 detached

新的 SSH 连接以后的进程关系:

├─sshd,9408
│   ├─sshd,2888142    
│   │   └─bash,2888170
│   └─sshd,2888330    
│       └─bash,2888357 # 新的SSH连接
│           └─pstree,2889432 -ap

查看 sshd.log:

# select 是一个 socket 接口函数
9408  select(95, [3], NULL, NULL, NULL) = 1 (in [3])
# 接收客户端连接
9408  accept(3, {sa_family=AF_INET, sin_port=htons(22670), sin_addr=inet_addr("123.139.68.156")}, [128->16]) = 4 
9408  fcntl(4, F_GETFL)                 = 0x2 (flags O_RDWR)
9408  pipe([5, 6])                      = 0
9408  socketpair(AF_UNIX, SOCK_STREAM, 0, [7, 8]) = 0
# 创建 SSHD 子进程
9408  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f1fc4482210) = 2888330
9408  close(6)                          = 0

...

# 打开 ptmx 伪终端主设备文件
2888330 openat(AT_FDCWD, "/dev/ptmx", O_RDWR) = 9
2888330 statfs("/dev/pts", {f_type=DEVPTS_SUPER_MAGIC, f_bsize=4096, f_blocks=0, f_bfree=0, f_bavail=0, f_files=0, f_ffree=0, f_fsid={val=[0, 0]}, f_namelen=255,

...

# 创建 bash 子进程
2888330 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f1994e47210) = 2888357
2888357 set_robust_list(0x7f1994e47220, 24) = 0

...

2888357 close(11 <unfinished ...>
2888357 <... close resumed>)            = 0
2888357 close(12 <unfinished ...>
2888357 <... close resumed>)            = 0
2888357 close(9 <unfinished ...>
2888357 <... close resumed>)            = 0

# 把自己设置为会话首进程
2888357 setsid( <unfinished ...>
2888357 openat(AT_FDCWD, "/dev/tty", O_RDWR|O_NOCTTY <unfinished ...>
2888357 <... openat resumed>)           = -1 ENXIO (No such device or address)
2888357 setsid( <unfinished ...>
2888357 <... setsid resumed>)           = 2888357
2888357 openat(AT_FDCWD, "/dev/tty", O_RDWR|O_NOCTTY <unfinished ...>
2888357 <... openat resumed>)           = -1 ENXIO (No such device or address)
2888357 ioctl(10, TIOCSCTTY, 0 <unfinished ...>
2888357 <... ioctl resumed>)            = 0

# 伪终端从设备文件
# 当 /bin/bash 进程打开这个文件时,就可以简单认为该进程已经连接了我们的输入(键盘)、输出单元(显示器)。
# 已经能实现数据的输入和输出,相当于连接了一个终端(这个终端是通过 [TCP/IP] 协议实现的)。
# Linux 内核(SSHD服务)模拟出一个终端:[0、1、2 标准输入、标准输出、标准错误]
2888357 openat(AT_FDCWD, "/dev/pts/1", O_RDWR <unfinished ...>

...

# 在终端输入 `ll` 的系统调用
# 说明伪终端通过 [TCP/IP] 传输
# 输入(键盘)
2888357 <... read resumed>"l", 1)       = 1

# socket 监听
2888330 select(12, [4 5 11], [], NULL, NULL <unfinished ...>
2888357 select(1, [0], NULL, [0], {tv_sec=0, tv_usec=0}) = 1 (in [0], left {tv_sec=0, tv_usec=0})
2888357 pselect6(1, [0], NULL, NULL, NULL, {[], 8}) = 1 (in [0])

# 输入(键盘)
2888357 read(0, "l", 1)                 = 1
2888357 select(1, [0], NULL, [0], {tv_sec=0, tv_usec=0}) = 0 (Timeout)

# 输出(显示器)
2888357 write(2, "ll", 2)               = 2

总结:

  1. 通过 ssh 客户端连接 sshd 服务(它是一个守护进程,实现协议是:TCP/IP);

  2. sshd 接收客户端连接之后,clone,fork 一个进程,同时打开伪终端主设备文件 /dev/ptmx

  3. 再fork一个进程,同时启动 bin/bash(etc/passwd)进程,该进程会打开一个伪终端从设备 dev/pts

  4. 伪终端能实现数据输入(键盘),还能实现数据输出(显示器),就是这个进程对应的键盘、显示器,键盘(0),显示器(1,2);

  5. 主从设备终端通过伪终端设备驱动程序进行通信;

  6. ssh 客户端输入的数据,可以当作远程服务器的键盘输入的数据;

  7. 远程服务器的输出,通过 TCP/IP 协议传输到 ssh 客户端;

理解这些对:进程组,会话,守护进程的学习很重要。