linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?

文件描述符是什么

  • 文件描述符(file descriptor)
    • 1、背景(何为文件?)
    • 2、定义
    • 3、具体应用
    • 4、与fd相关的内核维护的3个数据结构
    • 5、socket 和 文件描述符之间的关系
  • 参考

文件描述符(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 是错误
linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第1张图片

linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第2张图片
对于一般的计算机,输入流是键盘输出流是显示器错误流也是显示器,所以现在这个进程和内核连了三根线。因为硬件都是由内核管理的,我们的进程需要通过「系统调用」让内核进程访问硬件资源。

如果我们写的程序需要其他资源,比如打开一个文件进行读写,这也很简单,进行系统调用,让内核把文件打开,这个文件就会被放到files的第 4 个位置:
linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第3张图片
明白了这个原理,输入重定向就很好理解了,程序想读取数据的时候就会去files[0]读取,所以我们只要把files[0]指向一个文件,那么程序就会从这个文件中读取数据,而不是从键盘:

$ command < file.txt

linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第4张图片
同理,输出重定向就是把files[1]指向一个文件,那么程序的输出就不会写入到显示器,而是写入到这个文件中:

$ command > file.txt

linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第5张图片
管道符其实也是异曲同工,把一个进程的输出流另一个进程的输入流接起一条「管道」,数据就在其中传递,不得不说这种设计思想真的很优美:

$ cmd1 | cmd2 | cmd3

linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第6张图片

4、与fd相关的内核维护的3个数据结构

linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第7张图片

  • 进程级文件描述符表(file descriptor table)

    • 系统为每个进程维护一份文件描述符表,该表的每一个条目都记录了单个文件描述符的相关信息,包括:
      • 控制标志(flags),目前内核仅定义了一个,即close-on-exec
      • 打开文件描述体指针
  • 系统级打开文件表(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元素,从而关联物理文件
  • 文件系统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指针,最终对应到真实的文件
    linux程序运行过程中的文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?_第8张图片
    以上图为例进行简单实例说明:
  • 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

你可能感兴趣的:(linux,file,descriptor,socket)