linux原理和方法(linux系统命令大全分享)

小猪 百科排行 602

linux原理和方法(linux系统命令大全分享)-第1张图片-小猪号

剧本起因

无意间用 vim 打开了一个 10 G 的文件,改了一行内容,: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 版本如下:

操作面板系统版本: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 后面跟文件名就可:

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 的缩写):

mch_early_init();

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

/*

* Various initialisations shared with tests.

*/

common_init(?ms);

举个举例 test.txt 这样的参数必定要赋值到全局变量中,因为未来是要总是使用的。

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

static struct cmdname

{

    char_u      *cmd_name;      // name of the command

    ex_func_T   cmd_func;       // function for this command

    long_u      cmd_argt;       // flags declared above

    cmd_addr_T  cmd_addr_type;  // flag for address type

} 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();  // find real value of $HOME

// 保存交互参数

set_argv_var(paramp->argv, paramp->argc);

配置一下跟终端窗口展现有关的东西,这部分往往一般是一些终端库有关的:

// 初始化终端一些配置

termcapinit(params.term); // set terminal name and get terminal

// 初始化光标地点

screen_start();  // don't know where cursor is now

// 获取终端的一些消息

ui_get_shellsize();  // inits Rows and Columns

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

// Source startup scripts.

source_startup_scripts(?ms);

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

下面这种只是第一个交互了,等待客户敲下 enter 键:

wait_return(TRUE);

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

怎么打开文件?怎么展现字符到终端屏幕?

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

    /*

     * Create the requested number of windows and edit buffers in them.

     * Also does recovery if "recoverymode" set.

     */

    create_windows(?ms);

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

把资料读出去,读到内存;

把字符渲染到终端;

怎么把资料从磁盘上读出去,只是 IO。怎么渲染到终端这种我们不管,这种使用的是 termlib 或者 ncurses 等终端编程库来实现的,有兴趣的应该了解下。

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

create memfile:创建一个 memory + .swp 文件的抽象层,读写资料都会过这一层;

read file:读原始文件,并解码(用来展现到屏幕);

函数调用栈:

-> readfile

-> open_buffer

-> create_windows

-> vim_main2

-> main

真正干活的是 readfile 这种函数,评论一下,readfile 是一个 2533 行的函数。。。。。。

readfile 里面会择机创建 swp 文件(曾经一些话,应该用来复原资料),调用的是 ml_open_file 这种函数,文件创建好之后,size 占用 4k,里面往往一般是一些特殊的元资料(用来复原资料用的)。

划重要时机:.{文件名}.swp 这种掩藏文件是有格式的,前 4k 为 header,后面的内容也是根据一个个block 团队的。

再往后走,会调用到 read_eintr 这种函数,读取资料的内容:

long

read_eintr(int fd, void *buf, size_t bufsize)

{

    long ret;

    for (;;) {

     ret = vim_read(fd, buf, bufsize);

     if (ret >= 0 || errno != EINTR)

         break;

    }

    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原理和方法(linux系统命令大全分享)-第2张图片-小猪号

block 0 的 header 主要标识:

vim 的版本;

写文件的路径;

字符编码方法;

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

block0:头部 4k ,往往一般是存储一些文件的元资料,例如路径,编码模式,时间戳等等;

pointer block:树形内部节点;

data block:树形叶子节点,存储客户资料;

2 敲下 :w 背后的原理

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

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

那么第一个问题?客户的改写在哪里?

在 memline 的封装,只要没执行过 :w 保存,那么客户的改写就没改写到原文件上(小心哦,没保存曾经,一定没改写原文件哦),这时候,客户的改写可能在内存,也很有可能在 swp 文件。存储的资料结构为 block 。所以,:w 其实只是把 memline 里面的资料刷到客户文件而已。怎么刷?

重要时机步骤如下(以 test.txt 举例):

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

把原文件 test.txt truancate 截断为 0,等于清空原文件资料;

从 memline (内存 + .test.txt.swp)拷贝资料,从头开始写入原文件 test.txt;

删除备份文件 test.txt~;

以上只是 :w 做的全部事件了,下面我们看下代码。

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

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

bfd = mch_open((char *)backup

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

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

backup 循环如下:

// buf_write 

 while ((write_info.bw_len = read_eintr(fd, copybuf, WRITEBUFSIZE)) > 0)

 {

  if (buf_write_bytes(&write_info) == FAIL)

   // 如果失败,则终止

  // 否则直到文件结束

  }

 }

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

long write_eintr(int fd, void *buf, size_t bufsize) {

    long    ret = 0;

    long    wlen;

    while (ret < (long)bufsize) {

        // 封装的系统调用 write 

        wlen = vim_write(fd, (char *)buf + ret, bufsize - ret);

        if (wlen < 0) {

            if (errno != EINTR)

            break;

        } else

            ret += wlen;

    }

    return ret;

}

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

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

留条后路呀,搞错了还一些复原,这种才是真正的备份文件。

改写原文件曾经的第一步,ftruncate 原文件到 0,之后,从 memline (内存 + swp)中拷贝资料,写回原文件。

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

for (lnum = start; lnum <= end; ++lnum)

{

    // 从 memline 中获取资料,返回一个内存 buffer( memline 其实只是内存和 swap 文件的一个封装)

    ptr = ml_get_buf(buf, lnum, FALSE) - 1;

    // 将这种内存 buffer 写到原文件

    if (buf_write_bytes(&write_info) == FAIL)

    {

        end = 0;        // write error: break loop

        break;

    }

    // ...

}

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

这样就完成了文件的更新啦,末尾只要删掉 backup 文件就可。

// Remove the backup unless 'backup' option is set or there was a

// conversion error.

mch_remove(backup);

这种只是我们资料写入的完美流程啦。是不是没有你想的那么无脑!

无脑小结下:当改写了 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 。

linux原理和方法(linux系统命令大全分享)-第3张图片-小猪号

先说 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 种类别:

block 0 块:树的根,文件元资料;

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

data block:树的叶子节点,存储客户资料;

swap 文件团队:

linux原理和方法(linux系统命令大全分享)-第4张图片-小猪号

block 0 是特别块,结构体占用 1024 个字节内存,写到文件是根据 1 个page 对齐的,所以是 4096 个字节。

如下图:

linux原理和方法(linux系统命令大全分享)-第5张图片-小猪号

block 很多的两种类别:

pointer 类别:这种是中间的分支节点,指向 block 的;

data 类别:这种是叶子节点;

#define DATA_ID        (('d' << 8) + 'a')   // data block id

#define PTR_ID        (('p' << 8) + 't')   // pointer block id

这种 ID 等于魔数,在 swp 文件中很简无脑单查看出去,例如在下面的文件中第一个 4k 存储的是 block0,第二个 4k 存储的是 pointer 类别的 block。

linux原理和方法(linux系统命令大全分享)-第6张图片-小猪号

第三,第四个 4k 存储的是一个 data 类别的 block ,里面存储了原文件资料。

linux原理和方法(linux系统命令大全分享)-第7张图片-小猪号

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

linux原理和方法(linux系统命令大全分享)-第8张图片-小猪号

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 写超大文件的时候怎么慢?

往往一般状态下,你能直观感受到,慢在两个地方:

vim 打开的时候;

改写了一行内容,:w 保存的时候;

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

我的直观感受是:命令敲下之后,应该去泡杯茶,等茶凉了一点,差不多就能观看到的窗口了。怎么?

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

划重要时机:初始化的时候,readfile 会把整个文件读一次。

10 G的文件,你随便想想就了解了有多慢。我们应该算一下,根据单盘硬件 100 M/s 的带宽来算,也要 102 秒的时间。

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

先拷贝出一个 10G 的 test.txt~ 备份文件,102 秒就过去了;

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

总结

vim 写文件并不没有用黑魔法,还是用的 read,write,朴实无华;

vim 写超大文件,打开很慢,因为会读一次文件( readfile ),保存的时候很慢,因为会读写两遍文件(backup 一次,memfile 覆盖写原文件一次);

memfile 是 vim 抽象的一层虚拟存储空间(物理上由内存 block 和 swp 文件组成)对应一个文件的最新改写,存储单元由 block 构成。:w 保存的时候,只是从 memfile 读,写到原文件的过程;

memline 是基于 memfile 做的另一层封装,把客户的文件抽象成“行”的概念;

.test.txt.swp 文件是一直 open 的,memfile 会定时的交换资料进去,以便容灾复原;

test.txt~ 文件才是真正的备份文件,诞生于 :w 覆盖原文件曾经,泯灭于成功覆写原文件之后;

vim 基础都是整个文件的处理,并不是局部处理,大文件的写开始不适合 vim ,话说回去,正经人谁会用 vim 写 10 G 的文件?vim 只是个文本写器呀;

一个 readfile 函数 2533 行,一个 buf_write 函数 1987 行代码。。。不是我压力各位的积极性,这。。。反正我不想再看见它了。。。

标签: 命令 系统

抱歉,评论功能暂时关闭!