Linux fd 系列| “匿名句柄” 是一切皆文件背后功臣
匿名 fd 的样子?
我们经常在 /proc/${pid}/fd/
下面能看到 anon_inode :
前缀的句柄,如下:
root@ubuntu:~/temp# ll /proc/5398/fd
lr-x- 1 x x 64 Aug 4 9:9 8 -> anon_inode:inotify
lrwx- 1 x x 64 Aug 4 9:9 4 -> anon_inode:[eventpoll]
lrwx- 1 x x 64 Aug 4 9:9 5 -> anon_inode:[signalfd]
lrwx- 1 x x 64 Aug 4 9:9 7 -> anon_inode:[timerfd]
lrwx- 1 x x 64 Aug 4 9:9 9 -> anon_inode:[eventpoll]
如果是正常的文件句柄,一般显式的是一个路径:
root@ubuntu:~/temp# ll /proc/5398/fd
lr-x-- 1 x x 64 Aug 24 09:39 10 -> /proc/5398/mountinfo
lr-x-- 1 x x 64 Aug 24 09:39 12 -> /proc/swaps
当然 path 只是一个浅层次的感官,因为对于 socket 句柄来说也不算有人为理解上直观的 path ,但是它有完整的 inode,所以这个匿名其实匿的是 inode 。
匿名 inode 的诞生?
重点提一下匿名 fd 的事情,为什么会有匿名 fd ? 什么是匿名?
在 Linux 里一切皆文件,你理解的常见“文件”有什么特性?是路径,也就是 path ,匿名的意思说的就是没有路径。匿名 fd 其实说的是匿名 inode 。
在 Linux 的文件体系中,一个文件句柄,对应一个 file 结构体,关联一个 inode 。file/dentry/inode
这三驾马车是一定要配齐的,就算是匿名的(无 path,无效 dentry ),对于 file 结构体来说,一定要绑定 inode 和 dentry ,哪怕是伪造的、不完整的 inode。
anon_inodefs 就应运而生了,内核就帮你搞出来一个公共的 inode ,这就节省了所有有这样需求的内核模块,避免了内存的浪费,省了冗余重复的 inode 初始化代码。
匿名 fd 背后的是一个叫做 anon_inodefs 的内核文件系统( 位于 fs/anon_inodes.c
),这个文件系统极其简单,整个文件系统只有一个 inode ,这个 inode 是文件系统初始化的时候创建好的。之后,所有需要一个匿名 inode 的句柄都直接跟这个 inode 关联即可。
原理剖析
上面提到了,匿名 inode 是一个公共需求,我们不需要一个完整功能的 inode,而只是需要一个 inode 而已,绑定到到 dentry ,file 等结构体。
anon_inodes.c 用来创建一个绑定匿名 inode 的 file 结构体。
整个 anon_inodefs 就只有一个文件,操作系统初始化的时候会调用初始化函数 fs_initcall(anon_inode_init) ,其中 anon_inode_init 只做两件事:
创建出一个 vfsmount 实例,创建出来之后赋值给全局变量 anon_inode_mnt ; 创建出一个 inode 实例,创建出来之后赋值给全局变量 anon_inode_inode ;
这两个变量就是 anon_inodefs 这个文件系统的全部家当了。
anon_inodefs 只提供了 2 个实用函数,一个获取到一个绑定匿名 inode 的 file 实例,另一个更多一些封装,返回的是 fd 句柄。如下:
anon_inode_getfile
这个函数非常简单,只做两件事:
获取一个 inode ( 获取全局的 inode 变量 anon_inode_inode ,当然也可以通过一个参数控制来创建新的 inode ); 创建一个 file 结构体实例,并且把这个 inode 关联起来;
anon_inode_getfd
这个函数非常简单,只做两件事情:
创建一个新的 fd 句柄,返回的是一个非负整数; 创建一个 file 实例( 调用的是 anon_inode_getfile 来获取 ),然后把这个 fd 和 file 关联起来;
这两个函数就是 anon_inodefs 提供的两个对外的函数接口。获取到一个 file 实例,这个实例绑定到 anon_inodefs 公共的 inode 实例。
关于 anon_inodefs 的功能,其实在函数的注释中也提到了,太直白了,如下:
// anon_inode_getfile 和 anon_inode_getfd 的注释明确提到了 anon_inodefs 的两个目的:
// - 节省内存
// - 封装公共的冗余代码
* Creates a new file by hooking it on a single inode. This is
* useful for files that do not need to have a full-fledged inode in
* order to operate correctly. All the files created with
* anon_inode_getfd() will use the same singleton inode, reducing
* memory use and avoiding code duplication for the file/inode/dentry
* setup. Returns a newly created file descriptor or an error code.
为什么常见的匿名 fd 都有以 "anon_inode:" 这样开头?
其实这种看得到的字符串都是 path ,这个是和 dentry 对应起来的,对于这种匿名 inode 的 dentry ,有着统一的名字:
// dentry 的操作表
static const struct dentry_operations anon_inodefs_dentry_operations = {
.d_dname = anon_inodefs_dname,
};
// 操作表 .d_dname 方法的定制实现
static char *anon_inodefs_dname(struct dentry *dentry, char *buffer, int buflen)
{
return dynamic_dname(dentry, buffer, buflen, "anon_inode:%s", dentry->d_name.name);
}
那 dentry->d_name.name 又是怎么赋值的呢?来看一眼完整的调用栈,以 epoll fd 来举个例子:
epoll_create 函数入口
// epoll_create 函数入口 ( fs/eventpoll.c )
static int do_epoll_create(int flags)
{
// 创建匿名句柄 ...
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
}
创建一个匿名句柄
// 创建一个匿名句柄( fs/anon_inodes.c )
static struct file *__anon_inode_getfile(const char *name, const struct file_operations *fops, void *priv, int flags, const struct inode *context_inode, bool secure)
{
// name 被赋值了 "[eventpoll]"
file = alloc_file_pseudo(inode, anon_inode_mnt, name, flags & (O_ACCMODE | O_NONBLOCK), fops);
}
创建出一个伪 file 实例
// 创建出一个伪 file 实例
struct file *alloc_file_pseudo(struct inode *inode, struct vfsmount *mnt, const char *name, int flags, const struct file_operations *fops)
{
// 初始化字符串 "[eventpoll]"
struct qstr this = QSTR_INIT(name, strlen(name));
path.dentry = d_alloc_pseudo(mnt->mnt_sb, &this);
}
创建一个伪 dentry 实例
// 创建一个伪 dentry 实例
struct dentry *d_alloc_pseudo(struct super_block *sb, const struct qstr *name)
{
struct dentry *dentry = __d_alloc(sb, name);
}
创建并初始化 dentry 实例
// 创建并初始化 dentry 实例
static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
{
// 最后:把 name 赋值给 dentry->d_name.name,也就是 "[eventpoll]"
memcpy(dname, name->name, name->len);
smp_store_release(&dentry->d_name.name, dname); /* ^^^ */
}
所以,epoll fd 的名字组合起来就是 "anon_inode:[eventpoll]" 喽。
问题来了,那这个一般用在哪些地方呢?
其实就是个人性化的名字而已,最常见的就是在 proc 文件系统中。
我们在 proc 文件系统中,ls 的时候,其实就像想看名字,这个名字其实就是 path ,就会出发调用到哪步的 d_path 函数,这个函数就是把 dentry 转换成人类可读的字符串 path 的名字。
char *d_path(const struct path *path, char *buf, int buflen)
{
if (path->dentry->d_op && path->dentry->d_op->d_dname && (!IS_ROOT(path->dentry) || path->dentry != path->mnt->mnt_root))
// 返回 dentry 定制的名称;
return path->dentry->d_op->d_dname(path->dentry, buf, buflen);
}
在 Linux 中是一个倒挂树的设计,从根目录( / )开始,叶子结点为文件或者目录,从根节点到叶子结点这一段就称为 path 路径,在内存里面这颗倒挂的树就体现为 dentry 树,节点就是 dentry 结构体。
这里就有个重要的知识点:
划重点:一个 inode 上可以挂多个 dentry ,一个 dentry 只能属于一个 inode 。
还记得软链接和硬链接吗?
软链接就是创建了一个新的文件,链接文件里就是路径。inode,dentry 都创建了一个新的。
硬链接则没有创建新的 inode,而是只在目录文件中创建了一个 dirent ,在目录树中添加了一个 dentry 。硬链接的场景就是一个 inode 对应了多个 dentry 节点。
换句话说,一个 inode 可以出现在目录树的多个位置。
每个文件或者目录都会在这棵树上有自己的位置,内存用 struct path 结构体来表示唯一的位置。
struct path {
struct vfsmount *mnt; // 标识在哪个具体的文件系统实例
struct dentry *dentry; // 内存目录树节点
};
这里顺便再说另一个重要知识点:为什么内核之中,需要用 struct path 这个复合结构体来标识唯一的一个目录树位置呢?
文件系统的挂载最关键的就是把一个文件系统的实例和目录树上的一个 dentry 关联起来,而一个 dentry 可以关联多个文件系统实例。
换句话说:对于一个目录树路径其实是可以挂载多个文件系统实例。比如 /mnt/path 这么一个路径,其实是可以挂载多个文件系统的,不会报错,后面的挂载直接覆盖前面的。
为了知识的完善,这里补充一个知识点。其实关于匿名 inode 还有一种方式,这种方式以 alloc_anon_inode 函数提供,该函数传入一个超级块作为参数用于创建一个匿名 inode 。这个函数创建一个新的内存 inode 实例,这个 inode 不具备完备的功能,也是用来做匿名之用。
struct inode *alloc_anon_inode(struct super_block *s)
{
// ...
// 根据这个 superblock 实例来创建一个伪 inode
struct inode *inode = new_inode_pseudo(s);
// 初始化这个 inode 实例
// ...
return inode;
}
这种匿名 inode 就不是 anon_inodefs 的那个了,而是具体文件系统实例上的匿名 inode 。
随便列举一些 eventfd,eventpoll,timerfd,signalfd,inotifyfd,io_uring fd 等等,还有很多,但比较偏僻了,就不再举例了。童鞋们惊讶吗?
总结
anon_inodefs 是为了公共需求抽离出来的一个内核文件系统,只有一个 inode ,为了节省内存,抽象重复代码之用; 匿名句柄是因为 fd 对应的 file 实例背靠着的是匿名 inode ,anon_inodefs 提供了两个功能函数,都是用来获取匿名 fd 的; inode 上可以挂多个 dentry 节点,换句话说,一个 inode 可以出现在 Linux 目录树的多个位置; dentry 对应目录树的一个节点位置,最直观的是对应 path 路径的一个位置; 一个挂载路径可以挂多个文件系统实例,后面的覆盖前面的,所以光靠 dentry 无法唯一定位一个“文件”,Linux 内核才用两元组 < vfsmount, dentry > 来唯一定位一个“文件”;
~完~