文件I/O
C 标准I/O库函数回顾
打开/关闭文件
fopen
用法如下:
{
/*打开文件
char *__restrict __filename:字符串表示要打开文件的路径和名称
char *__restrict __modes:字符串表示访问模式
(1)"r":只读模式 没有文件打开失败报错
(2)"w":只写模式 存在文件写入会清空文件,不存在文件则创建新文件
(3)"a":只追加写模式 不会覆盖原有内容新内容写到末尾,如果文件不存在则创建
(4)"r+":读写模式 文件必须存在,写入是从头一个一个覆盖
(5)"w+":读写模式 可读取,写入同样会清空文件内容,不存在则创建新文件
(6)"a+":读写追加模式 可读取,写入从文件末尾开始,如果文件不存在则创建
return: FILE *结构体指针表示一个文件
报错返回NULL
FILE *fopen (const char *__restrict __filename,
const char *__restrict __modes)
*/
char *filename = "io.txt";
FILE *ioFile = fopen(filename, "a+");
if (ioFile == NULL)
{
printf("FAILED,a+不能打开不存在的文件\n");
}
else
{
printf("SUCCESS,a+能打开不存在的文件\n");
}
}
再编写Maklefile:
CC:=gcc
fopen_test: fopen_test.c
-$(CC)-o $@ $^
-./$@
-rm ./$@
fclose
用法如下:
// FILE *__stream:需要关闭的文件
// return:成功返回0,失败返回EOF(负数),通常失败会造成系统崩溃
// int fclose (FILE *__stream)
int result = fclose(ioFile);
if (result != 0)
{
printf("关闭文件失败");
return 1;
}
return 0;
向文件中写入数据
fputc函数
用法如下:
// 写入文件一个字符
// 读写权限记录在fopen方法中的参数
// int __c:写入的char,按照AICII值写入可提前声明一个char
// FILE *__stream:要写入的文件,写在哪里取决于访问模式
// return:成功返回char的值,失败返回EOF
// int fputc (int __c, FILE *__stream)
int putcR = fputc(97,ioFile);
if (putcR == EOF)
{
printf("写入字符失败\n");
}
else{
printf("写入字符成功:%c\n",putcR);
}
fputs函数
用法如下:
// 写入文件一个字符串
// 读写权限记录在fopen方法中的参数
// char *__restrict __s:需要写入的字符串
// FILE *__restrict __stream:要写入的文件,写在哪里取决于访问模式
// return:成功返回非负整数(一般是0,1)失败返回EOF
// int fputs (const char *__restrict __s, FILE *__restrict __stream)
int putsR = fputs(" love letter\n",ioFile);
if (putsR == EOF)
{
printf("写入字符串失败\n");
}else{
printf("写入字符串成功:%d\n",putsR);
}
fprintf函数
用法如下:
// FILE *__restrict __stream:要写入的文件,写在哪里取决于访问模式
// char *__restrict __fmt:格式化字符串
// ...可变参数:变长参数列表,填入格式化的长字符串
// 上面两个参数即可理解为printf中的两个参数
// return:成功返回正整数写入字符的个数(写入字符总数不包含换行符)失败返回EOF
// fprintf (FILE *__restrict __stream, const char *__restrict__fmt, ...)
char *name = "大海";
int fprintfR = fprintf(ioFile, "哎呀,那边窗户透出了什么光?\n那是东方,而你则是太阳!\n升起吧,骄阳,去让忌妒的月黯然失色!\n\t\t%s", name);
if (fprintfR == EOF) {
printf("写入字符串失败");
} else {
printf("写入字符串成功:%d\n", fprintfR);
}
从文件中读取数据
fgetc函数
用法如下:
// FILE *__stream:需要读取的文件
// return:读取的一个字节到文件结尾或出错返回EOF
// int fgetc (FILE *__stream)
char c = fgetc(ioFile);
while (c != EOF)
{
printf("%c",c);
c = fgetc(ioFile);
}
fgets函数
用法如下:
// char *__restrict __s:接收读取的数据字符串
// int __n:能够接收数据的长度
// FILE *__restrict __stream:需要读取的文件
// return:成功返回字符串失败返回NULL(可以直接用于while)
// fgets (char *__restrict __s, int __n, FILE *__restrict __stream)
char buffer[100];
while (fgets(buffer, sizeof(buffer), ioFile)) {
printf("%s", buffer);
}
fscanf函数
用法如下:
先创建usr.txt
罗密欧 18 朱丽叶
贾宝玉 14 薛宝钗
梁山伯 16 祝英台
海哥
// FILE *__restrict __stream:读取的文件
// char *__restrict __format:读取的匹配表达式
// ...:变长参数列表用于接收匹配的数据
// return:成功返回参数的个数失败返回0报错或结束返回EOF
// int fscanf (FILE *__restrict __stream, const char *__restrict__format, ...)
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);
}
输出如下:
标准输入/输出/错误
读写文件通常用于代码内部操作,如果想要和用户沟通交流,就需要使用标准输入、输出和错误了。
执行一个 Shell 命令行时通常会自动打开三个标准文件,即标准输入文件(stdin),通常对应终端的键盘;标准输出文件(stdout)和标准错误输出文件(stderr),这两个文件都对应终端的屏幕。进程将从标准输入文件中得到输入数据,将正常输出数据输出到标准输出文件,而将错误信息送到标准错误文件中。
#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;
}
系统调用
系统调用是操作系统内核提供给应用程序,使其可以间接访问硬件资源的接口,关于操作系统内核、应用程序等概念,我们会在第五章详细阐述。
常见系统调用
open
open()系统调用用于打开一个标准的文件描述符。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
/*
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 fd = open ("io.txt",O_RDONLY | O_CREAT,0666);
if(fd == -1){
printf("打开失败");
}
else printf("打开成功");
}
注意:Linux操作系统有文件权限的保护,默认创建的文件会被删除其它用户的写权限,可以有执行权限
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是__ssize_t的别名,后者是long int的别名,long是long int的简写,因此,ssize_t实际上是long类型的别名。
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()返回实际写入的字节数这个值可能小于size_t__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()
(1)系统调用_exit()
_exit()是由POSIX标准定义的系统调用,用于立即终止一个进程,定义在unistd.h中。这个调用确保进程立即退出,不执行任何清理操作。_exit()在子进程终止时特别有用,这可以防止子进程的终止影响到父进程(比如,防止子进程意外地刷新了父进程未写入的输出缓冲区)。_exit和_Exit功能一样
#include <unistd.h>
/**
*立即终止当前进程,且不进行正常的清理操作,如关闭文件、释放内存等。这个函数
通常在程序遇到严重错误需要立即退出时使用,或者在某些情况下希望避免清理工作时调
用。
* int status:父进程可接收到的退出状态码0表示成功非0表示各种不同的错误
*/
void _exit(int status);
void _Exit (int __status);
(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);
(3)使用场景
①通常在父进程中使用exit(),以确保程序在退出前能执行清理操作,如关闭文件和刷新输出。
②在子进程中,特别是在fork()之后立即调用了一个执行操作(如exec())但执行失败时,推荐使用_exit()或_Exit()来确保子进程的快速、干净地退出,避免执行标准的清理操作,这些操作可能会与父进程发生冲突或不必要的重复。
综合案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc, char const *argv[])
{
int fd = open("io123.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) {
//将读取的数据写入标准输出
// 文件描述符
// #define STDIN_FILENO 0 /* Standard input. */
// #define STDOUT_FILENO 1 /* Standard output. */
// #define STDERR_FILENO 2 /* Standard error output. */
write(STDOUT_FILENO, buffer, bytes_read);
}
if (bytes_read ==-1){
perror("read");
close(fd);
exit(EXIT_FAILURE);
}
close(fd); //使用完毕后关闭文件描述符
return 0;
}
perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数 s 所指的字符串会先打印出,后面再加上错误原因字串。此错误原因依照全局变量errno的值来决定要输出的字符串。
文件描述符
定义
在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.027/include/linux/path.h 文件中,从第 8 行开始。
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
struct vfsmount:是虚拟文件系统挂载点的表示,存储有关挂载文件系统的信息。
struct dentry:目录项结构体,代表了文件系统中的一个目录项。目录项是文件系统中的一个实体,通常对应一个文件或目录的名字。通过这个类型的属性,可以定位文件位置。
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.027/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.027/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 中的下标。