背景

inode 或 i节点 是指对 文件的索引。如一个系统,所有文件是放在磁盘或flash上,就要编个目录来说明每个文件在什么地方,有什么属性,及大小等。就像书本的目录一样,便于查找和管理。这目录是操作系统需要的,用来找文件或叫管理文件。许多操作系统都用到这个概念,如linux, 某些嵌入式文件系统等。当然,对某个系统来说,有许多i节点。所以对i节点本身也是要进行管理的。

在linux中,内核通过inode来找到每个文件,但一个文件可以被许多用户同时打开或一个用户同时打开多次。

这就有一个问题,如何管理文件的当前位移量,因为可能每个用户打开文件后进行的操作都不一样,这样文件位移量也不同,当然还有其他的一些问题。所以linux又搞了一个文件描述符(file descriptor)这个东西,来分别为每一个用户服务。

每个用户每次打开一个文件,就产生一个文件描述符,多次打开就产生多个文件描述符,一一对应,不管是同一个用户,还是多个用户。该文件描述符就记录了当前打开的文件的偏移量等数据。

所以一个 i节点 可以有 0个或多个文件描述符。多个文件描述符可以对应一个i节点。**

概述

  1. Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。<br />**文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符**。<br /> 程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。标准文件描述符图如下:<br />![image.jpeg](https://cdn.nlark.com/yuque/0/2021/jpeg/1614523/1613803575725-27e10d0f-636c-4416-b54d-a6a5b8f5bf49.jpeg#align=left&display=inline&height=149&margin=%5Bobject%20Object%5D&name=image.jpeg&originHeight=149&originWidth=722&size=85899&status=done&style=none&width=722)

文件描述与打开的文件对应模型如下图:
image.jpeg

文件描述限制

在编写文件操作的或者网络通信的软件时,初学者一般可能会遇到“Too many open files”的问题。

这主要是因为文件描述符是系统的一个重要资源,虽然说系统内存有多少就可以打开多少的文件描述符,但是在实际实现过程中内核是会做相应的处理的,一般最大打开文件数会是系统内存的10%(以KB来计算)(称之为系统级限制),查看系统级别的最大打开文件数可以使用sysctl -a | grep fs.file-max命令查看。

与此同时,内核为了不让某一个进程消耗掉所有的文件资源,其也会对单个进程最大打开文件数做默认值处理(称之为用户级限制),默认值一般是1024,使用ulimit -n命令可以查看。在Web服务器中,通过更改系统默认值文件描述符的最大值来优化服务器是最常见的方式之一,具体优化方式请查看http://blog.csdn.net/kumu_linux/article/details/7877770

系统ulimit限制进程占用资源
文件_NR_OPEN 与 NR_FILE 的区别

“too manay open files” 错误与文件描述符有关

系统级限制:sysctl命令和proc文件系统中查看到的数值是一样的,这属于系统级限制,它是限制所有用户打开文件描述符的总和 查看系统限制命令:   sysctl -a | grep -i file-max —color   cat /proc/sys/fs/file-max 用户级限制:ulimit命令看到的是用户级的最大文件描述符限制,也就是说每一个用户登录后执行的程序占用文件描述符的总数不能超过这个限制 查看用户限制命令:     ulimit -n 用户级修改在文件 /etc/security/limits.conf

文件描述符合打开文件之间的关系

每一个文件描述符会与一个打开文件打开文件就是才是file,指的是打开文件表项)相对应,同时,不同的文件描述符也会指向同一个文件。相同的文件可以被不同的进程打开也可以在同一个进程中被多次打开。

系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的**,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。具体情况要具体分析,要理解具体其概况如何,需要查看由内核维护的3个数据结构。
1. 进程级的文件描述符表
2. 系统级的打开文件描述符表
3. 文件系统的i-node表

进程级的描述符表的每一条目记录了单个文件描述符的相关信息。
1. 控制文件描述符操作的一组标志。(目前,此类标志仅定义了一个,即close-on-exec标志)
2. 对打开文件句柄的引用
3. 进程每次打开一个文件,系统就会在该进程的用户文件描述符表中分配一个相应的表项,表项的索引返回给该进程,用于标志该文件,找个索引就是常说的文件描述符。每个进程都有它独立的描述符表

如下例,这个 fd 一般就称作文件描述,就是一个常量数值的索引,索引值范围仅再这个进程内有效**

  1. int fd; //定义一个变量fd,存放文件描述符
  2. fd = open("home/chenhai/test/main.c",O_RDWR);

内核对所有打开的文件的文件维护有一个系统级的描述符表格(open file description table)。有时,也称之为打开文件表(open file table),并将表格中各条目称为打开文件句柄(open file handle)。一个打开文件句柄存储了与一个打开文件相关的全部信息,如下所示:
1. 当前文件偏移量(调用read()和write()时更新,或使用lseek()直接修改)
2. 打开文件时所使用的状态标识(即,open()的flags参数)
3. 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式)
4. 与信号驱动相关的设置
5. 对该文件i-node对象的引用
6. 文件类型(例如:常规文件、套接字或FIFO)和访问权限
7. 一个指针,指向该文件所持有的锁列表
8. 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
image.jpeg

下图展示了文件描述符、打开的文件句柄以及i-node之间的关系,图中,两个进程拥有诸多打开的文件描述符。
image.jpeg
在进程A中,文件描述符1和30都指向了同一个打开的文件句柄(标号23)。这可能是通过调用dup()、dup2()、fcntl()或者对同一个文件多次调用了open()函数而形成的。

进程A的文件描述符2和进程B的文件描述符2都指向了同一个打开的文件句柄(标号73)。这种情形可能是在调用fork()后出现的(即,进程A、B是父子进程关系),或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。再者是不同的进程独自去调用open函数打开了同一个文件,此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。
image.jpeg
此外,进程A的描述符0和进程B的描述符3分别指向不同的打开文件句柄,但这些句柄均指向i-node表的相同条目(1976),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况。
image.jpeg
也是对 文件_两个进程可以同时打开同一个文件吗?返回的文件描述符一样吗?的解释

文件描述符总结

1. 由于进程级文件描述符表的存在,不同的进程中会出现相同的文件描述符,它们可能指向同一个文件,也可能指向不同的文件
2. 两个不同的文件描述符,若指向同一个打开文件句柄,将共享同一文件偏移量。因此,如果通过其中一个文件描述符来修改文件偏移量(由调用read()、write()或lseek()所致),那么从另一个描述符中也会观察到变化,无论这两个文件描述符是否属于不同进程,还是同一个进程,情况都是如此。
3. 要获取和修改打开的文件标志(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可执行fcntl()的F_GETFL和F_SETFL操作,其对作用域的约束与上一条颇为类似。
4. 文件描述符标志(即,close-on-exec)为进程和文件描述符所私有。对这一标志的修改将不会影响同一进程或不同进程中的其他文件描述符

代码 - file - 文件对象(系统打开文件表项)

Linux用file结构体来保存打开的文件的位置,所以file称为打开的文件描述。这个需要好好理解一下!file结构形成一个双链表,称为系统打开文件表

  1. 565 struct file {
  2. 566 struct list_head f_list;
  3. 567 struct dentry *f_dentry;
  4. 568 struct vfsmount *f_vfsmnt;
  5. 569 struct file_operations *f_op;
  6. 570 atomic_t f_count;
  7. 571 unsigned int f_flags;
  8. 572 mode_t f_mode;
  9. 573 loff_t f_pos;
  10. 574 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
  11. 575 struct fown_struct f_owner;
  12. 576 unsigned int f_uid, f_gid;
  13. 577 int f_error;
  14. 578
  15. 579 size_t f_maxcount;
  16. 580 unsigned long f_version;
  17. 581
  18. 582 /* needed for tty driver, and maybe others */
  19. 583 void *private_data;
  20. 584
  21. 585 /* preallocated helper kiobuf to speedup O_DIRECT */
  22. 586 struct kiobuf *f_iobuf;
  23. 587 long f_iobuf_lock;
  24. 588 };

解释一些字段:

  • f_list:所有的打开的文件形成的链表!注意一个文件系统所有的打开的文件都通过这个链接到super_block中的s_files链表中!
  • f_dentry:与该文件相关的dentry
  • f_vfsmnt:该文件在这个文件系统中的安装点
  • f_op:文件操作,当进程打开文件的时候,这个文件的关联inode中的i_fop文件操作会初始化这个f_op字段
  • f_count:引用计数
  • f_flags:打开文件时候指定的标识
  • f_mode:文件的访问模式
  • f_pos:目前文件的相对开头的偏移
  • unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin:预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数
  • f_owner:记录一个进程ID,以及当某些事发送的时候发送给该ID进程的信号
  • f_uid:用户ID
  • f_gid:组ID
  • f_error:写操作错误码
  • f_version:版本号,当f_pos改变时候,version递增
  • private_data:私有数据( 文件系统和驱动程序使用 )


重点解释一些重要字段**:
首先,f_flags、f_mode和f_pos代表的是这个进程当前操作这个文件的控制信息。这个非常重要,因为对于一个文件,可以被多个进程同时打开,那么对于每个进程来说,操作这个文件是异步的,所以这个三个字段就很重要了。

第二:对于引用计数f_count,当我们关闭一个进程的某一个文件描述符时候,其实并不是真正的关闭文件,仅仅是将f_count减一,当f_count=0时候,才会真的去关闭它。对于dup,fork这些操作来说,都会使得f_count增加,具体的细节,以后再说。

第三:f_op也是很重要的!是涉及到所有的文件的操作结构体。例如:用户使用read,最终都会调用file_operations中的读操作,而file_operations结构体是对于不同的文件系统不一定相同。里面一个重要的操作函数式release函数,当用户执行close时候,其实在内核中是执行release函数,这个函数仅仅将f_count减一,这也就解释了上面说的,用户close一个文件其实是将f_count减一。只有引用计数减到0才关闭文件。

注意:对于“正在使用”和“未使用”的文件对象分别使用一个双向链表进行管理。

代码 - files_struct - 文件描述符表(用户打开文件表,指向file)

注意上面的file只是对一个文件而言,对于一个进程(用户)来说,可以同时处理多个文件,所以需要另一个结构来管理所有的files即:文件描述符表(用户打开文件表)—->files_struct

这个files_struct结构称为用户打开文件表,它是进程的私有数据

  1. struct files_struct {
  2. 173 atomic_t count;
  3. 174 rwlock_t file_lock; /* Protects all the below members. Nests inside tsk->alloc_lock */
  4. 175 int max_fds;
  5. 176 int max_fdset;
  6. 177 int next_fd;
  7. 178 struct file ** fd; /* current fd array */
  8. 179 fd_set *close_on_exec;
  9. 180 fd_set *open_fds;
  10. 181 fd_set close_on_exec_init;
  11. 182 fd_set open_fds_init;
  12. 183 struct file * fd_array[NR_OPEN_DEFAULT];
  13. 184 };

解释一些字段:

  • count:引用计数
  • file_lock:锁,保护下面的字段
  • max_fds:当前文件对象的最大的数量
  • max_fdset:文件描述符最大数
  • next_fd:已分配的最大的文件描述符+1
  • fd:指向文件对象指针数组的指针,一般就是指向最后一个字段fd_arrray,当文件数超过NR_OPEN_DEFAULT时候,就会重新分配一个数组,然后指向这个新的数组指针!
  • close_on_exec:执行exec()时候需要关闭的文件描述符
  • open_fds:指向打开的文件描述符的指针
  • close_on_exec_init:执行exec()时候需要关闭的文件描述符初始化值
  • open_fds_init:文件描述符初值集合
  • fd_array:文件对象指针的初始化数组,下标可能就为文件描述符fd,所以fd仅在当前进程有效,最大值 NR_OPEN_DEFAULT 就是 nr_open,最大文件描述符的数目,可通过 ulimit (系统ulimit限制进程占用资源)进行修改

文件描述符(file_struct)是操作系统用来管理文件的数据结构,当我们创建一个进程时,会创建文件描述符表,进程控制块PCB中的fs指针指向文件描述符表,当我们创建文件时,会为指向该文件的指针FILE关联一个文件描述符并添加在文件描述符表中。在文件描述符表中fd相当于数组的索引,FILE相当于数组的内容吗,指向一个文件结构体
image.jpeg

  • 与进程相关的文件
  • 首先,文件必须由进程打开,每个进程都有它自己当前的工作目录和它自己的根目录。task_struct的fs字段指向进程的fs_struct结构,files字段指向进程的files_struct结构。

    代码 - task_struct - 进程结构

    我们说fd是一个数字,那么这个数字是怎么计算出来的?在内核进程结构体 task_struct 中为每个进程维护了一个数组(files_struct),数组下标就是fd,里面存储的是对这个文件的描述了。里面就有files指针,维护着所有打开的文件信息:

    1. struct task_struct {
    2. ...
    3. /* filesystem information */
    4. struct fs_struct *fs; //这个
    5. /* open file information */
    6. struct files_struct *files; // 指向上面的文件描述符表
    7. ...
    8. }

    注意上面的 files_struct 和 file记录的是与进程相关的文件的信息,但是对于进程本身来说,自身的一些信息用什么表示,这里就涉及到fs_struct结构体。

    代码 - fs_struct - 进程文件信息结构

    1. 5 struct fs_struct {
    2. 6 atomic_t count;
    3. 7 rwlock_t lock;
    4. 8 int umask;
    5. 9 struct dentry * root, * pwd, * altroot;
    6. 10 struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
    7. 11 };

    解释一些字段:

  • count:引用计数

  • lock:保护锁
  • umask:打开文件时候默认的文件访问权限
  • root:进程的根目录
  • pwd:进程当前的执行目录
  • altroot:用户设置的替换根目录


注意**:实际运行时,这三个目录不一定都在同一个文件系统中。例如,进程的根目录通常是安装于“/”节点上的ext文件系统,而当前工作目录可能是安装于/etc的一个文件系统,替换根目录也可以不同文件系统中。
rootmnt,pwdmnt,altrootmnt:对应于上面三个的安装点。