autofs 内核模块的杂项设备控制操作¶
问题¶
autofs 在活跃重启(即当存在繁忙的挂载时重启 autofs)时存在一个问题。
在正常操作期间,autofs 使用在被管理目录上打开的文件描述符来发出控制操作。使用文件描述符使 ioctl 操作能够访问存储在超级块中的 autofs 特定信息。这些操作包括将 autofs 挂载设置为停用状态、设置过期超时以及请求过期检查。如下所述,某些类型的 autofs 触发的挂载最终可能会覆盖 autofs 挂载本身,这会阻止我们在没有打开文件描述符的情况下使用 open(2) 来获取用于这些操作的文件描述符。
目前,autofs 使用 “umount -l” (延迟卸载) 在重启时清除活跃的挂载。虽然延迟卸载适用于大多数情况,但任何需要向上遍历挂载树来构造路径的操作(例如 getcwd(2) 和 proc 文件系统 /proc/<pid>/cwd)都将不再工作,因为构造路径的起始点已与挂载树分离。
autofs 的实际问题是它无法重新连接到现有的挂载。人们立即会想到,只需添加重新挂载 autofs 文件系统的功能就可以解决这个问题,但遗憾的是,这行不通。这是因为 autofs 直接挂载和嵌套挂载树的 “按需挂载和过期” 实现将文件系统直接挂载在挂载触发目录项的顶部。
例如,有两种类型的自动挂载映射,直接映射(在内核模块源代码中,您将看到第三种类型称为偏移量,这只是伪装的直接挂载)和间接映射。
这是一个包含直接和间接映射条目的主映射
/- /etc/auto.direct
/test /etc/auto.indirect
以及相应的映射文件
/etc/auto.direct:
/automount/dparse/g6 budgie:/autofs/export1
/automount/dparse/g1 shark:/autofs/export1
and so on.
/etc/auto.indirect
g1 shark:/autofs/export1
g6 budgie:/autofs/export1
and so on.
对于上面的间接映射,autofs 文件系统挂载在 /test 上,并且每个子目录键都通过 inode 查找操作触发挂载。例如,我们可以看到 shark:/autofs/export1 挂载在 /test/g1 上。
直接挂载的处理方式是在每个完整路径(例如 /automount/dparse/g1)上创建一个 autofs 挂载,并将其用作挂载触发器。因此,当我们遍历路径时,我们将 shark:/autofs/export1 “挂载在此挂载点的顶部”。由于这些始终是目录,我们可以使用 follow_link inode 操作来触发挂载。
但是,直接和间接映射中的每个条目都可以有偏移量(使其成为多挂载映射条目)。
例如,间接挂载映射条目也可以是
g1 \
/ shark:/autofs/export5/testing/test \
/s1 shark:/autofs/export/testing/test/s1 \
/s2 shark:/autofs/export5/testing/test/s2 \
/s1/ss1 shark:/autofs/export1 \
/s2/ss2 shark:/autofs/export2
类似地,直接挂载映射条目也可以是
/automount/dparse/g1 \
/ shark:/autofs/export5/testing/test \
/s1 shark:/autofs/export/testing/test/s1 \
/s2 shark:/autofs/export5/testing/test/s2 \
/s1/ss1 shark:/autofs/export2 \
/s2/ss2 shark:/autofs/export2
autofs 第 4 版的一个问题是,当挂载具有大量偏移量的条目(可能具有嵌套)时,我们需要作为一个整体挂载和卸载所有偏移量。对于映射条目中具有大量偏移量的人来说,这并不是真正的问题。此机制用于众所周知的 “hosts” 映射,并且我们已经看到(在 2.4 中)可用挂载数量耗尽或可用特权端口数量耗尽的情况。
在第 5 版中,我们仅在遍历偏移量树时进行挂载,同样,在过期时也是如此,这解决了上述问题。该实现有一些更详细的信息,但这对于问题的解释不是必需的。一个重要的细节是,这些偏移量使用与上述直接挂载相同的机制实现,因此挂载点可以被挂载覆盖。
当前的 autofs 实现使用在挂载点上打开的 ioctl 文件描述符进行控制操作。描述符所持有的引用在确定挂载是否正在使用时进行检查,并且还用于访问挂载超级块中保存的 autofs 文件系统信息。因此,需要保留文件句柄的使用。
解决方案¶
为了能够重启 autofs 并保留现有的直接、间接和偏移量挂载,我们需要能够为这些可能被覆盖的 autofs 挂载点获取文件句柄。与其只实现一个孤立的操作,不如决定重新实现现有的 ioctl 接口并添加新操作以提供此功能。
此外,为了能够重建具有繁忙挂载的挂载树,需要提供触发挂载的最后一个用户的 uid 和 gid,因为这些可以用作 autofs 映射中的宏替换变量。它们在挂载请求时记录,并添加了一个操作来检索它们。
由于我们正在重新实现控制接口,因此解决了现有接口的几个其他问题。首先,当挂载或过期操作完成时,内核会通过 “发送就绪” 或 “发送失败” 操作将状态返回给内核。ioctl 接口的 “发送失败” 操作只能发送 ENOENT,因此重新实现允许用户空间发送实际状态。对于那些使用非常大映射的用户来说,用户空间中的另一个昂贵的操作是发现是否存在挂载。通常,这涉及到扫描 /proc/mounts,并且由于需要经常这样做,当挂载表中有许多条目时,可能会引入显著的开销。还添加了一个操作来查找挂载点目录项的挂载状态(无论是否被覆盖)。
当前的内核开发策略建议避免使用 ioctl 机制,而倾向于使用诸如 Netlink 之类的系统。尝试使用此系统进行实现以评估其适用性,结果发现它在这种情况下是不合适的。为此使用了通用 Netlink 系统,因为原始 Netlink 会导致复杂性显著增加。毫无疑问,通用 Netlink 系统是常见情况 ioctl 函数的优雅解决方案,但它不是一个完整的替代品,这可能是因为它的主要用途是作为消息总线实现,而不是专门作为 ioctl 的替代品。虽然可以解决这个问题,但有一个问题导致了不使用它的决定。原因是守护进程中的 autofs 过期操作变得过于复杂,因为枚举卸载候选对象几乎只是为了 “计数” 调用过期 ioctl 的次数。这涉及到扫描挂载表,对于具有大型映射的用户来说,这已被证明是一个巨大的开销。改进此操作的最佳方法是尝试恢复到很久以前的过期方式。也就是说,当针对挂载(文件句柄)发出过期请求时,我们应该不断回调守护进程,直到我们无法卸载任何更多挂载,然后将适当的状态返回给守护进程。目前,我们一次只过期一个挂载。由于消息总线体系结构的要求,通用 Netlink 实现将排除未来进行此开发的可能性。
autofs 杂项设备挂载控制接口¶
控制接口正在打开一个设备节点,通常是 /dev/autofs。
所有 ioctl 都使用一个通用结构来传递所需的参数信息并返回操作结果
struct autofs_dev_ioctl {
__u32 ver_major;
__u32 ver_minor;
__u32 size; /* total size of data passed in
* including this struct */
__s32 ioctlfd; /* automount command fd */
/* Command parameters */
union {
struct args_protover protover;
struct args_protosubver protosubver;
struct args_openmount openmount;
struct args_ready ready;
struct args_fail fail;
struct args_setpipefd setpipefd;
struct args_timeout timeout;
struct args_requester requester;
struct args_expire expire;
struct args_askumount askumount;
struct args_ismountpoint ismountpoint;
};
char path[];
};
ioctlfd 字段是 autofs 挂载点的挂载点文件描述符。它由 open 调用返回,并且由除检查给定路径是否为挂载点之外的所有调用使用,在检查给定路径是否为挂载点时,可以选择使用它来检查与给定挂载点文件描述符对应的特定挂载,以及请求 autofs 文件系统中目录上最后一次成功挂载的 uid 和 gid 时使用。
联合用于传递参数和调用结果,如下所述。
path 字段用于传递需要路径的位置,size 字段用于计算从用户空间发送的结构在转换时结构长度的增加。
可以使用 void 函数调用 init_autofs_dev_ioctl(struct autofs_dev_ioctl *
) 在设置特定字段之前初始化此结构。
所有 ioctl 都会将此结构从用户空间复制到内核空间,如果 size 参数小于结构本身的大小,则返回 -EINVAL,如果内核内存分配失败,则返回 -ENOMEM,如果复制本身失败,则返回 -EFAULT。其他检查包括检查编译到用户空间的版本与模块版本是否匹配,如果版本不匹配,则返回 -EINVAL。如果 size 字段大于结构大小,则假定存在路径,并检查以确保它以 “/” 开头并且以 NULL 结尾,否则返回 -EINVAL。在这些检查之后,对于除 AUTOFS_DEV_IOCTL_VERSION_CMD、AUTOFS_DEV_IOCTL_OPENMOUNT_CMD 和 AUTOFS_DEV_IOCTL_CLOSEMOUNT_CMD 之外的所有 ioctl 命令,都会验证 ioctlfd,如果它不是有效描述符或与 autofs 挂载点不对应,则会返回 -EBADF、-ENOTTY 或 -EINVAL(不是 autofs 描述符)错误。
ioctl¶
可以在 autofs 5.0.4 及更高版本中的文件 lib/dev-ioctl-lib.c 中看到使用此接口的实现的示例,该文件可从 kernel.org 的 /pub/linux/daemons/autofs/v5 目录下载的发行版 tar 文件中获得。
此接口实现的设备节点 ioctl 操作是
AUTOFS_DEV_IOCTL_VERSION¶
获取 autofs 设备 ioctl 内核模块实现的主版本号和次版本号。它需要一个初始化的 struct autofs_dev_ioctl 作为输入参数,并在传入的结构中设置版本信息。成功时返回 0,如果检测到版本不匹配,则返回错误 -EINVAL。
AUTOFS_DEV_IOCTL_PROTOVER_CMD 和 AUTOFS_DEV_IOCTL_PROTOSUBVER_CMD¶
获取已加载模块所理解的 autofs 协议版本的主版本号和次版本号。此调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为有效的 autofs 挂载点描述符,并将请求的版本号设置在 struct args_protover 的 version 字段或 struct args_protosubver 的 sub_version 字段中。这些命令在成功时返回 0,如果验证失败则返回负错误代码之一。
AUTOFS_DEV_IOCTL_OPENMOUNT 和 AUTOFS_DEV_IOCTL_CLOSEMOUNT¶
获取和释放 autofs 管理的挂载点路径的文件描述符。打开调用需要一个已初始化的 struct autofs_dev_ioctl,其中 path 字段已设置,size 字段已适当调整,以及 struct args_openmount 的 devid 字段设置为 autofs 挂载的设备号。设备号可以从 /proc/mounts 中显示的挂载选项中获取。关闭调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符。文件描述符的释放也可以使用 close(2) 完成,因此任何打开的描述符也会在进程退出时关闭。关闭调用包含在已实现的操作中,主要是为了完整性并提供一致的用户空间实现。
AUTOFS_DEV_IOCTL_READY_CMD 和 AUTOFS_DEV_IOCTL_FAIL_CMD¶
将挂载和过期结果状态从用户空间返回到内核。这两个调用都需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符,并且 struct args_ready 或 struct args_fail 的 token 字段设置为等待队列令牌号,该令牌号在之前的挂载或过期请求中由用户空间接收。struct args_fail 的 status 字段设置为操作的 errno。成功时设置为 0。
AUTOFS_DEV_IOCTL_SETPIPEFD_CMD¶
设置用于内核与守护进程通信的管道文件描述符。通常,这会在挂载时使用选项设置,但是当重新连接到现有挂载时,我们需要使用它来告知 autofs 挂载新的内核管道描述符。为了防止挂载错误地设置管道描述符,我们还要求 autofs 挂载处于休眠状态(请参阅下一个调用)。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符,并且 struct args_setpipefd 的 pipefd 字段设置为管道的描述符。成功时,该调用还会将用于识别控制进程(例如,拥有 automount(8) 守护进程)的进程组 ID 设置为调用者的进程组。
AUTOFS_DEV_IOCTL_CATATONIC_CMD¶
使 autofs 挂载点进入休眠状态。autofs 挂载将不再发出挂载请求,内核通信管道描述符将被释放,并且队列中任何剩余的等待也将被释放。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符。
AUTOFS_DEV_IOCTL_TIMEOUT_CMD¶
设置 autofs 挂载点内挂载的过期超时时间。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符。
AUTOFS_DEV_IOCTL_REQUESTER_CMD¶
返回上次成功触发给定路径 dentry 上的挂载的进程的 uid 和 gid。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 path 字段设置为所讨论的挂载点,并且 size 字段已适当调整。返回后,struct args_requester 的 uid 字段包含 uid,gid 字段包含 gid。
当使用活动挂载重建 autofs 挂载树时,我们需要重新连接到可能使用原始进程 uid 和 gid(或它们的字符串变体)进行映射条目内的挂载查找的挂载。此调用提供了获取此 uid 和 gid 的能力,以便用户空间可以将其用于挂载映射查找。
AUTOFS_DEV_IOCTL_EXPIRE_CMD¶
向内核发出 autofs 挂载的过期请求。通常,会调用此 ioctl,直到找不到其他过期候选者为止。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符。此外,可以通过将 struct args_expire 的 how 字段设置为 AUTOFS_EXP_IMMEDIATE 或 AUTOFS_EXP_FORCED,来请求独立于挂载超时的立即过期以及独立于挂载是否繁忙的强制过期。如果找不到过期候选者,则 ioctl 返回 -1,errno 设置为 EAGAIN。
此调用会导致内核模块检查与给定 ioctlfd 对应的挂载,以查找可以过期的挂载,向守护进程发出过期请求并等待完成。
AUTOFS_DEV_IOCTL_ASKUMOUNT_CMD¶
检查 autofs 挂载点是否正在使用。
该调用需要一个已初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从打开调用中获取的描述符,它在 struct args_askumount 的 may_umount 字段中返回结果,1 表示繁忙,0 表示不繁忙。
AUTOFS_DEV_IOCTL_ISMOUNTPOINT_CMD¶
检查给定路径是否为挂载点。
该调用需要一个已初始化的 struct autofs_dev_ioctl。有两种可能的变体。两者都使用 path 字段设置为要检查的挂载点路径,并且 size 字段已适当调整。一个使用 ioctlfd 字段来标识要检查的特定挂载点,而另一个变体使用 path 字段,以及可选的 struct args_ismountpoint 的 in.type 字段设置为 autofs 挂载类型。如果这是一个挂载点,则调用返回 1,并将 out.devid 字段设置为挂载的设备号,并将 out.magic 字段设置为相关的超级块魔数(如下所述),否则返回 0。在这两种情况下,设备号(由 new_encode_dev() 返回)都将返回到 out.devid 字段中。
如果提供了文件描述符,我们正在查找特定的挂载,不一定位于已挂载堆栈的顶部。在这种情况下,如果描述符对应的路径本身是一个挂载点,或者包含一个挂载(例如没有根挂载的多挂载),则该路径被视为挂载点。在这种情况下,如果描述符对应于一个挂载点,则返回 1,并且如果存在覆盖挂载,则还返回覆盖挂载的超级魔数,否则如果不是挂载点,则返回 0。
如果提供了路径(并且 ioctlfd 字段设置为 -1),则会查找该路径并检查它是否是挂载的根。如果还给定了类型,我们正在寻找特定的 autofs 挂载,如果找不到匹配项,则返回失败。如果找到的路径是挂载的根,则返回 1 以及挂载的超级魔数,否则返回 0。