[!abstract] Linux文件相关重点
- 复习C文件IO相关操作
- 认识文件相关系统调用接口
- 认识文件描述符,理解重定向
- 对比fd和FILE,理解系统调用和库函数的关系
- 理解文件系统中inode的概念
- 认识软硬链接,对比区别
- 认识动态静态库,学会结合gcc选项,制作动静态库
文件 = 内容 + 属性
所以对文件的所有操作被分为:
a. 对内容操作
b. 对属性操作
我们要访问一个文件的时候,都是要先把这个文件打开的:
打开前:是普通的磁盘文件
打开后:文件被加载到内存中
打开的步骤是由操作系统来做的!
一个进程可以打开很多文件,所以操作系统运行时被打开的文件是很多的,操作系统当然要对这些被打开的文件做管理,管理的方式是:先描述,再组织。因此,一个文件要被打开,一定要先在内核中,形成被打开的文件对象。
本文研究的文件操作的本质是:进程 和 内存中被打开(被加载)的文件 的关系
函数签名 | 描述 |
---|---|
FILE fopen(const char path, const char* mode) |
打开文件并返回指向文件的指针 |
int fclose(FILE *stream) |
关闭文件 |
模式 | 描述 |
---|---|
“r” | 读取:打开文件进行输入操作。文件必须存在。 |
“w” | 写入:创建一个空文件进行输出操作。如果同名文件已存在,其内容将被丢弃,文件被视为新的空文件。 |
“a” | 追加:打开文件进行输出操作,将数据追加到文件末尾。输出操作总是在文件末尾写入数据,扩展文件大小。重新定位操作(fseek、fsetpos、rewind)将被忽略。如果文件不存在,则创建文件。 |
“r+” | 读取/更新:打开文件进行更新操作(既可读又可写)。文件必须存在。 |
“w+” | 写入/更新:创建一个空文件并打开它进行更新操作(既可读又可写)。如果同名文件已存在,其内容将被丢弃,文件被视为新的空文件。 |
“a+” | 追加/更新:打开文件进行更新操作(既可读又可写),所有输出操作都在文件末尾写入数据。重新定位操作(fseek、fsetpos、rewind)影响下一次的输入操作,但输出操作将位置移回文件末尾。如果文件不存在,则创建文件。 |
[!Attention] 不带+号的模式在文件打开时会对原文件进行擦除(覆盖)操作,具体来说:
- "w"模式: 如果使用 “w” 模式打开一个文件,它会创建一个空文件,如果同名文件已存在,则会清空该文件的内容。换句话说,打开文件时,如果文件已经存在,原文件的内容将被抹掉。
- "a"模式: 如果使用 “a” 模式打开一个文件,文件指针会移动到文件末尾,写入的数据将追加到文件的末尾。如果文件不存在,则会创建一个新文件。原文件的内容不会被清空,而是保留在文件中。
这是在不带+号的写入模式下的行为。要同时进行读写而不清空文件内容,可以考虑使用带+号的模式,如 “r+” 或 “a+”。
实验一下:
#include
int main()
{
FILE *filePointer;
// 打开文件
filePointer = fopen("test.txt", "w");
if (filePointer == NULL) {
printf("文件打开失败。\n");
return 1;
}
printf("文件打开成功,执行其他文件操作...\n");
// 执行其他文件操作...
// 关闭文件
if (fclose(filePointer) == 0) {
printf("文件关闭成功。\n");
} else {
printf("文件关闭失败。\n");
}
return 0;
}
函数签名 | 描述 |
---|---|
int fputc(int c, FILE *stream) |
将一个字符写入文件 |
int fgetc(FILE *stream) |
从文件中读取一个字符 |
char *fgets(char *s, int size, FILE *stream) |
从文件中读取一行内容,并存储到字符串 s 中 |
int fputs(const char *s, FILE *stream) |
将字符串 s 写入文件 |
size_t fread(void *ptr, size_t size, size_t count, FILE *stream) |
从文件中读取二进制数据 |
size_t fwrite(void *ptr, size_t size, size_t count, FILE *stream) |
向文件中写入二进制数据 |
#include
int main() {
FILE *filePointer;
char ch;
// 写入文件
filePointer = fopen("test.txt", "w");
if (filePointer == NULL) {
printf("文件打开失败。\n");
return 1;
}
fputc('A', filePointer);
fclose(filePointer);
// 读取文件
filePointer = fopen("test.txt", "r");
if (filePointer == NULL) {
printf("文件打开失败。\n");
return 1;
}
ch = fgetc(filePointer);
printf("读取的字符:%c\n", ch);
fclose(filePointer);
return 0;
}
函数签名 | 描述 |
---|---|
int fseek(FILE *stream, long offset, int whence) |
设置文件指针偏移量,用于定位读写位置 |
long ftell(FILE *stream) |
返回当前文件指针的位置 |
void rewind(FILE *stream) |
将文件指针重置到文件开头 |
int feof(FILE *stream) |
检测是否到达文件末尾 |
#include
int main() {
FILE *filePointer;
// 写入文件
filePointer = fopen("test.txt", "w");
if (filePointer == NULL) {
printf("文件打开失败。\n");
return 1;
}
fputs("Hello, World!", filePointer);
fclose(filePointer);
// 随机读取文件
filePointer = fopen("test.txt", "r");
if (filePointer == NULL) {
printf("文件打开失败。\n");
return 1;
}
fseek(filePointer, 7, SEEK_SET); // 移动到文件第8个字符的位置
char ch = fgetc(filePointer);
printf("随机读取的字符:%c\n", ch);
fclose(filePointer);
return 0;
}
stdin
、stdout
和stderr
fopen
等函数手动打开。它们分别用于标准输入、标准输出和标准错误输出。stdin
(标准输入流):
stdin
代表标准输入流,通常与键盘输入相关联。FILE* stdin
。scanf
等函数从stdin
中读取输入数据。stdout
(标准输出流):
stdout
代表标准输出流,通常与屏幕输出相关联。FILE* stdout
。printf
等函数将输出写入到stdout
中。stderr
(标准错误输出流):
stderr
代表标准错误输出流,通常也与屏幕输出相关联。FILE* stderr
。stdout
相比,stderr
通常用于输出错误消息,以便在程序发生错误时将错误信息与正常输出区分开。这些标准流的使用使得C程序能够在不同环境中运行,而不用关心具体的输入和输出设备。在程序中,你可以直接使用这些流,而无需显式打开或关闭它们。例如,可以通过fprintf
将输出写入到文件,而不仅仅是屏幕,或者通过fscanf
从文件而不是键盘读取输入。
系统调用接口和库函数的关系,就是库函数封装了系统调用接口。
所以可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。
如何封装?
fopen函数在上层为用户申请FILE结构体变量,并返回该结构体的地址(FILE*),在底层通过系统接口open打开对应的文件,得到文件描述符fd,并把fd填充到FILE结构体当中的_fileno
变量中,至此便完成了文件的打开操作。
先介绍一个小技巧
关于Linux常用的传参方式:函数传入标志位的小技巧
C语言常通过一个整形来传递选项,但是当选项较多时,每一个选项都用一个整形太浪费空间,所以有人想出了办法 – 使用一个比特位来传递一个选项,这样一个整形就可以传递32种选项,大大节省了空间,具体案例如下:
#include
#define Print1 1 // 0001 #define Print2 (1<<1) // 0010 #define Print3 (1<<2) // 0100 #define Print4 (1<<3) // 1000 void Print(int flags) { if(flags&Print1) printf("hello 1\n"); if(flags&Print2) printf("hello 2\n"); if(flags&Print3) printf("hello 3\n"); if(flags&Print4) printf("hello 4\n"); } int main() { Print(Print1); Print(Print1|Print2); Print(Print1|Print2|Print3); Print(Print3|Print4); Print(Print4); return 0; } 如上,我们将宏与比特位对应,然后在 Print 函数中编写每一个宏对应的功能,之后我们就可以在其他函数中通过调用 Func 函数并传递对应的选项来达到我们想要的效果,并且我们可以通过按位或来实现同时传递几个选项。
系统调用open:用于打开或创建一个文件。open
函数是一个系统调用,用于打开文件或创建新文件,返回值是一个文件描述符,后续的文件操作可以使用这个文件描述符。
pathname: 要打开或创建的目标文件
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“按位或”运算,构成flags。
常用的flags的可选参数:
文件打开方式 | 含义 | 如果指定文件不存在 |
---|---|---|
O_RDONLY | 以只读形式打开 | 出错 |
O_WRONLY | 以只写形式打开 | 出错 |
O_RDWR | 以读写形式打开 | 出错 |
O_APPEND | 向文本文件尾添加数据 | 出错 |
O_CREAT | 如果文件不存在,创建新文件 | 建立一个新的文件 |
O_TRUNC | 打开文件时清空文件中之前的数据 | 出错 |
返回值
如果成功,则返回读取的字节数(0表示文件结束),并将文件位置提前该字节数。如果这个数字小于请求的字节数,则不会报错;例如,发生这种情况可能是因为现在实际可用的字节数减少了(可能是因为接近文件末尾,或者因为我们正在从管道或终端读取数据),或者因为read()被信号中断。发生错误时,返回-1,并适当地设置errno。在这种情况下,文件位置(如果有的话)是否改变是未指定的。
下面用read.c来测试read()系统调用:
#include
#include
#include
#include
#include
#define FILE_NAME "file1.txt"
int main() {
int fd = open(FILE_NAME, O_RDONLY);
if(fd == -1) {
perror("open");
return 1;
}
char buf[1024];
//C语言字符串以'\0'结尾,所以留一个位置来放置
int ret = read(fd, buf, sizeof(buf) - 1);
//read读到文件末尾返回0
while(ret != 0) {
buf[ret] = '\0';
printf("%s", buf);
ret = read(fd, buf, sizeof(buf) - 1);
}
close(fd);
}
下面用write.c来测试write()系统调用:
#include
#include
#include
#include
#include
#include
#define FILE_NAME1 "file1.txt" //已存在
#define FILE_NAME2 "file2.txt" //不存在
int main() {
//以只写形式打开,并清空文件中之前的数据
int fd1 = open(FILE_NAME1, O_WRONLY | O_TRUNC);
//创建文件并以只写形式打开,并指定文件的默认权限为0666(还受umask的影响)
//同时,我们可以通过umask接口手动设置当前进程的文件掩码,而不使用从父进程继承过来的umask
umask(0000);
int fd2 = open(FILE_NAME2, O_WRONLY | O_CREAT | O_TRUNC, 0666);
//错误处理
if(fd1 == -1 || fd2 == -1) {
perror("open");
return 1;
}
const char* buf1 = "hello file1\n";
const char* buf2 = "hello file2\n";
int cnt = 5;
while(cnt--) {
//注意:这里strlen求得的长度不用加1,因为字符串以'\0'结尾只是C语言的特性,而文件中并不这样规定
write(fd1, buf1, strlen(buf1));
write(fd2, buf2, strlen(buf2));
}
close(fd1);
close(fd2);
}
[!Attention] 上面的文件操作的三个细节:
如果在向文件中写入数据时没有指定
O_TRUNC
选项,而是直接写入数据,新数据会从文件的当前位置开始写入,而不会影响文件中原有的数据。如果新数据的长度小于文件当前的大小,那么文件的尾部会保留原有的数据,就比如先写入五行 hello world,再写入五行 hello:创建 file2.txt 时我们通过 umask 系统调用将 umask 由默认的 0002 设置为了 0000(第一个0代表八进制),然后将 open() 系统调用的最后一个参数 mode 设置为 0666,所以 file2 的最终权限为
文件的默认权限 & ~umask
即0666 & ~0000
亦即 0666:在C语言中,字符串是以
'\0'
(空字符或Null字符)结尾的字符数组。但是,在文件中存储字符串时,并不要求在文件中以'\0'
结尾。文件系统仅是按照写入的字节数来存储数据,而不关心字符串的结尾字符。因此,当你使用write
函数将字符串写入文件时,不需要把字符串结尾的'\0'
字符写入文件,只需写入字符串本身即可。
如果在写入文件时将'\0'
字符写入:
lseek
(“lseek"代表"long seek”)是Linux系统调用之一,用于在文件中移动文件指针的位置。它是对文件进行随机访问的关键系统调用之一。lseek
可以用于设置文件偏移量,以便在文件中执行读取或写入操作。
函数原型如下:
fd
是文件描述符,表示要操作的文件。offset
是文件偏移量,可以为正数、负数或零,用于指定相对于whence
参数的偏移位置。whence
指定了偏移量的基准位置,可以是以下值之一:
SEEK_SET
:相对于文件的起始位置进行偏移。SEEK_CUR
:相对于当前文件指针的位置进行偏移。SEEK_END
:相对于文件的末尾位置进行偏移。lseek
函数的返回值是新的文件偏移量,如果调用出现错误,则返回 -1
。
使用示例:
#include
#include
#include
int main() {
int fd = open("example.txt", O_RDWR);
if (fd == -1) {
perror("open");
return 1;
}
// 使用 lseek 将文件指针移动到文件末尾
off_t end_position = lseek(fd, 0, SEEK_END);
if (end_position == -1) {
perror("lseek");
close(fd);
return 1;
}
printf("当前文件大小:%lld 字节\n", (long long)end_position);
// 使用 lseek 将文件指针移动到文件开头
off_t start_position = lseek(fd, 0, SEEK_SET);
if (start_position == -1) {
perror("lseek");
close(fd);
return 1;
}
printf("文件指针已重置到文件开头\n");
close(fd);
return 0;
}
在上述示例中,lseek
函数用于将文件指针移动到文件的末尾,获取文件的大小,然后将文件指针重新设置到文件开头。这演示了lseek
在文件中移动文件指针的基本用法。