Linux文件描述符
Linux 系统中,把一切都看做是文件,当进程打开现有文件或创建新文件时,内核向进程返回一个文件描述符(file descriptor,fd)[1, 4],在windows下面,这玩意儿叫file handle,句柄。
文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4……[2]。
可以简单理解成系统维护的文件描述符表是一个数组,下标就是索引(文件描述符),数组内容就是一个个指向文件的指针(如0 -> stdin,1 -> stdout,2-> stderr)。
掌握它,有助于深入理解 Linux 文件系统、I/O 操作,
以及进程间通信(如管道(pipe)、套接字(Socket))的实现,可以去Ubuntu系统中简单完成下面的例子。<( ̄︶ ̄)↗[GO!]
优化界面的Blog:Linux文件描述符
用户程序与内核交互的基本过程
1. 打开文件
当一个用户程序需要访问某个文件时,它会通过系统调用(如 open())请求内核打开该文件。
内核会根据文件路径在文件系统中查找文件,并为该文件分配一个文件描述符。这个文件描述符是一个整数,表示该文件在内核中的唯一标识符。
内核维护着一个叫做 文件表(file table)的数据结构,文件描述符实际上就是对这个表中的一个条目的引用。
2. 使用文件描述符读写文件
用户程序使用文件描述符来进行后续的文件操作。例如:
读取文件:用户程序调用 read(fd, ...) 系统调用,内核通过文件描述符 fd 查找对应的文件,并从磁盘中读取数据,将数据返回给用户程序。
写入文件:用户程序调用 write(fd, ...) 系统调用,内核通过文件描述符 fd 查找文件,向文件中写入数据。
文件描述符使得内核能够识别哪个文件需要被操作,从而实现文件与程序的交互。
3. 文件描述符与文件表
内核通过文件描述符和文件表来管理已打开的文件。每个进程都有一个 文件描述符表,它是一个数组,其中每个索引对应一个文件描述符。这个文件描述符指向内核的文件表项,每个文件表项包含文件的状态信息(例如当前文件指针、文件权限等)。
4. 文件操作的内核层处理
用户程序和内核之间的交互通常通过系统调用来实现,文件描述符是这些系统调用的接口。内核会根据文件描述符执行相应的操作:
打开文件时,内核会创建或查找该文件的内核对象,并更新文件描述符。
对文件进行读写操作时,内核通过文件描述符在文件表中查找文件对象,然后执行 I/O 操作(例如读取磁盘或写入磁盘)。
5. 关闭文件
当用户程序完成文件操作后,它会通过系统调用 close(fd) 来关闭文件描述符。
内核会释放文件描述符所占用的资源,关闭文件的文件表项,并更新进程的文件描述符表。
通过文件描述符交互的具体例子
假设一个程序需要从文件中读取数据并进行处理,下面的示例代码:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buffer[128];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read");
close(fd);
return 1;
}
write(STDOUT_FILENO, buffer, bytesRead);
close(fd);
return 0;
}
在这个例子中,文件描述符的作用可以从以下几个步骤看到:
- 打开文件时,
open()
系统调用将返回一个文件描述符(fd),这个文件描述符在内核中表示 example.txt 文件的句柄。 - 读取文件时,
read()
系统调用使用文件描述符 fd 来访问内核中的文件表项,执行 I/O 操作并将文件数据读取到 buffer 中。 - 写入数据时,通过
write(STDOUT_FILENO, ...)
输出数据,STDOUT_FILENO 是标准输出的文件描述符(通常是 1)。 - 关闭文件时,
close(fd)
系统调用会释放文件描述符所占用的资源,告知内核文件已经关闭。
通过文件描述符,程序可以与操作系统内核进行有效的通信,完成文件系统和其他 I/O 操作。
Linux上查看文件描述符列表
在 Linux 上使用 vim 打开文件时,操作系统通过文件描述符与文件系统进行交互。下面是一个具体例子。
打开文件(使用 Vim)
首先,你使用 vim 打开一个文件(例如 helloworld.cpp
):
vim helloworld.cpp
这时,Vim 会启动并打开 helloworld.cpp
文件。在内核中,Vim 会使用文件描述符来与文件系统交互,即读取和编辑 helloworld.cpp
文件的内容。
在新 Shell 中查找 Vim 进程的 PID
接着,你可以打开另一个终端(Shell),通过 pidof
命令获取正在运行的 Vim 进程的进程 ID(PID):
pidof vim
假设返回的 PID 是 40133
,说明 Vim 进程的进程号是 40133
。
查看 Vim 进程的文件描述符
Linux 系统中的每个进程都有一个对应的 /proc/[pid]/fd 目录,里面列出了该进程打开的所有文件的文件描述符。你可以通过以下命令查看 Vim 进程所使用的文件描述符列表:
ll /proc/40133/fd
这里,40133
是你之前通过 pidof vim
命令获得的 Vim 进程的 PID。
ll
命令会列出该目录下的文件,其中每个文件都对应着一个打开的文件描述符(文件句柄)。输出会类似于:
total 0
dr-x------ 2 allen allen 0 Dec 29 15:58 ./
dr-xr-xr-x 9 allen allen 0 Dec 29 15:58 ../
lrwx------ 1 allen allen 64 Dec 29 15:58 0 -> /dev/pts/8
lrwx------ 1 allen allen 64 Dec 29 15:58 1 -> /dev/pts/8
l-wx------ 1 allen allen 64 Dec 29 15:58 19 -> /home/allen/.vscode-server/data/logs/20241229T150828/ptyhost.log
lrwx------ 1 allen allen 64 Dec 29 15:58 2 -> /dev/pts/8
l-wx------ 1 allen allen 64 Dec 29 15:58 20 -> /home/allen/.vscode-server/data/logs/20241229T150828/remoteagent.log
lrwx------ 1 allen allen 64 Dec 29 15:58 21 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 22 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 23 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 4 -> /home/allen/CPP/.helloworld.cpp.swp
这里的输出解释如下:
- 0,1,2:这是标准输入、标准输出和标准错误,它们通常会指向终端设备(如
/dev/pts/8
)。这些文件描述符是系统默认打开的,用于处理进程的 I/O 操作。 - 4:这个文件描述符指向你用 Vim 打开的文件
helloworld.cpp
。可以看到,/home/allen/CPP/.helloworld.cpp.swp
是文件描述符 4 对应的目标文件。
说明:
- 在 Linux 中,每个进程都会为打开的文件、管道、设备、套接字等分配一个文件描述符,文件描述符的值通常是从 0 开始递增的。
- 标准输入(0)、标准输出(1)、标准错误(2)是系统自动打开的,而打开的文件
helloworld.cpp
在 Vim 进程中会被分配到文件描述符 3 及以后。
可以看到新打开的 helloworld.cpp
的文件描述符,竟然是4,而不是从3开始,这里面有一番学问,涉及 vim
的原理。因为vim这种编辑器的原理是先打开源文件并拷贝,然后关闭源文件再打开自己的副本,修改完文件保存的时候直接将副本重命名覆盖源文件。所以打开源文件的时候用的文件描述符3,然后打开自己的副本是时候就该用文件描述符4了,然后关闭源文件,文件描述符3就被释放了,我们查看的时候就只剩下了4,这里它指向的是vim创建的副本文件[3](这里有更详细的解释,这里是一个通俗的理解)。
检查文件描述符的具体信息
可以通过查看符号链接来了解更多细节,例如,可以用 readlink
命令查看文件描述符指向的文件路径:
readlink /proc/40133/fd/4
深入理解 Linux 中的文件描述符及其背后的数据结构
要深入理解 Linux 中的文件描述符及其背后的数据结构,我们需要了解内核如何通过三个核心数据结构来管理文件描述符:
- 进程级的文件描述符表(Process File Descriptor Table)
- 系统级的打开文件描述符表(System-wide Open File Table)
- 文件系统的 i-node 表(File System i-node Table)
这三个数据结构[4]共同工作,使得 Linux 系统能够高效地管理文件 I/O 操作,并确保每个进程对文件的访问是独立且有序的。
1. 进程级的文件描述符表
每个运行中的进程都有一个 进程控制块(PCB),它包含了与进程相关的各种信息。在这个 PCB 中,文件描述符表(也称为 文件描述符数组)是一个非常重要的数据结构。
- 文件描述符表的功能:每个进程的文件描述符表记录着该进程所打开的文件描述符。文件描述符是一个整数,它对应着进程所打开的文件、套接字、管道等资源。
- 表的结构:文件描述符表是一个数组,每个文件描述符对应数组中的一个位置。例如,标准输入、标准输出、标准错误默认分别对应文件描述符 0、1、2,而其他文件则由内核为每个进程分配一个较大的文件描述符,如 3、4、5 等。
- 进程独立性:进程级文件描述符表是进程私有的。不同进程之间是独立的,进程 A 使用文件描述符 3 打开的文件,进程 B 如果也打开了一个文件,可能也会被分配文件描述符 3。它们的文件描述符对应的是不同的文件资源。
进程级文件描述符表的关键点:
- 每个进程都维护一个自己的文件描述符表。
- 文件描述符表的每个条目对应一个打开的文件或资源。
- 文件描述符表存储的只是文件描述符与内核内部文件对象的引用。
2. 系统级的打开文件描述符表
系统级的 打开文件描述符表 是内核维护的一个全局数据结构,用来管理系统中所有进程共享的文件资源。每当一个进程打开文件时,内核会在此表中创建一项记录,表示这个文件被打开。
该表中的每项记录包含以下信息:
- 当前文件偏移量:每个文件都有一个当前的读取或写入位置。当进程执行
read()
或write()
操作时,内核会根据该文件的偏移量进行相应的读写操作。在每次读取时,偏移量会自动更新,也可以通过lseek()
系统调用显式地修改偏移量。 - 打开文件时的标识:由
open()
系统调用的flags
参数指定,如只读、只写、读写等。内核在打开文件时会记录这些标识,用于后续的访问控制。 - 文件访问模式:当进程通过
open()
打开文件时,内核会记录文件的访问模式(如只读模式O_RDONLY
,只写模式O_WRONLY
,读写模式O_RDWR
)以及其他访问权限(如O_APPEND
、O_NONBLOCK
等)。 - 与信号驱动相关的设置:某些文件(如终端设备)可以通过信号驱动模式进行 I/O 操作。这些设置会记录在系统级的打开文件表中,以便内核在合适的时机处理信号。
- 与文件的 i-node 关联:系统级的打开文件描述符表项会保存指向文件系统 i-node 表项的指针。i-node 表项包含了该文件的元数据(如文件大小、权限、时间戳等)。
关键点:
- 每个进程打开文件时,系统级打开文件表会创建相应的记录。
- 所有进程共享系统级的打开文件描述符表,通过这个表来管理文件的偏移量和访问模式等信息。
3. 文件系统的 i-node 表
i-node 是 索引节点(Index Node)的缩写,是文件系统用来存储文件元数据的一种数据结构。每个文件都有一个对应的 i-node,i-node 不存储文件的内容,而是存储与文件相关的各种属性和元数据。
i-node 表包含以下内容:
- 文件类型:例如普通文件、目录文件、符号链接、套接字、FIFO 等。
- 文件权限:表示文件的访问权限(如读、写、执行权限)。
- 文件大小:文件的实际大小(字节数)。
- 时间戳:包括文件的创建时间、修改时间和访问时间(如
ctime
、mtime
、atime
等)。 - 指向文件数据块的指针:i-node 会存储指向文件实际数据块的指针(对于小文件直接存储指针,对于大文件使用间接块)。这些指针帮助操作系统在磁盘上定位文件内容。
- 文件锁列表:如果文件被加锁,i-node 会存储一个指向锁信息的指针。
- 引用计数:记录有多少个进程或文件描述符正在使用该文件。如果引用计数为 0,则表示该文件可以被删除。
i-node 的关键点:
- i-node 存储文件的元数据,而不存储文件的实际内容。
- 每个文件在文件系统中都有唯一的 i-node。
- 文件的内容由磁盘上的数据块来存储,而 i-node 中存储的是指向这些数据块的指针。
文件描述符如何协同工作
文件描述符表、系统级的打开文件描述符表和 i-node 表相互协作来管理文件资源:
- 进程级文件描述符表 存储进程所打开的文件描述符,它是进程私有的。当进程通过
open()
打开一个文件时,内核会在进程的文件描述符表中分配一个文件描述符,并且该文件描述符指向 系统级的打开文件描述符表 中的一个记录。 - 系统级的打开文件描述符表 存储所有打开文件的状态信息,如文件偏移量、访问模式等,并且每个表项都会指向对应文件的 i-node。
- i-node 表 存储文件的元数据(如权限、大小等)以及文件内容所在的磁盘位置。每个打开的文件都会通过 i-node 来访问文件的实际数据。
当进程进行文件操作时(如 read()
、write()
),操作首先通过进程级的文件描述符表查找对应的文件描述符,然后在系统级的打开文件描述符表中查找该文件的状态信息,并通过 i-node 访问文件的实际数据。
总结
- 进程级的文件描述符表:每个进程独立维护,记录当前进程打开的文件描述符。
- 系统级的打开文件描述符表:所有进程共享,记录文件的状态信息和 i-node 引用。
- 文件系统的 i-node 表:记录文件的元数据和实际数据的位置信息。
这三个数据结构协作,使得 Linux 系统能够高效且灵活地管理文件 I/O 操作,确保进程之间的文件访问独立且有序,并且能够在多进程环境中正确地管理文件资源。
参考
1、[理解Linux的文件描述符FD与Inode]
2、[理解linux中的file descriptor(文件描述符)]
3、[彻底弄懂 Linux 下的文件描述符(fd)]
4、[Linux文件描述符到底是什么?]
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。