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

Linux文件描述符fd到底是什么?

时间:2023-03-14 18:42:55 科技观察

上一篇小结我们知道读写文件有两种方式,一种是系统调用方式,操作的对象是整数fd,另一种是Go标准库自己封装的标准库IO,而操作对象是Go封装的文件结构,但内部还是对整型fd进行操作。那么万物的起源都是通过fd来操作的,那么这个fd到底是什么呢?关于这一点我们进行深入分析。什么是fd?fd是Filedescriptor的缩写,中文名称为:文件描述符。文件描述符是一个非负整数,本质上是一个索引值(这句话很重要)。你什么时候得到fd的?当打开一个文件时,内核返回一个文件描述符(由系统调用open获得)给进程。在读写这个文件的时候,只需要用这个文件描述符来标识这个文件,并把它作为参数传递给read、write即可。fd取值范围是多少?在POSIX语义中,0、1、2这三个fd值被赋予了特殊的意义,分别是标准输入(STDIN_FILENO)、标准输出(STDOUT_FILENO)、标准错误(STDERR_FILENO)。文件描述符有一个范围:0~OPEN_MAX-1,在最早的UNIX系统中这个范围很小,现在的主流系统对这个值的范围几乎没有限制,只是受限于系统硬件配置和系统配置的约束行政人员。可以通过ulimit命令查看当前系统配置:?ulimit-n4864如上,我系统上的进程默认最多可以打开4864个文件。什么是Linux内核fd上的间谍?不得不看一下Linux内核。用户使用系统调用open或creat打开或创建文件,在用户态得到的结果值为fd,后续所有IO操作均使用fd来标识该文件。可想而知,内核所进行的操作并不简单。我们接受下一步是揭开面纱。task_struct首先,我们知道进程的抽象是基于structtask_struct结构,它是linux中最复杂的结构之一,成员字段非常多。我们今天不需要详细解释这个结构。我稍微简化一下,只提取我们今天需要理解的字段如下:structtask_struct{//.../*Openfileinformation:*/structfiles_struct*files;//...}files;这个字段是今天的主角之一,files是一个指向structfiles_struct结构体的指针。这个结构是用来管理进程打开的所有文件的管理结构。重点理解一个概念:structtask_struct是对一个进程的抽象封装,标识一个进程,linux中进程的各种抽象视角就是这个结构体给你的。在创建进程的时候,其实就是一个新的structtask_struct出来了;files_struct好,structfiles_struct结构是通过上面的process结构引入的。这个结构管理着一个进程打开的所有文件的管理结构。结构本身比较简单:/**Openfiletablestructure*/structfiles_struct{//读取相关字段atomic_tcount;boolresize_in_progress;wait_queue_head_tresize_wait;//打开文件管理结构structfdtable__rcu*fdt;structfdtablefdtab;//写入相关字段unsignedintnext_fd;unsignedlongclose_on_exec_init[1];unsignedlongopen_fds_init[1];unsignedlongfull_fds_bits_init[1];structfile*fd_array[NR_OPEN_DEFAULT];};files_struct这个结构用来打开我们所说的所有文件。如何管理?本质上就是数组管理的方式,所有打开的文件结构都在一个数组中。这可能会让你想知道,数组在哪里?有两个地方:structfile*fd_array[NR_OPEN_DEFAULT]是一个静态数组,和files_struct结构一起分配,在64位系统上,静态数组的大小是64;structfdtable也是一个数组管理结构,不过这是一个动态数组,数组边界由字段描述;思考:为什么会有这种静态+动态的方法呢?性能和资源权衡!大多数进程只会打开少量文件,因此静态数组就足够了,因此不会分配额外的内存。如果超过静态数组的阈值,则动态扩展。大家可以回忆一下这个是不是类似于inode的直接索引和一级索引的优化思路。fdtable简单介绍一下fdtable结构体,它封装了用来管理fd的结构体,fd的秘密就在于此。简化结构如下:structfdtable{unsignedintmax_fds;structfile__rcu**fd;/*currentfdarray*/};注意fdtable.fd字段是二级指针,什么意思?即指向fdtable.fd的是一个指针字段,指向的内存地址仍然存放着指针(元素指针类型为structfile*)。也就是说,fdtable.fd指向一个数组,其元素是指针(指针类型是structfile*)。其中max_fds指定数组边界。files_struct总结file_struct本质上是用来管理所有打开的文件,内部核心是通过一个静态数组和动态数组管理结构实现的。还记得上面我们说过文件描述符fd本质上是一个索引吗?这里概念接上了,fd就是这个数组的索引,也就是数组的槽号。通过非负数fd可以得到对应structfile结构的地址。我们把概念串起来(注意这里简化了fdtable的管理,以突出fd的本质):fd其实只是files字段指向的指针数组的索引(仅此而已)。通过fd可以找到对应文件的structfile结构;file现在我们知道fd本质上是一个数组索引,数组元素是指向structfile结构体的指针。然后这里是一个structfile的结构。这个结构是做什么用的?该结构用于表示进程打开的文件。简化后的结构如下:structfile{//...structpathf_path;structinode*f_inode;conststructfile_operations*f_op;atomic_long_tf_count;unsignedintf_flags;fmode_tf_mode;structmutexf_pos_lock;loff_tf_pos;structfown_structf_owner;//...}这个结构很重要,要打开,下面解释一下与IO相关的几个最重要的字段:f_path:标识文件名f_inode:一个非常重要的字段,inode是vfs的inode类型,是基于具体文件系统的抽象封装;f_pos:这个字段很重要,offset,没错,就是当前文件的偏移量。还记得上次IO基础中也提到了offset吧?它指的是这个。f_pos在打开时会设置为默认值,seek时可以更改,影响write/read的位置;思考问题问题1:files_struct结构体只会属于一个进程,那么structfile结构体会不会只属于某个进程呢?或者可能由多个进程共享?要点:structfile是一个系统级的结构,换句话说,它可以被多个不同的进程共享。思考题2:多个进程的fd什么时候会指向同一个文件结构?比如fork时,父进程打开文件,然后fork一个子进程。在这种情况下,就会出现共享文件的场景。如图:思考题3:同一个进程中,多个fd是否可以指向同一个文件结构?能。dup函数就是这样做的。#includeintdup(intoldfd);intdup2(intoldfd,intnewfd);inode我们看到structfile结构中有一个inode指针,自然而然就引出了inode的概念。指向的inode并不是直接指向具体文件系统的inode,而是操作系统抽象出来的一层虚拟文件系统,称为VFS(VirtualFileSystem),然后真正的文件系统在VFS下,比如ext4类别。完整的架构图如下:思考:为什么会有这一层封装?其实很好理解,就是解耦。如果structfile直接连接一个文件系统比如structext4_inode,structfile的处理逻辑会很复杂,因为每次连接一个具体的文件系统都要考虑一个实现。因此,操作系统必须屏蔽底层文件系统,对外提供统一的inode概念,并为定义的接口注册回调。这样就可以统一inode的概念,Unix一切都是文件的基础由此而来。再来看同一个VFS的inode结构:structinode{//文件相关的基本信息(权限、模式、uid、gid等)umode_ti_mode;structaddress_space*i_mapping;//文件大小、atime、ctime、mtime等loff_ti_size;structtimespec64i_atime;structtimespec64i_mtime;structtimespec64i_ctime;//回调函数conststructfile_operations*i_fop;structaddress_spacei_data;//指向后端特定文件系统的特殊数据void*i*private;fsordeviceprivatepointer*/};其中包括一些基本的文件信息,包括uid、gid、大小、模式、类型、时间等。vfs和后端特定文件系统之间的链接:i_private字段。**用于传递一些特定文件系统使用的数据结构。至于i_op回调函数,在构造inode时,注册为后端文件系统函数,如ext4等。思考问题:一般VFS层定义了所有文件系统的公共inode,称为vfsinode,后端文件系统也有自己特殊的inode格式,是在vfsinode之上扩展的,如何获取它通过vfsinode具体文件系统的inode呢?我们以ext4文件系统为例(因为所有的文件系统都有相同的套路),ext4的inode类型是structext4_inode_info。亮点:方法其实很简单。这是C语言中常见的(也是独特的)编程技巧:强制转换。vfsinode在birth时是和ext4_inode_info结构一起分配的,ext4_inode_info结构可以通过强制获取vfsinode结构的地址直接得到。structtext4_inode_info{//ext4inode特征字段//...//重要!!!structinodevfs_inode;};例如,已知的inode地址和vfs_inode字段的内部偏移量如下:inode的地址为0xa89be0;ext4_inode_info中有一个内嵌字段vfs_inode,类型为structinode,该字段在结构体中的偏移量为64字节;那么可以得到:ext4_inode_info的地址是(structext4_inode_info*)(0xa89be0-64)强制传输的方法使用了一个叫container_of的宏,如下:structext4_inode_info,vfs_inode);}//强制实际包#definecontainer_of(ptr,type,member)\(type*)((char*)(ptr)-(char*)&((type*)0)->member)#endif那么,你明白了吗?在分配inode的时候,实际上是分配了ext4_inode_info结构体,其中包含了vfsinode,然后对外给出了vfs_inode字段的地址。VFS层使用inode的地址。底层文件系统强制打字后,使用的是外层inode的地址。以ext4文件系统为例:staticstructinode*ext4_alloc_inode(structusuper_block*sb){structext4_inode_info*ei;//内存分配,分配ext4??_inode_info地址ei=kmem_cache_alloc(ext4_inode_cachep,GFP_NOFS);//ext4_inode_info结构初始化//返回vfs_inode字段地址return&ei->vfs_inode;}vfs得到这个inode地址。要点:inode的内存由后端文件系统分配,不同文件系统的inode中嵌入了vfsinode结构。不同的层使用不同的地址,ext4文件系统使用ext4_inode_info结构体的地址,vfs层使用ext4_inode_info.vfs_inode字段的地址。这种用法在C语言编程中很常见,被认为是C的一个特性(仔细想想,这种用法类似于面向对象多态的实现)。思考题:如何理解vfsinode与ext2_inode_info、ext4_inode_info等结构的区别?所有文件系统共同的东西都抽象成vfsinode,不同文件系统不同的东西放在各自的inode结构中。总结当用户打开一个文件时,用户只得到一个fd句柄,但是内核做了很多事情。整理之后,我们得到了几个关键的数据结构。这些数据结构具有层次关系。下面简单梳理一下:进程结构体task_struct:表征进程实体,每个进程对应一个task_struct结构体,其中task_struct.files指向一个管理打开文件的结构体fiels_struct;文件入口管理结构files_struct:用于管理进程打开的文件列表,内部实现为一个数组(静态和动态数组的结合)。返回给用户的fd只是这个数组的编号索引,索引元素是一个文件结构;files_struct只属于某个进程;3.File文件结构:表示一个打开的文件,包含关键字段:当前文件偏移量,inode结构的地址;虽然结构是由进程触发的,但文件结构可以在进程之间共享;4、vfsinode结构:file结构指向vfs的inode,是操作系统的一个抽象层,用于屏蔽后端各种文件系统的inode差异;inode与具体进程无关,是文件系统级别的资源;5、ext4inode结构(指具体文件系统的inode):后端文件系统的inode结构,不同的文件系统自定义结构,ext2有ext2_inode_info,ext4有ext4_inode_info,minix有minix_inode_info,这些结构是内嵌的同一个vfsinode结构,原理是一样的;完整的架构图:思维实验现在我们已经深入了解了所谓非负整数fd所代表的深层含义,我们可以准备一些IO思维,举一反三。读写文件(IO)时会发生什么?写操作完成后,文件file中的当前文件偏移量增加写入的字节数,如果这导致当前文件偏移量超过当前文件长度,inode的当前长度设置为当前文件偏移量(即文件的长度)O_APPEND标志打开一个文件,相应的标志会被设置为该文件的文件状态标志。file的当前文件偏移量会先在inode结构中设置为文件长度,这样使得每一次写入的数据都追加到文件的当前末尾(这个操作为用户态提供了原子语义);如果Afileseek定位到文件的当前末尾,则file中的当前文件偏移量设置为inode的当前文件长度;seek函数的值在没有任何I/O操作的情况下修改文件中的当前文件偏移量;进程对有自己的文件,其中包含当前文件偏移量。当多个进程写入同一个文件时,由于一个文件IO只会在全局inode上结束,这种并发场景可能会产生用户意想不到的结果;总结一下,回归初衷,理解fd的概念有什么用?所有到系统层面的IO行为都是以fd的形式进行的。无论是C/C++、Go、Python、JAVA,还是任何语言,这都是最根本的。只有了解了fd相关的一系列结构,才能游刃有余的处理IO。小结:从姿势上来说,用户打开一个文件得到一个非负句柄fd,后续对该文件的IO操作都是基于这个fd;文件描述符fd本质上是一个数组索引,fd等于5,对应数组只是第5个元素,数组是进程打开的所有文件的数组,数组元素类型是struct文件;结构体task_struct对应一个抽象进程,files_struct是一个数组管理器,用于管理进程打开的文件数组。fd对应这个数组的编号。每个打开的文件都由一个文件结构表示,其中包含当前偏移量等信息;文件结构可以在进程间共享,属于系统级资源。同一个文件可能对应多个文件结构,文件内部有一个inode指针,指向文件系统的inode;inode是文件系统层面的一个概念,只由文件系统管理和维护,不会因进程而改变(文件是由进程创建的,进程打开同一个文件会造成多个文件,指向同一个索引节点);回头看看架构图:~完~后记内核做了最复杂的工作,只给你暴露了最简单的非负整数fd。所以大部分场景都会用到fd,不用想太多。当然,如果你能看得更深一些,知道为什么就更好了。本文为基础准备文章,希望能给大家带来不一样的IO视角。