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

,请讲解下,Linux编辑器之神vim的IO存储原理
最新回答
σ你眼中ノ起风〃)

2024-09-29 01:41:19

故事起因原创不易,更多干货,欢迎关注公众号:奇伢云存储

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

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

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

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

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

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

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

vim编辑文件的原理是啥,用了啥黑科技吗?

vim打开一个10G的大型文件,为什么这么慢,里面做了啥?

vim修改一个10G的大型文件,:w保存的时候,感觉更慢了?为什么?

vim好像会产生多余的文件?~文件?.swp文件?都是做啥的呢?

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

vim的io原理声明,系统和Vim版本如下:

操作系统版本:Ubuntu16.04.6LTS

VIM版本:VIM-ViIMproved8.2(2019Dec12,compiledJul25202108:44:54)

测试文件名:test.txt

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

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

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

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

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

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

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

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

/**Variousinitialisationssharedwithtests.*/common_init(&params);举个例子test.txt这样的参数必定要赋值到全局变量中,因为以后是要经常使用的。

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

staticstructcmdname{char_u*cmd_name;//nameofthecommandex_func_Tcmd_func;//functionforthiscommandlong_ucmd_argt;//flagsdeclaredabovecmd_addr_Tcmd_addr_type;//flagforaddresstype}cmdnames[]={EXCMD(CMD_write,"write",ex_write,EX_RANGE|EX_WHOLEFOLD|EX_BANG|EX_FILE1|EX_ARGOPT|EX_DFLALL|EX_TRLBAR|EX_CMDWIN|EX_LOCK_OK,ADDR_LINES),}划重点::w,:write,:saveas这样的vim命令,其实是对应到定义好的c回调函数:ex_write。ex_write函数是数据写入的核心函数。再比如,:quit对应ex_quit,用于退出的回调。

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

init_homedir();//findrealvalueof$HOME//保存交互参数set_argv_var(paramp->argv,paramp->argc);配置一下跟终端窗口显示相关的东西,这部分主要是一些终端库相关的:

//初始化终端一些配置termcapinit(params.term);//setterminalnameandgetterminal//初始化光标位置screen_start();//don'tknowwherecursorisnow//获取终端的一些信息ui_get_shellsize();//initsRowsandColumns再来会加载.vimrc这样的配置文件,让你的vim与众不同。

//Sourcestartupscripts.source_startup_scripts(&params);还会加载一些vim插件source_in_path,使用load_start_packages加载package。

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

wait_return(TRUE);我们经常看见的:“PressENTERortypecommandtocontinue“就是在这里执行的。确认完,就说明你真的是要打开文件,并显示到终端了。

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

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

/**Createtherequestednumberofwindowsandeditbuffersinthem.*Alsodoesrecoveryif"recoverymode"set.*/create_windows(&params);这里其实涉及到两个方面:

把数据读出来,读到内存;

把字符渲染到终端;

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

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

creatememfile:创建一个memory+.swp文件的抽象层,读写数据都会过这一层;

readfile:读原始文件,并解码(用于显示到屏幕);

函数调用栈:

->readfile->open_buffer->create_windows->vim_main2->main真正干活的是readfile这个函数,吐槽一下,readfile是一个2533行的函数。。。。。。

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

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

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

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

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

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

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

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

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

block0的header主要标识:

vim的版本;

编辑文件的路径;

字符编码方式;

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

block0:头部4k,主要是存储一些文件的元数据,比如路径,编码模式,时间戳等等;

pointerblock:树形内部节点;

datablock:树形叶子节点,存储用户数据;

敲下:w背后的原理进程初始化我们讲完了,现在来看下:w触发的调用吧。用户敲下:w命令触发ex_write回调(初始化的时候配置好的)。

所有的流程皆在ex_write,我们来看下这个函数做了什么。

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

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

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

所以,:w其实就是把memline里面的数据刷到用户文件而已。怎么刷?

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

创建一个backup文件(test.txt~),把原文件拷贝出来;

把原文件test.txttruancate截断为0,相当于清空原文件数据;

从memline(内存+.test.txt.swp)拷贝数据,重新写入原文件test.txt;

删除备份文件test.txt~;

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

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

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

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

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

backup循环如下:

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

mch_early_init();3backup文件拷贝完成之后,就可以准备动原文件了。

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

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

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

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

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

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

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

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

人机交互,:w触发调用ex_write回调函数,于do_write->buf_write完成写入;

具体操作是:先备份一个test.txt~文件出来(全拷贝);

接着,原文件test.txt截断为0,从memline(即内存最新数据+.test.txt.swap的封装)拷贝数据,写入test.txt(全拷贝);

【画图】

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

先说memline是啥?

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

memline基于memfile,那memfile又是啥?

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

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

block区分3种类型:

block0块:树的根,文件元数据;

pointerblock:树的分支,指向下一个block;

datablock:树的叶子节点,存储用户数据;

swap文件组织:

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

block其他两种类型:

point