为 autofs 内核模块提供的各种设备控制操作¶
问题¶
autofs 中存在活动重启的问题(也就是说,当存在繁忙挂载时重启 autofs)。
在正常操作期间,autofs 使用在正在管理的目录上打开的文件描述符,以便能够发出控制操作。使用文件描述符使 ioctl 操作可以访问存储在超级块中的 autofs 特定信息。这些操作包括将 autofs 挂载设置为 catatonic、设置过期超时以及请求过期检查。如下所述,某些类型的 autofs 触发挂载最终可能会覆盖 autofs 挂载本身,这会阻止我们在尚未打开文件描述符的情况下使用 open(2) 来获取这些操作的文件描述符。
目前,autofs 使用“umount -l”(惰性卸载)在重启时清除活动挂载。虽然惰性卸载适用于大多数情况,但任何需要向上遍历挂载树以构造路径的东西,例如 getcwd(2) 和 proc 文件系统 /proc/<pid>/cwd,都将不再起作用,因为从中构造路径的点已从挂载树中分离。
autofs 的实际问题在于它无法重新连接到现有挂载。人们立刻想到只需添加重新挂载 autofs 文件系统的功能即可解决此问题,但遗憾的是,这行不通。这是因为 autofs 直接挂载和嵌套挂载树的“按需挂载和过期”的实现直接将文件系统挂载在挂载触发目录 dentry 的顶部。
例如,有两种类型的自动挂载映射:直接挂载(在内核模块源代码中,您将看到第三种类型,称为偏移量,它只是伪装的直接挂载)和间接挂载。
这是一个包含直接和间接映射条目的主映射
/- /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 上。
处理直接挂载的方式是在每个完整路径上进行 autofs 挂载,例如 /automount/dparse/g1,并将其用作挂载触发器。因此,当我们遍历该路径时,我们将 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,并且由于它需要经常完成,因此当挂载表中有许多条目时,它可能会引入显著的开销。还添加了一个操作来查找挂载点 dentry(覆盖或未覆盖)的挂载状态。
当前的内核开发策略建议避免使用 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 时。
union 用于传递参数和调用结果,如下所述。
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 管理的挂载点路径的文件描述符。open 调用需要一个初始化的 struct autofs_dev_ioctl,其中 path 字段已设置,size 字段已适当调整,并且 struct args_openmount 的 devid 字段已设置为 autofs 挂载的设备号。设备号可以从 /proc/mounts 中显示的挂载选项中获取。close 调用需要一个初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从 open 调用获取的描述符。也可以使用 close(2) 完成文件描述符的释放,因此任何打开的描述符也会在进程退出时关闭。close 调用包含在已实现的操作中,主要是为了完整性并提供一致的用户空间实现。
AUTOFS_DEV_IOCTL_READY_CMD 和 AUTOFS_DEV_IOCTL_FAIL_CMD¶
将挂载和过期结果状态从用户空间返回到内核。这两个调用都需要一个初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从 open 调用获取的描述符,并且 struct args_ready 或 struct args_fail 的 token 字段设置为等待队列令牌号,该令牌号由用户空间在前述挂载或过期请求中接收。struct args_fail 的 status 字段设置为操作的 errno。成功时,它设置为 0。
AUTOFS_DEV_IOCTL_SETPIPEFD_CMD¶
设置用于内核与守护程序通信的管道文件描述符。通常,这是在挂载时使用选项设置的,但是当重新连接到现有挂载时,我们需要使用它来告诉 autofs 挂载新的内核管道描述符。为了保护挂载免受错误设置管道描述符的影响,我们还需要 autofs 挂载处于 catatonic 状态(请参阅下一个调用)。
该调用需要一个初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从 open 调用获取的描述符,并且 struct args_setpipefd 的 pipefd 字段设置为管道的描述符。成功后,该调用还会将用于标识控制进程(例如,拥有的 automount(8) 守护程序)的进程组 ID 设置为调用者的进程组。
AUTOFS_DEV_IOCTL_CATATONIC_CMD¶
使 autofs 挂载点处于 catatonic 状态。autofs 挂载将不再发出挂载请求,内核通信管道描述符将被释放,并且队列中剩余的任何等待都将被释放。
该调用需要一个初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从 open 调用获取的描述符。
AUTOFS_DEV_IOCTL_TIMEOUT_CMD¶
设置 autofs 挂载点中挂载的过期超时。
该调用需要一个初始化的 struct autofs_dev_ioctl,其中 ioctlfd 字段设置为从 open 调用获取的描述符。
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 字段设置为从 open 调用获取的描述符。此外,可以通过将 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 字段设置为从 open 调用获取的描述符,并在 struct args_askumount 的 may_umount 字段中返回结果,1 表示繁忙,0 表示否则。
AUTOFS_DEV_IOCTL_ISMOUNTPOINT_CMD¶
检查给定路径是否为挂载点。
该调用需要一个初始化的 struct autofs_dev_ioctl。有两种可能的变体。两者都使用设置为要检查的挂载点路径的 path 字段,并且 size 字段已适当调整。一个使用 ioctlfd 字段来标识要检查的特定挂载点,而另一个变体使用路径,并可以选择使用 struct args_ismountpoint 的 in.type 字段设置为 autofs 挂载类型。如果这是一个挂载点,则该调用返回 1,并将 out.devid 字段设置为挂载的设备号,out.magic 字段设置为相关的超级块魔术数(如下所述),如果不是挂载点,则返回 0。在这两种情况下,设备号(由 new_encode_dev() 返回)都在 out.devid 字段中返回。
如果提供了文件描述符,我们正在寻找一个特定的挂载,不一定位于已挂载堆栈的顶部。在这种情况下,描述符对应的路径被认为是挂载点,如果它本身是一个挂载点或包含一个挂载,例如没有根挂载的多挂载。在这种情况下,如果描述符对应于一个挂载点,则返回 1,如果存在覆盖挂载,则还返回覆盖挂载的超级魔术数,如果不是挂载点,则返回 0。
如果提供了路径(并且 ioctlfd 字段设置为 -1),则查找该路径并检查它是否为挂载的根。如果还给出了类型,我们正在寻找特定的 autofs 挂载,如果未找到匹配项,则返回失败。如果找到的路径是挂载的根,则返回 1,以及挂载的超级魔术数,否则返回 0。