autofs - 工作原理
目的
autofs 的目标是按需挂载和无竞争地自动卸载各种其他文件系统。这提供了两个关键优势
无需延迟启动,直到所有可能需要的文件系统都被挂载。尝试访问这些慢速文件系统的进程可能会被延迟,但其他进程可以自由继续。 这对于网络文件系统(例如 NFS)或存储在具有媒体更换机器人的媒体上的文件系统尤为重要。
文件系统的名称和位置可以存储在远程数据库中,并且可以随时更改。访问时数据库中的内容将用于为访问提供目标。 文件系统中名称的解释甚至可以是程序化的而不是数据库支持的,例如允许使用通配符,并且可以根据首次访问名称的用户而有所不同。
上下文
“autofs”文件系统模块只是 autofs 系统的一部分。 还需要有一个用户空间程序来查找名称和挂载文件系统。 这通常是“automount”程序,但包括“systemd”在内的其他工具也可以使用“autofs”。 本文档仅描述内核模块以及与任何用户空间程序所需的交互。 后续文本将此称为“automount 守护程序”或简称为“守护程序”。
“autofs”是一个 Linux 内核模块,它提供“autofs”文件系统类型。 可以挂载多个“autofs”文件系统,并且可以单独管理每个文件系统,或者由同一个守护程序管理所有文件系统。
内容
autofs 文件系统可以包含 3 种对象:目录、符号链接和挂载陷阱。 挂载陷阱是具有额外属性的目录,如下一节所述。
对象只能由 automount 守护程序创建:符号链接使用常规的 symlink 系统调用创建,而目录和挂载陷阱使用 mkdir 创建。 确定目录是否应为挂载陷阱基于主映射。autofs 查询此主映射以确定哪些目录是挂载点。 挂载点可以是直接/间接/偏移。 在大多数系统上,默认主映射位于 /etc/auto.master。
如果未给出直接或偏移挂载选项(因此挂载被认为是间接的),则根目录始终是常规目录,否则当它为空时是挂载陷阱,而当它不为空时是常规目录。请注意,直接和偏移的处理方式相同,因此简明扼要的总结是,仅当文件系统直接挂载且根目录为空时,根目录才是挂载陷阱。
仅当文件系统间接挂载且根目录为空时,才会在根目录中创建的目录成为挂载陷阱。
树中更深层次的目录取决于 maxproto 挂载选项,尤其是它是否小于 5。 当 maxproto 为 5 时,树中更深层次的目录永远不会成为挂载陷阱,它们始终是常规目录。 当 maxproto 为 4(或 3)时,只有当这些目录为空时,它们才是挂载陷阱。
因此:非空(即非叶)目录永远不会成为挂载陷阱。 空目录有时是挂载陷阱,有时不是,具体取决于它们在树中的位置(根、顶层或更低层)、maxproto 以及挂载是否为间接挂载。
挂载陷阱
autofs 实现的核心要素是 Linux VFS 提供的挂载陷阱。 文件系统提供的任何目录都可以指定为陷阱。 这涉及两个独立的功能协同工作,以使 autofs 能够完成其工作。
DCACHE_NEED_AUTOMOUNT
如果 dentry 设置了 DCACHE_NEED_AUTOMOUNT 标志(如果在 inode 上设置了 S_AUTOMOUNT,或者可以直接设置,则会设置该标志),那么它(可能)是一个挂载陷阱。 除“stat”之外的对该目录的任何访问都(通常)会导致调用 d_op->d_automount() dentry 操作。 此方法的任务是找到应挂载在该目录上的文件系统并返回它。 VFS 负责实际将该文件系统的根挂载在该目录上。
autofs 不会自己查找文件系统,而是向 automount 守护程序发送消息,要求它查找和挂载文件系统。 然后,autofs d_automount 方法等待守护程序报告一切已准备就绪。 然后它将返回“NULL”,指示挂载已经发生。 VFS 不会尝试挂载任何内容,而是沿着已经存在的挂载向下进行。
此功能对于某些挂载陷阱的用户(例如 NFS)来说已经足够了,NFS 创建陷阱以便服务器上的挂载点可以反映在客户端上。 但是,对于 autofs 来说还不够。 由于挂载到目录被认为是“超出 stat”,因此 automount 守护程序将无法在“陷阱”目录上挂载文件系统,而没有任何避免陷入陷阱的方法。 为此,还有另一个标志。
DCACHE_MANAGE_TRANSIT
如果 dentry 设置了 DCACHE_MANAGE_TRANSIT,那么将调用两种非常不同但相关的行为,它们都使用 d_op->d_manage() dentry 操作。
首先,在检查目录上是否已挂载任何文件系统之前,将使用设置为 false 的 rcu_walk 参数调用 d_manage()。 它可能会返回以下三件事之一
返回值为零表示此 dentry 没有任何特殊之处,应进行挂载和自动挂载的正常检查。
autofs 通常返回零,但首先等待任何到期(自动卸载已挂载的文件系统)完成。 这避免了竞争。
返回值为 -EISDIR 告诉 VFS 忽略目录上的任何挂载,并且不要考虑调用 ->d_automount()。 这有效地禁用了 DCACHE_NEED_AUTOMOUNT 标志,导致该目录毕竟不是挂载陷阱。
如果 autofs 检测到执行查找的进程是 automount 守护程序,并且已请求挂载但尚未完成,则 autofs 会返回此值。 如何确定这一点将在后面讨论。 这允许 automount 守护程序不会陷入挂载陷阱。
这里有一个微妙之处。 可能在第一个 autofs 文件系统下挂载第二个 autofs 文件系统,并且它们都由同一个守护程序管理。 为了使守护程序能够在第二个文件系统上挂载某些内容,它必须能够“向下走过”第一个文件系统。 这意味着 d_manage 不能总是为 automount 守护程序返回 -EISDIR。 只有在已请求挂载但尚未完成时,它才必须返回此值。
如果 dentry 不应该是挂载陷阱,则 d_manage 也会返回 -EISDIR,无论是由于它是符号链接还是由于它不为空。
任何其他负值都将被视为错误并返回给调用者。
autofs 可以返回
第二个用例仅在“RCU-walk”期间发生,因此将设置 rcu_walk。
RCU-walk 是一个快速而轻量级的过程,用于向下走文件名路径(即它就像踮着脚走路一样)。 RCU-walk 无法应对所有情况,因此当它遇到困难时,它会回退到“REF-walk”,后者速度较慢但更可靠。
RCU-walk 永远不会调用 ->d_automount;文件系统必须已经挂载,否则 RCU-walk 无法处理该路径。 为了确定挂载陷阱是否对于 RCU-walk 模式是安全的,它会调用设置为 true 的 ->d_manage() 与 rcu_walk。
在这种情况下,d_manage() 必须避免阻塞,并且应尽可能避免获取自旋锁。 它唯一目的是确定是否可以安全地向下进入任何已挂载的目录,并且它可能不安全的原因是挂载的到期正在进行中。
在 rcu_walk 情况下,d_manage() 无法返回 -EISDIR 来告诉 VFS 这是一个不需要 d_automount 的目录。 如果 rcu_walk 看到一个设置了 DCACHE_NEED_AUTOMOUNT 但没有任何内容挂载的 dentry,它将回退到 REF-walk。 d_manage() 无法使 VFS 保持在 RCU-walk 模式,但只能通过返回 -ECHILD 来告诉它退出 RCU-walk 模式。
因此,当设置了 rcu_walk 时,d_manage() 应在有任何理由认为进入已挂载的文件系统不安全时返回 -ECHILD,否则应返回 0。
如果已启动或正在考虑文件系统的到期,则 autofs 将返回 -ECHILD,否则它将返回 0。
挂载点到期
VFS 具有自动使未使用的挂载过期的机制,就像它可以使 dcache 中任何未使用的 dentry 信息过期一样。 这受 MNT_SHRINKABLE 标志的指导。 这仅适用于通过 d_automount() 返回要挂载的文件系统创建的挂载。 由于 autofs 不返回这样的文件系统,而是将挂载留给 automount 守护程序,因此它也必须让 automount 守护程序参与卸载。 这也意味着 autofs 对到期有更多的控制权。
VFS 还支持使用 MNT_EXPIRE 标志对 umount 系统调用“到期”挂载。 除非之前已经尝试过,并且自上次尝试以来文件系统一直处于非活动状态且未被触及,否则使用 MNT_EXPIRE 卸载将失败。 autofs 不依赖于此,而是拥有自己的内部跟踪文件系统最近是否使用过。 这允许 autofs 目录中的各个名称单独过期。
对于协议的第 4 版,automount 守护程序可以尝试卸载挂载在 autofs 文件系统上的任何文件系统,或随时删除任何符号链接或空目录。 如果卸载或删除成功,文件系统将返回到挂载或创建之前的状态,因此对名称的任何访问都将触发正常的自动挂载处理。 特别是,rmdir 和 unlink 不会在 dcache 中留下负条目,就像普通文件系统一样,因此尝试访问最近删除的对象会传递给 autofs 进行处理。
对于第 5 版,除了从顶层目录卸载外,这不安全。 由于较低级别的目录永远不是挂载陷阱,因此一旦文件系统卸载,其他进程将看到一个空目录。 因此,通常最安全的方法是使用下面描述的 autofs 到期协议。
通常,守护程序只想删除一段时间未使用的条目。 为此,autofs 在每个目录或符号链接上维护一个“last_used”时间戳。 对于符号链接,它确实会记录上次“使用”或遵循符号链接以查找它指向的位置的时间。 对于目录,该字段的使用方式略有不同。 该字段在挂载时以及在到期检查期间(如果发现它正在使用中(即打开文件描述符或进程工作目录)以及在路径遍历期间)进行更新。 在路径遍历期间完成的更新可防止频繁到期和频繁访问的自动挂载的立即挂载。 但是,如果 GUI 不断访问或应用程序频繁扫描 autofs 目录树,则可能会累积实际未使用的挂载。 为了满足这种情况,可以使用“strictexpire”autofs 挂载选项来避免路径遍历时“last_used”更新,从而防止这种明显无法使实际未使用的挂载过期的情况。
守护程序可以使用 ioctl 询问 autofs 是否有任何内容即将到期,如稍后所述。 对于直接挂载,autofs 会考虑是否可以卸载整个挂载树。 对于间接挂载,autofs 会考虑顶层目录中的每个名称,以确定是否可以卸载和清理其中的任何一个。
间接挂载有一个选项,可以考虑已挂载的每个叶子,而不是考虑顶层名称。 这最初是为了与 autofs 的第 4 版兼容,应被视为 Sun Format 自动挂载映射的已弃用选项。 但是,它可以再次用于 amd 格式的挂载映射(通常是间接映射),因为 amd 自动挂载器允许为单个挂载设置到期超时。 但是,在进行所需的更改方面存在一些困难。
当 autofs 考虑一个目录时,它会检查 last_used 时间,并将其与挂载文件系统时设置的“超时”值进行比较,尽管在某些情况下会忽略此检查。 它还会检查目录或其下方的任何内容是否正在使用中。 对于符号链接,仅考虑 last_used 时间。
如果两者都似乎支持使目录或符号链接过期,则会采取措施。
有两种方法可以要求 autofs 考虑到期。 第一种是使用 AUTOFS_IOC_EXPIRE ioctl。 这仅适用于间接挂载。 如果它在根目录中找到要过期的内容,它将返回该内容的名称。 一旦返回一个名称,automount 守护程序就需要正常卸载挂载在该名称下的任何文件系统。 如上所述,这对于版本 5 autofs 中的非顶层挂载是不安全的。 因此,当前的 automount(8) 不使用此 ioctl。
第二种机制使用 AUTOFS_DEV_IOCTL_EXPIRE_CMD 或 AUTOFS_IOC_EXPIRE_MULTI ioctl。 这适用于直接挂载和间接挂载。 如果它选择要过期的对象,它将使用下面描述的通知机制通知守护程序。 这将阻塞,直到守护程序确认到期通知。 这意味着“EXPIRE”ioctl 必须从与处理通知的线程不同的线程发送。
在 ioctl 阻塞时,该条目被标记为“过期”,并且 d_manage 将阻塞,直到守护程序确认卸载已完成(以及删除任何可能必要的目录),或者已中止。
与 autofs 通信:检测守护程序
automount 守护程序和文件系统之间存在几种形式的通信。 正如我们已经看到的,守护程序可以使用普通文件系统操作创建和删除目录和符号链接。 autofs 根据进程的进程组 ID 号(参见 getpgid(1))来知道请求某些操作的进程是否为守护程序。
除非给出“pgrp=”选项,否则挂载 autofs 文件系统时会记录挂载进程的 pgid,在这种情况下会记录该数字。 来自该进程组中的进程的任何请求都被认为来自守护程序。 如果守护程序必须停止并重新启动,则可以通过 ioctl 提供新的 pgid,如下所述。
与 autofs 通信:事件管道
挂载 autofs 文件系统时,必须使用“fd=”挂载选项传递管道的“写入”端。 autofs 会将通知消息写入此管道,供守护程序响应。 对于版本 5,消息的格式为
struct autofs_v5_packet {
struct autofs_packet_hdr hdr;
autofs_wqt_t wait_queue_token;
__u32 dev;
__u64 ino;
__u32 uid;
__u32 gid;
__u32 pid;
__u32 tgid;
__u32 len;
char name[NAME_MAX+1];
};
标头的格式为
struct autofs_packet_hdr {
int proto_version; /* Protocol version */
int type; /* Type of packet */
};
其中类型是
autofs_ptype_missing_indirect
autofs_ptype_expire_indirect
autofs_ptype_missing_direct
autofs_ptype_expire_direct
因此,消息可以指示名称缺失(有人尝试访问它但它不存在)或已被选中用于到期。
管道将被设置为“数据包模式”(等效于传递 O_DIRECT)到 _pipe2(2)_,以便从管道读取最多返回一个数据包,并且将丢弃数据包的任何未读取部分。
wait_queue_token 是一个唯一数字,可以标识要确认的特定请求。 通过管道发送消息时,受影响的 dentry 将被标记为“活动”或“过期”,并且对它的其他访问将阻塞,直到使用具有相关 wait_queue_token 的以下 ioctl 之一确认消息。
与 autofs 通信:根目录 ioctl
autofs 文件系统的根目录将响应多个 ioctl。 发出 ioctl 的进程必须具有 CAP_SYS_ADMIN 功能,或者必须是 automount 守护程序。
可用的 ioctl 命令是
- AUTOFS_IOC_READY:
已处理通知。 ioctl 命令的参数是与正在确认的通知相对应的“wait_queue_token”数字。
- AUTOFS_IOC_FAIL:
类似于上面,但指示错误代码 ENOENT 失败。
- AUTOFS_IOC_CATATONIC:
导致 autofs 进入“紧张”模式,这意味着它停止向守护程序发送通知。 如果写入管道失败,也会进入此模式。
- AUTOFS_IOC_PROTOVER:
这将返回正在使用的协议版本。
- AUTOFS_IOC_PROTOSUBVER:
返回协议子版本,它实际上是实现的版本号。
- AUTOFS_IOC_SETTIMEOUT:
这会传递指向一个无符号长的指针。 该值用于设置到期超时,并且当前超时值将通过指针存储回去。
- AUTOFS_IOC_ASKUMOUNT:
如果在指向的 int 中,文件系统可以被卸载,则返回 1。 这只是一个提示,因为情况可能随时发生变化。 此调用可用于避免更昂贵的完整卸载尝试。
- AUTOFS_IOC_EXPIRE:
如上所述,这会询问是否有什么适合到期。 需要指向数据包的指针
struct autofs_packet_expire_multi {
struct autofs_packet_hdr hdr;
autofs_wqt_t wait_queue_token;
int len;
char name[NAME_MAX+1];
};
用可以卸载或删除的对象的名称填充它。 如果没有任何内容可以过期,则 errno 设置为 EAGAIN。 即使结构中存在 wait_queue_token,也不会建立“等待队列”,也不需要确认。
- AUTOFS_IOC_EXPIRE_MULTI:
这类似于 AUTOFS_IOC_EXPIRE,只是它导致通知被发送到守护程序,并且它会阻塞,直到守护程序确认。 该参数是一个整数,可以包含两个不同的标志。
AUTOFS_EXP_IMMEDIATE 导致忽略 last_used 时间,并且如果对象未使用,则会使其过期。
AUTOFS_EXP_FORCED 导致忽略正在使用状态,并且即使对象正在使用中,也会使其过期。 这假定守护程序已请求这样做,因为它能够执行卸载。
AUTOFS_EXP_LEAVES 将选择一个叶子而不是顶层名称使其过期。 只有当 maxproto 为 4 时,这才是安全的。
与 autofs 通信:字符设备 ioctl
并非总是可以打开 autofs 文件系统的根目录,特别是直接挂载的文件系统。 如果 automount 守护程序重新启动,则无法使用上述任何通信渠道重新获得对现有挂载的控制权。 为了解决这一需求,有一个“杂项”字符设备(主设备号 10,次设备号 235),可用于直接与 autofs 文件系统通信。 它需要 CAP_SYS_ADMIN 才能访问。
可以在此设备上使用的“ioctl”在单独的文档 autofs 内核模块的杂项设备控制操作 中进行了描述,并在此处进行了简要总结。 每个 ioctl 都传递一个指向 autofs_dev_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[];
};
对于 OPEN_MOUNT 和 IS_MOUNTPOINT 命令,目标文件系统由 path 标识。 所有其他命令都通过 ioctlfd 标识文件系统,ioctlfd 是在根目录上打开的文件描述符,可以通过 OPEN_MOUNT 返回。
ver_major 和 ver_minor 是输入/输出参数,用于检查是否支持请求的版本,并报告内核模块可以支持的最大版本。
命令是
- AUTOFS_DEV_IOCTL_VERSION_CMD:
除了验证和设置版本号之外,什么也不做。
- AUTOFS_DEV_IOCTL_OPENMOUNT_CMD:
返回 autofs 文件系统根目录上的打开的文件描述符。 文件系统按名称和设备号标识,该名称和设备号存储在 openmount.devid 中。 现有文件系统的设备号可以在 /proc/self/mountinfo 中找到。
- AUTOFS_DEV_IOCTL_CLOSEMOUNT_CMD:
与 close(ioctlfd) 相同。
- AUTOFS_DEV_IOCTL_SETPIPEFD_CMD:
如果文件系统处于紧张模式,这可以在 setpipefd.pipefd 中提供新管道的写入端,以重新建立与守护程序的通信。 调用进程的进程组用于标识守护程序。
- AUTOFS_DEV_IOCTL_REQUESTER_CMD:
path 应该是文件系统中的一个名称,该文件系统已自动挂载。 成功返回后,requester.uid 和 requester.gid 将是触发该挂载的进程的 UID 和 GID。
- AUTOFS_DEV_IOCTL_ISMOUNTPOINT_CMD:
检查路径是否为特定类型的挂载点 - 有关详细信息,请参见单独的文档。
AUTOFS_DEV_IOCTL_PROTOVER_CMD
AUTOFS_DEV_IOCTL_PROTOSUBVER_CMD
AUTOFS_DEV_IOCTL_READY_CMD
AUTOFS_DEV_IOCTL_FAIL_CMD
AUTOFS_DEV_IOCTL_CATATONIC_CMD
AUTOFS_DEV_IOCTL_TIMEOUT_CMD
AUTOFS_DEV_IOCTL_EXPIRE_CMD
AUTOFS_DEV_IOCTL_ASKUMOUNT_CMD
这些都具有与类似命名的 AUTOFS_IOC ioctl 相同的功能,只是 FAIL 可以在 fail.status 中给出显式错误号,而不是假设 ENOENT,并且此 EXPIRE 命令对应于 AUTOFS_IOC_EXPIRE_MULTI。
紧张模式
如前所述,autofs 挂载可以进入“紧张”模式。 如果写入通知管道失败,或者如果通过 ioctl 显式请求,则会发生这种情况。
进入紧张模式时,管道会关闭,并且任何挂起的通知都会用错误 ENOENT 确认。
一旦进入紧张模式,尝试访问不存在的名称将导致 ENOENT,而尝试访问现有目录的处理方式与它们来自守护程序的方式相同,因此挂载陷阱不会触发。
挂载文件系统时,可以给出 _uid_ 和 _gid_,它们设置目录和符号链接的所有权。 当文件系统处于紧张模式时,任何具有匹配 UID 的进程都可以在根目录中创建目录或符号链接,但不能在其他目录中创建。
只能通过 /dev/autofs 上的 AUTOFS_DEV_IOCTL_OPENMOUNT_CMD ioctl 离开紧张模式。
“忽略”挂载选项
可以使用“忽略”挂载选项向应用程序提供一个通用指示器,表明在显示挂载信息时应忽略挂载条目。
在提供 autofs 并且基于内核挂载列表向用户空间提供挂载列表的其他操作系统中,允许使用空操作挂载选项(“忽略”是大多数常见操作系统上使用的选项),以便 autofs 文件系统用户可以选择使用它。
这旨在供用户空间程序使用,以便在读取挂载列表时排除 autofs 挂载的考虑。
autofs、名称空间和共享挂载
使用绑定挂载和名称空间,autofs 文件系统可能会出现在一个或多个文件系统名称空间中的多个位置。 为了使此操作合理地工作,应始终“共享”挂载 autofs 文件系统。 例如
mount --make-shared /autofs/mount/point
automount 守护程序只能管理 autofs 文件系统的单个挂载位置,并且如果该挂载不是“共享”的,则其他位置的行为将不如预期。 特别是,访问那些其他位置可能会导致 ELOOP 错误
Too many levels of symbolic links