avatar

zian

A text-focused Halo theme

  • Java
  • 面试
  • 首页
  • C语音
  • liunx
  • 数据结构与算法
  • 控制台
Home c语言 文件操作
文章

c语言 文件操作

Posted 2024-12-16 Updated 2025-01- 11
By Administrator
54~69 min read

文件操作

文件操作就两种: 读 和 写 , 这里读或者写都是站在程序的角度来看的。

文件路径

文件路径:文件在电脑中的位置

分类:

  1. 绝对路径 : 以盘符开始的路劲 C:/admin/a.txt
  2. 相对路径:以当项目作为参照文件路径: aaa/a.txt;

转义字符 ( \ )

转义字符:改变了原来字符的含义。

文件读取

步骤:

  1. fopen 打开文件
  2. fgetc | fgets | fgetd 读取文件
  3. 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;
}

注意点:

  1. 接收读取结果的数组应该大于等于一次读取总长度 (总长度 = 参数 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 ./$@

测试:

  1. 运行代码。
  2. 在控制台输入海哥,会出现对应的标准输出和错误输出。

系统调用

关于系统调用

系统调用是操作系统内核提供给应用程序,使其可以间接访问硬件资源的接口,关于操作系统内核、应用程序等概念,我们会在第五章详细阐述。

理解 : 用户空间程序与操作系统内核进行交互的接口

常见系统调用

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);
使用场景
  1. 通常在父进程中使用 exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。
  2. 在子进程中,特别是在 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 的一个引用或者句柄,它为用户提供了操作底层文件的入口。

image-gocd.png

C语音
License:  CC BY 4.0
Share

Further Reading

Jan 7, 2025

C语言 其他

作用域 关键字 register 在C语言中,register 关键字是一种存储类说明符,用于建议编译器将变量存储在CPU寄存器中,而不是内存中。这样做可以提高访问变量的速度,因为寄存器的访问速度通常比内存快得多。然而,这个关键字只是一个提示,编译器可以选择忽略它。 使用register关键字的变量

Jan 6, 2025

C语言 结构体

结构体 问题的引入 多个相同数据类型的数据可以用数组表示,那么,如果多个不同数据类型的数据如何用一个集合表示呢?? 前面我们所介绍的普通数据类型实际上远远未能满足我们对实际应用中的要求,比如说一个学生,可能包含的属性有年龄、姓名、分数等等,不可能用一个基本数据类型(int 、float 、char)

Dec 28, 2024

C语言 内存

内存 什么是内存 软件在运行是,临时用来存储数据的 操作系统会将内存按照字节划分为 N 多个小格子 什么是内存地址 其实就是格子的编号 32 位操作系统:以 32 位的二进制表示 64 位操作系统:以 64 位的二进制表示 内存地址的作用 快速操作内存中存储的数据 C 语言中如何获取内存地址 &变量

OLDER

C语言核心语法

NEWER

c 语言 数组

Recently Updated

  • 其他
  • Elasticsearch 面试
  • Spring 面试
  • RabbitMQ 面试
  • Redis 面试

Trending Tags

ruoyi docker java

Contents

©2025 zian. Some rights reserved.

Using the Halo theme Chirpy