hello,各位读者大大们你们好呀
系列专栏:【Linux初阶】
✒️✒️本篇内容:重新理解文件和文件操作,C语言实现的简单文件操作,文本初始权限,系统接口介绍(open、close、write、read)
作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
相信很多朋友在C语言学习阶段已经接触过文件操作的相关知识,在这篇文章中,我们会更深入的学习文件的知识,以操作系统的视角,重新认识和理解文件。
那么话不多数,让我们直接开始吧!
在学习文件之前,我们需要重新回顾一下文件的相关知识,让我们对文件有一个系统、立体的认识。
文件 = 内容 + 属性
;文件操作的本质:进程对文件的操作
;
所以,我们研究文件操作,实际上就是在研究进程和被打开文件的关系
实际上,除了C/C++,Java、Python、php、go等语言都有自己的文件操作接口,它们的文件操作接口都不一样!那么问题来了,有那么多的文件操作接口,我们应该怎么降低我们的学习成本呢?
在解答上面这个问题之前,我们要明确我们的文件是如何被操作的。我们的文件储存在我们的磁盘中,磁盘 -> 硬件 -> OS(硬件被操作系统管理)->所有人想访问磁盘都不能绕过OS -> 使用OS提供的接口(文件级接口)。
所以无论上层语言怎么变化,a.库函数底层必须使用系统调用接口;b.库函数可以千变万化,但是底层不变。这就得出了我们降低学习成本问题的答案,只要我们学习不变的东西,我们就可以降低我们学习文件操作知识的成本了。在这篇文章中,我会着重讲述文件操作的底层原理和常见系统调用接口
。
———— 我是一条知识分割线 ————
在博主使用 vim进行学习的过程中,常常需要对其中的代码进行大范围操作,经过网络搜索发现各种阅读量靠前的信息并不能满足自己对 vim操作快速便捷简明的要求,因此在此处根据自己的编码习惯对 vim相应的知识做相应的补充。
批量化注释代码:Ctrl+v
进入块选择模式(V-BLOCK模式),h、j、k、l
分别代表左下上右
,控制块的大小,输入大写 I
,此时下方会提示进入“insert”模式,再输入注释符//
,按Esc回到初始NORMAL模式
(返回后才会完成批量化注释)。
撤销上一次的操作:初始NORMAL模式下按 u。
取消批量注释:Ctrl+v进入块选择模式(V-BLOCK模式),选中你要删除的行首的注释符号,注意// 要选中两个,输入d 即可完成取消批量注释。
调整代码格式 + 批量删除代码:Ctrl+v进入块选择模式(V-BLOCK模式),下拉选中你要删除的代码首行,输入小写 d
,代码集体向前缩进一行
。输入大写 D
,删除选中行所有的代码
。
———— 我是一条知识分割线 ————
打开文件的操作如下,fopen(要打开的文件名, 操作形式),其中操作形式有 r(读)、w(写)、r+、w+、a(追加文件内容)、a+ 等。
//r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()【追加式写入】
FILE* fp = fopen(FILE_NAME, "w");
在下面的演示代码中,我们以 w为例,即文件不存在,创建+写入。以 w单纯打开文件,c会自动清空文件内部的数据。
#include
#include
// 我没有指明路径
#define FILE_NAME "log.txt"
int mian()
{
FILE* fp = fopen(FILE_NAME, "w"); //r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()
if (NULL == fp)
{
perror("fopen"); //验证文件打开是否成功
return 1;
}
int cnt = 5;
while (cnt)
{
fprintf(fp, "%s:%d\n", "hello world", cnt--);
}
fclose(fp);
}
最后,我们会把五个hello world写入到文件中
【补充】我们可以通过 cat + 文件名指令打印对应的文件内容
fget读取文件:以行为单位,从特定的文件流中获取数据,放到 s指向的缓冲区之中,如果成功返回 s,失败返回NUll。
在下面的演示代码中,我们以 r操作为例,即文件存在,读取文件。
FILE* fp = fopen(FILE_NAME, "r");
代码如下(示例):
#include
#include
#include
// 我没有指明路径
#define FILE_NAME "log.txt"
int mian()
{
FILE* fp = fopen(FILE_NAME, "r"); //r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()
if (NULL == fp)
{
perror("fopen"); //验证文件打开是否成功
return 1;
}
char buffer[64];
while(fgets(buffer, sizeof(buffer) - 1, fp) != NULL)//#include ,//fgets会把数组最后的\0(换行)也输入获取进去,所以要-1
{
buffer[strlen(buffer) - 1] = 0;//输入指令时需要按回车,回车也会被也输入获取进去,所以要将最后一位置0
puts(buffer);
}
fclose(fp);
}
代码运行成功之后,会输出文件内容
我们以 a操作
为例,即文件存在,追加文件内容。
#include
#include
#include
// 我没有指明路径
#define FILE_NAME "log.txt"
int mian()
{
FILE* fp = fopen(FILE_NAME, "r"); //r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()
if (NULL == fp)
{
perror("fopen"); //验证文件打开是否成功
return 1;
}
int cnt = 5;
while (cnt)
{
fprintf(fp, "%s:%d\n", "hello world", cnt--);
}
fclose(fp);
}
代码每成功运行一次,会向文件中追加5个hello world的内容
【注意】
其他语言会有其他的文件操作方法/接口,以C++为例,会有 std::ifsteam::opean()打开文件、std::ifsteam::ifsteam(传文件流)等,有兴趣的小伙伴可以自己去了解一下哦~
文件创建的初始权限为666,但是需要0666 & ~umask,umask的默认值为0002,因此我们普通文本类文件创建时大部分的权限都是664
。
664的权限表示为: -rw-rw-r–
在上面的章节中,我们学习的C语言进行文件操作的部分接口,但这并不是我们本次学习最重要的知识,下面通过对系统调用接口的学习,相信大家一定能对文件的底层有更深入的了解。
opean,打开或创建一个文件,我们可以通过man指令来查看一下 open的基本信息
man 2 open
#include
#include
#include
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
pathname: 要打开或创建的目标文件;
flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行“或”运算,构成flags;
mode: 权限,创建文件时设置文件的起始权限(通常设置为 0666);
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定一个且只能指定一个
O_CREAT : 若文件不存在,则创建它。需要使用mode选项,来指明新文件的访问权限
O_APPEND: 追加写
O_TRUNC: 清空原文件
返回值:
成功:新打开的文件描述符(descriptor)
失败:-1
———— 我是一条知识分割线 ————
下面是文档中对 flag的描述
【注意】我们可以使用树划线 | (通道),将不同的flag选项串联起来
。
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
在C语言中,我们以一个整数(int)作为一个标记位。因为一个int由32个比特位构成,我们让1存在于32位比特位中的不同的位置,使用不同的比特位组合,实现对不同情况的标识。open中的flag底层就是使用这样的原理进行选项传递的。
下面是一个应用比特位作为标识,输出不同结果的代码示例
#include
#include
#include
// 每一个宏,对应的数值,只有一个比特位是1,彼此位置不重叠
#define ONE (1<<0)
#define TWO (1<<1)
#define THREE (1<<2)
#define FOUR (1<<3)
void show(int flags)
{
if(flags & ONE) printf("one\n");
if(flags & TWO) printf("two\n");
if(flags & THREE) printf("three\n");
if(flags & FOUR) printf("four\n");
}
int main()
{
show(ONE);
printf("-----------------------\n");
show(TWO);
printf("-----------------------\n");
show(ONE | TWO);
printf("-----------------------\n");
show(ONE | TWO | THREE);
printf("-----------------------\n");
show(ONE | TWO | THREE | FOUR);
printf("-----------------------\n");
}
代码运行起来之后,我们就可以根据不同的标记位,输出不同的内容
———— 我是一条知识分割线 ————
close - 系统级调用,关闭文件
#include
#include
#include
#include
#include
#include
#include
#define FILE_NAME "log.txt"
int main()
{
// 以只读的方式打开
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
close(fd);
}
———— 我是一条知识分割线 ————
通过 man查看 write的文档,
通过观察 write的接口,我们可以发现,有const void *
类型参数的存在,这也告诉了我们,无论其他编程语言传入的是什么类型的数据(文本、图片),在系统调用接口(操作系统)的层面,都可以将它们对应的二进制数据读取进去。
我们之前就提及过,在C语言的文件操作接口中,中以 w单纯代开文件,c会自动清空文件内部的数据,这是为什么呢?我们的系统调用接口也会自动帮我们做吗?实际上,我们的系统调用接口并不会在下一次打开时自动清空里面的数据,C语言的接口能完成是因为它做了相应的封装。
那我们要怎么样才能让系统调用接口在打开时就清空呢?这里我们就需要在open中添加一些 flag选项了。
#include
#include
#include
#include
#include
#include
#include
#define FILE_NAME "log.txt"
int main()
{
// 只写 + 新建
//int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);
// 只写 + 新建 + 清除原文件内容
int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
int cnt = 5;
char outBuffer[64];
while (cnt)
{
sprintf(outBuffer, "%s:%d\n", "aaaa", cnt--);
// 你以\0作为字符串的结尾,是C语言的规定,和我文件有什么关系呢?
write(fd, outBuffer, strlen(outBuffer));
}
close(fd);
}
———— 我是一条知识分割线 ————
通过 man查看 read的文档,
read返回值,成功返回读到的字节数,0则代表读到了文件的结尾
。
假设我们的文件已经存在且其中保存的是字符串,虽然系统调用接口理论上可以读取任何类型的文件,但是我们还是需要根据实际情况,为read提供读取结束的依据,所以我们需要在下方增加一个字符串结束的判断。
int main()
{
int fd = open(FILE_NAME, O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
char buffer[1024];
ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
if (num > 0) buffer[num] = 0; // 0, '\0', NULL -> 0 为读取提供结尾
printf("%s", buffer);
close(fd);
}
———— 我是一条知识分割线 ————
这里我们以C语言的库函数接口为例,我们使用库函数接口实际上底层都调用了对应的系统调用接口。
也就是说,库函数接口是系统调用接口的封装
。对应的,系统调用接口会将对应的返回值返回给库函数接口,使其能完成对应的功能
。(其他编程语言以此类推)
基础IO - 文件操作(使用系统接口实现) 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux 的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!