
分析Overlay fs联合文件系统源自于培养OpenHarmony高端人才的动机,通过讲Overlay fs联合文件系统移植到Liteos_A内核的项目培养一批精通OpenHarmony内核的人才,也通过本文向各位热爱OpenHarmony内核的技术开发者和爱好者叙说一个复杂文件系统的具体实现过程和包含的软件思想,我们是一群热爱OpenHarmony,热爱开源,传递技术正能量的OpenHarmony开发工程师。
Overlay fs是一种联合文件系统,它以堆叠的形式将不同的目录挂载到同一个虚拟文件系统下。Overlayfs文件系统像其他文件系统一样,先被作为一种文件系统注册到Linux内核,而后用户通过Mount命令触发其挂载,然后才得以被用户使用。
需要注意的是,Overlay fs在Linux内核3.18后就被默认加入内核模块了,查看Linux内核版本可以使用如下命令:
uname -r为了更好地帮助我们理解overlay fs的作用,可以在Linux上开启overlay模块,并做些简单的实验,再去阅读源码会有更好的效果。在Linux上开启overlay fs模块的方法主要有两种,具体如下所示:
# 查看是否已加载overlay 模块
lsmod | grep overlay
# 若没有加载则可手动加载
modprobe overlay# 直接继续overlay fs的挂载,自动加载overlay模块
mount -t overlay overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work /merged加载overlay模块后,就可以进行一些实验,而本文主要关注Overlay fs挂载流程,结合Linux内核源码分析挂载过程的执行过程,并通过将该过程中涉及到的主要数据结构之间的联系绘制成结构图,来尽可能清晰地为读者展现一张文件系统核心数据结构整体图。
鉴于作者知识有限,仅以个人视角,一孔窥豹,不成体系,读者还需实地阅读代码才能加深理解。
若发现文中错误,可以联系笔者进行修改。
Overlay fs挂载命令格式如下:
# 该命令指定一个lower层,一个upper层
sudo mount -t overlay overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work /merged
# 若需要指定多个lower层,可以使用如下命令
sudo mount -t overlay overlay -o lowerdir=/lower1:/lower2:/lower3,upperdir=/upper,workdir=/work /merged其中,overlay文件系统挂载的命令参数解释如下:
Overlay文件系统的不同层合并规则以及读写规则有如下几点:
为了简要形象地说明Overlay文件系统挂载之后的堆叠(合并)规则,可以参考下图,图中含有一个lowedir和upperdir,两者合并成一个merged层:

这一小结中Overlay fs文件系统挂载的的命令行参数,就介绍到这里,下面主要关注Overlayfs的挂载过程。
使能Linux内核Overlay fs文件系统编译选项CONFIG_OVERLAY_FS,或者以=y静态方式编译进内核,或者先以=m内核模块方式编译再加载该内核模块,在Linux内核层添加对Overlay fs联合文件系统的支持。 然后以mount -t overlay overlayfs,触发mount系统调用,进行挂载Overlayfs文件系统的流程。
mount系统调用挂载Overlay fs文件系统的过程如下:
SYSCALL_DEFINE5(mount,,,,,,,) // fs/namespace.c
|-> user_path_at(AT_FDCWD, dir_name, LOOKUP_FOLLOW, &path);
|-> do_mount(kernel_dev, dir_name, kernel_type, flags, options);
|-> path_mount(dev_name, &path, type_page, flags, data_page);
|-> do_new_mount()
| // (1) 找到指定文件系统
|-> type = get_fs_type(fstype);
| |-> find_filesystem(name, len)
| |-> for (p = &file_systems; *p; p = &(*p)->next)
| |-> strncmp((*p)->name, name, len) == 0
| |-> return p;
|
| // (2) 创建fc
|-> fc = fs_context_for_mount(type, sb_flags);
| |-> alloc_fs_context(fs_type, NULL, sb_flags, 0, FS_CONTEXT_FOR_MOUNT);
| |-> fc = kzalloc(sizeof(struct fs_context), GFP_KERNEL);
| |-> fc->fs_type = get_filesystem(fs_type);
| |-> legacy_init_fs_context(fc);
| |-> fc->fs_private = kzalloc(sizeof(struct legacy_fs_context), GFP_KERNEL);
| |-> fc->ops = &legacy_fs_context_ops;
|
| // (3) 将fc的root指针指向overlayfs挂载点根root目录
|-> vfs_get_tree(fc)
| | // 通过fc的get_tree间接调用文件系统mount函数,以获得挂载节点root目录项(记录在fc中)
| | // fc->ops->get_tree(fc); => legacy_get_tree
| |-> legacy_get_tree(fc) // 将挂载点的root目录项赋值到fc的root
| |-> root = fc->fs_type->mount(fc->fs_type, fc->sb_flags, fc->source, ctx->legacy_data);
| |-> fc->root = root; // struct dentry *
|
| // (4) 创建mount实体,并将其挂载到supper_block的挂载链表中
|-> do_new_mount_fc(fc, path, mnt_flags);// path的形参即为mountpoint,是实际文件系统中mount挂载目标路径
|-> mnt = vfs_create_mount(fc);
| |-> mnt = alloc_vfsmnt(fc->source ?: "none");
| |-> mnt->mnt.mnt_sb = fc->root->d_sb;
| |-> mnt->mnt.mnt_root = dget(fc->root);
| |-> mnt->mnt_mountpoint = mnt->mnt.mnt_root;
| |-> mnt->mnt_parent = mnt;
| |-> list_add_tail(&mnt->mnt_instance, &mnt->mnt.mnt_sb->s_mounts);
|
|-> do_add_mount(real_mount(mnt), mp, mountpoint, mnt_flags);如上系统调用mount的简化调用堆栈中可以看出,整个mount流程可以总结为4个步骤:
(1)根据文件系统名称fstype,在内核所支持的文件系统链表中,找到对应的文件系统,若没有找到则先加载该模块。
(2)创建文件系统上下文空间,并初步初始化,对Overlay fs联合文件系统,fc的ops设置为legacy_fs_context_ops。
(3)由文件系统fc间接调用文件系统的mount接口,对Overlay fs文件系统来说,该指针指向ovl_mount(),由该函数完成Overlay fs文件系统的挂载。
(4)在Overlay fs文件系统挂载完成后,创建一个mount节点,并将其挂载到Overlayfs文件系统supper_block的挂载链表内。
上述几个步骤中**,关键结构体的链接情况**可以参考下图:

如上图,为了便于理解和记忆,可以将mount过程中的关键结构体抽象成4个层次,即mount层,fs_context层,fs文件系统层,以及目录项节点层。首先,从系统支持的文件系统链表中获得Overlayfs文件系统,然后创建fs_context对象,并以此间接地调用Overlay fs文件系统的mount接口函数。由ovl_mount()挂载函数完成Overlayfs文件系统所特有的挂载过程,有上图Overlayfs有一个独有的表示其文件系统结构的结构体类型struct ovl_fs,作为其super_block的私有数据,以有别与其他的文件系统。
在第3小结中,我们将详细地解析Overlay fs文件系统的挂载过程。
如前文所述,真正实现Overlay fs联合文件系统挂载过程的函数是由fc_context间接调用的ovl_mount()过程,过程也比较简单,就是申请一个supper_block,
然后使用Overlayfs文件系系统的信息将其填充。 具体过程如下:
ovl_mount() // fs/overlayfs/super.c
|-> mount_nodev(fs_type, flags, raw_data, ovl_fill_super); // return "dentry* s_root"
| // (1) 为新挂载点创建superblock
|-> s = sget() // struct super_block *
| |-> s = alloc_super(type, (flags & ~SB_SUBMOUNT), user_ns);
| |-> list_add_tail(&s->s_list, &super_blocks);
|
| // fill_super()
|-> ovl_fill_super(s, data, flags & SB_SILENT ? 1 : 0)
|-> struct ovl_fs *ofs; // private information held for overlayfs's superblock
|-> struct ovl_layer *layers;
|
|-> ofs = kzalloc(sizeof(struct ovl_fs), GFP_KERNEL);
|-> sb->s_d_op = &ovl_dentry_operations;
|-> ovl_parse_opt((char *) data, &ofs->config);
|-> numlower = ovl_split_lowerdirs(splitlower);
|
| // 创建联合文件系统层级数组
|-> layers = kcalloc(numlower + 1, sizeof(struct ovl_layer), GFP_KERNEL);
|-> ofs->layers = layers;
|
|-> sb->s_op = &ovl_super_operations;
|
| // 设置upperlayer:设置layer的trap执行upperpath目录的inode;依据upperpath在实际文件系统中的挂载点填充layer的挂载点(ovl挂载的root为实际文件系统中一指定目录);
|-> ovl_get_upper(sb, ofs, &layers[0], &upperpath);
| | // 获得upperlayer目录在实际文件系统上路径:若路径未打开过,则创建路径上的所有节点的dentry与inode
| |-> ovl_mount_dir(ofs->config.upperdir, upperpath);
| | // 为挂载点创建一个inode,保存在trap中:新inode的private数据指向实际文件系统中挂载点目录的inode
| |-> ovl_setup_trap(sb, upperpath->dentry, &upper_layer->trap, "upperdir");
| | // 拷贝upper层实际文件系统路径的挂载点为虚拟堆叠文件系统一个层次的新mount节点,且新的mount节点的root目录为uppper目录;
| |-> upper_mnt = clone_private_mount(upperpath);
| |-> upper_layer->mnt = upper_mnt;
|
| // 创建workdir: workbasedir为指定的实际文件系统内的目录,workbasedir_trap对workbasedir的inode的trap;workdir为workbasedir下创建的默认名称为“work”的目录
|-> ovl_get_workdir(sb, ofs, &upperpath);
| |-> ofs->workbasedir = dget(workpath.dentry);
| | // 申请一个ovl_inode,使其private指向实际文件系统inode,新对象地址保存在ofs->workbasedir_trap内
| |-> ovl_setup_trap(sb, ofs->workbasedir, &ofs->workbasedir_trap, "workdir");
| |
| |-> ovl_make_workdir(sb, ofs, &workpath);
|
| // 逐个获取lower层,检查是否已经有打上“trap”标记的inode,同时为每个lower层的root inode打上“trap”标记
|-> oe = ovl_get_lowerstack(sb, splitlower, numlower, ofs, layers);
|
| // 对每个层对应root inode的parent递归检查是否已经打上“trap”标记,重叠层可能是互为祖先关系
|-> ovl_check_overlapping_layers(sb, ofs);
|
|-> sb->s_fs_info = ofs;
|
|-> root_dentry = ovl_get_root(sb, upperpath.dentry, oe);
|
|-> sb->s_root = root_dentry;由于Overlay fs文件系统叠层联合文件系统,那么在执行mount命令时指定upper目录、lower目录及work目录,是如何在Overlayfs内被组织利用,进而实现层叠联合文件系统的呢?该过程为Overlayfs文件挂载的核心过程,主要在ovl_fill_super()函数内完成。
在ovl_fill_super()的主要过程为:
(1)从overlayfs挂载命令中获取Lower层的个数,并以此分配layers空间。
(2)解析Upper层,包括挂载路径,原系统的挂载节点、挂载目录等。
(3)解析Work工作目录,并做必要合法性检测。
(4)解析lower层,可能包含多个叠层,需要对每一层都进行解析,并将解析结果保存layers中。
(5)设置overlayfs系统根目录。
其中,比较复杂而且关系到Overlayfs整个实现机制的步骤为(2)、(4)和(5),在下面的内容中将着重介绍这些内容,之后便可以对整个Overlay fs联合文件系统的挂载过程形成一个整体蓝图。
Overlay fs对upper层目录的解析过程在ovl_get_upper()函数中完成,主要分为3个步骤:
(1)查找upper层目标目录的路径,是根据挂载Overlayfs提供的upperdir路径,层层查找得到。
(2)设置upper_layer的trap,trap即为Overlayfs文件系统中的一个inode,其私有数据指向upper目录在实际文件系统中的inode。
(3)创建一个upper层目录挂载节点的克隆体,并设置为upper_layer的mnt成员。
如下为ovl_get_upper()函数的调用堆栈:
ovl_get_upper(sb, ofs, &layers[0], &upperpath);
|-> struct vfsmount *upper_mnt;
| // (1)解析upper目录:沿着用户指定的upper目录路径解析,直至找到目录的目录项
|-> ovl_mount_dir(ofs->config.upperdir, upperpath);
| |-> tmp = kstrdup(name, GFP_KERNEL);
| |-> ovl_mount_dir_noesc(tmp, path);
| |-> kern_path(name, LOOKUP_FOLLOW, path);
| |-> filename_lookup(AT_FDCWD, getname_kernel(name), flags, path, NULL);
|
| // (2)设置upper_layer的trap:即从Overlayfs超级块新申请一个inode,并将upper目录的inode设置为其私有数据
|-> ovl_setup_trap(sb, upperpath->dentry, &upper_layer->trap, "upperdir");
| |-> trap = ovl_get_trap_inode(sb, dir);
| | |-> struct inode *key = d_inode(dir);
| | |-> struct inode *trap;
| | |-> trap = iget5_locked(sb, (unsigned long) key, ovl_inode_test, ovl_inode_set, key);
| | |-> return trap;
| |-> *ptrap = trap;
|
| // (3)创建一个upper层目录挂载节点clone体,并赋值给upper_layer的mnt成员
|-> upper_mnt = clone_private_mount(upperpath);
| |-> struct mount *old_mnt = real_mount(path->mnt);
| |-> struct mount *new_mnt;
| | // 创建一个upper层的mount节点的clone体,并且将其挂载到upper层超级块的挂载点链表上
| |-> new_mnt = clone_mnt(old_mnt, path->dentry, CL_PRIVATE);
| | |-> struct super_block *sb = old->mnt.mnt_sb;
| | |-> struct mount *mnt;
| | |
| | |-> mnt = alloc_vfsmnt(old->mnt_devname);
| | |-> mnt->mnt.mnt_sb = sb;
| | |-> mnt->mnt.mnt_root = dget(root);
| | |-> list_add_tail(&mnt->mnt_instance, &sb->s_mounts);
| |
| |-> return &new_mnt->mnt;
|-> upper_layer->mnt = upper_mnt;为方便理解,将ovl_get_upper()中涉及的结构类型ovl_layer、mount、dentry和inode等的关联关系绘制成如下图形:

Overlayfs对upper层目录的解析过程在ovl_get_lowerstack()函数中完成,lower层与upper层不同是lower层可能有多个目录叠加在一起,需要对lower层。
目录做批量处理,过程有些类似于ovl_get_upper()对upper层的解析过程:
static struct ovl_entry *
ovl_get_lowerstack(struct super_block *sb, const char *lower, unsigned int numlower, struct ovl_fs *ofs, struct ovl_layer *layers)
|-> struct path *stack = NULL;
|-> struct ovl_entry *oe;
|
| // (1)分配并初始化深度与lower层目录树对应的path数组
|-> stack = kcalloc(numlower, sizeof(struct path), GFP_KERNEL);
|-> for (i = 0; i < numlower; i++) {
| // 依次解析所有lower层目录
|-> err = ovl_lower_dir(lower, &stack[i], ofs, &sb->s_stack_depth);
|-> }
|
| // 根据lower层目录数组设置Overlayfs文件系统及其layers
|-> ovl_get_layers(sb, ofs, stack, numlower, layers);
| |-> ofs->fs = kcalloc(numlower + 1, sizeof(struct ovl_sb), GFP_KERNEL);
| |
| |-> ofs->fs[0].sb = ovl_upper_mnt(ofs)->mnt_sb;
| |-> ofs->fs[0].is_lower = false;
| |
| |-> for (i = 0; i < numlower; i++) {
| |-> ovl_setup_trap(sb, stack[i].dentry, &trap, "lowerdir");
| |-> mnt = clone_private_mount(&stack[i]);
| |-> layers[ofs->numlayer].trap = trap;
| |-> layers[ofs->numlayer].mnt = mnt;
| |-> ofs->fs[fsid].is_lower = true;
| |-> }
|
| // 设置overlayfs文件系统的根目录ovl_entry
|-> oe = ovl_alloc_entry(numlower);
|-> for (i = 0; i < numlower; i++) {
|-> oe->lowerstack[i].dentry = dget(stack[i].dentry);
|-> oe->lowerstack[i].layer = &ofs->layers[i+1];
|-> }
|
| // 释放临时存放lower层路径的path数组
|-> kfree(stack);
|-> return oe;仔细阅读如上代码,我们会发现除了批量执行类似于ovl_get_upper()中解析目录操作外,还增加了分配并初始化一个ovl_entry类型结构实例的过程。
该ovl_entry的在Overlayfs虚拟文件系统中的功能主要为Overlayfs文件系统中的目录提供额外的联合文件系统的Lower层目录信息,其作用我们将在root根目录初始化的过程中见到。
将ovl_get_lowerstack()内创建的数组和相关结构体类型的关系绘制成如下图形:

从上图可知,ovl_get_lowerstack()的功能相对于ovl_get_upper()揉进了额外的功能,除了构建ovl_fs的layers数组外,还构建了fs数组,另外还构建Overlayfs目录的ovl_entry结构体对象。可以明显的感觉到,这部分代码不像Linux内核一贯严谨的代码风格,在一个函数混合了多个功能。 其中的lowerstack关系图可以参考下图,分别用图表和代码的形式绘制。

文件系统的目录和文件操作是以root根目录为起点,而对于Overlayfs联合文件系统,知道root根目录的构建过程,是理解该联合文件系统操作目录的关键。Overlayfs文件系统依靠ovl_get_root()完成根目录的构建,具体调用堆栈如下:
static struct dentry *
ovl_get_root(struct super_block *sb, struct dentry *upperdentry, struct ovl_entry *oe)
|-> struct dentry *root;
|-> struct ovl_path *lowerpath = &oe->lowerstack[0];
|-> struct ovl_inode_params oip = {
|-> .upperdentry = upperdentry,
|-> .lowerpath = lowerpath,
|-> };
|
|-> root = d_make_root(ovl_new_inode(sb, S_IFDIR, 0));
|-> root->d_fsdata = oe;
|
|-> ovl_inode_init(d_inode(root), &oip, ino, fsid);
| |-> OVL_I(inode)->__upperdentry = oip->upperdentry;
| |-> OVL_I(inode)->lower = igrab(d_inode(oip->lowerpath->dentry));
|
|-> ovl_dentry_update_reval(root, upperdentry, DCACHE_OP_WEAK_REVALIDATE);
|-> return root;为便于理解其中繁杂的结构体之间的组合关心,还是将他们直接的关系绘制成如下图形:

在构建root根目录的过程中,ovl_get_root()首先调用d_make_root(ovl_new_inode(sb, S_IFDIR, 0))创建一个ovl_inode类型的Overlay fs系统的inode节点,该节点中包含一个VFS系统的inode结构体;再创建一个跟目录价的dentry目录项,并将ovl_inode包含的inode成员的地址赋值给dentry目录项的d_inode指针。然后,ovl_get_root()将ovl_get_lowerstack()解析lower层目录时创建的ovl_entry结构体对象作为root根目录dentry的私有数据。最后,ovl_get_root()借助upper和lower目录在ovl_inode_init()函数内初始化了root根目录在VFS中的inode节点,借此间接设置了ovl_inode对象的上层目录的dentry和第一个lower层的inode节点地址。
至此,我们完成了挂载Overlayfs文件系统的整个过程。
Overlay fs是一种虚拟文件系统,它的实现是在现有文件系统上又添加了一个抽象层,从而达到实现联合文件系统的目的。而这个抽象层的实现借助了负责的结构体类型以及他们之间的关联关系而实现,所以,要理解Overlay fs联合文件系统的关键就是理清这些结构体的组织关系。
为了能明白地解析Overlayfs文件系统挂载过程,本文只尽可能详细跟踪了代码执行过程,同时也省略了“自以为”不影响描述整个过程的代码,主要目的也是为了是读者更加关注在挂载流程。至于其他机制,例如目录的合并、目录或者文件的删除、目录的遍历等,这里不做涉及,将在后续的博客中介绍。
我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta