一、标准IO库
1.1 打开/关闭文件
fopen
新建 fopen_test.c,写入以下内容:
#include <stdio.h>
int main()
{/* 打开文件函数:FILE *fopen (const char *__restrict __filename,const char *__restrict __modes)参数:char *__restrict __filename: 字符串表示要打开文件的路径和名称char *__restrict __modes: 字符串表示访问模式(1)"r": 只读模式 没有文件打开失败(2)"w": 只写模式 存在文件写入会清空文件,不存在文件则创建新文件(3)"a": 只追加写模式 不会覆盖原有内容 新内容写到末尾,如果文件不存在则创建(4)"r+": 读写模式 文件必须存在 写入是从头一个一个覆盖(5)"w+": 读写模式 可读取,写入同样会清空文件内容,不存在则创建新文件(6)"a+": 读写追加模式 可读取,写入从文件末尾开始,如果文件不存在则创建return: FILE * 结构体指针 表示一个文件*/char *filename = "io.txt";FILE *ioFile = fopen(filename, "a+");if (ioFile == NULL){printf("FAILED, a+不能打开不存在的文件\n");}else{printf("SUCCESS, a+能打开不存在的文件\n");}
}
新建 Makefile,写入以下内容:
CC:=gcc
fopen_test: fopen_test.c-$(CC) -o $@ $^-./$@-rm ./$@
说明:
1. 有时编译器不只是 gcc,我们将编译器定义为变量 CC,当切换编译器时只需要更改该变量的定义,而无须更改整个 Makefile。
2. $@相当于当前 target 目标文件的名称,此处为 fopen_test。
3. $^相当于当前 target 所有依赖文件列表,此处为 fopen_test.c
4. ./$@的作用是执行目标文件
5. rm ./$@的作用是在执行完毕后删除目标文件,如果没有这个操作,当源文件fopen_test.c 未更改时就无法重复执行,会提示: make:“fopen_test”已是最新。此处删除目标文件,使得我们在不更改源文件的情况下可以多次执行。
6. 所有命令前都添加了“-”符号以忽略错误,确保即便上面的命令执行失败,仍然会向下执行。这样做是为了在发生错误时,确保删除目标文件,使得再次执行相同 target时不会提示: make:“fopen_test”已是最新,可以重新执行 target 下的命令。
fclose
创建 fclose_test.c 文件,写入以下内容:
#include <stdio.h>
int main()
{char *filename = "io1.txt";FILE *ioFile = fopen(filename, "r");if (ioFile == NULL){printf("r 不能打开不存在的文件\n");}else{printf("r 能打开不存在的文件\n");}/*函数:int fclose (FILE *__stream)FILE *__stream: 需要关闭的文件return: 成功返回 0 失败返回 EOF(负数) 通常失败会造成系统崩溃*/int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");return 1;}return 0;
}
Makefile 中补充以下内容:
fclose_test: fclose_test.c-$(CC) -o $@ $^-./$@-rm ./$@
1.2 向文件中写入数据
fputc 函数
创建 fputc_test.c 文件,写入以下内容:
#include <stdio.h>
int main()
{char *filename = "io.txt";FILE *ioFile = fopen(filename, "a+");if (ioFile == NULL){printf("a+不能打开不存在的文件\n");}else{printf("a+能打开不存在的文件\n");}/*写入文件一个字符函数:int fputc (int __c, FILE *__stream)int __c: 写入的 char 按照 AICII 值写入 可提前声明一个 charFILE *__stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回 char 的值 失败返回 EOF*/int putcR = fputc(97, ioFile);if (putcR == EOF){printf("写入字符失败\n");}else{printf("写入字符成功:%c\n", putcR);}int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");return 1;}return 0;
}
在 Makefile 中补充以下内容:
fputc_test: fputc_test.c-$(CC) -o $@ $^-./$@-rm ./$@
fputs 函数
创建 fputs_test.c 文件,写入以下内容:
#include <stdio.h>
int main()
{char *filename = "io.txt";FILE *ioFile = fopen(filename, "a+");if (ioFile == NULL){printf("a+不能打开不存在的文件\n");}else{printf("a+能打开不存在的文件\n");}/*写入文件一个字符串函数:int fputs (const char *__restrict __s, FILE *__restrict __stream)char *__restrict __s: 需要写入的字符串FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回非负整数(一般是 0,1) 失败返回 EOF*/int putsR = fputs(" love letter\n", ioFile);if (putsR == EOF){printf("写入字符串失败\n");}else{printf("写入字符串成功:%d\n", putsR);}int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");return 1;}return 0;
}
fprintf 函数
创建 fprintf_test.c 文件,写入以下内容:
#include <stdio.h>
int main()
{char *filename = "io.txt";FILE *ioFile = fopen(filename, "a+");if (ioFile == NULL){printf("a+不能打开不存在的文件\n");}else{printf("a+能打开不存在的文件\n");}/*函数:fprintf (FILE *__restrict __stream, const char *__restrictFILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式char *__restrict __fmt: 格式化字符串...: 变长参数列表return: 成功返回正整数(写入字符总数不包含换行符) 失败返回 EOF __fmt, ...)*/char *name = "大海";int fprintfR = fprintf(ioFile, "今天是2025年1月12号!/n我是guuilin", name);if (fprintfR == EOF){printf("写入字符串失败");}else{printf("写入字符串成功:%d\n", fprintfR);}int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");fprintf(stderr, "%s\n", filename);return 1;}return 0;
}
1.3 从文件中读取数据
fgetc 函数
创建 fgetc_test.c 文件,写入以下内容:
#include <stdio.h>
int main()
{// 打开文件FILE *ioFile = fopen("io.txt", "r");if (ioFile == NULL){printf("不能读不存在的文件");}/*函数:int fgetc (FILE *__stream)FILE *__stream: 需要读取的文件return: 读取的一个字节 到文件结尾或出错返回 EOF*/char c = fgetc(ioFile);while (c != EOF){printf("%c", c);c = fgetc(ioFile);}int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");return 1;}return 0;
}
fgets 函数
创建 fgets_test.c,写入以下内容:
#include <stdio.h>
int main()
{// 打开文件FILE *ioFile = fopen("io.txt", "r");if (ioFile == NULL){printf("不能读不存在的文件");}/*函数:fgets (char *__restrict __s, int __n, FILE *__restrict __stream)char *__restrict __s: 接收读取的数据字符串int __n: 能够接收数据的长度FILE *__restrict __stream: 需要读取的文件return: 成功返回字符串 失败返回 NULL(可以直接用于 while)*/char buffer[100];while (fgets(buffer, sizeof(buffer), ioFile)){printf("%s", buffer);}int result = fclose(ioFile);if (result != 0){printf("关闭文件失败");return 1;}return 0;
}
fscanf 函数
新建文件 user.txt,写入以下内容:
罗密欧 18 朱丽叶
贾宝玉 14 薛宝钗
梁山伯 16 祝英台
--2025.1.12 guilin
创建 fscanf_test.c,写入以下内容:
#include <stdio.h>
int main()
{ /*函数:int fscanf (FILE *__restrict __stream, const char *__restrict__format, ...)FILE *__restrict __stream: 读取的文件char *__restrict __format: 读取的匹配表达式...: 变长参数列表 用于接收匹配的数据return: 成功返回参数的个数 失败返回 0 报错或结束返回 EOF*/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;
}
1.4 标准输入/输出/错误
读写文件通常用于代码内部操作,如果想要和用户沟通交流,就需要使用标准输入、输出和错误了。
创建文件 stdin_out_err_test.c,写入以下内容:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{// malloc 动态分配内存 也可以用 char ch[100]接收数据char *ch = malloc(100);// char ch1[100];/*stdin: 标准输入 FILE **/fgets(ch, 100, stdin);printf("你好:%s", ch);/*stdout: 标准输出 FILE * 写入这个文件流会将数据输出到控制台printf 底层就是使用的这个*/fputs(ch, stdout); /*stderr: 错误输出 FILE * 一般用于输出错误日志*/fputs(ch, stderr);return 0;
}
二、系统调用
系统调用是操作系统内核提供给应用程序,使其可以间接访问硬件资源的接口。
2.1 常见系统调用
open()系统调用用于打开一个标准的文件描述符:
int open(const char *__path, int __oflag, ...);
/*
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 以指示错误原因。
*/
read()系统调用用于读取已经打开的文件描述符:
ssize_t read(int __fd, void *__buf, size_t __nbytes);
/*
int __fd:一个整数,表示要从中读取数据的文件描述符
void *__buf:一个指向缓冲区的指针,读取的数据将被存放到这个缓冲区中
size_t __nbytes:一个 size_t 类型的整数,表示要读取的最大字节数 系统调用将尝试读取最多这么多字节的数据,、但实际读取的字节数可能会少于请求的数量。return: (1) 成功时,read()返回实际读取的字节数 这个值可能小于__nbytes,如果遇到了文件结尾(EOF)或者因为网络读取等原因提前结束读取(2) 失败时,read()将返回-1
*/
write()系统调用用于对打开的文件描述符写入内容:
ssize_t write(int __fd, const void *__buf, size_t __n);
/*
参数:int __fd:一个整数,表示要写入数据的文件描述符void *__buf:一个指向缓冲区的指针,写入的数据需要先存放到这个缓冲区中size_t __n:一个 size_t 类型的整数,表示要写入的字节数 write()函数会尝试写入__n 个字节的数据,但实际写入的字节数可能会少于请求的数量return: (1) 成功时,write()返回实际写入的字节数 这个值可能小于__n,如果写入操作因故提前结束,例如: 磁盘满、网络阻塞等情况(2) 失败时,write()将返回-1
*/
close()系统调用用于在使用完成之后,关闭对文件描述符的引用:
int close(int __fd);
/*
参数:int __fd:一个整数,表示要关闭的文件描述符return: (1) 成功关闭时 返回 0(2) 失败时 返回-1
*/
exit 和_exit()
系统调用_exit():立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。
库函数 exit():终止当前进程,但是在此之前会执行 3 种清理操作:
(1) 调用所有通过 atexit()注册的终止处理函数(自定义);
(2) 刷新所有标准 I/O 缓冲区(刷写缓存到文件);
(3) 关闭所有打开的标准 I/O 流(比如通过 fopen 打开的文件)。
使用场景:
1.通常在父进程中使用 exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。
2.在子进程中,特别是在 fork()之后立即调用了一个执行操作(如 exec())但执行失败时,推荐使用_exit()或_Exit()来确保子进程的快速、干净地退出,避免执行标准的清理操作,这些操作可能会与父进程发生冲突或不必要的重复。
2.2 综合案例
创建文件 system_call_test.c,写入以下内容:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{int fd = open("io.txt", O_RDONLY);if (fd == -1){perror("open");exit(EXIT_FAILURE);}char buffer[1024]; // 创建一个缓冲区来存放读取的数据ssize_t bytes_read;while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0){// 将读取的数据写入标准输出write(STDOUT_FILENO, buffer, bytes_read);}if (bytes_read == -1){perror("read");close(fd);exit(EXIT_FAILURE);}close(fd); // 使用完毕后关闭文件描述符return 0;
}
三、文件描述符
在 Linux 系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor, FD),这是一个非负整数,我们可以通过它来进行读写等操作。然而,文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或“句柄”。在 Linux 中,文件描述符 0、 1、 2 是有特殊含义的。
➢ 0 是标准输入(stdin)的文件描述符
➢ 1 是标准输出(stdout)的文件描述符
➢ 2 是标准错误(stderr)的文件描述符
3.1 文件描述符关联的数据结构
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)));
struct path:
struct path
{struct vfsmount *mnt; // 是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息struct dentry *dentry; // 目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。
} __randomize_layout;
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;
3.2 文件描述符表关联的数据结构
struct files_struct 是用来维护一个进程(下文介绍)中所有打开文件信息的。
struct files_struct
{...... struct fdtable __rcu *fdt; // 指向当前使用的文件描述符表(fdtable)...... unsigned int next_fd; // 存储下一个可用的最小文件描述符编号...... struct file __rcu *fd_array[NR_OPEN_DEFAULT]; // struct file 指针的数组,大小固定,用于快速访问。
};
打开文件描述符表底层的数据结构是 struct fdtable。
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 字段控制。它可以根据需要动态扩展,以容纳更多的文件描述符。
3.3 文件描述符引用图解
總結:当我们执行 open() 等系统调用时,内核会创建一个新的 struct file,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将 struct file 维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在struct file_operations 数据结构中。文件描述符实质上是底层数据结构 struct file 的一个引用或者句柄,它为用户提供了操作底层文件的入口。