autofs - 工作原理
目的
autofs 的目标是提供按需挂载和无竞争自动卸载各种其他文件系统。这提供了两个关键优势
无需延迟启动,直到所有可能需要的文件系统都被挂载。尝试访问这些慢速文件系统的进程可能会被延迟,但其他进程可以自由继续。这对于网络文件系统(例如 NFS)或存储在带有媒体更换机器人的介质上的文件系统尤为重要。
文件系统的名称和位置可以存储在远程数据库中,并且可以随时更改。访问时数据库中的内容将用于提供访问目标。文件系统中名称的解释甚至可以是程序化的,而不是数据库支持的,例如允许使用通配符,并且可以根据首次访问名称的用户而变化。
上下文
“autofs”文件系统模块只是 autofs 系统的一部分。还需要一个用户空间程序来查找名称和挂载文件系统。这通常是“automount”程序,尽管包括“systemd”在内的其他工具也可以使用“autofs”。本文档仅描述内核模块以及与任何用户空间程序所需的交互。后续文本将其称为“自动挂载守护进程”或简称为“守护进程”。
“autofs”是一个 Linux 内核模块,它提供“autofs”文件系统类型。可以挂载多个“autofs”文件系统,它们可以单独管理,也可以由同一个守护进程管理。
内容
autofs 文件系统可以包含 3 种类型的对象:目录、符号链接和挂载陷阱。挂载陷阱是具有额外属性的目录,如下一节所述。
对象只能由自动挂载守护进程创建:符号链接使用常规的symlink系统调用创建,而目录和挂载陷阱使用mkdir创建。目录是否应该为挂载陷阱的确定基于主映射。autofs 查询此主映射以确定哪些目录是挂载点。挂载点可以是直接/间接/偏移。在大多数系统中,默认主映射位于 /etc/auto.master。
如果未提供 direct 或 offset 挂载选项(因此挂载被认为是间接的),则根目录始终是常规目录,否则当它为空时是挂载陷阱,当它不为空时是常规目录。请注意,direct 和 offset 的处理方式相同,因此简洁的总结是,仅当文件系统是直接挂载且根目录为空时,根目录才是挂载陷阱。
在根目录中创建的目录仅当文件系统是间接挂载且它们为空时才是挂载陷阱。
树中更深层的目录取决于 maxproto 挂载选项,特别是它是否小于 5。当 maxproto 为 5 时,树中更深层的目录永远不是挂载陷阱,它们始终是常规目录。当 maxproto 为 4(或 3)时,这些目录只有在它们为空时才是挂载陷阱。
因此:非空(即非叶子)目录永远不是挂载陷阱。空目录有时是挂载陷阱,有时不是,具体取决于它们在树中的位置(根、顶层或较低层)、maxproto 以及挂载是否是间接的。
挂载陷阱
autofs 实现的核心要素是 Linux VFS 提供的挂载陷阱。文件系统提供的任何目录都可以指定为陷阱。这涉及两个独立的功能,它们协同工作以允许 autofs 执行其工作。
DCACHE_NEED_AUTOMOUNT
如果一个目录项设置了 DCACHE_NEED_AUTOMOUNT 标志(如果 inode 设置了 S_AUTOMOUNT,或者可以直接设置,则会设置该标志),那么它(可能)是一个挂载陷阱。除了“stat”之外,对该目录的任何访问都将(通常)导致调用 d_op->d_automount() 目录项操作。此方法的任务是找到应挂载到该目录上的文件系统并返回它。VFS 负责实际将此文件系统的根挂载到该目录上。
autofs 本身不查找文件系统,而是向自动挂载守护进程发送消息,要求它查找并挂载文件系统。然后,autofs d_automount 方法等待守护进程报告一切准备就绪。然后,它将返回“NULL”,表明挂载已经发生。VFS 不尝试挂载任何内容,而是向下跟随已有的挂载。
此功能足以满足某些挂载陷阱的用户,例如 NFS,它创建陷阱,以便服务器上的挂载点可以反映在客户端上。但是,对于 autofs 来说,这还不够。由于在目录上挂载被认为是“超越stat”,因此自动挂载守护进程将无法在“陷阱”目录上挂载文件系统,而没有任何避免被陷阱捕获的方法。为此,还有另一个标志。
DCACHE_MANAGE_TRANSIT
如果目录项设置了 DCACHE_MANAGE_TRANSIT,则会调用两种非常不同但相关的行为,这两种行为都使用 d_op->d_manage() 目录项操作。
首先,在检查目录上是否挂载了任何文件系统之前,将调用 d_manage(),并将 rcu_walk 参数设置为 false。它可能会返回以下三种结果之一
返回值 0 表示此目录项没有任何特殊之处,应继续执行正常的挂载和自动挂载检查。
autofs 通常返回 0,但首先会等待任何过期(自动卸载已挂载的文件系统)完成。这样可以避免竞争。
返回值 -EISDIR 告诉 VFS 忽略目录上的任何挂载,并且不考虑调用 ->d_automount()。这实际上禁用了 DCACHE_NEED_AUTOMOUNT 标志,导致该目录不再是挂载陷阱。
如果 autofs 检测到执行查找的进程是自动挂载守护进程,并且已请求挂载但尚未完成,则 autofs 会返回此值。稍后将讨论它是如何确定这一点的。这允许自动挂载守护进程不会被挂载陷阱捕获。
这里有一个微妙之处。有可能在第一个 autofs 文件系统下方挂载第二个 autofs 文件系统,并且它们都由同一个守护进程管理。为了使守护进程能够在第二个文件系统上挂载某些东西,它必须能够“走过”第一个文件系统。这意味着 d_manage 不能总是为自动挂载守护进程返回 -EISDIR。它必须仅在已请求挂载但尚未完成时才返回它。
如果目录项不应是挂载陷阱,则 d_manage 也返回 -EISDIR,因为它是一个符号链接或因为它不是空的。
任何其他负值都被视为错误并返回给调用方。
autofs 可以返回
第二种用例仅在“RCU 遍历”期间发生,因此会设置 rcu_walk。
RCU 遍历是一种快速轻量级的方法,用于遍历文件名路径(即,它类似于踮着脚尖行走)。RCU 遍历无法处理所有情况,因此当遇到困难时,它会回退到“REF 遍历”,后者速度较慢但更稳健。
RCU 遍历永远不会调用 ->d_automount;文件系统必须已经挂载,否则 RCU 遍历无法处理该路径。为了确定挂载陷阱是否适合 RCU 遍历模式,它会调用 ->d_manage(),并将 rcu_walk 设置为 true。
在这种情况下,d_manage() 必须避免阻塞,并且应尽可能避免使用自旋锁。其唯一目的是确定是否可以安全地进入任何已挂载的目录,并且可能不安全的唯一原因是正在进行挂载过期。
在 rcu_walk 情况下,d_manage() 不能返回 -EISDIR 来告诉 VFS 这是一个不需要 d_automount 的目录。如果 rcu_walk 看到一个设置了 DCACHE_NEED_AUTOMOUNT 但没有任何挂载的目录项,它将回退到 REF 遍历。d_manage() 无法使 VFS 保持在 RCU 遍历模式,但只能通过返回 -ECHILD 来告诉它退出 RCU 遍历模式。
因此,当调用 d_manage() 并设置 rcu_walk 时,如果任何理由认为进入已挂载的文件系统是不安全的,则应返回 -ECHILD,否则应返回 0。
如果已启动或正在考虑文件系统过期,则 autofs 将返回 -ECHILD,否则返回 0。
挂载点过期
VFS 具有自动使未使用的挂载过期的机制,就像它可以使 dcache 中任何未使用的目录项信息过期一样。这由 MNT_SHRINKABLE 标志引导。这仅适用于通过 d_automount() 返回要挂载的文件系统而创建的挂载。由于 autofs 不返回此类文件系统,而是将挂载留给自动挂载守护进程,因此它也必须让自动挂载守护进程参与卸载。这也意味着 autofs 对过期有更多的控制权。
VFS 还支持使用 `umount` 系统调用的 MNT_EXPIRE 标志来“过期”挂载。使用 MNT_EXPIRE 进行卸载将会失败,除非之前进行过尝试,并且自上次尝试以来文件系统一直处于非活动状态且未被触及。autofs 不依赖于此,但它有自己的内部跟踪机制来记录文件系统最近是否被使用过。这允许 autofs 目录中的各个名称单独过期。
在协议的第 4 版中,自动挂载守护程序可以尝试卸载任何挂载在 autofs 文件系统上的文件系统,或者随时删除任何符号链接或空目录。如果卸载或删除成功,文件系统将恢复到挂载或创建之前的状态,以便任何对名称的访问都将触发正常的自动挂载处理。特别是,`rmdir` 和 `unlink` 不会在 dcache 中留下负条目,就像普通文件系统那样,因此尝试访问最近删除的对象会被传递给 autofs 进行处理。
在第 5 版中,除了从顶层目录卸载外,这并不安全。由于较低级别的目录永远不是挂载陷阱,其他进程会在文件系统卸载后立即看到一个空目录。因此,通常最安全的方法是使用下面描述的 autofs 过期协议。
通常,守护程序只想删除一段时间未使用的条目。为此,autofs 在每个目录或符号链接上维护一个 “`last_used`” 时间戳。对于符号链接,它确实记录了上次“使用”或跟随符号链接以查找其指向位置的时间。对于目录,该字段的使用方式略有不同。该字段在挂载时更新,并且在过期检查期间如果发现正在使用(即,打开的文件描述符或进程工作目录)以及在路径遍历期间也会更新。在路径遍历期间进行的更新可以防止频繁过期和频繁挂载经常访问的自动挂载点。但是,如果 GUI 不断访问或应用程序频繁扫描 autofs 目录树,则可能会累积实际上未使用的挂载。为了解决这种情况,可以使用 “`strictexpire`” autofs 挂载选项来避免在路径遍历时更新 “`last_used`”,从而防止这种表面上无法使实际上未使用的挂载过期的情况。
守护程序可以使用稍后讨论的 `ioctl` 来询问 autofs 是否有任何条目需要过期。对于*直接*挂载,autofs 会考虑是否可以卸载整个挂载树。对于*间接*挂载,autofs 会考虑顶层目录中的每个名称,以确定是否可以卸载和清理其中的任何一个。
对于间接挂载,可以选择考虑已挂载的每个叶子节点,而不是考虑顶层名称。这最初是为了与 autofs 的第 4 版兼容,对于 Sun 格式的自动挂载映射,应视为已弃用。但是,它可以再次用于 amd 格式的挂载映射(通常是间接映射),因为 amd 自动挂载器允许为单个挂载设置过期超时。但是,要进行所需的更改存在一些困难。
当 autofs 考虑一个目录时,它会检查 `last_used` 时间,并将其与挂载文件系统时设置的 “timeout” 值进行比较,尽管在某些情况下会忽略此检查。它还会检查该目录或其下的任何内容是否正在使用。对于符号链接,只会考虑 `last_used` 时间。
如果两者都似乎支持使目录或符号链接过期,则会采取相应的操作。
有两种方法可以要求 autofs 考虑过期。第一种是使用 **AUTOFS_IOC_EXPIRE** ioctl。这仅适用于间接挂载。如果它发现根目录中有要过期的内容,它将返回该内容的名称。一旦返回了一个名称,自动挂载守护程序就需要正常卸载该名称下挂载的任何文件系统。如上所述,这对于版本 5 autofs 中的非顶层挂载是不安全的。因此,当前的 `automount(8)` 不使用此 ioctl。
第二种机制使用 **AUTOFS_DEV_IOCTL_EXPIRE_CMD** 或 **AUTOFS_IOC_EXPIRE_MULTI** ioctl。这适用于直接和间接挂载。如果它选择要过期的对象,它将使用下面描述的通知机制通知守护程序。这将阻塞,直到守护程序确认过期通知。这意味着 “`EXPIRE`” ioctl 必须从与处理通知的线程不同的线程发送。
当 ioctl 阻塞时,该条目被标记为 “expiring”,并且 `d_manage` 将阻塞,直到守护程序确认卸载已完成(以及删除任何可能必要的目录)或已中止。
与 autofs 通信:检测守护程序
自动挂载守护程序和文件系统之间存在多种形式的通信。正如我们已经看到的,守护程序可以使用普通的文件系统操作创建和删除目录和符号链接。autofs 基于其进程组 ID 号(请参阅 getpgid(1))来了解请求某些操作的进程是否是守护程序。
挂载 autofs 文件系统时,除非给出 “pgrp=” 选项,否则将记录挂载进程的 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 将被标记为 “active” 或 “expiring”,并且对它的其他访问将阻塞,直到使用以下带有相关 `wait_queue_token` 的 ioctl 之一确认消息。
与 autofs 通信:根目录 ioctl
autofs 文件系统的根目录将响应多个 ioctl。发出 ioctl 的进程必须具有 CAP_SYS_ADMIN 功能,或者必须是自动挂载守护程序。
可用的 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 文件系统的根目录,特别是*直接*挂载的文件系统。如果自动挂载守护程序重新启动,则它无法使用上述任何通信通道重新获得对现有挂载的控制。为了解决此需求,有一个“杂项”字符设备(主设备号 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
自动挂载守护进程只能管理 autofs 文件系统的单个挂载位置,如果该位置上的挂载不是“共享”的,则其他位置的行为将不符合预期。特别是,访问这些其他位置很可能会导致 ELOOP 错误
Too many levels of symbolic links