论一切都是文件之匿名 inode

作者 | 宋宝华       责编 | 欧阳姝黎

唯有文件得人心

当一个女生让你替她抓 100 只萤火虫,她一定不是为了折磨你,而是因为她爱上了你。当你们之间经历了无数的恩恩怨怨和彼此伤害,她再次让你替她抓 100 只萤火虫,那一定是因为她还爱着你。

为什么?因为这就是套路,是在下偶尔瞟一眼古装肥皂剧总结出来的套路。

Linux 里面最大的套路,就是“一切都是文件”。爱一个人,就为她捉萤火虫;做一件事,就让它成为一个“文件”。

为什么自古深情留不住,唯有“文件”得人心呢?因为文件在用户态最直观的形式是随着一次 open,获得一个 fd,有了这个 fd,长城内外,你基本可以为所欲为:

  • 在本进程内,fd 的最直观操作是 open、close、mmap、ioctl、poll这些。mmap 让你具备把 fd 透射到内存的能力,所以你可以通过指针访问文件的内容。再者,这个 mmap,如果底层透射的是framebuffer、V4L2、DRM 等,则让我们具备了从用户态操作底层显存、多媒体数据等的能力;比如,无论是 V4L2 还是 DRM,都支持把底层的 dma_buf 导出为 fd。poll 则提供给用户阻塞等待某事件发生的能力。至于 ioctl,就更加不用说了,你可以透过 ioctl 灵活地为 fd 添加控制命令。

  • 在跨进程的情况下,Linux 支持 fd 的跨进程 socket 传输,从而可以实现共享内存、dma_buf 跨进程共享等。比如一个进程可以通过 send_fd 可以把 fd 发送出去:

而另外一个进程可以通过 recv_fd 把 fd 收过来:

这种 fd 在长城内外可以互访,fd 最终可以指向 dma_buf 同时可以被mmap,而 dma_buf 又最终可以被显卡、显示控制器、video decoder/encoder 等设备访问的能力,让fd打通了设备、CPU 和跨进程的障碍,从此可以横着走。

我们在《世上最好的共享内存(Linux共享内存最透彻的一篇)》一文中已经详细阐述过这个过程,这里我们就不再赘述了。本文的重点在于匿名 inode。

inode 源头 file 活水

我们把文件想象成一个 object,那么 inode 描述的是本源,和最终的object一一对应;dentry 是 inode 的一个路径马甲,比如我们可以通过" ln "命令为同一个 inode 创建很多的硬链接马甲;而 file 则是活水,进程对object的一次“ open ”,获得一个 file,导致用户态得到一个" fd "的句柄来操作这个object。

经典的 inode、dentry、file 谁都不缺席的模型是这样的:

上图中,我们有一个 inode,这个 inode 有 2 个 dentry,进程 A、B open 的是第一个 dentry;而进程 C、D open 的是第二个 dentry。变了的是 file 和 fd,不变的是 inode,中间的 dentry 马甲没那么重要。

但是在 inode、dentry、file 这个经典铁三角中,从来都是可以有一个缺席者的,那就是 dentry,因为,有时候用户态想获得长城内外行走的便利,但是却不想这个 inode 在文件系统里面留下一个路径的痕迹。简单来说,我希望有个fd,但是这个 fd,你在从"/"往下面搜索的任何一条路径下,你都找不到它,它根本在根文件系统以下不存在路径,它是无名氏,它没有马甲,它是个传说。

比如,近期名震江湖的剑客 usefaultfd 允许我们在用户空间处理 page fault,我们是通过 userfaultfd 这个系统调用先获得一个 fd,之后就可以对它进行各种 ioctl 了:

我们透过 userfaultfd 系统获得了一个 fd,它在/xxx/yyy/zzz这样的文件系统下没有路径。这种情况下的 fd,对应着的是一个没有名字的匿名inode,你显然没有办法像 fd = open ("xxx", ..)那样来得到匿名 inode 的 fd,因为"xxx"是一个路径,而匿名 inode 没有 xxx,所以你是直接透过 syscall userfaultfd 这样的系统调用,来获得 anon_inode 在你的进程里面对应的 fd的:

人过留名,雁过留声;杀人者,打虎武松也。但是 anon inode 不吃这一套,它是一个绝顶的轻功高手,它给与的,是透过 fd 长城内外行走的能力,但是,在文件系统里面却从未来过。这是用户真实的需求,如果这种需求一定要透过一个 dentry 的 open 才能实现,这未免有点画蛇添足了。

匿名 inode 的内核实例

我们接下来可以随便打开个 anon inode 的实例来看看它是怎么工作的了。首先 userfaultd 是一个系统调用:

这个代码里面比较核心的是就是,它通过:

anon_inode_getfd_secure()

生成一个匿名 inode,并获得一个句柄 fd。重点别忘记了,这种“文件”也是可以有 file_operations 的,比如上面 anon_inode_getfd_secure()参数中的userfaultfd_fops:

这样,我们就可以在 file_operations的ioctl,poll,read 等 callback 里面实现自己特别的“文件”逻辑,这是我们自由发挥的舞台。

说起anon_inode_getfd_secure(),它再往底层走一级是__anon_inode_getfd():

进而再走一级是__anon_inode_getfile():

所以本质上,是先造一个 anon_inode,然后再在这个anon_inode上面造一个pseudo 的 file,最后通过 fd_install(fd, file),把 fd 和 file 缠在一起。再次强调,用户有了这个fd就可以为所欲为;而内核本身,则是通过 file_operations 的不同实现来为所欲为的。

anon_inode 之上添加一个系统调用,造一种特殊的 fd,让用户去 poll,去 ioctl,把想象空间拉大了。这种实现方法,如此拉风灵活,以至于它本身也成为了一种套路。比如内核里面 fs 目录下的:

eventfd,eventpoll,fscontext,io_uring,fanotify,inotify,signalfd,timerfd.......

正所谓, 待到秋来九月八,我花开后百花杀。冲天香阵透长安,满城尽带黄金甲。文件,哪怕最终是匿名的,都以冲天的香阵,弥漫整个 Linux 的世界。

用户使用匿名inode

到了要说再见的时刻了,用户可见的就是 fd,通过 fd 来使用匿名 inode。下面我们来制造一个 page fault 的例子,让用户态来处理它,这个例子直接简化自 userfaultfd 的 man page。我们在主线程中,通过mmap申请一页内存,然后通过 userfaultfd 的 ioctl 告诉内核这页的开始地址和长度,以及通过 UFFDIO_REGISTER 告诉内核这页的 page fault 想用户空间处理:

然后我们在 pthread_create()创建的 fault_handler_thread 线程中,poll userfaultfd 等待事件,之后把一页全是 0x66 的内容拷贝到 page fault 发生的那一页:

我们运行这个程序得到的输出如下:

我们主线程在执行 addr[0]=0x5A5A5A5A 的时候,触发了 page fault。在fault 线程里面,page fault 发生后,poll 阻塞返回,之后用户通过 read()读到了一个 uffd_msg 的结构体,里面的成员包含了 page fault 的地址。之后,我们通过 UFFDIO_COPY 这个 ioctl,把内容为 0x66 的页面拷贝给 page fault 的页面。

所以,最终主线程在执行 printf 打印的时候,addr[0]里面读到了 5A5A5A5A,剩下的addr[1]里面读到了 66666666。看到 page fault 由用户态灵活这么灵活自如地处理,我的小伙伴们都吓尿了。

可以看出来:

  • poll()在等什么,完全被定制化了;

  • read()能读什么,完全被定制化了;

  • ioctl()能控制什么,完全被定制化了。

我们通过“文件”这个不变的“静”,制造了 poll、read、ioctl 的灵动自如。兵法有云,以不变应万变,以万变应不变。

CSDN 问答上线《冲榜分奖金》活动!每周采纳榜前五名的答主可获得现金和会员卡,剩余用户会随机抽取送出幸运礼物!

 ☞“UNIX的名字是我起的”——对话UNIX开发者Brian W. Kernighan ☞苹果“撞上”反垄断,围墙花园能否坚挺? ☞“炸毁 70% 的互联网!”德州一男子欲袭击亚马逊数据中心被捕
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值