硬盘分区介绍

这篇文章介绍跟硬盘分区相关的知识和概念。我觉得和这相关的内容比较难理解,是因为一部分是硬件上的概念,一部分是软件的(文件系统),很多资料介绍的时候,没有放到一起对比,读者看到的时候就会对一些概念很模糊。比如硬盘的分区有分区类型,文件系统有类型,这两种类型有啥区别?硬盘有扇区大小,文件系统有 block 大小,这两者又有什么区别?这篇文章试图深入浅出,从基本的原理讲起,介绍一些概念,它们分别是做什么的,为什么要这么做。

认识硬盘

硬盘在 Linux 中,就是一个 block device,就是存储数据用的。你把数据输入到硬盘中,硬盘帮你存到一个位置。下次需要的时候,再从这个位置读出来。

那么给硬盘一个位置,它怎么去找这个位置的数据的呢?

这要从硬盘的结构说起(虽然现在大部分的机器都使用 SSD 了,但是很多资料都是基于机械硬盘的,所以这里以机械硬盘为例,介绍一下 CHS 寻址的原理。)。硬盘是由几张碟片组成的,每张碟片的正反两面都可以保存数据。

所以这个问题就转换成了:在几个圆面中,如何确定一个位置。首先我们想,在一个圆面中,确定一个位置需要几个参数?很显然是2个,距离圆心的距离可以确定一个圆,再加上一个“角度”可以确定这个圆上的一个点。那么在硬盘的结构中,再加上一个参数确定是第几个圆面就可以了。

这几个参数我们分别叫它:

  • Cylinder/Track:磁道,柱面,确定距离圆心的位置;
  • Head: 磁头,这个是读写数据的物理装置,实际上硬盘在运行的时候,是盘片在转的,磁头负责移动,调整读取的柱面;
  • Sector: 扇区。上面两个参数确定了一个圆形,Sector 就可以确定这个圆形中哪个扇区了。

这就是 CHS 寻址的方式。

从中可以也看出,硬盘存储的读写单位是“一个扇区”。实际上,在分区的时候,(分区软件)也会用“第几个到第几个扇区”来表示,不会让你涉及到 Track 和 Head 的,这是属于硬件自己用来寻址的东西。

使用 fdisk 我们可以看到硬盘有多少个扇区,一个扇区多大。实际上从 1980 年以来,几乎所有的硬盘扇区大小都是 512bytes@yiran 纠正:现在一些新磁盘的物理扇区是 4k 了,系统中看到的逻辑扇区是512字节)

为什么要分区?

现在我们知道有了扇区的位置,硬盘就可以把数据写入或读出。那为什么要分区呢?原因以下几点:

  • 隔离文件系统的腐烂(鸡蛋不放在一个篮子里)。我们要在设备上建立文件系统,操作系统才能使用文件系统来读写。文件系统是对存储设备的规划,记录着每一块都存了什么(inode, block)。万一文件系统的元数据错乱了,那么整个文件系统的数据可能都读不到了;
  • 提高存储的利用率。参考之前的博文:Linux 文件系统 inode 介绍,一个文件最小将占用一个 block,如果 block 太大的话,将会浪费很多空间。比如 block size 是 4k,而存储的都是 1k 的文件,那么有 3/4 的空间是浪费的。如果 block 太小,那性能就很低,因为 kernel 是以 block 为单位拷贝的;我们可以分一个区,建一个 block size 为 512bytes 的文件系统来专门存储这些小文件;
  • 限制文件增长。crontab 写日志太多了导致所有的进程都挂了,这肯定是不合理的。但是文件的增长不会越过文件系统,跑到另一个分区上,所以我们可以通过分区,来给特定的进程分配写空间。

基于此,我们可以在系统中为不同的存储内容划分区。比如为用户程序 /usr 单独分区,/home 单独分区。

分区的本质是什么?

不同的分区还是在一块硬盘上,相当于是对不同的扇区分组管理罢了。那么这个分组信息保存在哪里呢?

答案是第一个硬盘的第一个扇区上。硬盘的第一个扇区也是系统启动的时候第一个读的地方(基于 BIOS 的启动流程)。前面说到,一个扇区的大小是 512bytes,这 512bytes 都有什么呢?

在 Linux 中一切都是文件,硬盘也是一个文件,用 /dev/sda 表示(这是 SCSI 接口,IDE 接口会是 /dev/hda ,具体命名方式和编号见此)。这样,我们就可以将这个“文件”的前 512bytes 拷贝出来。

然后可以用 Vim 带的 xxd 命令看一下这个文件的内容:

这里面的内容可以分成4部分:

  1. 001-440bytes(一共 440bytes):给 BIOS 执行的代码; 这个其实很有意思,感兴趣的朋友可以将这段 dump 成机器码看一下。启动系统需要将代码加载到内存,但是我们需要系统启动才能加载代码。所以这个过程又叫做 boot,即 “pull oneself over a fence by one’s bootstraps”
  2. 441-446bytes(一共 6bytes):MBR Disk 签名;
  3. 447-510(一共 64bytes):分区表,一共 4 部分,每部分16 bytes;
  4. 最后的 511 和 512(一共2bytes):固定为 0x55AA,表示硬盘可以用于启动;

00001be 到最后 00001fd 之间,记录的都是分区表的信息。

  • 分区1: 0004 0104 82fe c2ff 0008 0000 0048 4500
  • 分区2: 80fe c2ff 83bb c1bb 0050 4500 0000 e001
  • 分区3:0000 0000 0000 0000 0000 0000 0000 0000
  • 分区4:0000 0000 0000 0000 0000 0000 0000 0000

根据上面 fdisk -l 显示,我这个机器只有两个分区,所以分区3和4是空的。这 16bytes 里面都记录的什么呢?我们拿其中一个分区来说明,这里就用第2个分区说吧:

80fe c2ff 83fe c1bb 0050 4500 0000 e001

0字节,80,是一个标志:

  • 80 此分区可以用于系统启动;
  • 00 此分区不能用于系统启动;

1-3字节,fe c2ff 这就是用我们上面说的 CHS 地址表示,以及后面的 5-7 字节,分别表示此分区开始的位置是:

  • fe Cylinder位置是 fe;
  • c2 Head 开始位置是 c2;
  • ff Sector 开始位置是 ff;

相应的,这个分区的结束位置是 bb c1 bb

开始和结束中间的第4字节,是分区类型。在这里是 83 ,表示 type 是 Linux.

fdisk 中可以通过 l 命令列出所有的 type:

但其实,这个分区类型在 Linux 中用处并不大,无论是 ext2 还是 ext3 还是其他 Linux 分区,都是 83。这个标志位不同的操作系统有不同的解释方法,比如 Windows,会用这个标志来区分不同的分区类型,所以你看到在这个表中,FAT32 和 NTFS 这些常见的 Windows 分区都分别占用了一种标志位。说到底,这个标志位其实就是个普通的标志,怎么解释归操作系统的,甚至不同的操作系统安装在同一个硬盘上也是可行的,比如 0x07 ,OS/2 认为这个标志位是 HPFS 类型的分区,Windows 认为是 NTFS 类型的分区。

要注意的是,这个标志位和文件系统并没有本质的关系。既然 Linux 不关心这个标志位,那么无论这个分区的类型是什么,我都可以在这上面建一个文件系统。甚至我可以在系统运行的时候覆盖写入这个标志位。比如我把当前的这个分区改成 FAT12,也是一点问题都没有的。

8-11字节:0050 4500 逻辑 block 地址的第一个扇区的绝对地址。

11-15字节:0000 e001 此分区一共有多少个扇区。

MBR 分区的限制:从这里可以看出,4个字节表示第一扇区的绝对地址,4个字节表示此分区有多少个扇区,那么 MBR 分区表最多可以支持的硬盘大小是:

512 * (2^32 -1 ) * 2 ,是 4TiB -1Kb。

然而,这样分区的话,必须要最后一个分区是 2TiB,这样才能利用起 4Tib。如果一个用户有一个 4TiB 的硬盘,想要平均分成4区,每个分区 1TiB,是不行的。这会对很多用户造成困惑,所以在商业宣传的时候,就直接说 MBR 支持 2TiB。参考1 参考2 yiran补充

主分区和扩展分区

从这里也可以看出,分区数据一共 64bytes,每个分区表需要 16bytes 的信息。那么一共可以有 4 个分区。我第一次用电脑的时候,是 Windows,一直不明白“本地磁盘 CDEF”是什么意思。其实就是分区软件的快速分区模式默认平均将硬盘分了4个区而已。

分区表决定了我们只能创建 4 个分区,如果我们想要更多的分区怎么办?

还记得在文件系统中 block 寻址的时候如果超过 inode 能存放的 block 怎么办吗?答案是:inode 存放的 block,实际的内容是指向真正的 block 的地址。这里也用了同样的原理,我们可以创建一个类型为 Extended 类型(标志位是 5)的主分区,然后这个分区中每个分区的最后都保存着指向下一个分区的地址。

逻辑分区必须是连续的(显而易见),但是主分区可以不连续。除此之外逻辑分区和主分区在使用上并没有差别。逻辑分区也可以启动系统。

介绍到这里,应该能解决读者大部分的问题了(至少这些内容回答了我的很多疑问)。更加深入的问题,可能就要读者基于这些内容,自行搜索更详细的资料了。

扇区大小和Block大小

看文本文你应该对这个问题有所了解,扇区是一个硬盘的概念,几乎所有的硬盘扇区都是 512Bytes,如果不是,可能会出问题的。而 Block 指的是一个逻辑上的概念。但是可能在一些情景下依然对它们有些困惑。我研究了一番相关的内容,所以在这里多少一些,以便将来跟我有同样疑问的朋友,能找到这里,节省一些时间。

扇区大小的概念,出入很小。但是 Block 在不同的情景下是有不同的含义的。

首先是文件系统的 block,这里的 block 会影响存储文件使用的 block 大小。道理很简单,文件系统以 block 为单位寻址,如果 block 大小为 4k,那么即使文件写入 1k,也需要占用 4k。

创建文件系统,会自动分配 inode 和 block:

IO 中的 block:IO 是以 block 为单位的,这个 block 不一定是文件系统的 block 大小,也不一定是扇区的大小,可以比扇区更小,但是这是一种浪费,因为硬盘每次写会写 512bytes,如果 IO 的 block 是 256bytes,那么相当于写入相同一个扇区的内容,用了两次物理写入操作。此外,我们写入磁盘必须经过 syscall,在用户空间和 kernel 空间之间拷贝数据,也是以 block 为单位。我们可以用 madvice 这个系统调用向 Kernel 建议 IO block size。

以下是我用 dd 从硬盘拷贝相同的数据,使用不同的 block size,可以见期速度的影响。

但是 IO 其实是一个很复杂的问题,三言两语是说不清楚的,推荐一本书 Linux System Programming,里面用了四章介绍 IO 相关的话题。

除此之外,在看到 block 的时候,你还要注意它说的是什么语境。比如 ls -s 命令展示的 block,是以每个 block=1024bytes 展示的,而 stat 里面的 block 是 512bytes

建议用相关工具实践一下分区,建议在虚拟机里面操作,不用担心搞坏宿主机。玩一下这些命令:

  • xxd (vim提供)
  • fdisk
  • mount
  • grub
  • ss
  • dd

这篇文章参考的资料:

  1. Linux Partition HOWTO
  2. 分区标志
  3. Parition Types
  4. Linux System Administrators Guide: Chapter 5. Using Disks and Other Storage Media
  5. Linux 是如何启动的?
  6. Linux MBR
  7. 分区类型和文件系统类型的区别
  8. 如何确定 block size
 

编译LFS的一些经验

LFS 全称叫做 Linux From Scratch ,顾明思议,就是“从头开始的 Linux”。官方网址 中文翻译网址

LFS 可以说是一个项目,也可以说是一本书。它会教你如何从头构建你自己的 Linux 发行版。这里说的“从头”,是纯粹的从头的,包括编译工具,也是需要自己构建出来的。比如 Gcc,这是整个构建过程中比较核心的工具,LFS 先从 Gcc 源码编译了一个 Gcc,又编译了一个 Glibc。因为 Libstdc++ 是依赖 Glibc 的,所以有了 Glibc 之后,才可以第二次编译 Gcc(这次带上 Libstdc++)。其他工具也是一样,这样就有了一个临时的工具集。当编译一个 Linux 所有的工具链都准备好之后,就用 chroot 进入临时的系统,独立于宿主系统。在这个临时系统中编译 Linux。即,这个工具从编译工具到最后 boot 启动成功,都是需要自己编译的,没有使用现成的 binary。

LFS 让我感到很惊艳,这是个了不起的工程。从上面的例子可以看出,这个工程需要经验老到的梳理很多循环依赖,和编译错误。

现在的编程世界有了各种方便的包管理工具,和提前编译好的二进制文件。一般我们需要什么工具,只要找对应平台的二进制来下载使用就好了。却从来没有想过,如果“从头”编译一个东西会是怎样的。会不会有一天,像《基地》里面的帝国那样,机器都无比复杂,没有任何一个人清楚怎么修理,而导致帝国的崩坏呢?

小时候我经常陷入这样的幻想:如果从零制作一个东西,会是怎么样的。比如说桌子,造桌子需要斧头,和钉子,这些又需要钢铁,那么就需要炼钢。炼钢技术需要有火,还有要XX等等。这个项目,也算满足了我的一个幻想。

废话了这么多,只想跟大家推荐一下 LFS,真的很值得一试。即使最后你不使用最终构建的 Linux 作为自己日常使用的 Linux,从中也能学到很多东西。比如,我今天从这个项目中才知道 su - 后面的这个 - 是什么含义

这篇博客,也记录一下我构建 LFS 时候的一些坑,因为我发现中文方面的资料和讨论比较少。希望对后面去玩 LFS 的人有些帮助。

环境问题

我是在 Mac 上,使用 VirtualBox 虚拟化出来的 Fedora 构建的(推荐大家这么做)。也可以直接使用 Linux 系统编译,不用虚拟化和 Docker 啥的,因为书里面就会教你如何构建一个纯净的工具系统。但是不推荐用 OS X 来构建,APFS 会有很多坑。

最开始需要分区,挂载到宿主系统。我在虚拟机里面没有搞定,然后想起来,我在 Virtualbox 里面呀!遂直接用 Virtualbox 挂载了一个新的硬盘上去。搞定。

第一次编译 LFS 尽量不要自己做定制,严格按照书上的来,不然可能会白费一些力气。

下载

要下载的内容基本都是源代码,在准备的章节中,LFS 整理了一个 URL 列表,可以直接用 wget 下载。我发现即使用代理,有些下载还是很慢,2个小时都没下载完。后来直接去 DigitalOcean 开了一台 VPS,3min就下完了。推荐还是用外网的机器直接下载然后 scp 拷贝回来吧。

机器配置

LFS 每一个工具的构建都写的很标准,标明了构建耗时。用的单位是 SBU (Standard Build Unit),表示时间需要构建 BinUtils 的几倍长。有些构建相当耗时,我一开始给构建用的机器分配了 2CPU/8G MEM ,最后加到了 4CPU/8G MEM。但是还是觉得挺慢,所以推荐大家有多少资源就给多少吧。LFS 整个过程大多数时间都在等待编译完成。

在 LFS 中可以学到什么

尽量看懂书里面的每一行脚本,和解释,能学到很多东西的,不要急着粘贴命令。

The key to learning what makes a Linux system work is to know what each package is used for and why you (or the system) may need it.

操作仔细

注意一下编译的输出。由于编译太耗时了,有可能编译结束的时候,就忘记自己当前在哪一步了。一定要仔细一些,我有次 make 之后忘记执行 make install 了,其实后面执行命令的时候,通过 ./configure 输出,可以看到某些工具用的不是工具链里面,是系统里面的,是可以提前发现的。LFS 越到后面,发现错误的成本越大。轻则需要从错误处再走一遍,重则找不到错误的源头,功夫白费了。

编译完第5章建议备份一下 $LFS/tools ,以后编译别的东西可以直接用这个工具链。

一些可能的坑

  • 进入第六章之后,如果中途中断了(总不能一口气编译完吧)。下次进入的时候要重新操作 6.26.4,进行 mount 和进入 chroot 和 mount 一些虚拟文件系统;

    If the virtual kernel file systems have been unmounted, either manually or through a reboot, ensure that the virtual kernel file systems are mounted when reentering the chroot. This process was explained in Section 6.2.2, “Mounting and Populating /dev” and Section 6.2.3, “Mounting Virtual Kernel File Systems”.

  • 即使编译 stable 版本的 LFS,也可能遇到一些书中没有标明的测试失败。只要不是大面积的失败,几个失败可能并不是重要的。我编译的过程中遇到的未预期的测试失败记录在这里了,供后面的人参考。
  • 推荐使用 virtualbox 添加一块新盘来做,这样即使 host 机器有什么问题(比如 grub 设置错误导致无法启动),可以很方便的将这个新盘 mount 到另外一台虚拟机,直接更改里面文件。
  • 如果使用 virtualbox 的话,在设置 grub 的时候,要注意将 grub 安装到 /dev/sda,然后 boot 的磁盘添加 /dev/sdb.

相关资料推荐

推荐一些相关的资料,解决了我的疑惑,也可能解决你的疑惑。

  1. The magic behind configure, make, make install 编译的过程中你要运行无数次 ./configure && make && make install ,这篇文章解释了这些命令是哪里来的,以及 ./configure 和 Makefile 是从哪里来的;
  2. Libraries: Static or shared?

电影推荐

有些编译很漫长,第三轮 glibc 要将近1小时,第三轮 Gcc(加上 check)需要4h左右。准备些爆米花边看电影边编译吧。

  • 《勇士》 汤姆·哈迪主演的拳击题材的电影;
  • Rudderless 一个比较复杂的话题,音乐题材;
  • 《血钻》 莱昂纳多·迪卡普里奥主演的战争动作片,讲南非黑钻的故事。

最后,如果在构建的过程中遇到问题,可以在这里留言,大家交流学习。

 

/bin /usr/bin 和 /usr/local/bin 的故事

Linux 系统的 $PATH 变量通常会包含 /bin /usr/bin /usr/local/bin 。这三个目录下都有一些 binary 程序,至于有什么区别,网上有很多错误的解释。(就像介绍 HTTP GET 和 POST 有什么区别这个问题一样)。本文试图说明这个问题,基于我的理解和 Linux 文档。(本文里说的 Linux,更准确的说,应该是 Unix系系统)

TL;DR

这些 bin 的位置,仅仅是几个目录而已,并没有本质的区别。哪些命令放在哪里,完全看用户和发行版的喜好。比如,有一些发行版/bin 作为 -> /usr/bin 的符号链接[命令,Ubuntu 放在 /usr/bin 下,OS X 就放在了 /bin 下。

但是通常来说,我们都认为这几个目录这样安排:

  • /bin 放置系统的关键程序,比如 ls cat ,对于“关键”的定义,不同的发行版会有不同的理解;
  • /usr/bin 放置发行版管理的程序,比如 Ubuntu 自带 md5sum ,这个 binary 就会在这个目录下;
  • /usr/local/bin 放置用户自己的程序,比如你编译了一个 gcc,那么 gcc 这个可执行 binary 应该在这个目录下;
  • 除此之外,还有对应的三个目录 /sbin /usr/sbin /usr/local/sbin ,放置系统管理的程序,比如 deluser chroot service ;

需要再次强调,这是一种文件的管理方式而已,你甚至可以把自己的 binary 放到 $HOME/bin 下。还有,OS X 用 homebrew 安装的软件,会放在 /usr/local/Cellar 下,然后在 /usr/local/bin 创建一个指向相关 bin 目录的符号链接;但是在 Ubuntu 下,会放到 /usr/bin 下。

更深的理解

(这一段来自知乎的 in nek)另外,需要知道 / /usr /usr/local 这些都是 prefix,你编译一个软件的之后,要执行 ./configure --prefix=/usr/local 然后 make && make install 。那么 /usr/local 就会作为 prefix,库文件就放在 /usr/local/lib 下面,配置文件就放在 /usr/local/etc 下面,可执行文件(binary)就放在 /usr/local/bin 下面。

然后我们看看这些prefix是怎么选择的。如果你编译过FreeBSD一类的系统,你会发现,这些系统的系统库,基础工具和内核是放到一套代码树中的,编译这个代码,内核和核心库,工具是一同完成的,这些都被认为是操作系统的一部分。这些核心文件,就以根目录作为prefix。所以,/是所有操作系统核心程序的prefix。

在这个核心之外增加新的程序,构成一个发行版,这个发行版增加的程序就用/usr作为prefix。

你把发行版安装好了,安装发行版之外应用程序,那些程序通常用/opt, /srv作为prefix。

但如果你自己从源代码开始编译一个应用程序,这些程序是专门向你这个Site编译的,这种情况下,默认的prefix是/usr/local。

一些历史…

可以看到,不同的发行版会有不同的理解。所以就有 Filesystem Hierarchy Standard 想要指定一个文件等级的标准。(其实我认为这个标准没有太大意义,这种规范不能强迫所有的发行版去理解。与其去理解这个,不如去理解你的发行版是怎么思考的,适用和学习你称手的发行版)。

即使现在大多数人认为这三个目录的含义就像本文开头讲的那样,但事实是,这三个目录创建的时候的目的不是这样的。

事实是,Ken Thompson 和 Dennis Ritchie 在 1969 年创建 Unix 的时候,用的是 PDP-11,磁盘是两块 RK05,每块只有 1.5M。

后来系统变得越来越大了,一块磁盘不够用了,需要用到第二块磁盘,于是就 mount 了第二块,叫做 /usr 用来放用户的文件(想象一下,有两块磁盘,一块放系统,一块放用户数据,非常合理)。然后把系统的目录,/bin /sbin lib … 都复制到新磁盘下,在新磁盘读写。

后来他们有了第三块磁盘(啊!!),mount 在了 /home 下,只把用户的文件移动了过去。这样通过不同的磁盘挂载到不同的地方,系统可以利用起3块磁盘了。

当然,他们必须有个规则:“当系统第一次启动的时候,第一块磁盘里面必须有所有需要的程序,来挂在第二块磁盘到 /usr,比如 mount 。如果 mount 放在 /usr/bin/mount 这里,就会遇上先有鸡还是先有蛋的问题。” 非常合理。

/bin 和 /usr/bin 的分裂是人为的,1970 年的这个实现细节一直延续了下来。要怪就怪那些墨守成规,不问为什么的人。

可以说这个分裂是“没有必要的”,如果磁盘空间足够的话。但是后来由于种种原因,这个“没有必要”的假设就被打破了。其中一个原因是,后来引入了共享库(动态链接),/lib 和 /usr/bin 必须 match。之前没有这个问题,因为所有的东西都是静态链接的。

Anyway,自从有了这两个目录,人们便开始赋予它们含义:/ 用来存放上游的文件,/usr 放本地的内容;后来演变成 / 放从 AT&T 官方发行的内容,/usr 放发行版的内容,那时是 IBM AIX 或 Dec Ultrix,/usr/local 放自己本地的内容;再后来人们觉得 /usr/local 放安装的新 package 不够好,那再加一个 /opt ! 将来说不定还有 /opt/local

后来有一些组织尝试将它们标准化,比如我们前面提到的 Filesystem Hierarchy Standard ,但是他们并没有尝试去理解一开始为什么会这样……

 

 

参考资料:

  1. Filesystem Hierarchy Standard
  2. THDP: Linux Filesystem Hierarchy
  3. Understanding the bin, sbin, usr/bin , usr/sbin split
 

你的Linux上有一个超酷的 TCP 代理!

TL;DR 本文只用 Linux 自带的工具实现了一个可以将 TCP 流量打印出来的中间人代理程序(透明的)。脚本来源于这里。本文是对其工作原理的解释。

 

一般我们想知道一个 TCP 连接收发的内容时,会用 wireshark/tcpdump 这种工具,来把包抓下来分析查看。我开发 IRedis 的时候,就经常有这种需求,需要看看我的客户端对 server 到底发送了什么和接收了什么。用抓包工具不太方便,信息比较杂乱,操作也比较繁琐。在服务器上部署抓包工具的时候,还要给这些工具特殊权限。

其实这种需求可以不抓包的。因为 Redis 这种程序(RESP3协议)是基于 TCP 的,我们可以做一个代理,将发往 redis-server 的流量打印出来(输出到 stdout),然后再发给 redis-server。将 redis-server 的 Response 打印出来,再返回给客户端。相当于是一个中间人。

实现正向代理和反向代理都可以,反向代理不需要客户端支持,只是开一个端口,将所有发往这个端口的流量转发到 redis-server,所以我们这里用反向代理来实现。

在这篇文章中,我们只用 Linux 自带的工具,nc 来实现这个代理。还要用到 named pipe(因为 Linux 的 pipe 只支持单项传递,这里我们需要一个双向的代理),sed(用来对输出稍作格式化),trap (负责在退出的时候清理 named pipe)。

工作原理

我可以用一个最小化的版本解释这个中间人代理的工作原理,然后我们再来处理格式化输出。

这个中间人代理只需要两个 nc ,一个负责接收客户端那边的输出,将这个输入传给另一个 nc;另一个 nc 接收前一个 nc 的输入,然后传给 Server(比如 redis-server)。Server 传回来的信息也一样,先进入后一个 nc (后面称呼它为 nc2 好了),然后传给前一个 nc (后面叫做 nc1)。在 nc1 -> nc2 和 nc2 -> nc1 的过程中,就可以把传递的东西给打印出来了。

我们先写一个简单的版本:

这一行是监听 8800 端口,把收到的内容通过管道发给 nc2,然后 nc2 将内容发给 kawabangga.com 的 80 端口。其实就是一个 HTTP 代理啦。

用 curl 来试一下:

注意这个 curl 是卡住了,并没有信息返回。

而两个 nc 那一行的输出是:

可以看到,第二个 nc 把服务器的返回直接输出到 stdout 了,而并没有返回给 curl。这样的代理是不合格的,因为我们不想夺走本该属于 curl 的 response。

一个优秀的代理应该完整的将服务器的回复还给客户端(curl),像下面这样。至于 stdout 的问题,我们先不管。

Linux 的 pipe 只支持从一边传给另一边,是单向的。那怎么做到后面的进程往前面的进程传呢?答案就是 namedpipe。Named pipe,嗯……顾名思义,就是有名字的管道。在本文中,理解成有了这个名字,我就可以控制重定向的方向,就好了。

我们用这一行,就完全可以实现图2那种传输。即 nc2 通过 named pipe 传给 nc1 ,nc 默认会将自己的 stdin 的内容传回给连接到 nc 的 TCP 另一头。

用 curl 再来试一下:

而 nc 那个命令行没有任何输出。

中间人打印TCP内容

OK,代理已经工作了,接下来要做的是将 TCP 内容打印出来。这里要做的是 nc 在将内容输出到 stdout (即管道)的同时,还要打印出来。

……当然是要用 tee 啦!

这样可以用 tee 将 TCP 流量分别保存到 in.txt 和 out.txt 里面。可以用上面用过的 curl 测试一下。

因为 stdout 会被管道重定向,所以我们这里无法直接输出到屏幕了。那咋办呢?一不做二不休,再用 named pipe 将 tee 写入到 named pipe 中。

curl 之后,用 cat logging 就能看到 TCP 的内容啦。(这里如果不使用 cat 读出来 pipe 的内容,curl 会被 block 住)。

最终的脚本

最后,加点细节。写个脚本自动生成 named pipe,使用 trap 在退出的时候把创建的 named pipe 删掉。用 sed 格式化一下输出,分别加上 => 和 <=

 

代码如下(BSD 的 nc,适用于 OS X):

 

nc 的 BSD 版本和 GNU 版本不一样。如果你用的不是 Mac,要改下最后一行,加一个 -p. GNU 版本的代码如下:

 

使用效果如下(还是用 curl 测试):

 

作为 redis-server 的代理效果:

Pretty cool uh? Now I am going to drink some tea!

 

Vim LSP 配置

用了好几年的 YouCompleteMe,体验还是不错的,最爽的一点是能基于 Token 补全在当前 buffer 中作补全,任何语言用 Vim 就有一个“基本可用”的体验,即使写 git commit message 的时候也很好用。

但是最近玩一些神奇的东西,YouCompleteMe 如果没有支持这种语言的话,就没办法做到一个比较好的体验了。

对于一种语言,YouCompleteMe 都要去实现这种语言的支持,才能起作用。每增加一种语言,YouCompleteMe 都要增加一种工作量。不光 YouCompleteMe,像 Emacs,VS Code,都要去实现每一个语言的自动补全。这就形成了很大的浪费:每一种编辑器要去自己实现每一种语言。如果有人写了一个新的语言想要其他编辑器支持,需要一个一个实现编辑器的补全工具。

这就形成了一种交叉关系,假如有三个语言,三个编辑器存在,就需要9个补全的实现。

所以 VS Code 就提出了 “Language Server” (简称 LSP)这种先进的概念。这是一种 Client-Server 架构,每个语言实现自己的 Language Server,每个编辑器去实现自己对接 Language Server 的前端。这样一个语言只需要实现一次,就可以支持所有的编辑器。上面那种情况,只要有3个语言的补全实现+3个编辑器自己的 Language Server 实现就可以了。

其实 YouCompleteMe 也支持 LSP 的,但是配置上看起来资料比较少,像是不是支持的特别好,所以我索性放弃 YCM 了,转向了 vim-lsp

配置 vim-lsp

这里以 Python 为例,介绍一下配置方法。

首先需要安装 Python 的 Language Server。推荐使用 pipx 安装。

然后修改 .vimrc 的配置修改,以下是一个最小化配置:

安装的插件需要4个,下面分别介绍一下这四个是干什么用的。

async.vim

因为补全是支持 Vim 和 Neovim 的,async.vim 封装了两个 Vim Async 相关的接口。

asyncomplete.vim

利用 async.vim 做的补全,相当于一个补全引擎。(到这里是和 lsp 还没有什么事儿,配置的都是补全插件。)

这个补全只是负责补全引擎,具体的补全还是要交给依赖此引擎的实现。比如我们想要对路径进行自动补全,需要先安装这个插件,再安装基于此插件实现的路径补全插件,最后对补全引擎注册这个插件,才能使用。安装路径补全插件和注册的配置如下。

还有很多挺有意思的补全,比如这个 tmux-completion,是基于 tmux 的窗口 buffer 来补全,在 vim 中抄写 tmux 里面的 git hash,URL 啥的挺有帮助。但是我没找到禁用它的方法,导致它会一直弹出来,太烦人了,就没用。

vim-lsp

是一个与 LSP 交互的插件,类似于一个 Vim 的 SDK。

asyncomplete-lsp

将 lsp 中的内容交给 asyncomplete 做补全。

 

后面的两个配置,一个是设置用 Tab 触发补全(判断补全窗口有没有打开,有打开的话就将tab键映射为补全选择键。

然后注册 Python 的 LSP。每一个 LSP 都需要注册。

效果如下:

配置好这些插件之后,再配置其他语言的 LSP 就简单多了。分成两步:

  1. 安装该语言的 LSP,这里有一个列表
  2. 在 Vim 中注册该 LSP;

以 Bash 为例,我们首先按照仓库的介绍,安装该语言的 LSP:

然后在 .vimrc 中用以下配置注册:

换成其他语言的话,只要将 cmd 换成 LSP 的启动命令,然后通过 whitelist 或者 blacklist 设置生效的文件类型就好了。

 

Snip 的支持

Language Server 支持补全一段代码模板,比如写 Elixir 的时候自动补全 defmodule 。但是 vim-lsp 本身是不支持的,snip 在 vim-lsp 中会补全成这个样子:

将这个片段正确的显示在 vim 中,并且支持跳转,需要通过插件来解析 Language Server 返回的片段。

这里又涉及到 N 多插件。首先你需要一个代码片段引擎,负责在 Vim 中生成片段,在片段中需要填写的字段中进行来回跳转,补全。之前在博客中介绍过 Ultisnips ,这里就以它为例。

除了 SirVer/ultisnips 本身,还要安装两个插件来将 LSP 中的 snip 补全到 vim 中来。总共需要安装的插件如下:

然后有一个小坑需要注意,Snip 必须确认选中才能展开,也就是菜单中将焦点移动到这个 Snip 上去之后,需要按 Ctrl + y 来展开。

但是这个快捷键可以更改,比如我换成了 Ctrl + o