文件描述符是个什么鬼?

237 阅读8分钟

网络上有很多文章都涉及到文件描述符这个概念,但是大都只是一笔带过,只告诉你他就是一个代表进程所打开的一个文件的整数而已。这其实会让人感到十分的疑惑,一个整数怎么能代表一个打开的文件了?本文就带你彻底理解文件描述符这个简单而又疑惑的概念,let's go~

我们先从整体上来了解linux进程如何描述文件资源来引入。大家都知道linux采用PCB结构来描述一个进程信息,而进程信息中肯定包括和这个进行有关的文件信息。而PCB中有两个成员变量:fs_struct和files_struct来分别描述进程进程当前的工作目录以及它自己的根目录文件描述符的使用情况(即进程打开的文件)。如图: image.png

  • task_struct就是PCB,用来描述一个进程信息
  • fs_struct和files_struct是PCB的两个成员结构体,分别描述进程当前工作目录及根目录
  • dentry就是代表一个目录,indoe则是一个文件的索引节点,前文有详细介绍这两个结构体(注:本文会涉及到dentry和inode的结构,建议了解)
  • files_struct和file这两个结构体就和本文要介绍的文件描述符息息相关,下面会重点简介

大家知道pwd这个linux命令可以获取当前所在目录,其实这就是获取fs_struct这个结构体的pwd成员。

接下来我们就来揭开文件描述符的神秘面纱。

按照国际惯例,先给出看不懂的定义: 文件描述符在形式上是一个非负整数,实际上,它是一个数组(这个数组就被称为文件描述符表)的索引值,而这个数组存放的是一个指针,这个指针叫做文件指针,它指向一个代表文件的结构体。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。 如果此时去打开一个新的文件,它的文件描述符会是3(前面提到的数组的最大索引值+1)。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。也就是说,一个程序能够访问文件是因为给这个程序分配了文件描述符。

前面说到task_struct结构体有一个成员files_struct和文件描述符息息相关,那么就以此为切入点。每个进程用一个files_struct结构来记录文件描述符的使用情况,这个files_struct结构称为用户打开文件表,它是进程的私有数据。files_struct结构在include/linux/sched.h中定义如下:

struct files_struct {
   //共享该表的进程数
   atomic_t count; 
   //保护以下的所有域,以免在tsk->alloc_lock中的嵌套
   rwlock_t file_lock; 
   //当前文件对象的最大数    
   int max_fds; 
   //当前文件描述符的最大数    
   int max_fdset; 
   ///该进程下一次打开新文件的时候使用的文件描述符id 
   int next_fd; 
   //指向文件对象指针数组的指针
   struct file * fd; 
   //文件对象指针的初始化数组
   struct file ** fd_array[32];
   //指向执行exec( )时需要关闭的文件描述符
   fd_set *close_on_exec; 
   //指向打开文件描述符的指针
   fd_set *open_fds; 
   //执行exec( )时需要关闭的文件描述符的初 值集合
   fd_set close_on_exec_init;
   //文件描述符的初值集合    
   fd_set open_fds_init; 
};

我们来关注两个重要的字段:

  • fd:结构体file指针
  • fd_array:指向以结构体file的指针为元素的数组的指针

看来现在不得不引出file这个结构体了,下面是file struct的结构:

struct file {
  union {
       //文件对象链表指针linux/include/linux/list.h
     struct list_head fu_list;
      
       //RCU(Read-Copy Update)是Linux 2.6内核中新的锁机制
     struct rcu_head fu_rcuhead; 
  } f_u;

    //包含dentry和mnt两个成员,用于确定文件路径
  struct path f_path; 
     //指向属于文件相关的inode实例的地址空间映射
  struct address_space *f_mapping;
    //文件操作所调用到的各个函数  
  const struct file_operations *f_op;

    //文件的引用计数(有多少进程打开该文件)
  atomic_t f_count; 
    //对应于open时指定的flag
  unsigned int f_flags; 
    //读写模式:open的mod_t mode参数
  mode_t f_mode; 
    //该文件在当前进程中的文件偏移量
  off_t f_pos;
    //该结构的作用是通过信号进行I/O时间通知的数据
  struct fown_struct f_owner; 
     //文件所有者id,所有者组id
  unsigned int f_uid, f_gid;
    //在linux/include/linux/fs.h中定义,文件预读相关
  struct file_ra_state f_ra; 
    struct list_head f_ep_links;
    
    ...
        
};

struct path {  
    //表示当前文件所在文件系统的挂载根目录
    struct vfsmount *mnt;  
    //f_path的成员之一,当前文件的dentry结构(即目录项)
    struct dentry *dentry;  
};  

这个file结构体,就代表一个进程所打开的文件,当进程打开一个文件,内核就动态创建一个file对象,同一个文件在不同的进程中有不同的file对象。那么为什么这个file结构体可以代表一个文件,我们来具体看一下它的几个重要成员:

  • f_path:这也是一个结构体,里面包含了一个dentry结构体指针(即目录),因此这个成员就是关联一个文件的关键
  • f_flags:表示打开文件的权限
  • f_pos:表示当前读写文件的位置
  • f_count:这个是一个相对来说比较重要的参数,表示打开文件的引用计数,如果有多个文件指针指向它,就会增加f_count的值
  • f_mode:设置对文件的访问模式,例如:只读,只写
  • f_op:file_operations结构体指针,这个结构体中的成员除了struct module* owner 其余都是函数指针,file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。file_operations结构如下:
struct file_operations {
    struct module *owner;               
    //指向拥有该模块的指针;
    loff_t (*llseek) (struct file *, loff_t, int);   
    //llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. 
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);  
    //用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    //发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    //初始化一个异步读 -- 可能在函数返回前不结束的读操作.
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
    //初始化设备上的一个异步写.
    int (*readdir) (struct file *, void *, filldir_t);
    //对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对**文件系统**有用.
    unsigned int (*poll) (struct file *, struct poll_table_struct *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    int (*mmap) (struct file *, struct vm_area_struct *);
    //mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.
    int (*open) (struct inode *, struct file *);
    //打开一个文件
    int (*flush) (struct file *, fl_owner_t id);
    //flush 操作在进程关闭它的设备文件描述符的拷贝时调用;
    int (*release) (struct inode *, struct file *);
    //在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.
    int (*fsync) (struct file *, struct dentry *, int datasync);
    //用户调用来刷新任何挂着的数据.
    int (*aio_fsync) (struct kiocb *, int datasync);
    int (*fasync) (int, struct file *, int);
    int (*lock) (struct file *, int, struct file_lock *);
    //lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.
    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    int (*check_flags)(int);
    int (*flock) (struct file *, int, struct file_lock *);
    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    int (*setlease)(struct file *, long, struct file_lock **);
};

好了,file struct介绍完了之后,大家应该不难看出前面给出的国际惯例定义中提到的数组应该指的就是fd_array了,它指向了一个数组,这个数组的元素存放的就是代表一个进程打开的文件的file结构体的指针,而我们的文件描述符,就是这个fd_array数组的索引! image.png

因此,在一个进程中,使用了fd_array数组的索引来作为文件描述符,所以它才是一个非负整数,而知道了索引就知道了fd_array数组中的元素,即指向代表打开文件的file结构体。而这个file结构体又关联了一个dentry结构体,而dentry又关联着一个inode,所以file才能代表一个当前进程打开的文件。如图: image.png

注意:

不同进程可以拥有相同的文件描述符,他们可以指向同一个文件,也可以指向不同文件;同一个进程内的文件描述符可以指向同一个文件,也可以指向不同文件。