【Linux初阶】基础IO - 文件操作(使用系统接口实现) | vim批量注释代码

🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:重新理解文件和文件操作,C语言实现的简单文件操作,文本初始权限,系统接口介绍(open、close、write、read)
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-



前言

相信很多朋友在C语言学习阶段已经接触过文件操作的相关知识,在这篇文章中,我们会更深入的学习文件的知识,以操作系统的视角,重新认识和理解文件。

那么话不多数,让我们直接开始吧!


一、重新谈论文件

在学习文件之前,我们需要重新回顾一下文件的相关知识,让我们对文件有一个系统、立体的认识。

  1. 空文件,也要占据空间;
  2. 文件 = 内容 + 属性
  3. 文件操作 = 对内容/属性 or 内容+属性的操作;
  4. 标定一个文件,必须使用:文件路径+文件名【唯一性】;
  5. 如果没有指明对应的文件路径,默认是在当前路径下进行文件访问;
  6. 当我们把 fopen,fclose,fread,fwrite等接口写好,代码编译完形成二进制可执行文件之后,如果没有运行起来,文件对应的操作不会被执行;文件操作的本质:进程对文件的操作
  7. 一个文件要被访问,就必须先被打开(用户进程调用接口,OS帮我们打开);

所以,我们研究文件操作,实际上就是在研究进程和被打开文件的关系


二、重新谈论文件操作

1.除了C/C++,其他语言有文件接口吗?

实际上,除了C/C++,Java、Python、php、go等语言都有自己的文件操作接口,它们的文件操作接口都不一样!那么问题来了,有那么多的文件操作接口,我们应该怎么降低我们的学习成本呢?

在解答上面这个问题之前,我们要明确我们的文件是如何被操作的。我们的文件储存在我们的磁盘中,磁盘 -> 硬件 -> OS(硬件被操作系统管理)->所有人想访问磁盘都不能绕过OS -> 使用OS提供的接口(文件级接口)

所以无论上层语言怎么变化,a.库函数底层必须使用系统调用接口;b.库函数可以千变万化,但是底层不变。这就得出了我们降低学习成本问题的答案,只要我们学习不变的东西,我们就可以降低我们学习文件操作知识的成本了。在这篇文章中,我会着重讲述文件操作的底层原理和常见系统调用接口

———— 我是一条知识分割线 ————

2.vim知识补充 - 如何批量注释代码

在博主使用 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删除选中行所有的代码

———— 我是一条知识分割线 ————

3.C语言实现的简单文件操作

(1)写入文件

打开文件的操作如下,fopen(要打开的文件名, 操作形式),其中操作形式有 r(读)、w(写)、r+、w+、a(追加文件内容)、a+ 等。

 //r,w, r+(读写,不存在出错),w+(读写, 不存在创建), a(append, 追加), a+()【追加式写入】
 FILE* fp = fopen(FILE_NAME, "w");

在下面的演示代码中,我们以 w为例,即文件不存在,创建+写入。以 w单纯打开文件,c会自动清空文件内部的数据。

#include <stdio.h>
#include <unistd.h>

// 我没有指明路径
#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 + 文件名指令打印对应的文件内容

(2)读取文件

fget读取文件:以行为单位,从特定的文件流中获取数据,放到 s指向的缓冲区之中,如果成功返回 s,失败返回NUll。

在这里插入图片描述

在下面的演示代码中,我们以 r操作为例,即文件存在,读取文件。

FILE* fp = fopen(FILE_NAME, "r"); 

代码如下(示例):

#include <stdio.h>
#include <unistd.h>
#include <string.h>

// 我没有指明路径
#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 <string.h>,//fgets会把数组最后的\0(换行)也输入获取进去,所以要-1
    {
        buffer[strlen(buffer) - 1] = 0;//输入指令时需要按回车,回车也会被也输入获取进去,所以要将最后一位置0
        puts(buffer);
    }

    fclose(fp);

}

代码运行成功之后,会输出文件内容

(3)追加文件内容

我们以 a操作为例,即文件存在,追加文件内容。

#include <stdio.h>
#include <unistd.h>
#include <string.h>

// 我没有指明路径
#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语言进行文件操作的部分接口,但这并不是我们本次学习最重要的知识,下面通过对系统调用接口的学习,相信大家一定能对文件的底层有更深入的了解。

1.open接口

opean,打开或创建一个文件,我们可以通过man指令来查看一下 open的基本信息

man 2 open 
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

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

———— 我是一条知识分割线 ————

2.open接口中的flag标记位

(1)flag标记位介绍

下面是文档中对 flag的描述

在这里插入图片描述

  • O_RDONLY - 只读
  • O_WRONLY - 只写
  • O_RDWR - 读写
  • O_CREAT - 创建
  • O_TRUNC - 清空文件

【注意】我们可以使用树划线 | (通道),将不同的flag选项串联起来

int fd = open(FILE_NAME, O_WRONLY | O_CREAT | O_TRUNC, 0666);

(2)flag底层原理(仅作了解)

在C语言中,我们以一个整数(int)作为一个标记位。因为一个int由32个比特位构成,我们让1存在于32位比特位中的不同的位置,使用不同的比特位组合,实现对不同情况的标识。open中的flag底层就是使用这样的原理进行选项传递的。

下面是一个应用比特位作为标识,输出不同结果的代码示例

#include <stdio.h>
#include <unistd.h>
#include <string.h>

// 每一个宏,对应的数值,只有一个比特位是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");
}

代码运行起来之后,我们就可以根据不同的标记位,输出不同的内容

在这里插入图片描述

———— 我是一条知识分割线 ————

3.close

close - 系统级调用,关闭文件

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>


#define FILE_NAME "log.txt" 

int main()
{
	// 以只读的方式打开
    int fd = open(FILE_NAME, O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        return 1;
    }

    close(fd);
}

———— 我是一条知识分割线 ————

4.write接口

通过 man查看 write的文档,

参数:fd - 想往哪个文件写,buf - 对应文件的缓冲区在哪里,count - 缓冲区对应的字节个数。
在这里插入图片描述

通过观察 write的接口,我们可以发现,有const void *类型参数的存在,这也告诉了我们,无论其他编程语言传入的是什么类型的数据(文本、图片),在系统调用接口(操作系统)的层面,都可以将它们对应的二进制数据读取进去

我们之前就提及过,在C语言的文件操作接口中,中以 w单纯代开文件,c会自动清空文件内部的数据,这是为什么呢?我们的系统调用接口也会自动帮我们做吗?实际上,我们的系统调用接口并不会在下一次打开时自动清空里面的数据,C语言的接口能完成是因为它做了相应的封装。

那我们要怎么样才能让系统调用接口在打开时就清空呢?这里我们就需要在open中添加一些 flag选项了。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>


#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);
}

———— 我是一条知识分割线 ————

5.read接口

通过 man查看 read的文档,

参数:fd - 读哪个文件,buf - 对应文件的缓冲区在哪里,count - 缓冲区对应的字节个数。
在这里插入图片描述

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);
}

———— 我是一条知识分割线 ————

6.库函数接口和系统调用接口的关系

这里我们以C语言的库函数接口为例,我们使用库函数接口实际上底层都调用了对应的系统调用接口。

也就是说,库函数接口是系统调用接口的封装。对应的,系统调用接口会将对应的返回值返回给库函数接口,使其能完成对应的功能。(其他编程语言以此类推)

在这里插入图片描述


结语

🌹🌹 基础IO - 文件操作(使用系统接口实现) 的知识大概就讲到这里啦,博主后续会继续更新更多C++ 和 Linux 的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值