Linux | 文件描述符
文章目录
- Linux | 文件描述符
- 1. 文件描述符概述
- 2. 与文件描述符关联的数据结构
- 2.1 进程级的文件描述符表(`struct files_struct`)
- 2.2 文件描述符表项(`struct fdtable`)
- 2.3 文件对象(`struct file`)
- 2.4 索引节点(`struct inode`)
- 3. 数据结构之间的关系
- 4. 文件描述符相关系统调用
- 4.1 `open` 函数
- 4.2 `read` 函数
- 4.3 `write` 函数
- 4.4 `close` 函数
- 5. 代码示例及现象观察
- 5.1 示例代码
- 5.2 代码解释
- 5.3 编译和运行
- 5.4 现象观察
- 6. 标准文件描述符示例
Linux | 文件描述符
1. 文件描述符概述
在 Linux 系统里,文件描述符(File Descriptor)是一个非负整数,它是对文件或者 I/O 设备(如键盘、显示器等)的抽象表示。当进程打开一个现有文件或者创建一个新文件时,内核会为该进程分配一个文件描述符,进程通过这个文件描述符来对文件进行各种操作,例如读取、写入、关闭等。
每个进程都有一个对应的文件描述符表,文件描述符就是该表的索引。系统默认会为每个进程打开三个标准的文件描述符:
- 标准输入(
stdin
):文件描述符为 0,通常关联键盘,用于读取用户输入。 - 标准输出(
stdout
):文件描述符为 1,通常关联显示器,用于输出信息。 - 标准错误(
stderr
):文件描述符为 2,也关联显示器,主要用于输出错误信息。
2. 与文件描述符关联的数据结构
2.1 进程级的文件描述符表(struct files_struct
)
每个进程都有一个 files_struct
结构体,它管理着该进程打开的所有文件描述符。这个结构体包含一个指针数组,数组的索引就是文件描述符,每个数组元素指向一个 file
结构体。
struct files_struct {
atomic_t count;
struct fdtable __rcu *fdt;
struct fdtable fdtab;
// 其他成员...
};
count
:表示当前结构体的引用计数。fdt
:指向一个fdtable
结构体,该结构体包含了文件描述符表的具体信息。
2.2 文件描述符表项(struct fdtable
)
fdtable
结构体包含了文件描述符数组以及其他相关信息,它是 files_struct
结构体中 fdt
指针所指向的对象。
struct fdtable {
unsigned int max_fds;
struct file __rcu **fd; /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
// 其他成员...
};
max_fds
:表示当前文件描述符表的最大容量。fd
:是一个指向file
结构体指针的数组,数组的索引就是文件描述符。
2.3 文件对象(struct file
)
file
结构体代表一个打开的文件,它包含了文件的状态信息、文件偏移量、操作函数指针等。
struct file {
//.....
atomic_long_t f_count; /*引用计数,管理文件对象的生命周期*/
struct mutex f_pos_lock; /*保护文件未知的互斥锁*/
loff_t f_pos; /*当前文件位置(读写位置)*/
//.....
struct path f_path; /*记录文件路径*/
struct inode *f_inode; /*指向与文件相关联的inode对象的指针,该对象用于维护文件元数据,如文件类型、访问权限等*/
const struct file_operations *f_op;
/*指向文件操作函数表的指针,定义了文件支持的操作,如读、写、锁定等*/
unsigned int f_flags; /*表示文件的打开标志,如`O_RDONLY`、`O_WRONLY`等*/
fmode_t f_mode; /*表示文件的访问模式*/
// 其他成员...
};
2.4 索引节点(struct inode
)
inode
结构体代表文件在磁盘上的元数据,包含了文件的权限、大小、创建时间、数据块位置等信息。
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;
// 其他成员...
};
i_mode
:表示文件的权限和类型。i_uid
和i_gid
:分别表示文件的所有者用户 ID 和组 ID。i_op
:指向一个inode_operations
结构体,该结构体包含了对inode
进行各种操作的函数指针。i_sb
:指向文件所在的超级块结构体。
3. 数据结构之间的关系
当一个进程调用 open
系统调用打开一个文件时,内核会进行以下操作:
- 在内核中创建一个
file
结构体,用于表示这个打开的文件,并初始化其成员变量,如f_flags
、f_mode
等。 - 从进程的
files_struct
结构体中找到一个空闲的文件描述符,该文件描述符对应的fdtable
数组元素指向新创建的file
结构体。 - 根据文件路径找到对应的
inode
结构体,并将file
结构体中的f_inode
指针指向该inode
结构体。
这样,进程就可以通过文件描述符访问对应的 file
结构体,进而通过 file
结构体中的 f_op
指针调用相应的操作函数对文件进行读写等操作。
4. 文件描述符相关系统调用
4.1 open
函数
open
函数用于打开或创建一个文件,并返回一个文件描述符。其原型如下:
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname
:要打开或创建的文件的路径。flags
:打开文件的方式,例如O_RDONLY
(只读)、O_WRONLY
(只写)、O_RDWR
(读写)等。mode
:如果创建新文件,指定文件的权限,例如0666
表示所有用户都有读写权限。
4.2 read
函数
read
函数用于从文件描述符对应的文件中读取数据。其原型如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
fd
:文件描述符。buf
:用于存储读取数据的缓冲区。count
:要读取的字节数。
4.3 write
函数
write
函数用于向文件描述符对应的文件中写入数据。其原型如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
fd
:文件描述符。buf
:要写入的数据的缓冲区。count
:要写入的字节数。
4.4 close
函数
close
函数用于关闭一个文件描述符,释放相关资源。其原型如下:
#include <unistd.h>
int close(int fd);
fd
:要关闭的文件描述符。
5. 代码示例及现象观察
5.1 示例代码
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int main() {
int fd;
char buffer[BUFFER_SIZE] = "Hello, File Descriptor!";
char read_buffer[BUFFER_SIZE];
// 打开一个文件,如果文件不存在则创建,以只写模式打开
fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd == -1) {
perror("open");
return 1;
}
// 从数据结构角度看,此时内核创建了 file 结构体,从进程的 files_struct 找到空闲 fd 指向该 file 结构体,file 结构体的 f_inode 指向文件对应的 inode 结构体
// 向文件中写入数据
ssize_t bytes_written = write(fd, buffer, strlen(buffer));
if (bytes_written == -1) {
perror("write");
close(fd);
return 1;
}
printf("Written %zd bytes to test.txt\n", bytes_written);
// 关闭文件
if (close(fd) == -1) {
perror("close");
return 1;
}
// 以只读模式打开文件
fd = open("test.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
// 从文件中读取数据
ssize_t bytes_read = read(fd, read_buffer, BUFFER_SIZE);
if (bytes_read == -1) {
perror("read");
close(fd);
return 1;
}
read_buffer[bytes_read] = '\0';
printf("Read %zd bytes from test.txt: %s\n", bytes_read, read_buffer);
// 关闭文件
if (close(fd) == -1) {
perror("close");
return 1;
}
return 0;
}
5.2 代码解释
- 打开文件:使用
open
函数以只写模式打开test.txt
文件,如果文件不存在则创建,同时截断文件内容。如果打开成功,会返回一个文件描述符。从数据结构角度,内核会创建一个file
结构体,从进程的files_struct
中找到一个空闲的文件描述符,让该文件描述符对应的fdtable
数组元素指向新创建的file
结构体,并且file
结构体中的f_inode
指针会指向文件对应的inode
结构体。 - 写入数据:使用
write
函数将字符串"Hello, File Descriptor!"
写入文件。此操作会通过文件描述符找到对应的file
结构体,然后根据file
结构体中的f_op
指针调用相应的写入函数。 - 关闭文件:使用
close
函数关闭文件描述符,释放资源。此时会减少file
结构体的引用计数f_count
,当引用计数为 0 时,内核会释放file
结构体。 - 再次打开文件:以只读模式打开
test.txt
文件,重复上述打开文件的流程。 - 读取数据:使用
read
函数从文件中读取数据,并将其存储在read_buffer
中。同样是通过文件描述符找到file
结构体,调用f_op
中的读取函数。 - 输出结果:将读取到的数据输出到终端。
5.3 编译和运行
将上述代码保存为 file_descriptor.c
,然后使用以下命令编译和运行:
gcc -o file_descriptor file_descriptor.c
./file_descriptor
5.4 现象观察
- 运行程序后,会在当前目录下创建一个
test.txt
文件,并将字符串"Hello, File Descriptor!"
写入该文件。 - 程序会输出写入和读取的字节数,以及读取到的内容。
6. 标准文件描述符示例
#include <unistd.h>
int main() {
const char *message = "Hello, Standard Output!\n";
// 使用标准输出文件描述符(1)写入数据
write(1, message, strlen(message));
return 0;
}
在这个示例中,我们直接使用标准输出的文件描述符(1)将字符串 "Hello, Standard Output!"
输出到终端。编译并运行该程序,你会看到字符串显示在终端上。标准文件描述符对应的 file
结构体在进程启动时就已经初始化好,分别关联到标准输入、标准输出和标准错误设备。