从 TTY 到 PTY,程序的输入、输出与控制

TTY 设备在 Linux 系统中的应用比较广泛,但很少有人去深度探讨 TTY 设备在整个 Linux 系统交互中起到的关键性作用。本文整合了几篇较为经典的文章对 TTY 设备整体工作原理、机制及应用的探讨,希望能够为读者带来帮助。 从功能性的角度看 其实,从功能性的角度来看,Linux 的 TTY 设备就是受操作系统管理的双向管道,用户通过外设(一般是键盘)输入的数据会进入到 TTY 设备文件,经过终端驱动,传递给操作系统进行处理,操作系统输出到 TTY 设备的数据则会被输出到外设(一般是屏幕),让用户能够看见,本质上就是一个从外设到操作系统的双向管道。 PTY 和 TTY 在功能上是一致的,不过它可以被用户进程创建。用户进程可以通过 Linux 系统提供的接口创建一个 PTS Pair,一个 PTS Pair 分为 Master 设备和 Slave 设备两种,输出到 Master 设备的数据会被传递到 Slave 设备的输入,而输出到 Slave 设备的数据会被传递到 Master 设备的输入。如果我们将显示器绑定到 Master 设备的输出上面,再将键盘绑定到 Master 设备的输入上面,那么我们就能够通过 Master 设备间接操作绑定到 Slave 设备的进程了。因为 PTY 和 TTY 本身并没有太大的差别,为避免过于啰嗦,在下面我只针对 PTY 进行讲述。 一个例子:SSH SSH 的设计中也使用了 PTY 设备,我们在通过 SSH 登入主机进入 Shell 后,可以通过 tty 命令去获取当前的 Shell 进程运行在哪个 PTY 上面。 上面tty命令给出的结果/dev/pts/dev是当前运行的 Shell 绑定的 PTY Slave 设备,而对应的 Master 设备文件描述符只有通过 /dev/ptmx创建 PTY Pair 的进程持有,都指向/dev/ptmx,可以通过sudo lsof /dev/ptmx查看。我们在使用 SSH 的时候,本地的 Terminal Emulator 将输入的字符发送到 SSH 进程,SSH 进程收到字符以后通过网络传输给服务器上面的 SSHD 进程,SSHD 进程将字符写入到 Master 设备,Shell 就能够从 Slave 设备中读取到字符。同理,Shell 的输出写入到 Slave 设备,被 SSHD 从 Master 设备处读取,通过网络传递给 SSH 客户端,最终 SSH 客户端将字符显示到屏幕上(当然,一般我们都是在 Terminal Emulator 里面运行的 SSH 进程,所以本地的 SSH 进程也是要通过 TTY 设备和键盘以及屏幕通讯的,这里图方便就没有画出来,大概样子和图 1 差不多)。...

March 3, 2023 · 3 min · H1R4

Netlink 笔记

在用户空间的程序有的时候可能需要和内核进行消息交互,内核需要提供一种机制来满足用户空间程序的这一需求。虽然系统调用确实提供了一个用户空间程序和内核的交互机制,但是系统调用在一些轻量级的交互上未免显得过于重量级。为此,Linux 内核在用于请求特定信息的经典系统调用以外,还提供了另外的几种用户空间和内核的通信接口,分别是procfs,sysctl,sysfs,ioctl以及 netlink 套接字。procfs和sysfs都是虚拟文件系统,这一部分暂且不提,sysctl对应的是/proc/sys文件,实际上是一个内核变量,这一部分也不在这里详谈,ioctl则是在设备控制上做的工作,细说起来估计也能单开一篇文章了。本文主要还是对 Netlink 的机制做一个简单的概括与总结。 Netlink 是什么?为什么是 Netlink? 按书里的描述总结来看,Netlink 是一个允许内核内部以及内核与用户空间的程序相互进行通信的消息传输机制,是对标准套接字实现的拓展。在 Netlink 诞生之前,一般使用 procfs、sysfs 和 ioctl 来进行内核和用户空间程序之间的数据交互。相比其他机制,Netlink 具有一些更明显的优势: 任何一方都不需要轮询,像 procfs 和 sysfs 这类利用文件进行信息传递的机制,用户空间程序需要不断轮询文件来及时获取信息的更新。 系统调用和 ioctl 也能够从用户空间向内核传递信息,但是相比 Netlink 更难以实现。另外,使用 Netlink 不会与其他的内核模块产生冲突,但模块和系统调用显然配合得不是很好。(这里是《深入 Linux 内核架构》的原话,不过我还不是很理解) 内核可以直接向用户层发送信息,而不需要用户空间程序事先向内核请求。这一点使用文件也能做到,但是系统调用和 ioctl 显然不能够做到。 用户只需要使用标准的套接字和内核进行交互。 内核不仅支持单播消息,也支持多播,并且 Netlink 的工作方式是异步的。Netlink(3) 和 Netlink(7) 两个手册页提供了 Netlink 机制的文档,其中 netlink(3) 描述了内核中用于操作、访问、创建 Netlink 数据报的宏。手册页 netlink(7) 包含了有关 Netlink 套接字的一般性信息,并给出了这里其的数据结构的文档。另外,/proc/net/netlink里面包含了关于当前活动的 Netlink 连接的一些信息。 Netlink 是怎么工作的? 从创建说起 从用户空间程序来看,Netlink 和普通的 BSD 套接字长得很像。在创建 Netlink 套接字的时候,也需要用socket()函数创建一个 socket 描述符,再为其分配一个地址。创建 Netlink 套接字的时候,需要指定 family 为AF_NETLINK,而 type 既可以指定为SOCK_RAW,也可以是SOCK_DGRAM,最终创建的都是一个netlink_sock对象。至于 protocol,根据该 Netlink 套接字的用途,有不同的选择,具体可以看《深入 Linux 内核架构》第 650 页或者是 Netlink 的手册。...

December 12, 2022 · 2 min · H1R4

《Linux 内核设计的艺术》第一部分笔记

前言 最近粗略拜读了朋友推荐的《Linux 内核设计的艺术》这本书。不过相比于 Linux 内核设计的艺术,我从这本书中获得的最大启发其实是 Linux 启动的流程。尽管这本书中的 Linux 版本比较古早,和现行的 Linux 发行版的启动方式大相径庭,不过也从一个更底层的角度向我展示了整个操作系统的引导流程,不管是 I/O 子系统还是中断子系统,也让我对内核的工作方式和存在结构有了全新的认识。遗憾的是,我的这些问题,如果早点读到《操作系统真象还原》这本书的话,可能能得到更好的解答(当然,《操作系统真象还原》这本书讲的并不是 Linux,而是 x86 体系上的通用操作系统概念)。下文的讲述将可能引用《操作系统真象还原》这本书的内容,简洁起见(省得打字),下面将《操作系统真象还原》简称为《真象》,而《Linux 内核设计的艺术》简称为《艺术》。 第一行指令 在先前的计算机组成课中,我了解到了指令在系统中是如何流转的,但这种就是一个比较底层的视角,我还是没能计算机组成与操作系统这两门课很好地结合到一起。这本书与其他讲述 Linux 内核的书籍不同之处在于,不是一上来就基于代码模块对整个 Linux 内核进行分块的剖析,而是从按下开机键上电到 Linux 启动完毕的流程娓娓道来,这正是我所欠缺的,将操作系统与计算机组成统一起来的视角。 指令从何而来? 计算机的心脏,CPU,从上电那一刻起就在不停地搏动。那么第一下搏动从何而来呢?每条指令需要在 CPU 上执行,才能产生效果,因此触发第一条指令执行的,必定不是某一条指令,而是只有不需要前置条件,上电即可触发的硬件。在 Intel 8086 CPU 架构中,上电之后 CPU 的 CS:IP 会被指向地址0xFFFF0,这个地址下会有一条指令,跳转到 BIOS 程序的真正入口点。那么问题又来了,这第一条指令又是从哪里来的呢? 这个点在《真象》里面是这么解释的。在 I/O 子系统中,我们曾经学过一种 CPU 的 I/O 方式,即内存映射 IO,内存映射 I/O 通过将外设于一块特别的内存区域绑定到一块,这样指令可以直接通过访问那块区域的内存来读写外设上面的数据。在 CPU 能够访问到的地址空间里面,我们可以将一部分地址空间用于 I/O,剩下的内存地址空间再绑定到物理内存上面。因为 BIOS 是计算机上面执行的第一行指令,所以 BIOS 必定是由硬件加载的,而这里的硬件,就是 ROM。ROM 也是一块内存,不过这块内存是非易失性的,这块内存的地址空间被硬件映射到内存地址空间0xF0000~0xFFFFF处,因此,当我们访问这块地址的时候,实际上我们是在访问 ROM 中的内容,就不会有加载的问题。 BIOS 的工作是什么? 当 BIOS 的第一行指令开始上 CPU 执行后,BIOS 的指令就会被源源不断地读进 CPU 并执行了。现在 CPU 正处于一个叫做实模式的状态下。为什么会是这个模式,以及实模式和保护模式的区别将在后续的内容中讲到,这里只需要注意,实模式下的访存方式,和我们在计算机组成里面学到的访存机制基本一致,是直接将 CS:IP 的值组装成地址之后通过地址总线访问,不需要额外的转换工作。Intel 8086 留给实模式的地址线只有 20 根,所以只能够访问到 1MB 的内存。随着 BIOS 的指令执行,它会检查所有外设的状态信息,如果没有检测到错误,那么下一步,就要开始准备中断向量表和中断服务程序,为下面的操作系统引导铺路。...

December 10, 2022 · 2 min · H1R4

WireGuard 配置笔记

碎碎念 之前一直想着部署一套系统,好让自己不在家的时候也能访问家中部署的服务。然而使用 frp 之类的工具直接将服务暴露在公网上还是过于危险了一些,出于安全考虑,决定使用 VPN 工具组建一个虚拟的局域网,这样要进入家中的私有网络就必须先攻破 VPN 的防护,起到了一定的安全保障作用,让人比较放心。 说起 VPN,马上想到的自然是 OpenVPN。可惜我花了一两周时间也没搞明白我到底要部署哪些东西,写哪些配置文件,实在是太繁琐了。而且官方提供的 OpenVPN Server 貌似还只能提供两个客户端的连接支持,也不太够我几台设备用,所以后面看到 WireGuard 配置起来那么方便果断放弃了折腾 OpenVPN,省下了不少头发。WireGuard 配置起来又省心又方便,虽然功能可能没有 OpenVPN 那么强大,但对于简单的建立隧道的需求来说,WireGuard 已经完全够用了,而稳定性与安全性方面有 Linux 内核的背书,无需顾虑。比较蛋疼的一点是这个在 BSD 系统上支持不算好,在我的 pfSense 防火墙上配置需要一点手动操作,不过无伤大雅。这里就我配置 WireGuard 的过程作一简单记录,以供参考。 Wireguard 是怎么工作的 先简单讲讲 WireGuard 的工作流程。WireGuard 的工作核心是 TUN 网卡。对 Linux 网络稍微有点了解的人可能知道 TUN 设备是什么,简单来说就是虚拟出来的一张网卡,向这张网卡发送的数据会被送到指定的应用进行处理。建立隧道的两端需要分别运行 WireGuard,然后两边的 WireGuard 各创建一个虚拟网卡,网卡通向 WireGuard,两边 WireGuard 再建立一条网络连接,一根虚拟的网线就插上了,我们就有了一个点对点的 VPN 隧道。 除了两个 WireGuard 客户端之间点对点的隧道,WireGuard 还支持同一个接口连接多个客户端,这时候就有点像在接口之间插上了一个路由器,由 WireGuard 在这些接口之间转发数据,就有点像家里路由器和电脑的关系。这两种模式就是 WireGuard 的两种主要工作模式,非常简单。 WireGuard 的配置 WireGuard 套件的安装就不再赘述,官网上提供了各大发行版的详细信息。安装完毕后,有两种模式建立 WireGuard 隧道。一种方式是直接使用命令行工具,很方便,但后续调整起来比较麻烦。个人倾向于先写配置文件,再通过 wg-quick 这一工具根据配置文件启动 WireGuard 隧道。 wg-quick 用到的配置文件以 .conf 结尾,格式和 INI 很像,默认存储在 /etc/wireguard 目录下。配置文件主要由两部分构成,一个 [Interface] 部分用来设置本地端点的信息,而 [Peer] 则描述要连接到的远程接口的信息。一个 ....

January 3, 2022 · 2 min · H1R4

使用反射在 Go 中动态解析 json

最近在用 Go 写一个对接某个 API 的 Adapter 时碰到了一点关于 json 反序列化的问题。对方通过 websocket 传递 json 来进行推送消息,但是推送的格式不太统一,而且种类繁多,难以使用少量通用的结构体类型来对这些消息进行反序列化。不过好在这些消息中都有一个共同的 type 字段用于标识该 json 对象的类型,不至于手足无措。在经过几天的资料查询参考后,通过一些解析手段和反射完成了对推送消息的解析,做记录如下。 动态 JSON 解析 对于大量格式不同的 json 对象,最为简单粗暴的方法当然就是用万能的 interface {} 去进行解析,再复杂的 json 对象也能解析成简单的基本类型组合。这样子做虽然在解析的时候舒服了不少,但是后续访问对象字段就麻烦了,需要不断使用字典之类的访问方法对其进行访问,而且 interface {} 类型对 IDE 的智能提示也非常不友好,后续的维护也更为繁琐,容易出问题。以上问题一叠加,interface {} 自然是下下策。那么有什么办法来在运行时根据 json 对象自动选择相应类型进行序列化呢? 延迟解析 一个比较好的方法是使用 json.RawMessage 类型来延后解析。json.RawMessage 是 json 标准库中提供的一个类型,预先被定义为这个类型的字段在 json.Unmarshal 时,json 库将不会对这个字段进行递归解析,而是将这个字段对应的 raw json 数据存入该字段,并可用于后续解析。 下面用一个简单的例子来说明这个方法,假设我们的接口会返回如下两种格式的 json: // Fruit json { "type": "fruit", "data": { "name": "orange", "color": "red", "taste": "sweet" } } //vegetable json { "type": "vegetable", "data": { "name": "corn", "size": "big", "juicy": false } } 以上两种格式的 json 的 data 字段下存放的 json 对象格式不同,而 data 字段所存放的 json 格式由 type 字段规定。这就给了我们一个思路,在首次解析中保留 data 的 RawMessage 数据,然后通过解析出来的 type 字段值来确定使用哪个静态类型对 data 字段的 RawMessage 进行解析。解析的代码可能看起来像这个样子:...

October 5, 2021 · 3 min · H1R4