Linux fd 系列 | 匿名 fd 是什么?

匿名 fd 是什么?

我们经常在 /proc/${pid}/fd/ 下面能看到 anon_inode : 前缀的句柄,如下:

root@ubuntu:~/temp# ll /proc/5398/fd

lr-x------ 1 root root 64 Aug 24 09:39 11 -> anon_inode:inotify
lrwx------ 1 root root 64 Aug 24 09:39 4 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 Aug 24 09:39 5 -> anon_inode:[signalfd]
lrwx------ 1 root root 64 Aug 24 09:39 7 -> anon_inode:[timerfd]
lrwx------ 1 root root 64 Aug 24 09:39 9 -> anon_inode:[eventpoll]

如果是正常的文件句柄,一般显式的是一个路径:

root@ubuntu:~/temp# ll /proc/5398/fd

lr-x------ 1 root root 64 Aug 24 09:39 10 -> /proc/5398/mountinfo
lr-x------ 1 root root 64 Aug 24 09:39 12 -> /proc/swaps

当然 path 只是一个浅层次的感官,因为对于 socket 句柄来说也不算有 path ,所以这个匿名其实匿的是 inode

匿名 inode 的诞生?

重点提一下匿名 fd 的事情,为什么会有匿名 fd ? 什么是匿名?

在 Linux 里一切皆文件,你理解的常见“文件”有什么特性?是路径,也就是 path ,匿名的意思说的就是没有路径( 在内核里面说的就是没有有效的 dentry )。

在 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 关联即可。

原理剖析

anon_inodefs 的初始化

上面提到了,匿名 inode 是一个公共需求,我们不需要一个完整功能的 inode,而只是需要一个 inode 而已,绑定到到 dentry ,file 等结构体。

anon_inodes.c 用来创建一个绑定匿名 inode 的 file 结构体。

整个 anon_inodefs 就只有一个文件,操作系统初始化的时候会调用初始化函数 fs_initcall(anon_inode_init) ,其中 anon_inode_init 只做两件事:

  1. 创建出一个 vfsmount 实例,创建出来之后赋值给全局变量 anon_inode_mnt ;
  2. 创建出一个 inode 实例,创建出来之后赋值给全局变量 anon_inode_inode ;

这两个变量就是 anon_inodefs 这个文件系统的全部家当了。

anon_inodefs 的做了啥?

anon_inodefs 只提供了 2 个实用函数,一个获取到一个绑定匿名 inode 的 file 实例,另一个更多一些封装,返回的是 fd 句柄。如下:

anon_inode_getfile

这个函数非常简单,只做两件事:

  1. 获取一个 inode ( 获取全局的 inode 变量 anon_inode_inode ,当然也可以通过一个参数控制来创建新的 inode );
  2. 创建一个 file 结构体实例,并且把这个 inode 关联起来;

anon_inode_getfd

这个函数非常简单,只做两件事情:

  1. 创建一个新的 fd 句柄,返回的是一个非负整数;
  2. 创建一个 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.

为什么叫这个名字 “anon_inode:${dentry_name}” ?

为什么常见的匿名 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 函数入口 ( 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 实例
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 实例
struct dentry *d_alloc_pseudo(struct super_block *sb, const struct qstr *name)
{
    struct dentry *dentry = __d_alloc(sb, name);
}

// 创建并初始化 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);

    // ...
}

inode 可以对应多个 dentry

在 Linux 中是一个倒挂树的设计,从根目录( / )开始,叶子结点为文件或者目录,从根节点到叶子结点这一段就称为 path 路径,在内存里面这颗倒挂的树就体现为 dentry 树,节点就是 dentry 结构体。

这里就有个重要的知识点:

划重点:一个 inode 上可以挂多个 dentry ,一个 dentry 只能属于一个 inode 。

还记得软链接和硬链接吗?

软链接就是创建了一个新的文件,链接文件里就是路径。inode,dentry 都创建了一个新的。

硬链接则没有创建新的 inode,而是只在目录文件中创建了一个 dirent ,在目录树中添加了一个 dentry 。

4256a3f722d81d5599d29550cfe38d56.png

换句话说,一个 inode 可以出现在目录树的多个位置。

每个文件或者目录都会在这棵树上有自己的位置,内存用 struct path 结构体来表示唯一的位置。

struct path {
    struct vfsmount *mnt;   // 标识在哪个具体的文件系统实例
    struct dentry *dentry;  // 内存目录树节点
};

这里顺便再说另一个重要知识点:为什么内核之中,需要用 struct path 这个复合结构体来标识唯一的一个目录树位置呢?

其实所谓的挂载是把文件系统实例和目录树上的一个 dentry 关联起来,形成一个 vfsmount 结构体实例。而一个 dentry 是可以绑定多个文件系统实例的。

换句话说:对于一个目录树路径其实是可以挂载多个文件系统实例。比如 /mnt/path 这么一个路径,其实是可以挂载多个文件系统的,不会报错,后面的挂载直接覆盖前面的。

aa7936126e95bdc457ebec649e3ca082.png

其实还有一类匿名

为了知识的完善,这里补充一个知识点。其实关于匿名 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 。

谁用到了匿名 inode

随便列举一些 eventfd,eventpoll,timerfd,signalfd,inotifyfd,io_uring fd 等等,还有很多,但比较偏僻了,就不再举例了。童鞋们惊讶吗?

总结

  1. anon_inodefs 是为了公共需求抽离出来的一个内核文件系统,只有一个 inode ,为了节省内存,抽象重复代码之用;
  2. 匿名句柄是因为 fd 对应的 file 实例背靠着的是匿名 inode ,anon_inodefs 提供了两个功能函数,都是用来获取匿名 fd 的;
  3. inode 上可以挂多个 dentry 节点,换句话说,一个 inode 可以出现在 Linux 目录树的多个位置
  4. dentry 对应目录树的一个节点位置,最直观的是对应 path 路径的一个位置;
  5. 一个挂载路径可以挂多个文件系统实例,后面的覆盖前面的,所以光靠 dentry 无法唯一定位一个“文件”,Linux 内核才用两元组 < vfsmount, dentry > 来唯一定位一个“文件”;

坚持思考,方向比努力更重要。关注公众号:奇伢云存储,获取更多干货。 关注我公众号, 获取更多干货