服务器之家

服务器之家 > 正文

Linux 编辑器之神 vim 的 IO 存储原理

时间:2021-08-16 22:59     来源/作者:良许Linux

 Linux 编辑器之神 vim 的 IO 存储原理

Linux 编辑器之神 vim 的 IO 存储原理

故事起因

无意间用 vim 打开了一个 10 G 的文件,改了一行内容,:w 保存了一下,慢的我哟,耗费的时间够泡几杯茶了。这引起了我的好奇,vim 打开和保存究竟做了啥?

vim — 编辑器之神

vim 号称编辑器之神,以极其强大的扩展性和功能闻名。vi/vim 作为标准的编辑器存在于 Linux 的几乎每一种发行版里。vim 的学习曲线比较陡峭的,前期必须有一个磨炼的过程。

vim 是一个终端编辑器,在可视化的编辑器横行的今天,为什么 vim 还如此重要?

因为有些场景非它不可,比如线上服务器终端,除 vi/vim 这种终端编辑器,你别无选择。

vim 的历史很悠久,Github 有个文档归纳了 vim 的历史进程:vim 历史,Github 开源代码:代码仓库。

笔者今天不讲 vim 的用法,这种文章网上随便搜一大把。笔者将从 vim 的存储 IO 原理的角度来剖析下 vim 这个神器。

思考几个小问题,读者如果感兴趣,可以继续往下读哦:

  1.  vim 编辑文件的原理是啥,用了啥黑科技吗?
  2.  vim 打开一个 10G 的大型文件,为什么这么慢,里面做了啥?
  3.  vim 修改一个 10G 的大型文件,:w  保存的时候,感觉更慢了?为什么?
  4.  vim 好像会产生多余的文件?~ 文件 ?.swp 文件 ?都是做啥的呢?

划重点:由于 vim 的功能过于强大,一篇分享根本说不完,本篇文章聚焦 IO,从存储的角度剖析 vim 原理。

vim 的 io 原理

声明,系统和 Vim 版本如下:

操作系统版本:Ubuntu 16.04.6 LTSVIM 版本:VIM - Vi IMproved 8.2 (2019 Dec 12, compiled Jul 25 2021 08:44:54)测试文件名:test.txt

vim 就是一个二进制程序而已。读者朋友也可以 Github 下载,编译,自己调试哦,效果更佳。

一般使用 vim 编辑文件很简单,只需要 vim 后面跟文件名即可:

  1. vim test.txt 

这样就打开了文件,并且可以进行编辑。这个命令敲下去,一般情况下,我们就能很快在终端很看到文件的内容了。

Linux 编辑器之神 vim 的 IO 存储原理

这个过程发生了什么?先明确下,vim test.txt  到底是啥意思?

本质就是运行一个叫做 vim 的程序,argv[1] 参数是 test.txt 嘛。跟你以前写的 helloworld 程序没啥不一样,只不过 vim 这个程序可以终端人机交互。

所以这个过程无非就是一个进程初始化的过程,由 main 开始,到 main_loop(后台循环监听)。

 1   vim 进程初始化

vim 有一个 main.c 的入口文件,main 函数就定义在这里。首先会做一下操作系统相关的初始化( mch 是 machine 的缩写):

  1. mch_early_init(); 

然后会,做一下赋值参数,全局变量的初始化:

  1. /*  
  2. * Various initialisations shared with tests.  
  3. */ 
  4. common_init(&params); 

举个例子 test.txt 这样的参数必定要赋值到全局变量中,因为以后是要经常使用的。

另外类似于命令的 map 表,是静态定义好了的:

  1. static struct cmdname  
  2.  
  3.     char_u      *cmd_name;      // name of the command  
  4.     ex_func_T   cmd_func;       // function for this command  
  5.     long_u      cmd_argt;       // flags declared above  
  6.     cmd_addr_T  cmd_addr_type;  // flag for address type  
  7. } cmdnames [] = {  
  8. EXCMD(CMD_write, "write", ex_write,  
  9.  EX_RANGE|EX_WHOLEFOLD|EX_BANG|EX_FILE1|EX_ARGOPT|EX_DFLALL|EX_TRLBAR|EX_CMDWIN|EX_LOCK_OK,  
  10.  ADDR_LINES),  

划重点::w,:write,:saveas 这样的 vim 命令,其实是对应到定义好的 c 回调函数:ex_write 。  ex_write 函数是数据写入的核心函数。再比如,:quit 对应 ex_quit ,用于退出的回调。

换句话说,vim 里面支持的类似 :w ,的命令,其实在初始化的时候就确定了。人为的交互只是输入字符串,vim 进程从终端读到字符串之后,找到对应的回调函数,执行即可。再来,会初始化一些 home 目录,当前目录等变量。

  1. init_homedir();  // find real value of $HOME  
  2. // 保存交互参数  
  3. set_argv_var(paramp->argv, paramp->argc); 

配置一下跟终端窗口显示相关的东西,这部分主要是一些终端库相关的:

  1. // 初始化终端一些配置  
  2. termcapinit(params.term); // set terminal name and get terminal  
  3. // 初始化光标位置  
  4. screen_start();  // don't know where cursor is now  
  5. // 获取终端的一些信息  
  6. ui_get_shellsize();  // inits Rows and Columns 

再来会加载 .vimrc 这样的配置文件,让你的 vim 与众不同。

  1. // Source startup scripts.  
  2. source_startup_scripts(&params); 

还会加载一些 vim 插件 source_in_path ,使用 load_start_packages  加载 package 。

下面这个就是第一个交互了,等待用户敲下 enter 键:

  1. wait_return(TRUE); 

我们经常看见的:“Press ENTER or type command to continue“ 就是在这里执行的。确认完,就说明你真的是要打开文件,并显示到终端了。

怎么打开文件?怎么显示字符到终端屏幕?

这一切都来自于 create_windows 这个函数。名字也很好理解,就是初始化的时候创建终端窗口来着。

  1. /*  
  2.  * Create the requested number of windows and edit buffers in them.  
  3.  * Also does recovery if "recoverymode" set. 
  4.  */  
  5. create_windows(&params); 

这里其实涉及到两个方面:

  1.  把数据读出来,读到内存;
  2.  把字符渲染到终端;

怎么把数据从磁盘上读出来,就是 IO。怎么渲染到终端这个我们不管,这个使用的是 termlib 或者 ncurses 等终端编程库来实现的,感兴趣的可以了解下。

这个函数会调用到我们的第一个核心函数:open_buffer ,这个函数做两个时间:

  1.  create memfile:创建一个 memory + .swp 文件的抽象层,读写数据都会过这一层;
  2.  read file:读原始文件,并解码(用于显示到屏幕);

函数调用栈:

  1. -> readfile  
  2. -> open_buffer  
  3. -> create_windows  
  4. -> vim_main2  
  5. -> main 

真正干活的是 readfile 这个函数,吐槽一下,readfile 是一个 2533 行的函数。。。。。。

readfile 里面会择机创建 swp 文件(以前有的话,可以用于恢复数据),调用的是 ml_open_file 这个函数,文件创建好之后,size 占用 4k,里面主要是一些特定的元数据(用来恢复数据用的)。

划重点:.{文件名}.swp 这个隐藏文件是有格式的,前 4k 为 header,后面的内容也是按照一个个block 组织的。

再往后走,会调用到 read_eintr 这个函数,读取数据的内容:

  1. vlong  
  2. read_eintr(int fd, void *buf, size_t bufsize)  
  3.  
  4.     long ret;  
  5.     for (;;) {  
  6.      ret = vim_read(fd, buf, bufsize);  
  7.      if (ret >= 0 || errno != EINTR)  
  8.          break;  
  9.     }  
  10.     return ret;  

这是一个最底层的函数,是系统调用 read 的一个封装,读出来之后。这里回答了一个关键问题:vim 的存储原理是啥?

划重点:本质上调用 read,write,lseek 这样朴素的系统调用,而已。

readfile 会把二进制的数据读出来,然后进行字符转变编码(按照配置的模式),编码不对就是乱码喽。每次都是按照一个固定 buffer 读数据的,比如 8192 。

划重点:readfile 会读完文件。这就是为什么当 vim 打开一个超大文件的时候,会非常慢的原因。

这里提一点题外话:memline 这个封装是文件之上的,vim 修改文件是修改到内存 buffer ,vim 按照策略来 sync memfile 到 swp 文件,一个是防止丢失未保存的数据,第二是为了节省内存。

mf_write 把内存数据写到文件。在 .test.txt.swp 中的就是这样的数据结构:

Linux 编辑器之神 vim 的 IO 存储原理

block 0 的 header 主要标识:

  1.  vim 的版本;
  2.  编辑文件的路径;
  3.  字符编码方式;

这里实现提一个重要知识点:swp 文件里存储的是 block,block 的管理是以一个树形结构进行管理的。block 有 3 种类型:

  1.  block0:头部 4k ,主要是存储一些文件的元数据,比如路径,编码模式,时间戳等等;
  2.  pointer block:树形内部节点;
  3.  data block:树形叶子节点,存储用户数据;

 2   敲下 :w 背后的原理

进程初始化我们讲完了,现在来看下 :w 触发的调用吧。用户敲下  :w 命令触发 ex_write 回调(初始化的时候配置好的)。所有的流程皆在 ex_write ,我们来看下这个函数做了什么。

先撇开代码实现来说,用户敲下 :w 命令其实只是想保存修改而已。

那么第一个问题?用户的修改在哪里?

在 memline 的封装,只要没执行过 :w 保存,那么用户的修改就没修改到原文件上(注意哦,没保存之前,一定没修改原文件哦),这时候,用户的修改可能在内存,也可能在 swp 文件。存储的数据结构为 block 。所以,:w 其实就是把 memline 里面的数据刷到用户文件而已。怎么刷?

重点步骤如下(以 test.txt 举例):

  1.  创建一个 backup 文件( test.txt~ ),把原文件拷贝出来;
  2.  把原文件 test.txt truancate 截断为 0,相当于清空原文件数据;
  3.  从 memline (内存 + .test.txt.swp)拷贝数据,重新写入原文件 test.txt;
  4.  删除备份文件 test.txt~;

以上就是 :w 做的所有事情了,下面我们看下代码。

触发的回调是 ex_write ,核心的函数是 buf_write ,这个函数 1987 行。

在这函数,会使用 mch_open 创建一个 backup 文件,名字后面带个 ~ ,比如 test.txt~ ,

  1. bfd = mch_open((char *)backup 

拿到 backup 文件的句柄,然后拷贝数据(就是一个循环喽), 每 8K 操作一次,从 test.txt  拷贝到 test.txt~ ,以做备份。

划重点:如果是 test.txt 是超大文件,那这里就慢了哦。

backup 循环如下:

  1. // buf_write   
  2.  while ((write_info.bw_len = read_eintr(fd, copybuf, WRITEBUFSIZE)) > 0)  
  3.  {  
  4.   if (buf_write_bytes(&write_info) == FAIL)  
  5.    // 如果失败,则终止  
  6.   // 否则直到文件结束  
  7.   }  
  8.  } 

我们看到,干活的是 buf_write_bytes ,这是 write_eintr 的封装函数,其实也就是系统调用 write 的函数,负责写入一个 buffer 的数据到磁盘文件。

  1. long write_eintr(int fd, void *buf, size_t bufsize) {  
  2.     long    ret = 0 
  3.     long    wlen;  
  4.     while (ret < (long)bufsize) {  
  5.         // 封装的系统调用 write   
  6.         wlen = vim_write(fd, (char *)buf + ret, bufsize - ret);  
  7.         if (wlen < 0) {  
  8.             if (errno != EINTR)  
  9.             break;  
  10.         } else  
  11.             ret += wlen;  
  12.     }  
  13.     return ret;  

backup 文件拷贝完成之后,就可以准备动原文件了。

思考:为什么要先文件备份呢?

留条后路呀,搞错了还有的恢复,这个才是真正的备份文件。

修改原文件之前的第一步,ftruncate 原文件到 0,然后,从 memline (内存 + swp)中拷贝数据,写回原文件。

划重点:这里又是一次文件拷贝,超大文件的时候,这里可能巨慢哦。

  1. for (lnum = start; lnum <= end; ++lnum)  
  2.  
  3.     // 从 memline 中获取数据,返回一个内存 buffer( memline 其实就是内存和 swap 文件的一个封装)  
  4.     ptr = ml_get_buf(buf, lnum, FALSE) - 1;  
  5.     // 将这个内存 buffer 写到原文件  
  6.     if (buf_write_bytes(&write_info) == FAIL)  
  7.     {  
  8.         end = 0;        // write error: break loop 
  9.          break;  
  10.     }  
  11.     // ...  

划重点:vim 并不是调用 pwrite/pread 这样的调用来修改原文件,而是把整个文件清空之后,copy 的方式来更新文件。涨知识了。

这样就完成了文件的更新啦,最后只需要删掉 backup 文件即可。

  1. // Remove the backup unless 'backup' option is set or there was a  
  2. // conversion error.  
  3. mch_remove(backup); 

这个就是我们数据写入的完整流程啦。是不是没有你想的那么简单!

简单小结下:当修改了 test.txt 文件,调用 :w 写入保存数据的时候发生了什么?

  1.  人机交互,:w 触发调用 ex_write 回调函数,于 do_write -> buf_write 完成写入 ;
  2.  具体操作是:先备份一个 test.txt~ 文件出来(全拷贝);
  3.  接着,原文件 test.txt 截断为 0,从 memline( 即 内存最新数据 + .test.txt.swap  的封装)拷贝数据,写入 test.txt (全拷贝) ;

数据组织结构

之前讲的太细节,我们从数据组织的角度来解释下。vim 针对用户对文件的修改,在原文件之上,封装了两层抽象:memline,memfile 。分别对应文件 memline.c ,memfile.c 。

Linux 编辑器之神 vim 的 IO 存储原理

先说 memline 是啥?

对应到文本文件中的每一行,memline 是基于 memfile 的。

memline 基于 memfile,那 memfile 又是啥?

这个是一个虚拟内存空间的实现,vim 把整个文本文件映射到内存中,通过自己管理的方式。这里的单位为 block,memfile 用二叉树的方式管理 block 。block 不定长,block 由 page 组成,page 为定长 4k 大小。

这是一个典型虚拟内存的实现方案,编辑器的修改都体现为对 memfile 的修改,修改都是修改到 block 之上,这是一个线性空间,每个 block 对应到文件的要给位置,有 block number 编号,vim 通过策略会把 block 从内存中换出,写入到 swp 文件,从而节省内存。这就是 swap 文件的名字由来。

block 区分 3 种类型:

  1.  block 0 块:树的根,文件元数据;
  2.  pointer block:树的分支,指向下一个 block;
  3.  data block:树的叶子节点,存储用户数据;

swap 文件组织:

Linux 编辑器之神 vim 的 IO 存储原理

block 0 是特殊块,结构体占用 1024 个字节内存,写到文件是按照 1 个page 对齐的,所以是 4096 个字节。如下图:

Linux 编辑器之神 vim 的 IO 存储原理

block 其他两种类型:

  •  pointer 类型:这个是中间的分支节点,指向 block 的;
  •  data 类型:这个是叶子节点; 
  1. #define DATA_ID        (('d' << 8) + 'a')   // data block id  
  2. #define PTR_ID        (('p' << 8) + 't')   // pointer block id 

这个 ID 相当于魔数,在 swp 文件中很容易识别出来,比如在下面的文件中第一个 4k 存储的是 block0,第二个 4k 存储的是 pointer 类型的 block。

Linux 编辑器之神 vim 的 IO 存储原理

第三,第四个 4k 存储的是一个 data 类型的 block ,里面存储了原文件数据。

Linux 编辑器之神 vim 的 IO 存储原理

当用户修改一行的时候,对应到 memline 的一个 line 的修改,对应到这行 line 在哪个 block 的修改,从而定期的刷到 swap 文件。

Linux 编辑器之神 vim 的 IO 存储原理

vim 特殊的文件 ~ 和 .swp ?

假设原文件名称:test.txt 。

 1   test.txt~ 文件

test.txt~ 文件估计很多人都没见过,因为消失的太快了。这个文件在修改原文件之前生成,修改原文件之后删除。作用于只存在于 buf_write ,是为了安全备份的。

划重点:test.txt~ 和 test.txt 本质是一样的,没有其他特定格式,是用户数据。

读者朋友试试 vim 一个 10 G的文件,然后改一行内容,:w  保存,应该很容易发现这个文件(因为备份和回写时间巨长 )。

 2   .test.txt.swp 文件

这个文件估计绝大多数人都见过,.swp 文件生命周期存在于整个进程的生命周期,句柄是一直打开的。很多人认为 .test.txt.swp 是备份文件,其实准确来讲并不是备份文件,这是为了实现虚拟内存空间的交换文件,test.txt~ 才是真正的备份文件。swp 是 memfile 的一部分,前面 4k 为 header 元数据,后面的为 一个个 4k 的数据行封装。和用户数据并不完全对应。

memfile = 内存 + swp 才是最新的数据。

思考解答

 1   vim 存储原理是啥?

没啥,就是用的 read,write 这样的系统调用来读写数据而已。

 2   vim 的过程有两种冗余的文件?

test.txt~ :是真正的备份文件,诞生于修改原文件之前,消失于修改成功之后;.test.txt.swp :swap 文件,由 block 组成,里面可能由用户未保存的修改,等待:w 这种调用,就会覆盖到原文件;

 3   vim 编辑超大文件的时候为什么慢?

一般情况下,你能直观感受到,慢在两个地方:

  1.  vim 打开的时候;
  2.  修改了一行内容,:w 保存的时候;

先说第一个场景:vim 一个 10G 的文件,你的直观感受是啥?

我的直观感受是:命令敲下之后,可以去泡杯茶,等茶凉了一点,差不多就能看到界面了。为什么?

在进程初始化的时候,初始化窗口之前,create_windows -> open_buffer 里面调用 readfile 会把整个文件读一遍(完整的读一遍),在屏幕上展示编码过的字符。

划重点:初始化的时候,readfile 会把整个文件读一遍。 10 G的文件,你可想而知有多慢。我们可以算一下,按照单盘硬件 100 M/s 的带宽来算,也要 102 秒的时间。

再说第二个场景:喝了口茶,改了一个单词,:w 保存一下,妈呀,命令敲下之后,又可以去泡杯茶了?为什么?

  1.  先拷贝出一个 10G 的 test.txt~ 备份文件,102 秒就过去了;
  2.  test.txt 截断为 0,再把 memfile( .test.txt.swp )拷贝回 test.txt ,数据量 10 G,102 秒过去了(第一次可能更慢哦);

 4   vim 编辑大文件的时候,会有空间膨胀?

是的,vim 一个 test.txt 10 G 的文件,会存在某个时刻,需要 >=30 G 的磁盘空间。

  •  原文件 test.txt 10 G
  •  备份文件 test.txt~ 10G
  •  swap 文件 .test.txt.swp >10G

总结

  1.  vim 编辑文件并不没有用黑魔法,还是用的 read,write,朴实无华;
  2.  vim 编辑超大文件,打开很慢,因为会读一遍文件( readfile ),保存的时候很慢,因为会读写两遍文件(backup 一次,memfile 覆盖写原文件一次);
  3.  memfile 是 vim 抽象的一层虚拟存储空间(物理上由内存 block 和 swp 文件组成)对应一个文件的最新修改,存储单元由 block 构成。:w 保存的时候,就是从 memfile 读,写到原文件的过程;
  4.  memline 是基于 memfile 做的另一层封装,把用户的文件抽象成“行”的概念;
  5.  .test.txt.swp 文件是一直 open 的,memfile 会定期的交换数据进去,以便容灾恢复;
  6.  test.txt~ 文件才是真正的备份文件,诞生于 :w  覆盖原文件之前,消失于成功覆写原文件之后;
  7.  vim 基本都是整个文件的处理,并不是局部处理,大文件的编辑根本不适合 vim ,话说回来,正经人谁会用 vim 编辑 10 G 的文件?vim 就是个文本编辑器呀;
  8.   一个 readfile 函数 2533 行,一个 buf_write 函数 1987 行代码。。。不是我打击各位的积极性,这。。。反正我不想再看见它了。。。

后记

对于 vim 的好奇让笔者撸了一遍源码,学习了下其中的 IO 知识,不想被动辄几千行一个的函数教育了一番。我再也不想撸它了。。你学 fei 了吗?

原文链接:https://mp.weixin.qq.com/s/HBNhsG2rwYsWiw9E4rXozg

标签:

相关文章

热门资讯

yue是什么意思 网络流行语yue了是什么梗
yue是什么意思 网络流行语yue了是什么梗 2020-10-11
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全
2020微信伤感网名听哭了 让对方看到心疼的伤感网名大全 2019-12-26
背刺什么意思 网络词语背刺是什么梗
背刺什么意思 网络词语背刺是什么梗 2020-05-22
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总
苹果12mini价格表官网报价 iPhone12mini全版本价格汇总 2020-11-13
2021德云社封箱演出完整版 2021年德云社封箱演出在线看
2021德云社封箱演出完整版 2021年德云社封箱演出在线看 2021-03-15
返回顶部