文件描述符(file descriptor)
1、背景(何为文件?)
在Linux操作系统中,可以将一切都看作是文件,包括普通文件,目录文件,字符设备文件(如键盘,鼠标…),块设备文件(如硬盘,光驱…),套接字等等,所有一切均抽象成文件,提供了统一的接口,方便应用程序调用
既然在Linux操作系统中,你将一切都抽象为了文件,那么对于一个打开的文件,我应用程序怎么对应上呢?
2、定义
文件描述符:简称fd,为了高效管理已被打开的文件所创建的索引
。其fd本质上就是一个非负整数
(通常是小整数)。
- 当应用程序请求内核打开/新建一个文件时,内核会返回一个文件描述符用于对应这个打开/新建的文件,读写文件也是需要使用这个文件描述符来指定待读写的文件的。
- 在linux系统中,所有的文件操作,都是通过fd来定位资源和状态的。
3、具体应用
先说files
,它是一个文件指针数组
。一般来说,一个进程会从files[0]读取
输入,将输出写入files[1]
,将错误信息写入files[2]
。
举个例子,以我们的角度 C语言的
printf函数
是向命令行打印字符,但是从进程的角度来看,就是向files[1]写入数据
;同理,scanf函数
就是进程试图从files[0]
这个文件中读取数据。
每个进程被创建时,files的前三位
被填入默认值,分别指向标准输入流
、标准输出流
、标准错误流
。
- 如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码。
我们常说的「文件描述符」就是指这个文件指针数组的索引
,所以程序的文件描述符默认情况下 0 是输入
,1 是输出
,2 是错误
。
对于一般的计算机,输入流是键盘
,输出流是显示器
,错误流也是显示器
,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理
的,我们的进程需要通过「系统调用
」让内核进程
访问硬件资源。
如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到files的第 4 个位置:
明白了这个原理,输入重定向
就很好理解了,程序想读取数据的时候就会去files[0]读取
,所以我们只要把files[0]指向一个文件
,那么程序就会从这个文件中读取数据,而不是从键盘:
$ command < file.txt
同理,输出重定向
就是把files[1]指向一个文件
,那么程序的输出就不会写入到显示器,而是写入到这个文件中:
$ command > file.txt
管道符
其实也是异曲同工,把一个进程的输出流
和另一个进程的输入流
接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美:
$ cmd1 | cmd2 | cmd3
4、与fd相关的内核维护的3个数据结构
-
进程级文件描述符表
(file descriptor table)
- 系统为每个进程维护一份文件描述符表,该表的每一个条目都记录了单个文件描述符的相关信息,包括:
- 控制标志(flags),目前内核仅定义了一个,即
close-on-exec
- 打开文件描述体指针
- 控制标志(flags),目前内核仅定义了一个,即
- 系统为每个进程维护一份文件描述符表,该表的每一个条目都记录了单个文件描述符的相关信息,包括:
-
系统级打开文件表
(open file table)
- 内核对所有打开的文件维护一个系统级别的打开文件描述表
(open file description table
)。表中的条目称为打开文件描述体(open file description
),存储了与一个打开的文件相关的全部信息,包括:- 1、
文件偏移量(current file offset)
,调用read()和write()更新,调用lseek()直接修改 - 2、
访问模式(file status flags)
,由open()调用设置,例如:只读、只写或读写等 - 3、
i-node对象指针(v-node ptr)
,指向一个inode元素,从而关联物理文件
- 1、
- 内核对所有打开的文件维护一个系统级别的打开文件描述表
-
文件系统i-node表
(i-node table)
- 就像进程用pid来描述和定位一样,在linux系统中,文件使用inode号来描述,inode存储了文件的很多元信息。
- 每个文件系统会为存储于其上的所有文件(包括目录)维护一个i-node表,单个i-node包含以下信息:
- 文件类型(file type),可以是常规文件、目录、套接字或FIFO
- 文件的字节数
- 文件拥有者的User ID 文件的Group ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
- 链接数,即有多少文件名指向这个inode
- 文件数据block的位置
3个数据结构对应关系
- 1、应用程序进程拿到的
文件描述符ID
,等于拿到进程文件描述符表的索引
; - 2、通过索引拿到
文件指针
,指向系统级文件描述符表的文件偏移量
; - 3、再通过文件偏移量找到
inode指针
,最终对应到真实的文件
以上图为例进行简单实例说明: - 1、在进程A中,文件
描述符1和30
都指向了同一个打开的文件句柄(标号23
)。这可能是通过调用dup()、dup2()、fcntl()
或者对同一个文件多次调用了open()函数而形成的。 - 2、进程A的文件
描述符2
和进程B的文件描述符2
都指向了同一个打开的文件句柄(标号73
)。这种情形可能是:- 在调用fork()后出现的(即,进程A、B是父子进程关系),
- 或者当某进程通过UNIX域套接字将一个打开的文件描述符传递给另一个进程时,也会发生。
- 再者是不同的进程独自去调用open函数打开了同一个文件,此时文件描述符相同。
- 3、进程A的
描述符0
和进程B的描述符3
分别指向不同的打开文件句柄,但这些句柄均指向i-node表
的相同条目(1976
),换言之,指向同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了open()调用。同一个进程两次打开同一个文件,也会发生类似情况。
5、socket 和 文件描述符之间的关系
套接字也是文件。具体数据传输流程如下:
-
当
server端
监听到有连接时,应用程序会请求内核创建Socket; -
Socket创建好后会
返回一个文件描述符
给应用程序; -
当有数据包过来网卡时,内核会通过数据包的
源端口,源ip,目的端口
等在内核维护的一个ipcb双向链表
中找到对应的Socket,并将数据包赋值到该Socket的缓冲区
; -
应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据
注意:
操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。
参考
1、https://www.cnblogs.com/zhangmingda/p/11715113.html
2、https://zhuanlan.zhihu.com/p/105086274
3、https://segmentfault.com/a/1190000009724931