【Linux】解锁系统编程奥秘,高效文件IO的实战技巧
文件
- 1. 知识铺垫
- 2. C文件I/O
- 2.1. C文件接口
- 2.2 fopen()与重定向
- 2.3. 当前路径
- 2.4. stdin、stdout、stderr
- 3. 系统文件I/O
- 3.1. 前言
- 3.2. open
- 3.2.1. flags</h3>
- 3.2.2. mode</h3>
- 3.2.3. 返回值fd
- 3.3. write</h2>
- 3.4. read
- 3.5. close</h2>
- 3.6. lseek</h2>
1. 知识铺垫
- 文件=内容+属性。
文件属性、文件内容都是二进制数据,都需要占据磁盘的存储空间。
对文件的所有操作,本质上是要么对文件的内容做操作,要么对文件的属性做操作。
- 打开或修改文件,都是通过执行代码(如:文本编辑器)的方式来完成的。
文件打开、编辑、保存的流程:软件启动 -> 文件加载 -> 读取文件内容 -> 用户交互 -> 文件修改 -> 保存修改。
- 需要先打开文件,才能访问它 —> 将文件加载到内存中。
代码本身不能直接修改磁盘上的数据,CPU执行代码来修改这个文件时,实际上是在内存中进行的,因为CPU只能访问内存,所以磁盘的数据需要通过OS提供的文件系统调用接口和磁盘IO操作,来加载到内存中,然后CPU才能对这些数据进行处理。
- 谁在打开文件?进程。
打开文件之前,需要把访问这个文件的程序,编译形成的可执行程序,通过某种方式启动变成进程,等待cpu调度,执行完fopen后,文件才会打开。
-
一个进程可以打开多个文件吗?可以。
-
进程与文件的关系:结构体struct task_struct和结构体struct file之间的关系。
在一段时间内,系统存在多个进程,也可能同时存在多个被打开的文件,OS需要管理多个被打开的文件(先描述,再组织),所以内核中一定有描述被打开文件的结构体,并用其定义对象。
- 系统中是不是所有的文件都被打开了?不是,未打开的文件存放在磁盘中,被称为磁盘文件;打开的文件,存放在内存中,被称为内存文件。
2. C文件I/O
2.1. C文件接口
fopen、fclose
fread、fwrite; fgetc、fputc;fgets、fputs; fscanf、fprintf
fseek、ftell、rewind
feof、ferror
有关C文件接口,请看C语言博客中的文件内容,都有详细的介绍。
2.2 fopen()与重定向
- fopen以"w"方式打开:如果文件不存在,先会创建一个文件 / 如果文件存在,先会清空文件内容,然后再从头进行写入操作。
- 相当于输出重定向(>)。
//文件以写的方式打开,如果文件不存在,新建文件 / 如果文件存在,先会清空文件内容,然后再从头进行写入操作
FILE *fp = fopen("example.txt", "w");
if (fp == NULL) { //打开失败perror("Failed to open file"); return 1;
}fprintf(fp, "New content\n"); //向文件中进行写入
fclose(fp) //关闭文件
2. fopen以"a"方式打开:本质也是写入,如果文件不存在,先会创建一个文件,然后进行写入 / 如果文件存在,会在文件原有内容的末尾处追加写入。
- 相当于追加重定向(>>)。
//文件以追加的方式打开,如果文件不存在,先会创建一个文件,然后进行写入 / 如果文件存在,会在文件原有内容的末尾处追加写入
FILE *fp = fopen("example.txt", "a");
if (fp == NULL) {perror("Failed to open file");return 1;
}fprintf(fp, "Additional content\n");
fclose(fp);
3. fopen以"r"方式打开:读取文件的内容,如果文件不存在,则读取失败,返回NULL 。
- 相当于输入重定向(<)。
//以读的方式打开文件,读取文件的内容,如果文件不存在,则读取失败,返回NULL
FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {perror("Failed to open file");return 1;
}char buffer[1024];
if(fgets(buffer, sizeof(buffer), fp) != NULL) //文件存在printf("%s", buffer);
2.3. 当前路径
- 当一个进程在启动时,它的当前工作目录(当前路径),通常是启动该进程时所在的路径。这个路径通常由父进程(Shell)决定,它会被保存到/proc/pid/cwd中。
fopen(“data.txt”,“w”),如果data.txt文件不存在,就会在当前路径下创建此文件。
int chdir(const char* path);
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>int main()
{ printf("self id:%d\n", getpid());printf("更改前:当前工作目录\n");sleep(25);chdir("/root/tmp");printf("更改后:当前工作目录\n");sleep(25); FILE* fp = fopen("110.txt","w"); //此处表示,如果不存在110.txt文件,就会在当前路径(cwd)下,创建此文件if(fp == NULL) return 1;fclose(fp);printf("文件创建成功\n");return 0;
}
2.4. stdin、stdout、stderr
一、标准输入流stdin
-
定义:标准输入是程序可以从中读取输入数据的位置,它默认指向键盘,但也可以被重定向为文件或者其他输入设备。
-
作用:允许用户通过键盘或者其他输入设备向用户提供数据,也可以从文件中读取数据。
-
文件描述符:在linux系统中,stdin文件描述符为0。
二、标准输出流stdout
-
定义:标准输出是程序用于发送其输出数据的位置,它默认指向终端屏幕,但也可以被重定向为文件或者其他输出设备。
-
作用:stdout用于显示程序的正常输出,包括结果、状态信息、其他非错误信息。
-
文件描述符:在linux系统中,stdout文件描述符为1。
-
缓冲:stdout通常是行缓冲的,意味着输出会先存储在缓冲区中,直到遇到换行符或者缓冲区满才会刷新到目的地。
三、标准错误输出流stderr
-
定义:标准错误是程序用于发送错误、异常信息的位置,它默认指向终端屏幕,但也可以被重定向为文件或者其他输出设备。
-
作用:用于输出错误信息,以便用户能够识别并解决问题。
-
文件描述符:在linux系统中,stderr文件描述符为2。
-
缓冲:stderr是非缓冲的,意味着错误信息会被立即发送到目的地,以便用户能够尽快的看到它。
💡注意:程序在启动时,默认会打开stdin、stdout、stderr流。即:可以直接使用它们,无需手动打开。
如:printf是向标准输出流stdout发送格式化数据,因为stdout在启动时已经被打开了,并关联到了输出设备(显示器),所以printf可以直接使用它们,而无需进行额外的打开操作。
问:为什么程序在启动时,默认会打开stdin、stdout、stderr流?
- 原因:大部分的程序员,无论你用什么语言进行编程,需求就是让你的计算机去帮你加工和处理你的数据,就需要处理这三个问题(用户的数据从哪里来,计算完毕后怎么让用户看到对应的计算结果,计算过程中出错了怎么办),所以我们就需要stdin,给用户提供一个输入方式把数据输入进来,需要stdout,向一个特定的显示设备进行打印,方便给用户把计算结果显示出来,需要stderr,帮助用户把错误信息打印到特定设备中。大部分程序默认情况都要项你的程序输入数据,由你自己的程序计算完毕后,把结果进行返回,如果语言不提供,就需要程序员自己打开,太麻烦,所以程序默认给你打开这三个流(不用声明和定义,程序在启动时,就已经帮你初始化列表,直接使用就可以了)。
3. 系统文件I/O
3.1. 前言
一、访问文件不仅有C语言上的文件接口,OS必须提供对应的访问文件的系统调用接口。即:C标准库中的文件IO接口,底层一定封装了系统调用接口。
问:为什么C语言的文件接口,底层要封装对应的系统调用接口?
- 简化了操作,增强了安全性和稳定性。
文件进行读写操作,需要访问硬件,而OS作为软硬件资源的管理者,所以用户不能越过OS来直接访问硬件,OS会提供一组访问文件的系统调用接口 。
- 保证了语言的跨平台性、可移植性。
不同的操作系统有不同的系统调用接口和文件的操作机制。如果直接使用系统调用接口进行文件操作,那么编写的代码很难在不同OS之间移植。C语言标准库通过封装系统调用,为开发者提供了一套统一的文件操作接口,这样开发者就能够使用相同的代码,在不同OS上进行文件操作,提高了代码的跨平台性。
💡Tisp:如果让语言直接使用原生的系统接口,就注定了这个语言不具备跨平台性、可移植性。
二、在语言上,C、C++在访问文件时的接口和方式是不同的,在OS看来,底层原理是一样的,都是对底层系统调用接口的封装。
在C++中,主要是通过istream库中的fstream(ifstream、ofstream)类来实现,成员函数封装了对这些资源的操作,包括调用系统调用接口来实现底层的读写操作。
所有语言的文件操作不一样,但底层原理都是一样的,不随着意志而转移。上层就只要熟悉用法。
3.2. open
int open(const char* pathname,int flags);
-
功能:打开或创建一个文件。
-
参数pathname:要打开或者创建文件的路径名。
如果pathname为绝对路径时,当需要创建一个文件,会在pathname路径下创建它;
如果pathname为文件名时,当需要创建一个文件,会在当前路径(进程启动时所在的路径)下创建它。
- 返回值:打开成功,返回非负整数,即:文件描述符(用于后续文件操作);如果失败,返回-1,并设置errno以指示错误的原因。
fopen:"r"-> open:O_RDONLY
fopen:"w"-> open:O_WRONLY|O_CREAT|O_TRUNC
fopen:"a"-> open:O_WRONLY|O_CREAT|O_APPEND
- 底层调用Open,传递不同的参数,在上层表现为fopen以r、w、a方式打开文件。即:不同的fopen风格,代表着open传递了不同的选项。
3.2.1. flags
- 参数flag:标记位,用于指定文件的打开模式(只读、只写、追加等)和其他选项(创建文件、截断文件等)。
//宏
#define O_RDONLY 0x00000000 // 只读模式 16进制
#define O_WRONLY 0x00000001 // 只写模式
#define O_RDWR 0x00000002 // 读写模式
//以上三个常量,必须且只能指定一个
#define O_CREAT 0x00000100 // 如果文件不存在,则创建文件
#define O_TRUNC 0x00000200 // 如果文件存在,则截断为零长度
#define O_APPEND 0x00000400 // 每次写入时追加到文件末尾
#define O_EXCL 0x00000800 // 如果文件已存在,则打开失败
#define O_NONBLOCK 0x00001000 // 非阻塞模式
- flag参数是一个整数,每个比特位代表一个标记位。通过位操作(|、&),可以一次性向函数传递多个标记位。
在编程中,涉及需要向函数传递多个布尔选项(标记位)时,使用单个整数(int,32位),并通过位操作来设置和检查这些选项,这种方法被称为"位图",是一种非常高效和节省资源的方法。
- 使用宏定义,来表示各种标记位,每个宏定义只有一位为1(每个宏中为1的位是错开的),其余位全为0。在这个整数中为1的位,用来表示某个特定的选项是否被设置。多个宏通过位操作(|按位或)组合,一次性地向函数传递多个标记位。即:通过位图的方式,传递多个标记位。
#include<stdio.h> #define ONE 1
#define TWO (1 << 1)
#define THREE (1 << 2)
#define FOUR (1 << 3)void print(int flag)
{if(flag & ONE) printf("1\n");if(flag & TWO) printf("2\n");if(flag & THREE) printf("3\n");if(flag & FOUR) printf("4\n");
}int main(){print(ONE);printf("-----------\n");print(ONE|TWO);printf("-----------\n");print(ONE|TWO|THREE); //相当于是将两个宏中为1的位,进行了合并printf("-----------\n");print(ONE|TWO|THREE|FOUR); printf("-----------\n");return 0;}
3.2.2. mode
- 参数mode:可选参数,用于创建文件时设置文件权限。
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>int main()
{//以"w"方式打开文件,文件存在,就在当前路径下新建文件int fd = open("data.txt", O_WRONLY|O_CREAT|O_TRUNC);if(fd == -1) //打开失败{perror("open");return 1;}return 0;
}
现象:新建文件的权限乱码了。 -> 原因:新建文件,未设置文件权限。
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>int main()
{//以"w"方式打开文件,文件存在,就在当前路径下新建文件int fd = open("data.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);//int fd = open("data.txt", O_WRONLY|O_CREAT|O_TRUNC);if(fd == -1) //打开失败{perror("open");return 1;}return 0;
}
现象:文件权限并不是666。
现象:文件权限不为666。
原因:默认(最终)权限计算公式 = 起始权限 & (~umask值) , 本质是从起始权限中去掉在umask权限中出现的权限,如果在起始权限中某权限位不存在,但umask中该权限位存在,该权限位的结果为0,"去掉"不是删除。
mode_t umask(mode_t mask);
- 功能:设置文件的权限掩码。
umask函数只会影响调用它的进程所创建的权限掩码,而不会对父进程或其他进程的权限掩码产生影响。
#include <sys/stat.h>
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h>int main()
{umask(0); //只改变此进程所创建的权限掩码//以"w"方式打开文件,文件存在,就在当前路径下新建文件int fd = open("data.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666);//int fd = open("data.txt", O_WRONLY|O_CREAT|O_TRUNC);if(fd == -1) //打开失败{perror("open");return 1;}return 0;
}
3.2.3. 返回值fd
一、FILE*与fd的关系
在底层上,fd是文件描述符,用于标识一个打开的文件,用于后续的文件操作。在语言上,FILE*是C语言标准I/O库定义的类型,用于表示文件流,用于后续的文件操作 —> 所以FILE结构体对象必定封装了fd。
-
FILE类型是C语言IO库定义的一个结构体类型,用于表示一个流,这个流可以关联到一个文件、内存区域或其他输出/输入资源。FILE结构体内部封装了与流操作相关的各种信息(如:文件描述符fd、文件读写位置、缓冲区、文件状态等)。
-
流:抽象的概念,它提供了统一的方式来处理数据的输入、输出,无论这个数据是来自于文件、标准输入输出(键盘、显示器)或其他形式的输入输出资源。流的本质是数据传输。
#include<stdio.h>
#include<sys/types.h>
#include<fcntl.h> int main()
{int fd1 = open("data1.txt", O_WRONLY|O_CREAT|O_TRUNC);int fd2 = open("data2.txt", O_WRONLY|O_CREAT|O_TRUNC);int fd3 = open("data3.txt", O_WRONLY|O_CREAT|O_TRUNC);printf("fd1:%d\nfd2:%d\nfd3:%d\n", fd1, fd2, fd3);printf("\n"); //FILEl类型是由C标准库封装的结构体printf("stdin:%d\n", stdin->_fileno);printf("stdout:%d\n", stdout->_fileno);printf("stderr:%d\n", stderr->_fileno);return 0;
}
二、C语言文件接口,不仅在接口上进行了封装,在类型上也进行了封装。
- 接口层面的封装
定义:将文件操作的具体实现隐藏起来,只向用户暴露了一组预定义的函数,用户就可以通过这组函数间接与文件系统进行交互。这种方式使得用户不需要了解文件系统背后复杂的机制。如:磁盘读写。
如:C语言标准库中的fopen、fclose、fread、fwrite等高级函数,底层封装了对应的系统调用接口open、close、read、write。用户通过这些函数可以打开文件、关闭文件、读取文件内容、写入文件内容,而不需要关心这些操作是如何在底层实现的。
- 类型层面的封装
定义:为文件操作定义了一个或多个特定的数据类型(如:FILE类型),用于表示文件状态和上下文。
如:FILE结构体封装了文件描述符、文件读写位置、缓冲区、文件状态等其他相关信息。
3.3. write
ssize_t write(int fd, const void *buf, size_t count);
-
功能:向打开的文件中写入数据。
-
参数:fd,表示写入数据的文件或设备; buf,指向要写入数据的缓冲区的指针; count,要写入的字节数。
如果buf为const char*类型,strlen(buff),不需要在后面+1,即:不需要把字符串结束标志\0写入进去。因为c语言字符串以\0结尾,\0不是字符串内容,而是作为字符串结束标记,与文件无关,若把\0写入,则会造成乱码。
- 返回值:如果成功,返回实际写入的字节数。如果出错,则返回-1,并设置errno以指示错误。
3.4. read
ssize_t read(int fd, void *buf, size_t count);
-
功能:从打开的文件中读取数据。
-
参数:fd,表示要读取数据的文件或设备; buf,指向读取数据的缓冲区的指针; count,要读取的最大字节数。
-
返回值:如果成功,返回实际读取的字节数。如果出错,则返回-1,并设置errno以指示错误。如果到达文件末尾(EOF),则返回0。
3.5. close
int close(int fd);
-
功能:关闭一个打开的文件描述符。
-
返回值:如果成功,返回0。如果失败,返回-1,并设置errno以指示错误。
3.6. lseek
off_t lseek(int fd, off_t offset, int whence);
-
功能:移动文件指针的位置。
-
参数offset:是从whence指定位置开始的偏移量,以字节为单位(在左边,则为负值、在右边,则为正值)。
参数whence:指定偏移量的参考点。
- 返回值:如果成功,返回新的文件偏移量(相对于文件开头的字节数)。如果出错,则返回-1,并设置errno以指示错误。