文件描述符是什么,简单解释一下?

607
  1. 相较于维基百科,文件描述符的更简化描述是什么?它们为什么是必须的?以shell进程为例,它如何应用?

  2. 进程表是否包含多个文件描述符?如果是,为什么?


4
stdin、stdout和stderr等概念是什么?比如,我有一个浏览器进程打开了一些临时文件来显示我的HTML页面。该进程是否使用相同的fd来读/写这些文件?还有,进程表中的条目……它们具有fd0指针、fd1指针、fd2指针等,这是否意味着所有这些文件都在RAM中?否则为什么要用指针? - Nishant
87
打开文件时,操作系统会创建一个流并将该流连接到打开的文件上,描述符实际上代表了该流。同样地,操作系统也创建了一些默认流,这些流连接到终端而非文件上。因此,当您在终端中输入内容时,它会进入 stdin 流和操作系统。而当您在终端中键入 "ls" 命令时,操作系统会将输出写入 stdout 流。stdout 流连接到您的显示器终端,因此您可以在那里看到输出结果。 - Tayyab
2
关于浏览器示例,浏览器不必保持文件打开。这取决于浏览器的实现方式,但在大多数情况下,浏览器会打开一个临时文件,写入文件,然后关闭文件,因此即使网页打开,也不必打开文件。描述符只是保存文件信息,并不一定将文件保存在RAM中。当您从描述符读取数据时,操作系统会从硬盘中读取数据。文件描述符中的信息仅表示硬盘上文件的位置等信息。 - Tayyab
7
文件描述符与文件之间不是一对一的映射。我可以通过多次调用 open() 来获得 4 个不同的文件描述符,每个文件描述符根据传递给 open() 的标志可以用于读取、写入或同时读写相应的文件。至于文件是存在 RAM 还是磁盘上这一点,由内核和它的各种缓存隐藏起来。最终,缓存中的数据将与磁盘上的数据匹配(在写入时),如果数据已经在缓存中,则内核不会再回到磁盘中进行读取。 - Beano
10
这是一篇易于理解的好文章 https://www.bottomupcs.com/file_descriptors.xhtml - Krishan Gopal
显示剩余6条评论
13个回答

812
简单来说,当您打开一个文件时,操作系统会创建一个条目来表示该文件并存储有关该打开文件的信息。因此,如果您的操作系统中有100个文件已打开,则在操作系统中(某个地方在内核中)将有100个条目。这些条目由整数表示(如…100、101、102…)。此条目号称为文件描述符。 因此,它只是一个整数号码,用于唯一表示进程所打开的文件。 如果您的进程打开了10个文件,则您的进程表将具有10个文件描述符条目。 同样,当您打开网络套接字时,它也由一个整数表示,并称为套接字描述符。 希望您理解。

13
如果你一次性打开了很多文件,就会用尽文件描述符,这也是为什么会出现这种情况。而*nix系统会一直打开/proc中的文件描述符,这可能导致系统无法继续运行。请注意,文件描述符是一种操作系统资源,用于识别打开的文件。 - Spencer Rathbun
9
@ErbenMo:不一定相同。当您打开文件时,操作系统会分配一个可用的FD,当您关闭文件时,操作系统会释放该FD,并可能将该FD分配给此后打开的另一个文件。这是操作系统跟踪已打开的文件的方式,与特定文件无关。 - Tayyab
116
“因此,它只是一个整数数字,在操作系统中唯一地表示已打开文件。”这是不正确的。该整数唯一地表示进程内已打开的文件。例如,文件描述符0在一个进程中表示一个已打开的文件,在另一个进程中则代表完全不同的已打开文件。 - Keith Thompson
47
我认为你误解了。文件描述符0、1和2是每个运行进程的标准输入、标准输出和标准错误。即使另一个正在运行的进程恰好也有文件描述符3,成功的open()初始化调用将会给你返回文件描述符3。请参阅 open() 的 POSIX 定义:"open()函数将返回命名文件的文件描述符,该文件描述符是当前未打开的最低文件描述符,对于该进程而言"(强调添加)。 - Keith Thompson
36
@KeithThompson: 是的,你说得对。实际上它涉及到抽象层次。实际上有两个表维护,第一个是每进程的,第二个是系统级别的。在每进程表中(即fdtable)的FD在整个系统中不是唯一的。然而它映射到包含系统级别唯一条目的v-node表。因此,当您调用fopen()和fileno()函数来检查描述符时,可以在2个不同的进程中获得相同的FD号码,因为它返回的是每进程的fdtable的索引。感谢您提出这个问题! - Tayyab
显示剩余12条评论

193
文件描述符是一个不透明的句柄,用于标识用户和内核空间之间的文件/套接字资源接口。因此,当您使用open()socket()(系统调用与内核进行接口),您将获得一个文件描述符,它是一个整数(实际上是进程u结构中的索引-但这并不重要)。因此,如果您想直接与内核进行接口,使用系统调用read()write()close()等,你需要使用的句柄就是文件描述符。
系统调用之上有一层抽象层,称为stdio接口。它提供比基本系统调用更多的功能/特性。对于这个接口,您获得的不透明句柄是一个FILE*,由fopen()调用返回。有许多使用stdio接口的函数,如fprintf()fscanf()fclose(),这些函数旨在使您的编程更加容易。在C语言中,stdinstdoutstderr都是FILE*,在UNIX中分别映射到文件描述符012

164
听取专家的建议:APUE(Richard Stevens)。
对于内核而言,所有打开的文件都是通过文件描述符来引用的。文件描述符是一个非负整数。
当我们打开一个已存在的文件或创建一个新文件时,内核会返回一个文件描述符给进程。内核维护了一个包含所有打开文件描述符的表格。文件描述符通常是顺序分配的,它们被分配到池中作为下一个可用的文件描述符。当我们关闭文件时,文件描述符就会被释放,并且可以再次被分配使用。
当我们想要读取或写入文件时,我们通过在 open()create() 函数调用时返回的文件描述符来标识文件,并将其作为参数传递给 read()write() 函数。
UNIX系统的shell约定将文件描述符0与进程的标准输入相关联,将文件描述符1与标准输出相关联,将文件描述符2与标准错误输出相关联。文件描述符范围从0到OPEN_MAX,最大的文件描述符值可以通过 ulimit -n 命令获得。更多信息请参阅APUE书籍的第三章。
详见以下图片:
Two Process

1
由于0、1、2与进程的“stdin”、“stdout”和“stderr”相关联,我们是否可以同时将这些描述符用于不同的进程? - Tarik
2
@Tarik:文件描述符是每个进程独立的。您可以通过下载osquery并在bash shell中执行osqueryi <<< echo '.all process_open_files'来验证这一点。 - Ben Creasy
你说:“从马嘴里听到:APUE(Richard Stevens)。”然而,不清楚你引用的是 Stevens 的 APUE 中的哪一部分。请使用 > 来突出引用的段落。 - undefined

101

其他答案已经提供了很好的内容,我只想补充我的看法。

据维基百科所知:文件描述符是一个非负整数。我认为最重要的一点是缺失了以下内容:

文件描述符与进程ID绑定。

我们知道最常用的文件描述符是0、1和2。0对应于STDIN,1对应于STDOUT,2对应于STDERR

以shell进程为例,它如何应用这个概念?

请查看此代码。

#>sleep 1000 &
[12] 14726

我们创建了一个进程,其id为14726(PID)。 使用lsof -p 14726可以获得如下信息:

COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF    NODE NAME
sleep   14726 root  cwd    DIR    8,1     4096 1201140 /home/x
sleep   14726 root  rtd    DIR    8,1     4096       2 /
sleep   14726 root  txt    REG    8,1    35000  786587 /bin/sleep
sleep   14726 root  mem    REG    8,1 11864720 1186503 /usr/lib/locale/locale-archive
sleep   14726 root  mem    REG    8,1  2030544  137184 /lib/x86_64-linux-gnu/libc-2.27.so
sleep   14726 root  mem    REG    8,1   170960  137156 /lib/x86_64-linux-gnu/ld-2.27.so
sleep   14726 root    0u   CHR  136,6      0t0       9 /dev/pts/6
sleep   14726 root    1u   CHR  136,6      0t0       9 /dev/pts/6
sleep   14726 root    2u   CHR  136,6      0t0       9 /dev/pts/6

第四列FD和紧随其后的TYPE对应于文件描述符和文件描述符类型。

FD的一些取值可能是:

cwdCurrent Working Directory
txtText file
memMemory mapped file
mmapMemory mapped device

但是真正的文件描述符在以下位置:

NUMBER – Represent the actual file descriptor. 

数字后面的字符,例如"1u",代表文件打开的模式。r表示读取,w表示写入,u表示读取和写入。

TYPE指定文件的类型。一些TYPE值包括:

REG – Regular File
DIR – Directory
FIFO – First In First Out

但是所有的文件描述符都是CHR-字符特殊文件(或字符设备文件)。

现在,我们可以使用lsof -p PID轻松地识别STDINSTDOUTSTDERR的文件描述符,或者如果我们使用ls /proc/PID/fd,我们也可以看到相同的内容。

请注意,内核跟踪的文件描述符表与文件表或inode表不同。正如其他答案所解释的那样,它们是分开的。

fd table

您可能会问自己这些文件描述符在物理上位于哪里,例如/dev/pts/6中存储了什么。

sleep   14726 root    0u   CHR  136,6      0t0       9 /dev/pts/6
sleep   14726 root    1u   CHR  136,6      0t0       9 /dev/pts/6
sleep   14726 root    2u   CHR  136,6      0t0       9 /dev/pts/6

嗯,/dev/pts/6 仅存在于内存中。这些不是普通的文件,而是所谓的字符设备文件。你可以通过以下命令来检查:ls -l /dev/pts/6 ,它们将以c开头,在我的情况下是 crw--w----

只是为了回忆一下,大多数像 Linux 的操作系统定义了七种类型的文件:

  • 普通文件
  • 目录
  • 字符设备文件
  • 块设备文件
  • 本地域套接字
  • 命名管道(FIFOs)
  • 符号链接

5
谢谢。确实需要指出这是每个进程!这有助于更好地可视化事物。 - Nishant
4
你在回答中提到的操作系统定义的文件类型,确实有助于更深入地理解文件。 - Rohan Bhale
我在哪里可以学习更多关于文件描述符及其相关内容的知识? - the artist
@theartist 一本操作系统的书籍,例如Arpaci-Dusseau的《操作系统:三个简单部分》(链接:https://pages.cs.wisc.edu/~remzi/OSTEP/),是一个很好的起点,特别是关于文件系统的章节。然后是Stevens的《Unix环境高级编程》和/或Kerrisk的《Linux编程接口》。 - undefined
我会将“文件描述符绑定到进程ID”翻译为“文件描述符与特定进程绑定/唯一”。前者让人误以为文件描述符是根据进程ID计算出来的。 - undefined

29
文件描述符(FD)
在Linux/Unix中,一切皆为文件。常规的“文件”、目录,甚至设备都是文件。每个文件都有一个关联的数字,称为文件描述符(FD),它是一个非负整数,从0开始。
你的终端/控制台是一个设备,因此与之关联的有一个文件描述符。当一个正在执行的程序将某些内容打印到屏幕上时,输出会被发送到屏幕的文件描述符,然后由相关设备在屏幕上显示出来。类似地,如果程序的输出被发送到打印机的文件描述符,程序的输出就会被打印出来。
每当你在终端执行一个程序/命令时,shell会默认打开三个文件,每个文件都有一个关联的文件描述符和预先分配的角色。
文件 文件描述符 默认连接到 行为 标准输入 0 键盘 进程用于从某个源获取输入。默认情况下,从键盘获取。 标准输出 1 终端/控制台 进程用于将正常输出发送到某个接收器。默认情况下,发送到终端。 标准错误 2 终端/控制台 进程用于将错误输出发送到某个接收器。默认情况下,发送到终端。
因为它们默认打开并在任何给定的进程中具有预先分配的角色,它们被统称为标准流。

错误重定向

这些标准流对于重定向和管道非常重要。例如,如果提供的目录不存在,命令ls会将错误输出到终端:

$ ls ./non-existent
ls: ./nonexistent: No such file or directory

然而,通过使用输出重定向,您可以将输出发送到另一个文件,例如errors.log,通过使用标准错误的输出重定向元字符2>
$ ls ./non-existent 2> errors.log

这意味着“无论标准错误输出是什么,都将其重定向到文件errors.log,而不是屏幕”。现在errors.log包含了之前发送到屏幕的输出。
管道
标准流也使得将一个程序的输出作为另一个程序的输入变得简单。在shell中,您可以使用管道(|)元字符来实现这一点。
$ ls -l | wc -l

这意味着将ls -l命令的标准输出发送到wc -l命令的标准输入,然后将结果发送到屏幕上。

26

文件描述符 (File Descriptors,简称FD) 是与已打开文件相关联的非负整数 (0, 1, 2, ...)

  1. 0, 1, 2 是标准FD,对应于在程序启动时由shell默认打开的 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO(在 unistd.h 中定义)。

  2. FD按顺序分配,意味着分配的整数值越小,则优先级越高。

  3. 特定进程的FD可以在 Unix 系统上的 /proc/$pid/fd 中查看。


19

除了其他答案之外,unix 将一切都视为文件系统。从内核的角度来看,您的键盘是一个只读文件。屏幕则是一个只写文件。同样地,文件夹、输入输出设备等也被认为是文件。每当打开一个文件时,例如当设备驱动程序(用于设备文件)请求open()或进程打开用户文件时,内核会分配一个文件描述符,即指定该文件访问权限的整数,如只能读取,只能写入等。[参考:https://en.wikipedia.org/wiki/Everything_is_a_file]


1
文件描述符也可以指代文件系统中不存在的东西,比如匿名管道和网络套接字。 - kbolino
一切皆为文件系统,我想你的意思是“一切皆为文件”。 - undefined

16

文件描述符

  • 对于内核来说,所有打开的文件都通过文件描述符来引用,包括那些并非文件本身的实体,如匿名管道和网络套接字。
  • 文件描述符是一个非负整数,从0开始。
  • 当我们打开一个已存在的文件或创建一个新文件时,内核会将一个文件描述符返回给调用代码。
  • 当我们想要读取或写入一个文件时,我们使用由open()create()等函数返回的文件描述符来标识文件,并将其提供给read()write()
  • 初始时,每个UNIX进程都有20个可用的文件描述符,编号从0到19,但许多系统将其扩展到了63个。
  • 当进程派生一个子进程时,子进程会继承父进程的文件描述符。

12

文件描述符是非负整数,作为“文件”或I/O资源(如管道、套接字或数据流)的抽象句柄。这些描述符帮助我们与这些I/O资源交互,并使处理它们变得非常容易。对于用户进程而言,I/O系统可视为一系列字节(I/O流)。Unix进程使用描述符(小的无符号整数)来引用I/O流。与I/O操作相关的系统调用会将描述符作为参数。

有效的文件描述符范围从0到可配置的最大描述符号码(ulimit、/proc/sys/fs/file-max)。内核为FD表中的标准输入(0)、标准输出(1)和标准错误(2)分配描述符号。如果文件打开不成功,fd返回-1。FD

当进程成功请求打开一个文件时,内核会返回一个文件描述符,该描述符指向内核全局文件表中的一个条目。文件表条目包含诸如文件的inode、字节偏移和该数据流的访问限制(只读、只写等)之类的信息。


这是一个很好的图表,你从哪里得到的? - undefined

5
任何操作系统都有运行的进程(p),例如p1,p2,p3等。每个进程通常会持续使用文件。
每个进程由一个进程树(或进程表)组成。
通常,操作系统通过数字(也就是说,在每个进程树/表中)来表示每个进程中的每个文件。
在进程中使用的第一个文件是file0,第二个是file1,第三个是file2,以此类推。
任何这样的数字都是文件描述符。
文件描述符通常是整数(0、1、2而不是0.5、1.5、2.5)。
我们经常将进程描述为“进程表”,并且由于表具有行(条目),因此我们可以说每个条目中的文件描述符单元用于表示整个条目。
类似地,当您打开网络套接字时,它具有套接字描述符。
在某些操作系统中,您可能会耗尽文件描述符,但这种情况非常罕见,普通计算机用户不必担心。
文件描述符可能是全局的(进程A从0开始,结束于1;进程B从2开始,结束于3)等等,但据我所知,通常在现代操作系统中,文件描述符不是全局的,实际上是进程特定的(进程A从0开始,结束于5,而进程B从0开始,结束于10)。

在Linux中了解更多关于FD的内容,请访问此处:http://unix.stackexchange.com/questions/358022/is-it-true-to-conclude-that-there-are-4-types-of-output-we-can-reference-to - user3578082

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接