c语言 文件操作
文件操作
文件操作就两种: 读 和 写 , 这里读或者写都是站在程序的角度来看的。
文件路径
文件路径:文件在电脑中的位置
分类:
- 绝对路径 : 以盘符开始的路劲
C:/admin/a.txt
- 相对路径:以当项目作为参照文件路径:
aaa/a.txt
;
转义字符 ( \ )
转义字符:改变了原来字符的含义。
文件读取
步骤:
fopen
打开文件fgetc | fgets | fgetd
读取文件fclose
关闭文件,并且清洗流中数据 【在关闭流之前将流中的所有数据都冲刷到文件中或者程序中】
fgetc
特点:一个一个字符读取,读取不到内容返回 -1
#include <stdio.h>
int main() {
// 读取文件
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\a.txt", "r");
// 按照字节读取文件
/*char c = fgetc(f);
printf("%c ", c);*/
// 利用循环读取文件 , fgetc 读取不到内容就是返回 -1
int c;
while ((c = fgetc(f)) != -1) {
printf("%c", c);
}
fclose(f);
return 0;
}
fopen("C:\\Users\\admin\\Desktop\\html\\a.txt", "r");
第二个参数设置文件读写模式。
fgets
语法:fgets(读取到文件放入哪个地方[数组],每次最大读取多少个字符,文件对象)
注意:读取文件结果放入的地方【数组】的长度一般是写 1024 的整数倍。
#include <stdio.h>
int main() {
// 读取文件
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\a.txt", "r");
// 利用 fgets 来读取文件
// 参数介绍:参数1:读取到的内容存储到哪里, 参数2: 一次读取多个个字节 3. 文件流
// 细节:1. 每一次读一行的数据,根据是否有换行符来确定是否是一行数据
// 细节:2. 当读不到文件的内容返回 null ,返回值中才是 null
char str[1024];
// fgets 会返回文件的读取结果,同时也会讲读取结果放回参数一种
while (fgets(str, 1024, f) != NULL) {
printf("%s", str);
}
fclose(f);
return 0;
}
fred
作用:读取文件,一次读多少自定义
fread 函数
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
参数说明:
ptr
:指向一个缓冲区的指针,该缓冲区将用来存储从文件流中读取的数据。读取的数据都放入该局域中。新读取的内容会覆盖旧的size
:每个元素所占的字节数。例如,如果要读取int
类型的数据,那么size
就应该是sizeof(int)
。一般写出 char 也就是 1 字节,比较具有通用性。(以什么字节读取数据, 如果以 int 类型读取 4 )nmemb
:希望读取的元素数目。与size
结合起来可以确定总共需要读取的字节数(即size * nmemb
)。一次最多读取多少个字节。(即size * nmemb
), 读取元素的个数。stream
:指向一个已经打开的文件流的指针,数据将从此文件流中读取。- 返回值
size_t
是一个整数代表一次读取到多少个有效字符。换行符也是有效字符
注意点:fread 函数读取文件每次读取都会尽量将有效字符装满缓存区。
#include <stdio.h>
int main() {
// 读取文件
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\a.txt", "r");
// 利用 fread 读取文件
char arr[3];
// 返回值 n 代表每次读取的有效字符个数
//int n = fread(arr, sizeof(char), 1024, f); // 有问题的代码,拿着 3 字节的数组,去接收有可能返回 1024 字节的数据
//printf("%d \n", n);
//printf("%s \n", arr);
// char 对应英文字符是一个字节,但是在 win64中一个中文占用
//
int n;
while ((n = fread(arr, 1, 3, f)) != 0) {
for (int i = 0; i < n; i++)
{
// 按照字符遍历数组中的数据,如果数据中存在中文字符,会自动拼接中文字符【控制台做的】 【一个中文占用两个字节】
printf("%c", arr[i]);
}
}
fclose(f);
return 0;
}
注意点:
- 接收读取结果的数组应该大于等于一次读取总长度 (总长度 = 参数 2 * 参数3 ),不然出错。
fscanf 函数
新建文件 user.txt
,写入以下内容。
罗密欧 18 朱丽叶
贾宝玉 14 薛宝钗
梁山伯 16 祝英台
海哥
创建 fscanf_test.c
,写入以下内容。
#include <stdio.h>
int main() {
/*
FILE *__restrict __stream: 读取的文件
char *__restrict __format: 读取的匹配表达式
...: 变长参数列表 用于接收匹配的数据
return: 成功返回参数的个数 失败返回0 报错或结束返回EOF
int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...)
如果是读取到空行就进行优化操作。
*/
FILE *userFile = fopen("user.txt", "r");
if (userFile == NULL) {
printf("不能打开不存在的文件");
}
char name[50];
int age;
char wife[50];
int scanfR;
while (fscanf(userFile, "%s %d %s\n", name, &age, wife) != EOF) {
printf("%s在%d岁爱上了%s\n", name, age, wife);
}
int result = fclose(userFile);
if (result != 0) {
printf("关闭文件失败");
return 1;
}
return 0;
}
Makefile
补充以下内容。
fscanf_test: fscanf_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
注意:
- 字符串存储到只读常量区中是不能被修改的。
- 读取文件的函数,底层个结果赋值的方式是一个字节一个字节赋值的。
- 不要使用字符串常量来接收读取文件的函数,中读取文件的内容(接受返回结果的参数中不要使用字符串常量。)
- 可以使用栈空间的(字符数组) ,堆空间的(字符指针) 等等。
文件写入
fputc
语法:
int fputc(int char, FILE *stream)
参数:
- char -- 这是要被写入的字符。该字符以其对应的 int 值进行传递。
- stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符的流。
示例:
#include <stdio.h>
int main() {
// 1. 获取文件写入流
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\b.txt", "w");
// 写出细节:1. 会覆盖原来的内容
// 2. 调用写出方法 fputc / fputs / fwrite
// fputc 每次写出一个字符, 返回值就是写入的那个字符
int c = fputc(97, f);
printf("%c \n", c);
// 3. 关闭文件写出流
fclose(f);
return 0;
}
fputs
下面是 fputs() 函数的声明。
int fputs(const char *str, FILE *stream)
参数
- str -- 这是一个数组,包含了要写入的以空字符终止的字符序列。
- stream -- 这是指向 FILE 对象的指针,该 FILE 对象标识了要被写入字符串的流。
返回值
该函数返回一个非负值,如果发生错误则返回 EOF。
示例:
#include <stdio.h>
int main() {
// 1. 获取文件写入流
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\b.txt", "r");
// fputs 每次写出一个字符串,如果写入成功返回一个非负数的整数,写入失败会抛出错误
int n = fputs("你好adc", f);
printf("%d\n", n);
// 3. 关闭文件写出流
fclose(f);
return 0;
}
fwrite
下面是 fwrite() 函数的声明。
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
参数
- ptr -- 这是指向要被写入的元素数组的指针。
- size -- 这是要被写入的每个元素的大小,以字节为单位。
- nmemb -- 这是元素的个数,每个元素的大小为 size 字节。写入的总大小 = size * number
- stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
返回值
- 如果成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与 nmemb 参数不同,则会显示一个错误。
示例:
#include <stdio.h>
int main() {
// 1. 获取文件写入流
FILE* f = fopen("C:\\Users\\admin\\Desktop\\html\\b.txt", "a");
// fwrite 每次写入多个数据,如果写入成功会返回写入字节的总长度
char* str = "我的发c";
int n2 = fwrite(str, 1, 7, f);
printf("%d\n", n2);
// 3. 关闭文件写出流
fclose(f);
return 0;
}
fprintf
下面是fprintf() 函数的声明。
int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)
参数
- stream : 文件指针
- restrict :带格式都字符串
...
: 可变参数,赋值给字符串中的占位符。
返回值
- 如果成功返回写入的字符数 不包括 \n 中的字符 , 如果失败返回 EOF
示例:
#include <stdio.h>
int main(int argc, char const* argv[]) {
char path[] = "./io.txt";
FILE* file = fopen(path, "w");
if (file == NULL) {
perror("打开文件失败 :");
return -1;
} else {
perror("打开文件成功 \n");
}
/**
*
* int fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)
*/
char* name = "大聪明";
int res = fprintf(file, "adbcd \n \n\n %s \n", name);
if (res == EOF) {
perror("写入文件失败 \n");
} else {
printf("写入文件成功 字符数为:%d \n", res);
}
// 关闭文件
int flag = fclose(file);
if (flag == EOF) {
perror("文件关闭失败 \n");
return -1;
} else {
printf("文件关闭成功 \n");
}
return 0;
}
文件操作模式:
在 C 语言中,fopen
函数用于打开一个文件并返回一个指向该文件的 FILE
类型的指针。fopen
函数接受两个参数:文件名和一个表示文件打开模式的字符串。下面是常用的文件模式以及它们的含义:
文件模式 | 描述 |
---|---|
"r" |
打开一个文本文件用于只读。文件指针定位在文件头。(开始读取的指针) |
"w" |
打开一个文本文件用于只写。如果文件存在则会被截断为零长度, 如果不存在则创建新文件。文件指针定位在文件头(开始写入的指针)。 注意:参数文件的父目录必须是存在的 |
"a" |
打开一个文本文件用于追加。如果文件存在,写 入将出现在文件的结尾;如果不存在,则创建新文件。文件指针定位在文件尾。【续写模式】 |
"r+" |
打开一个文本文件用于读写。文件指针定位在文件头。 |
"w+" |
打开一个文本文件用于读写。如果文件存在则会被截断为零长度, 如果不存在则创建新文件。文件指针定位在文件头。 |
"a+" |
打开一个文本文件用于读写。如果文件存在,写入将出现在文件的结尾; 如果不存在,则创建新文件。文件指针定位在文件尾。 |
"rb" , "wb" , "ab" , "r+b" , "w+b" , "a+b" |
上述模式的二进制版本,适用于读写二进制文件。 |
注意事项:
- 如果文件模式中包含
'b'
,则以二进制模式打开文件。这对于 Windows 系统特别重要,在 Windows 中,文本模式会自动转换\r\n
为\n
,而二进制模式不会进行这种转换。 - 如果没有指定
'b'
,那么在 Windows 上默认是以文本模式打开文件。 - 在 Linux 和其他类 Unix 系统上,文本模式和二进制模式没有区别。
文件拷贝
#include <stdio.h>
int main(int argc, char const* argv[]) {
// 文件拷贝
char* path1 = "/home/zian/图片/38754.jpg";
FILE* file1 = fopen(path1, "r");
if (file1 == NULL) {
perror("打开文件失败");
return 1;
}
char* path2 = "/home/zian/图片/22.png";
FILE* file2 = fopen(path2, "w");
void* ch;
// fread 参数二:被读取的每个元素的大小,以字节为单位。
while (fread(ch, 1, 1024, file1) > 0) {
// printf("%s \n", ch);
// fwrite 参数二:被写入的每个元素的大小,以字节为单位。
fwrite(ch, 1, 1024, file2);
}
fclose(file2);
fclose(file1);
return 0;
}
标准输入/输出/错误
读写文件通常用于代码内部操作,如果想要和用户沟通交流,就需要使用标准输入、输出和错误了。
创建文件 stdin_out_err_test.c
,写入以下内容。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const* argv[]) {
int len = 10;
char* str = malloc(len);
// 标准输入 (从控制台中获取内容)
fgets(str, len, stdin);
// 标准输出 (输出内容到控制台)
fputs(str, stdout);
printf("\n");
// 错误输出 (输出内容到控制台)
fputs(str, stderr);
free(str);
return 0;
}
stdin_out_err_test: stdin_out_err_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
测试:
- 运行代码。
- 在控制台输入海哥,会出现对应的标准输出和错误输出。
系统调用
关于系统调用
系统调用是操作系统内核提供给应用程序,使其可以间接访问硬件资源的接口,关于操作系统内核、应用程序等概念,我们会在第五章详细阐述。
理解 : 用户空间程序与操作系统内核进行交互的接口
常见系统调用
open
open()
系统调用用于打开一个标准的文件描述符。
#include <unistd.h>
#include <fcntl.h>
/*
const char *__path: 文件路径
int __oflag: 用于指定打开文件的方式,可以是以下选项的组合:
(1) O_RDONLY: 以只读方式打开文件
(2) O_WRONLY: 以只写方式打开文件
(3) O_RDWR: 以读写方式打开文件
(4) O_CREAT: 如果文件不存在,则创建一个新文件
(5) O_APPEND: 将所有写入操作追加到文件的末尾
(6) O_TRUNC: 如果文件存在并且以写入模式打开,则截断文件长度为0
还有其他标志,如O_EXCL(当与O_CREAT一起使用时,只有当文件不存在时才创建新文件)、
O_SYNC(同步I/O)、O_NONBLOCK(非阻塞I/O)等
可选参数: mode -> 仅在使用了O_CREAT标志且文件尚不存在的情况下生效,用于指定新创建文件的权限位
权限位通常由三位八进制数字组成,分别代表文件所有者、同组用户和其他用户的读写执行权限
return: (1) 成功时返回非负的文件描述符。
(2) 失败时返回-1,并设置全局变量errno以指示错误原因。
*/
int open (const char *__path, int __oflag, ...);
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const* argv[]) {
char* path = "./io.1txt";
/**
* open 系统调用:用户空间程序与操作系统内核进行交互的接口
* int open (const char *__path, int __oflag, ...)
* 参数一:路径
* 参数二:操作类型
* 可变参数:如果操作类型中有 O_CREAT 就代表文件不存在的时候创建文件,这个可变参数就是文件的权限符号
* 注意要使用八进制表示 (八进制以0作为前缀)
* return: (1) 成功时返回非负的文件描述符。
* (2) 失败时返回-1,并设置全局变量errno以指示错误原因。
* 当权限符号设置为 666 的时候,创建出来的文件权限却是 664 ,
* 是因为触发了系统的保护机制,系统默认会将其他的用户的写权限去除掉
*/
// O_RDONLY | O_CREAT ,| 位运算符,都是零,才是零,其他为 1 , 在这里可以理解为两个数相加。
int fd = open(path, O_RDONLY | O_CREAT, 0664);
if (fd == EOF) {
perror("打开文件失败");
} else {
printf("文件描述符为: %d \n", fd);
}
return 0;
}
read
read()
系统调用用于读取已经打开的文件描述符。
#include <unistd.h>
/*
int __fd:一个整数,表示要从中读取数据的文件描述符
void *__buf:一个指向缓冲区的指针,读取的数据将被存放到这个缓冲区中
size_t __nbytes:一个size_t类型的整数,表示要读取的最大字节数
系统调用将尝试读取最多这么多字节的数据,但实际读取的字节数可能会少于请求的数量
return: (1) 成功时,read()返回实际读取的字节数
这个值可能小于__nbytes,如果遇到了文件结尾(EOF)或者因为网络读取等原因提前结束读取
(2) 失败时,read()将返回-1
*/
ssize_t read (int __fd, void *__buf, size_t __nbytes);
ssize_t --> long
ssize_t
相关的宏定义如下。
typedef __ssize_t ssize_t;
__STD_TYPE __SSIZE_T_TYPE __ssize_t;
#define __STD_TYPE typedef
#define __SSIZE_T_TYPE __SWORD_TYPE
#define __SWORD_TYPE long int
ssize_t
是 __ssize_t
的别名,后者是 long int
的别名,long
是 long int
的简写,因此,ssize_t
实际上是 long
类型的别名。
size_t --> unsigned long
相关定义如下。
typedef __SIZE_TYPE__ size_t;
#define __SIZE_TYPE__ long unsigned int
unsigned long
是 long unsigned int
的简写,size_t
实质上是 unsigned long
。
write
write()
系统调用用于对打开的文件描述符写入内容。
#include <unistd.h>
/*
int __fd:一个整数,表示要写入数据的文件描述符
void *__buf:一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中
size_t __n:一个size_t类型的整数,表示要写入的字节数 write()函数会尝试写入__n个字节的数据,
但实际写入的字节数可能会少于请求的数量
return: (1) 成功时,write()返回实际写入的字节数
这个值可能小于__n,如果写入操作因故提前结束,例如: 磁盘满、网络阻塞等情况
(2) 失败时,write()将返回-1
*/
ssize_t write (int __fd, const void *__buf, size_t __n);
close
close()
系统调用用于在使用完成之后,关闭对文件描述符的引用。
#include <unistd.h>
/*
int __fd:一个整数,表示要关闭的文件描述符
return: (1) 成功关闭时 返回0
(2) 失败时 返回-1
*/
int close (int __fd);
exit 和 _exit()
系统调用 _exit()
_exit()
是由 POSIX 标准定义的系统调用,用于立即终止一个进程,定义在 unistd.h
中。这个调用确保进程立即退出,不执行任何清理操作。
_exit()
在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。
_exit
和 _Exit
功能一样。
#include <unistd.h>
/**
* 立即终止当前进程,且不进行正常的清理操作,如关闭文件、
释放内存等。这个函数通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调用。
*
* int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误
*/
void _exit(int status);
void _Exit (int __status) ;
库函数 exit()
推荐
exit()
函数是由 C 标准库提供的,定义在 stdlib.h
中。
#include <stdlib.h>
/**
* 终止当前进程,但是在此之前会执行3种清理操作
* (1) 调用所有通过atexit()注册的终止处理函数(自定义)
* (2) 刷新所有标准I/O缓冲区(刷写缓存到文件)
* (3) 关闭所有打开的标准I/O流(比如通过fopen打开的文件)
*
* int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误
*/
void exit(int status);
使用场景
- 通常在父进程中使用
exit()
,以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。 - 在子进程中,特别是在
fork()
之后立即调用了一个执行操作(如exec()
)但执行失败时,推荐使用_exit()
或_Exit()
来确保子进程的快速、干净地退出,避免执行标准的清理操作,这些操作可能会与父进程发生冲突或不必要的重复。
综合案例
使用标准的系统调用,来对第二章的文件进行简单的读写操作:
**创建文件 **system_call_test.c
,写入以下内容。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 使用系统调用 实现效果:读取一个文件,写出到控制台中
char* path = "io.txt";
// open:打开一个文件,返回一个文件描述符
int fd = open(path, O_RDONLY);
if (fd == -1) {
perror("打开文件失败");
// 退出当前进程
exit(EXIT_FAILURE);
}
char buffer[1024];
// 读取文件
/**
* ssize_t read (int __fd, void *__buf, size_t __nbytes)
* 参数一:文件描述符
* 参数二:读取的内容放入哪里(缓冲区)
* 参数三:要读多个字节
* return:返回每次读取的字节个数,读到文件末尾 0 出错了返回 -1
* */
ssize_t nByte;
while ((nByte = read(fd, buffer, sizeof(buffer))) > 0) {
// printf("%s \n", buffer);
write(STDOUT_FILENO, buffer, nByte);
}
if (nByte == -1) {
perror("读取失败 : ");
close(fd);
exit(EXIT_FAILURE);
}
// 关闭文件描述符
close(fd);
return 0;
}
Makefile
中补充以下内容。
CC:= gcc
system_call_test: system_call_test.c
-$(CC) -o $@ $^
-./$@
-rm ./$@
文件描述符
定义
在 Linux 系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD),这是一个非负整数,我们可以通过它来进行读写等操作。
然而,文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或“句柄”。
在 Linux 中,文件描述符 0、1、2 是有特殊含义的。
- 0 是标准输入(stdin)的文件描述符
- 1 是标准输出(stdout)的文件描述符
- 2 是标准错误(stderr)的文件描述符
文件描述符关联的数据结构
struct file
**每个文件描述符都关联到内核一个 **struct file
类型的结构体数据,结构体定义位于 Linux 系统的 /usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/fs.h
文件中,从 992 行开始。
该结构体的部分关键字段如下。
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; // 指向文件操作函数表的指针,
// 定义了文件支持的操作,如读、写、锁定等
......
void *private_data; // 存储特定驱动或模块的私有数据
......
} __randomize_layout
__attribute__((aligned(4)));
**这个数据结构记录了与文件相关的所有信息,其中比较关键的是 **f_path
记录了文件的路径信息,f_inode
,记录了文件的元数据。
struct path
结构体定义位于 Linux 系统的 /usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/path.h
文件中,从第 8 行开始。
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
该结构体只有两个属性,这里不再通过定义展开。
struct vfsmount
:是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息。struct dentry
:目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。
struct inode
**结构体定义位于 Linux 系统的 **/usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/fs.h
文件中,从 639 行开始。
struct inode
结构的部分定义如下。
struct inode {
umode_t i_mode; // 文件类型和权限。这个字段指定了文件是普通文件、目录、
// 字符设备、块设备等,以及它的访问权限(读、写、执行)。
unsigned short i_opflags;
kuid_t i_uid; // 文件的用户ID,决定了文件的拥有者。
kgid_t i_gid; // 文件的组ID,决定了文件的拥有者组。
unsigned int i_flags;
......
unsigned long i_ino; // inode编号,是文件系统中文件的唯一标识。
......
loff_t i_size; // 文件大小
} __randomize_layout;
文件描述符表关联的数据结构
打开的文件表数据结构
struct files_struct
是用来维护一个进程(下文介绍)中所有打开文件信息的。
**结构体定义位于 **/usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/fdtable.h
文件中,从 49 行开始。
部分字段如下。
struct files_struct {
......
struct fdtable __rcu *fdt; // 指向当前使用的文件描述符表(fdtable)
......
unsigned int next_fd; // 存储下一个可用的最小文件描述符编号
......
struct file __rcu *fd_array[NR_OPEN_DEFAULT]; // struct file指针的数组,大小固定,用于快速访问。
};
fdt
维护了文件描述符表,其中记录了所有打开的文件描述符和 struct file
的对应关系。
打开文件描述符表
**打开文件描述符表底层的数据结构是 **struct fdtable
。
**结构体定义位于 **/usr/src/linux-hwe-6.5-headers-6.5.0-27/include/linux/fdtable.h
文件中,从 27 行开始。如下。
struct fdtable {
unsigned int max_fds; // 文件描述符数组的容量,即可用的最大文件描述符
struct file __rcu **fd; // 指向struct file指针数组的指针
unsigned long *close_on_exec;
unsigned long *open_fds;
unsigned long *full_fds_bits;
struct rcu_head rcu;
};
fd_array 和 fd
fd_array
是一个定长数组,用于存储进程最常用的 struct file
。
fd
是一个指针,可以指向任何大小的数组,其大小由 max_fds
字段控制。它可以根据需要动态扩展,以容纳更多的文件描述符。
**当打开文件描述符的数量不多于 **NR_OPEN_DEFAULT
时,fd
指向的通常就是 fd_array
,当文件描述符的数量超过 NR_OPEN_DEFAULT
时,会发生动态扩容,会将 fd_array
的内容复制到扩容后的指针数组,fd
指向扩容后的指针数组。这一过程是内核控制的。
文件描述符和 fd 或 fd_array 的关系
- 文件描述符是一个非负整数,其值实际上就是其关联的
struct file
在fd
指向的数组或fd_array
中的下标。
文件描述符引用图解
小结
当我们执行 open()
等系统调用时,内核会创建一个新的 struct file
(当文件时第一次被打开的时候),这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将 struct file
维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在 struct file_operations
数据结构中。文件描述符实质上是底层数据结构 struct file
的一个引用或者句柄,它为用户提供了操作底层文件的入口。