目录
1. 文件I/O与标准I/O的区别
1.1 概念
1.2 缓冲方式
1.3 使用便捷性
1.4 可移植性
1.5 性能特点
2. 文件描述符
2.1 定义
2.2 文件描述符关联的数据结构
2.3 文件描述符表关联的数据结构
2.4 引用图解
2.5 小结
3.标准I/O
3.1 打开/关闭文件
3.2 向文件写入数据
3.3 从文件读取数据
3.4 标准输入/输出/错误
4.文件I/O
4.1 数据类型说明
4.2 open
4.3 read
4.4 write
4.5 close
5. exit和_exit()
5.1 系统调用_exit()
5.2 库函数exit()
5.3 使用场景
6.综合案例
6.1 标准I/O
6.2 系统调用
1. 文件I/O与标准I/O的区别
1.1 概念
- 文件 I/O(Input/Output):也称为低级 I/O,它是操作系统提供的基本 I/O 操作接口,直接对文件描述符进行操作。文件描述符是一个非负整数,用于标识一个打开的文件。在 Unix/Linux 系统中,像
open
、read
、write
、close
等系统调用都属于文件 I/O 操作。 - 标准 I/O:也称为高级 I/O,它是在文件 I/O 的基础上构建的一种更为方便的 I/O 库。标准 I/O 提供了缓冲机制,它会自动为输入输出操作分配缓冲区,减少系统调用的次数,提高效率。在 C 语言中,
stdio.h
头文件中定义的函数(如printf
、scanf
、fgets
、fputs
等)都属于标准 I/O。
1.2 缓冲方式
- 文件 I/O:通常没有自带的缓冲机制。每次
read
或write
操作都会直接调用系统调用,将数据从用户空间缓冲区复制到内核缓冲区或者反之。这意味着频繁的小数据读写可能会导致大量的系统调用开销。例如,使用write
系统调用向文件写入一个字节的数据,就会触发一次系统调用。 - 标准 I/O:带有缓冲机制。有三种类型的缓冲区:全缓冲、行缓冲和无缓冲。
- 无缓冲:数据直接写入文件或者设备,不经过缓冲区。例如标准错误输出(
stderr
)通常是无缓冲的,这样可以确保错误信息能够及时显示,不会因为缓冲区的原因而延迟。 - 行缓冲:当遇到换行符
\n
或者缓冲区满时进行 I/O 操作。比如在标准输出(stdout
)中,默认是行缓冲的。所以当使用printf
输出带有换行符的数据时,数据会立即显示在屏幕上;如果没有换行符,数据可能会暂时存放在缓冲区中,直到缓冲区满或者遇到下一个换行符才输出。 - 全缓冲:当缓冲区填满时才进行实际的 I/O 操作。例如,向一个文件使用标准 I/O 写入数据,数据会先存放在缓冲区中,直到缓冲区满了,才会将数据一次性写入文件。这种方式适用于文件等大多数情况,能够有效减少系统调用次数。
- 无缓冲:数据直接写入文件或者设备,不经过缓冲区。例如标准错误输出(
1.3 使用便捷性
- 文件 I/O:使用相对复杂。因为它是直接和操作系统的底层接口打交道,需要手动管理文件描述符,处理错误返回值等。例如,在使用
open
系统调用打开文件时,需要检查返回的文件描述符是否为负数,以判断打开文件是否成功。而且在进行读写操作时,需要明确指定读取或写入的字节数等参数。 - 标准 I/O:使用起来更加方便。它提供了更高级的接口,隐藏了很多底层细节。例如,使用
fgets
函数可以很方便地从文件中读取一行数据,不需要考虑像文件 I/O 那样的文件描述符管理和字节数计算等复杂问题。同时,标准 I/O 函数的参数形式通常更符合人们的编程习惯。
1.4 可移植性
- 文件 I/O:在不同的操作系统上,系统调用的实现细节和参数可能会有所不同。这就导致使用文件 I/O 编写的程序在跨平台移植时可能需要进行较多的修改。例如,
open
系统调用在 Unix/Linux 和 Windows 系统下的参数和返回值可能有差异。 - 标准 I/O:具有更好的可移植性。因为标准 I/O 库在不同的操作系统上都有相应的实现,并且尽可能地提供了统一的接口。只要是遵循标准 C 语言规范的编译器,基本都支持标准 I/O 函数,这样在不同平台之间移植程序时,使用标准 I/O 的部分代码通常不需要太多修改。
1.5 性能特点
- 文件 I/O:对于少量数据的频繁读写,由于没有缓冲机制,系统调用开销较大,性能可能较差。但是在某些特定场景下,比如对实时性要求极高,不希望数据被缓冲的情况下,文件 I/O 可以直接操作文件,避免缓冲带来的延迟。
- 标准 I/O:通过缓冲机制可以减少系统调用次数,对于大量数据的读写或者频繁的小数据读写(只要缓冲区大小合适)能够提高性能。不过,如果缓冲区设置不合理,可能会导致数据延迟写入或者占用过多内存等问题。
2. 文件描述符
2.1 定义
在Linux系统中,当我们打开或创建一个文件(或套接字)时,操作系统会提供一个文件描述符(File Descriptor,FD),这是一个非负整数,我们可以通过它来进行读写等操作。
文件描述符本身只是操作系统为应用程序操作底层资源(如文件、套接字等)所提供的一个引用或“句柄”。
在Linux中,文件描述符0、1、2是有特殊含义的。
- 0是标准输入(stdin)的文件描述符
- 1是标准输出(stdout)的文件描述符
- 2是标准错误(stderr)的文件描述符
2.2 文件描述符关联的数据结构
1.struct file
每个文件描述符都关联到内核一个struct 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; // 指向文件操作函数表的指针,定义了文件支持的操作,如读、写、锁定等...... void *private_data; // 存储特定驱动或模块的私有数据......
} __randomize_layout__attribute__((aligned(4)));
这个数据结构记录了与文件相关的所有信息,其中比较关键的是f_path记录了文件的路径信息,f_inode,记录了文件的元数据。
2.struct path
结构体定义如下:
struct path {struct vfsmount *mnt;struct dentry *dentry;
} __randomize_layout;
- struct vfsmount:是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息。
- struct dentry:目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。
3.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;
2.3 文件描述符表关联的数据结构
1.打开的文件表数据结构
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指针的数组,大小固定,用于快速访问。
};
fdt维护了文件描述符表,其中记录了所有打开的文件描述符和struct file的对应关系。
2.打开文件描述符表
打开文件描述符表底层的数据结构是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;
};
3.fd_array和fd
fd_array是一个定长数组,用于存储进程最常用的struct file。
fd是一个指针,可以指向任何大小的数组,其大小由max_fds字段控制。它可以根据需要动态扩展,以容纳更多的文件描述符。
当打开文件描述符的数量不多于NR_OPEN_DEFAULT时,fd指向的通常就是fd_array,当文件描述符的数量超过NR_OPEN_DEFAULT时,会发生动态扩容,会将fd_array的内容复制到扩容后的指针数组,fd指向扩容后的指针数组。这一过程是内核控制的。
4.文件描述符和fd或fd_array的关系
文件描述符是一个非负整数,其值实际上就是其关联的struct file在fd指向的数组或fd_array中的下标。
2.4 引用图解
2.5 小结
执行open()等系统调用时,内核会创建一个新的struct file,这个数据结构记录了文件的元数据(文件类型、权限等)、文件路径、支持的操作等,然后分配文件描述符,将struct file维护在文件描述符表中,最后将文件描述符返回给应用程序。我们可以通过后者对文件执行它所支持的各种函数操作,而这些函数的函数指针都维护在struct file_operations数据结构中。文件描述符实质上是底层数据结构struct file的一个引用或者句柄,它为用户提供了操作底层文件的入口。
3.标准I/O
3.1 打开/关闭文件
1.fopen
函数作用:打开文件
函数原型: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 * 结构体指针 表示一个文件
2.fclose
函数作用:关闭文件
函数原型:int fclose (FILE *__stream)
函数解析:
FILE *__stream: 需要关闭的文件return: 成功返回0 失败返回EOF(负数) 通常失败会造成系统崩溃
3.2 向文件写入数据
1.fputc
函数作用:写入一个字符
函数原型:int fputc (int __c, FILE *__stream)
函数解析:
int __c: 写入的char按照AICII值写入 可提前声明一个charFILE *__stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回char的值 失败返回EOF
2.fputs
函数作用:写入一个字符串
函数原型:int fputs (const char *__restrict __s, FILE *__restrict __stream)
函数解析:
char *__restrict __s: 需要写入的字符串FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式return: 成功返回非负整数(一般是0,1) 失败返回EOF
3.fprintf
函数作用:将格式化的数据输出到指定的文件流
函数原型:fprintf (FILE *__restrict __stream, const char *__restrict __fmt, ...)
函数解析:
FILE *__restrict __stream: 要写入的文件,写在哪里取决于访问模式char *__restrict __fmt: 格式化字符串...: 变长参数列表return: 成功返回正整数(写入字符总数不包含换行符) 失败返回EOF
3.3 从文件读取数据
1.fgetc
函数作用:读取一个字符
函数原型:int fgetc (FILE *__stream)
函数解析:
FILE *__stream: 需要读取的文件return: 读取的一个字节 到文件结尾或出错返回EOF
2.fgets
函数作用:读取一个字符串
函数原型:fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
函数解析:
char *__restrict __s: 接收读取的数据字符串int __n: 能够接收数据的长度FILE *__restrict __stream: 需要读取的文件return: 成功返回字符串 失败返回NULL(可以直接用于while)
3.fscanf
函数作用:从指定的文件流中读取格式化的数据
函数原型:int fscanf (FILE *__restrict __stream, const char *__restrict __format, ...)
函数解析:
FILE *__restrict __stream: 读取的文件char *__restrict __format: 读取的匹配表达式...: 变长参数列表 用于接收匹配的数据return: 成功返回参数的个数 失败返回0 报错或结束返回EOF
3.4 标准输入/输出/错误
stdin: 标准输入FILE *
stdout: 标准输出FILE * 写入这个文件流会将数据输出到控制台
stderr: 错误输出FILE * 一般用于输出错误日志
4.文件I/O
4.1 数据类型说明
1. ssize_t
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类型的别名。
2. size_t
size_t相关的宏定义如下:
typedef __SIZE_TYPE__ size_t;
#define __SIZE_TYPE__ long unsigned int
unsigned long是long unsigned int的简写,size_t实质上是unsigned long。
4.2 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以指示错误原因。
4.3 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
4.4 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
4.5 close
函数作用:在使用完成之后,关闭对文件描述符的引用
函数原型:int close (int __fd);
函数解析:
int __fd:一个整数,表示要关闭的文件描述符return: (1) 成功关闭时 返回0(2) 失败时 返回-1
5. exit和_exit()
5.1 系统调用_exit()
_exit()是由POSIX标准定义的系统调用,用于立即终止一个进程,定义在unistd.h中。这个调用确保进程立即退出,不执行任何清理操作。
_exit()在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。
_exit和_Exit功能一样。
#include <unistd.h>/*** 立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。这个函数通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调用。* * int status: 父进程可接收到的退出状态码 0表示成功 非0表示各种不同的错误*/
void _exit(int status);
void _Exit (int __status) ;
5.2 库函数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);
5.3 使用场景
① 通常在父进程中使用exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。
② 在子进程中,特别是在fork()之后立即调用了一个执行操作(如exec())但执行失败时,推荐使用_exit()或_Exit()来确保子进程的快速、干净地退出,避免执行标准的清理操作,这些操作可能会与父进程发生冲突或不必要的重复。
6.综合案例
6.1 标准I/O
1.程序
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>int main(int argc, char const *argv[])
{char *filename = "io.txt";FILE* file_fd;// 以追加读写模式打开文件,不存在则创建file_fd = fopen(filename,"a+");if (file_fd == NULL){printf("failed 打开文件失败\n");}else{printf("success 打开文件成功\n");}// 写入字符'a'int put_result = fputc(97,file_fd);if (put_result == EOF){printf("写入文件失败\n");}else {printf("写入%c成功\n",put_result);}// 写入字符串int putsR = fputs(" love letter\n",file_fd);if (putsR == EOF){printf("写入字符串失败\n");}else {printf("写入字符串成功\n");}// 格式化写入字符串char *name = "printfR";int printfR = fprintf(file_fd,"我是 %s",name);if (printfR == EOF){printf("字符串写入失败\n");}else{printf("字符串写入成功\n");}// 将文件指针移动到文件开头fseek(file_fd, 0, SEEK_SET);// 读取字符char c = fgetc(file_fd);if(c != EOF){printf("读取字符成功:%c",c);}printf("\n");// 读取字符串char buffer[100];char *str;str = fgets(buffer,sizeof(buffer),file_fd); if(str != NULL){printf("读取字符串成功:%s",str);}// 格式化读取字符串int scnafR;char na[50];char me[50];scnafR = fscanf(file_fd,"%s %s",na,me);if(scnafR != EOF){printf("读取字符串成功:%s %s",na,me);}// 关闭文件int closefd = fclose(file_fd);if (closefd == EOF){printf("关闭文件失败\n");}return 0;
}
2.运行结果
控制台输出:
文件显示内容:
6.2 系统调用
1.程序
#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;
}
2.运行结果