作者 | 田伟然

回首向来萧瑟处,归去,也无风雨也无晴。

杏仁工程师,关注编码和诗词。

前言

​文件描述符在unix系统中几乎无处不在

  • 网络接口 select、poll、epoll 涉及到文件描述符
  • IO接口 read、write 也涉及到文件描述符

从形式上来看文件描述就是一个整数,那么我们可不可以更进一步去了解一下呢?本文打算通过一步一步实验去了解文件描述符到底是什么, 并在最后通过Linux内核相关的源码进行验证。

一个获取文件描述符的实例

我们可以通过 open 系统调用得到一个指定文件的文件描述符。

open 函数需要传入一个文件路径和操作模式, 调用会返回一个整型的文件描述符, 具体方法签名如下

/**
* path 代表文件路径
* oflag 代表文件的打开模式,比如读,写等
*/
int open(char *path,  int oflag, ...)

我们写一段简单的代码来验证一下

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>int main(int argc, char* argv[]) {// 以只读模式打开 demo.txt 文件int fd = open("demo.txt", O_RDONLY);if (fd == -1) {perror("open demo.txt errorn");return EXIT_FAILURE;}// 打印获取到的文件描述符printf("demo.txt fd = %d n", fd);return EXIT_SUCCESS;
}

然后使用 GCC 编译,执行编译后的程序,我们就可以得到 demo.txt 的文件描述符了。

不出意外你将得到以下的执行结果:

$ echo hello>>demo.txt
$ gcc test.c -o -test
$ ./test
$ demo.txt fd = 3

和方法签名一致,文件描述符是一个整型数。你可以尝试多次执行该程序, 你会发现打印的文件描述符始终都是 3。难道每个文件的文件描述符都是固定的?

每个文件的描述符是固定的吗?

为了验证前面的猜想,我在程序里面连续调用两次 open 函数,并打印两次返回的文件描述符, 代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>int main(int argc, char* argv[]) {int fd_a = open("demo.txt", O_RDONLY);int fd_b = open("demo.txt", O_RDONLY);printf("fd_a = %d, fd_b = %d n", fd_a, fd_b);return EXIT_SUCCESS;
}

下面是最终的执行结果:

$ gcc test.c -o test
$ ./test
$ fd_a = 3, fd_b = 4

尽管是同一个文件, 得到的文件描述符却并不一样,说明每个文件的描述符并不是固定的。可是文件描述符每次都是从 3 开始的,这是为什么呢 ?

熟悉UNIX系统的同学应该知道,系统创建的每个进程默认会打开3个文件

  • 标准输入(0)
  • 标准输出(1)
  • 标准错误(2)

为什么是 3 ?因为 0、1、2 被占用了啊......等等!文件描述符难道是递增的?我也不知道啊, 要不写个程序试试。

这里应该还有一个疑问:为什么前一节多次执行程序都是返回 3 ,而在代码里调用两次 open 打开同样的文件却是 3 和 4 ?这个问题在后面多进程时会再提到。

文件描述符是递增的吗?

为了验证文件描述符是递增的, 我设计了这样一个程序1. 调用两次 open , 分别得到两个文件描述符 3、 42. 调用 close 函数将文件描述符 3 关闭3. 再次调用 open 函数打开同一个文件如果文件描述符的规则是递增的,第 3 步返回的结果就应该是 5 。

Show me the code:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char* argv[]) {// 第一次打开int a = open("demo.txt", O_RDONLY);// 第二次打开int b = open("demo.txt", O_RDONLY);printf("a = %d, b = %d n", a, b);// 关闭a文件描述符close(a);// 第三次打开int c = open("demo.txt", O_RDONLY);printf("b = %d, c = %d n", b, c);return EXIT_SUCCESS;
}

编译执行

$ gcc test.c -o test
$ ./test
$ a = 3, b = 4b = 4, c = 3

第三次打开的结果是 3 ,这说明文件描述符不是递增的。而且从结果上来看,文件描述符被回收掉后是可以再次分配的。前面讨论的上下文都是在单进程下,如果是多个进程同时打开同一个文件,文件描述符会一样吗?

文件描述符和多进程

fork 函数可以创建多个进程, 该函数返回一个 int 值, 当返回值为 0 时代表当前是子进程正在执行,非 0 就为父进程在执行。(为了简化代码,就不考虑进程创建失败的情况了)

程序很简单,就是父子进程各自打开同一个文件, 并打印该文件的文件描述符。PS: 下面的代码并不规范,可能会产生僵尸进程和孤儿进程,但这并不是本文的重点......

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>int main(int argc, char* argv[]) {int npid = fork();if (npid == 0 ){// 子进程int child_fd = open("demo.txt", O_RDONLY);pid_t child_pid = getpid();printf("child_pid = %d, child_fd = %d n", child_pid, child_fd);} else {// 父进程int parent_fd = open("demo.txt", O_RDONLY);pid_t parent_pid = getpid();printf("parent_pid = %d, parent_fd = %d n", parent_pid, parent_fd);}return EXIT_SUCCESS;
}

编译执行

$ gcc test_process.c -o test_process
$ ./test_process
$ child_pid = 28212, child_fd = 3parent_pid = 28210, child_fd = 3

每个进程打开的都是同一个文件,而且返回的文件描述符也是一样的。前面我们已经得知每个文件的描述符并不是固定的,这样看来,每个进程都单独维护了一个文件描述符的集合啊。还记得最开始实验时,我们对编译好的程序多次执行都是打印的 3,但是在代码里对同一个文件 open 两次却是返回的 3 和 4 吗?这是因为在 shell 每次执行程序,其实都是创建了一个新的进程在执行。而在代码里连续调用两次,始终是在一个进程下执行的。

先总结一下

通过上面的实验,我们可以得出文件描述的一些规律1.文件描述符就是一个整形2.每个进程默认打开 0、1、2 三个文件描述符, 新的文件描述符都是从 3 开始分配3.一个文件描述符被回收后可以再次被分配 (文件描述符并不是递增的)4.每个进程单独维护了一个文件描述符的集合

Show me the code

talk is cheap , show me the code
By: Linus Benedict Torvalds

下面就需要在 Linux内核 的源码中去寻找真相了。既然实验表明每个进程单独维护了文件描述符集合, 那就从和进程相关的结构体task_struct 入手,该结构体放在 /include/linux/sched.h 头文件中。我将这个结构体的代码精简了一下, 只保留了一些分析需要关注的属性

struct task_struct {.../* Filesystem information: */struct fs_struct        *fs;/* Open file information: */struct files_struct     *files;.../* -1 unrunnable, 0 runnable, >0 stopped: */volatile long           state;pid_t               pid;pid_t               tgid;...};

注意 struct files_struct *files ,注释说该属性代表着打开的文件信息,那这就没得跑了。继续看 files_struct 结构体,该结构体定义在 /include/linux/fdtable.h头文件中:

struct files_struct {// 打开的文件数atomic_t count;...// fdtable 维护着所有的文件描述符struct fdtable  *fdt;struct fdtable fdtab;...// 下一个可用文件描述符unsigned int next_fd;...
}

相信你也一眼就看见了 fdtable 这个结构体,见名知意,这不就是文件描述符表吗?那么它是不是维护了所有的文件描述符呢?

struct fdtable {// 最大文件描述符unsigned int max_fds;// 所有打开的文件struct file **fd;      /* current fd array */...
};

fdtable 里面有一个 file 结构体的数组,这个数组代表着该进程打开的所有文件。先将上面的结构用一个图画下来:

这个源码结构展示了每个进程单独维护了一个文件描述符的集合的信息。但是文件描述符是什么,以及它的生成规则还是没有找到。那只能换个方向,从函数调用去寻找了。openclose 都涉及到对文件描述符的操作,由于 close 函数更加简单,就从 close 为入口进行分析。

下面是 close 函数的内部系统调用:

SYSCALL_DEFINE1(close, unsigned int, fd)
{int retval = __close_fd(current->files, fd);...return retval;
}

close调用了__close_fd函数, 该函数定义在/fs/file.c文件中,下面是简化后的代码:

int __close_fd(struct files_struct *files, unsigned fd)
{struct file *file;struct fdtable *fdt;// 获取fdtablefdt = files_fdtable(files);// *** 通过文件描述符获取file结构体指针file = fdt->fd[fd];rcu_assign_pointer(fdt->fd[fd], NULL);// 回收文件描述符
__put_unused_fd(files, fd);return filp_close(file, files);
}

这里面又出现了我们熟悉的结构体 files_struct ,注意 file = fdt->fd[fd] 这一段代码。fdt 就是 fdtable 结构体,它的 fd 属性就是打开的所有文件数组,这样一看也就恍然大悟了。用户传进来的 fd 参数实际就是 fdtable 内的文件数组的索引。所以,文件描述符其实就是file结构体数组的索引

相信关于后面

  • 被回收后的文件描述符如何再次分配
  • 文件描述符为什么从0开始
  • 文件描述符为什么不能为负数

这些问题你都能迎刃而解了。

参考

1. Linux内核源码在线查看

2. 孤儿进程与僵尸进程总结

3. 文件描述符

全文完


我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。

ipynb和py文件一样吗_文件描述符了解一下相关推荐

  1. jsp文件上传_文件上传

    一.文件上传的目的--脚本文件 文件上传的一共可造成三种危害,从低到高分别是,任意内容文件,html文件,脚本文件. 任意内容文件 任意内容文件指的是虽然文件后缀不可控,但是文件内容可控,比如我可以上 ...

  2. httpclient base64 文件上传_文件上传下载

    说道文件上传下载,这个业务需求并不是很复杂思想如下 1.将文件上传到 某台服务器上的指定的路径下也可以这样理解 文件上传就是将本地图片发送到别的地方,下载就是将别的地方的图片放在本地 2.将路径同文件 ...

  3. httppostedfilebase.saveas后文件被占用_文件过多时ls命令为什么会卡住?

    不知道你有没有遇到过当一个文件夹下文件特别多,在下面执行ls命令的时候要等好长时间才能展现出来的问题?如果有,你有想过这是为什么吗,我们该如何解决? 要想深入理解这个的问题产生的原因,我们就需要从文件 ...

  4. python引入文件并执行_文件操作和导入os模块执行文件和目录管理操作

    文件概念以及文本文件和二进制文件的区别 计算机的CPU如果想要访问保存在磁盘上的文件,第一步需要把磁盘上的文件数据加载到内存中.因为内存的读写速度要比磁盘的读写速度快很多. 计算机只能识别0101这种 ...

  5. java上传文件的二进制_文件的上传:二进制文件的上传;

    ***二进制文件上传的方法: ***在lib目录下:导入文件上传的开源架包:commons-fileupload-1.2.1.jar,commons-io-2.0.jar: 建立Servlet类:Up ...

  6. win10文件夹加密_文件加密精灵,保护你的隐私!

    官方介绍: 文件加密精灵是一款使用方便,安全可靠的文件加密利器.电脑上的任何文件都能做加密处理,有效的保证你的隐私. 主要功能: 文件加密 .文件夹加密 .软件加密(程序锁.软件锁).文件夹伪装.文件 ...

  7. channelsftp 上传文件为空_文件上传踩坑记及文件清理原理探究

    目录 1. 糟糕的异步存储文件实现 2. 异常原因推理 3. 问题解决方式 4. spring清理文件原理 5. tomcat清理文件原理 最近搞一个文件上传功能,由于文件太大,或者说其中包含了比较多 ...

  8. java 实现文件秒传_文件传输和秒传

    InetAddress类: InetAddress类是IP地址的封装类,就是把设定的某个ip封装成InetAddress对象,然后使用这个对象能够进行相关的操作.例如获取域名或主机名.上网ip等等.这 ...

  9. python打开文件夹对话框_文件对话框打开文件夹中的文件(tkinter)

    我想把它实现到我自己的代码中,但是当我运行这个(没有我的代码,只有你看到的代码)时,所有显示的文件夹都是空的,我实际上不能打开任何东西.在from tkinter import * from tkin ...

最新文章

  1. 设计模式 之美 -- 建造者模式
  2. PowerDesigner逆向工程mysql
  3. 【问底】严澜:数据挖掘入门——分词
  4. #035 大数阶乘 PTA题目6-10 阶乘计算升级版 (20 分)
  5. C# SQLite编程总结
  6. VS2005 ATL WINDOWS服务感想
  7. Android studio3.0打开Device File Explore(文件管理器)的方法(图文教程)
  8. js把日期字符串转换成时间戳
  9. Intent的一些用法
  10. 抽象同步器AQS应用之--阻塞队列BlockingQueue,如何保证任务一定被消费?
  11. 程序员常用字体(vs2008配色方案)
  12. Visio 图案填充-设置形状格式
  13. 快速求2的n次幂(防Time Limit Exceeded)
  14. 采样示波器和实时示波器的原理及优势
  15. 高级控件及自动提示文本框与下拉列表
  16. 前端-h5移动端星空效果登录界面
  17. 记一次利用tomcat manager部署war包失败并解决的经历
  18. python 高德地图交通态势爬取(存入mysql)
  19. 从虚拟走向现实!数字孪生迎来崛起
  20. 为什么我选择离开工作9年的腾讯?

热门文章

  1. 《SQL Server企业级平台管理实践》读书笔记——几个系统库的备份与恢复
  2. typedef BOOL(WINAPI *MYFUNC) (HWND,COLORREF,BYTE,DWORD);语句的理解
  3. 掌握技术思想比解决问题本身更重要
  4. NetBeans 时事通讯(刊号 # 124 - Nov 11, 2010)
  5. 解惑:为什么云计算和物联网会同时出现——微云网络
  6. MPLS标签分配控制方式——Vecloud
  7. intel 汇编中断解释
  8. HTML5新特性介绍
  9. 多看看把,条件太多了--leetcode 93. 复原 IP 地址
  10. Leetcode 25.K个一组翻转链表