当前位置: 首页 > 科技观察

一切都是文件-匿名inode

时间:2023-03-17 01:28:22 科技观察

01只有文件才能赢得人心当一个女孩让你为她捉100只萤火虫时,她一定不是为了折磨你,而是因为她爱上了你。当你们经历了无数的委屈,互相伤害,她又要你为她捉100只萤火虫,那一定是因为她还爱着你。为什么?因为这是套路,偶尔看一眼古装肥皂剧就总结出来的。Linux最大的套路就是“一切皆文件”。爱一个人,就为她捉萤火虫;如果你做了什么,就把它做成一个“文件”。为什么自古留不住情,唯有“档”得人心?因为在用户态下文件最直观的形式就是打开一个就得到一个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同时可以mmaped,dma_buf可以最终访问显卡、显示控制器和视频解码器/编码器等设备的能力让fd突破了设备、CPU和跨进程的壁垒,从此可以横着走。这个过程我们在《宋宝华:世上最好的共享内存(Linux共享内存最透彻的一篇)》一文中已经详细解释过了,这里不再赘述。本文重点介绍匿名inode。02inode源文件流水我们把文件想象成一个对象,那么inode描述了源,和最终的对象一一对应;dentry是inode的一个路径马甲,比如我们可以使用“ln”命令为同一个inode创建多个硬链接马甲;文件流水时,进程“打开”对象一次,获得一个文件,导致用户态获得一个“fd”句柄来操作对象。inode、dentry、file的经典模型是这样的:在上图中,我们有一个inode,这个inode有2个dentry,进程A和B打开的是第一个dentry;而进程C和D打开的是第二个dentry。变化的是file和fd,不变的是inode,中间的dentry马甲就没那么重要了。但是在inode、dentry、file这个经典的铁三角中,总能有一个缺席者,那就是dentry,因为有时候用户态想获得穿梭于长城内外的便利,却又不想文件系统中的这个inode。留下一条小路的痕迹。简单的说,我希望有一个fd,但是这个fd,你从“/”到最下面的任何路径下都找不到。它根本不存在于根文件系统下。它是无名氏,它没有马甲,它是一个传奇。比如最近大名鼎鼎的剑客usefaultfd,让我们可以在用户空间处理pagefault。我们先通过userfaultfd系统调用得到一个fd,然后我们可以对其进行各种ioctl:我们通过userfaultfd系统得到了一个fd,它在/xxx/yyy/zzz这样的文件系统下没有路径。本例中的fd对应于一个没有名称的匿名inode。你显然没有办法像fd=open("xxx",..)那样获取到匿名inode的fd,因为"xxx"是一个路径,匿名inode没有xxx,所以你直接获取对应的fd你进程中的anon_inode通过syscalluserfaultfd等系统调用:人留名,雁留声;但是anoninode不拿这种东西,它是一个极其轻巧的高手,它给的是fd长城内外行走的能力,但是从来没有在文件系统里面。这是用户的真实需求。如果这个需求一定要通过开一个dentry来实现,那就有点画蛇添足了。03Kernelinstanceofanonymousinode接下来,我们可以打开一个anoninode的实例,看看它是如何工作的。首先,userfaultd是一个系统调用:这段代码的核心是通过:anon_inode_getfd_secure()生成一个匿名inode,并获得一个句柄fd。别忘了这种“文件”还可以有file_operations,比如上面anon_inode_getfd_secure()参数中的userfaultfd_fops:这样我们就可以在ioctl、poll、read等实现自己特殊的“文件”逻辑file_operations的回调,这是我们自由发挥的舞台。说到anon_inode_getfd_secure(),再往上一层就是__anon_inode_getfd():再往上一层就是__anon_inode_getfile():所以本质上就是先创建一个anon_inode,然后在这个anon_inode上创建一个pseudofile,最后通过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世界的芬芳。04用户是时候告别使用匿名inode了。用户可以看到fd,并通过fd使用匿名inode。让我们创建一个页面错误的例子,让用户模式处理它。这个例子是直接从userfaultfd的手册页中简化而来的。在主线程中,我们通过mmap申请了一页内存,然后通过userfaultfd的ioctl告诉内核这个页面的起始地址和长度,通过UFFDIO_REGISTER告诉内核这个页面的pagefault要用户空间处理:然后我们创建在pthread_create()中的fault_handler_thread线程,polluserfaultfd等待事件,然后复制一个充满0x66内容的页面到页面错误发生的页面:我们运行这个程序时得到的输出如下:我们的主线程在执行addr[0]=0x5A5A5A5A时触发了pagefault。在fault线程中,pagefault发生后,poll被阻塞返回,然后用户通过read()读取到一个uffd_msg结构体,其中的成员包含了pagefault的地址。之后,我们使用ioctlUFFDIO_COPY将内容为0x66的页面复制到pagefault页面。所以主线程执行printf打印时,在addr[0]中读取到5A5A5A5A,在剩余的addr[1]中读取到66666666。看到页面错误被用户态处理得如此灵活自如,小伙伴们都吓尿了。可见:poll()等待的是完全自定义的;read()可以读取的内容是完全自定义的;ioctl()可以控制的是完全自定义的。通过“文件”不变的“安静”,我们创造了poll、read、ioctl的灵活性。兵法有云,以同应异,以异应同。本文转载自微信公众号“Linux代码阅读领域”,可通过以下二维码关注。转载本文请联系Linux代码阅读领域公众号。