[Linux]:文件(上)

img

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:Linux学习
贝蒂的主页:Betty’s blog

1. C语言文件操作

C语言文件操作接口如下,详情可参照——C语言文件

文件操作函数功能
fopen打开文件
fclose关闭文件
fputc写入一个字符
fgetc读取一个字符
fputs写入一个字符串
fgets读取一个字符串
fprintf格式化写入数据
fscanf格式化读取数据
fwrite向二进制文件写入数据
fread从二进制文件读取数据
fseek设置文件指针的位置
ftell计算当前文件指针相对于起始位置的偏移量
rewind设置文件指针到文件的起始位置
ferror判断文件操作过程中是否发生错误
feof判断文件指针是否读取到文件末尾

读写方式如下:

文件使用方式含义如果指定文件不存在
“r”(只读)为了输入数据,打开一个已经存在的文本文件出错
“w”(只写)为了输出数据,打开一个文本文件建立一个新的文件
“a”(追加)向文本文件尾添加数据出错
“rb”(只读)为了输入数据,打开一个二进制文件出错
“wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
“ab”(追加)向一个二进制文件尾添加数据出错
“r+”(读写)为了读和写,打开一个文本文件出错
“w+”(读写)为了读和写,建议一个新的文件建立一个新的文件
“a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
“rb+”(读写)为了读和写打开一个二进制文件出错
“wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
“ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

下面是一个使用C语言文件的示例:

#include<stdio.h>
int main()
{
    FILE*fp=fopen("log.txt","w");
    if(fp==NULL)
    {
        perror("fopen fail:");
        return 1;
    }
    //open success
    const char*msg="hello betty!\n";
    int count=5;
    while(count--)
    {
        fputs(msg,fp);
    }
    fclose(fp);
    return 0;
}

一般而言如果没有定义对应的log.txt文件,系统会在当前路径自动创建该文件。并且当前路径并不是指可执行程序所处的路径,而是指该可执行程序运行成为进程时我们所处的路径。比如我们可以在上级目录执行testfile文件:

可以看见log.txt是在该对应路径创建的,而不是对应可执行文件所在目录创建的。

其中我们也可以通过监视进程的方式,观察一下:

然后我们可以看见两个软连接cwdexe,分别对应的就是进程运行时我们所处的路径,以及可执行文件所处路径

2. 三个默认打开流

我们常说Linux下一切皆文件,那么我们的键盘与显示器自然也是文件。我们向键盘输入数据,本质就是操作系统向键盘文件中读取数据;我们能从显示器看见数据,本质就是操作系统向显示器文件写入数据。但是我们在使用键盘与显示器时并没有手动进行任何文件相关的读写操作,那我们又是如何对键盘文件与显示器文件进行读写的呢?

答案自然是操作系统自动帮我们打开的,任何进程在运行时,操作系统都会默认打开三个输入输出流,分别为:标准输入流标准输出流以及标准错误流。对于C语言分别就是:stdinstdout以及stderr。对于C++分别就是:cincoutcerr,自然其他语言也会有相似的概念,因为这是操作系统所支持的,而不是某个语言所独有的。

我们可以在Linux中的man查看对应的声明:

其中标准输入流对应的就是我们的键盘,而标准输出流与标准错误流对应的就是我们显示器。

其中我们也可以通过fputs函数验证一下:

#include<stdio.h>
int main()
{
  //向显示器打印
  fputs("hello betty!\n",stdout);
  fputs("hello betty!\n",stdout);
  fputs("hello betty!\n",stdout);
  fputs("hello betty!\n",stdout);
  return 0;
}

3. 系统文件I/O

在前面我们学习操作系统时知道,为了方便用户使用,一般我们会对系统接口进行封装。我们的文件操作也不例外,像fopenfclose等接口本质其实对操作系统提供的文件接口的封装。接下来我们就来学习一下系统提供的文件接口。

3.1 open函数

首先我们来介绍文件打开操作的系统接口。

  • pathname:表示打开或者创建的目标文件,若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。
  • ·flags:表示打开文件的方式。
  • mode:表示创建文件的默认权限(八进制数)。

其中常用文件打开方式有如下几个:

参数选项含义
O_RDONLY以只读的方式打开文件
O_WRNOLY以只写的方式打开文件
O_APPEND以追加的方式打开文件
O_RDWR以读写的方式打开文件
O_CREAT当目标文件不存在时,创建文件

如果想同时兼具多个打开方式,可以使用逻辑与|链接两个选项。比如说我们想打开文件并且文件不存在时创建文件,可以写成:

O_WRNOLY|O_CREAT

这些选项本质也就是一个宏定义,其中flags是一个整型,若将一个比特位作为一个标志位,则理论上flags可以传递32种不同的标志位。

所以我们也可以使用按位与&操作来检测是否设置某个选项:

if (flags&O_RDONLY){
    //设置了O_RDONLY选项
}
if (flags&O_WRONLY){
    //设置了O_WRONLY选项
}
if (flags&O_RDWR){
    //设置了O_RDWR选项
}
if (flags&O_CREAT){
    //设置了O_CREAT选项
}
//...

并且如果我们打开的文件已存在就使用第一个接口(两个参数),如果打开的文件不存在就需要使用第二个接口(三个参数),即需要为创建的文件设置默认权限。

如果我们要为文件设置默认权限,就需要考虑文件默认掩码umask的影响。我们之前讲过文件的默认权限为:mode&(~mask),我们除了可以在命令行通过指令umask 八进制数来修改默认的掩码umask(默认为002)外,还能在程序中调用umask函数进行修改。比如我们将umask设置为0:

umask(0); //将文件默认掩码设置为0

最后再来探究一下open的返回值,也就是文件描述符fd

#include<stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
    umask(0);//设置文件掩码为0
    int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
    return 0;
}

运行之后我观察到文件描述符是从3开始的,并且依次递增,这起始并不是偶然。至于为什么,我们等会儿在揭晓。

当然这只是文件成功返回的情况,如果文件打开失败,那将返回-1。

3.2 close函数

我们可以调用系统接口close来关闭指定文件,其原型为:

int close(int fd);

使用close函数时传入需要关闭文件的文件描述符即可,若关闭文件成功则返回0,若关闭文件失败则返回-1。

3.3 write函数

同样我们也能通过系统接口write对文件进行写入,其原型为:

ssize_t write(int fd, const void *buf, size_t count);

其中fd指的是文件描述符,buf为用户缓冲区,而count为期望写的字节数。如果写入成功返回实际写入的字节数,若写入失败则返回-1。

注意:ssize_t其实就是一个有符号整型,具体来说就是被typedef重新定义过:typedef int ssize_t

以下我们可以利用write函数对一个log.txt文件进行写入:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd=open("log.txt",O_WRONLY|O_CREAT);
    if(fd<0)
    {
        //open error
        perror("open fail:");
        return 1;
    }
    const char*msg="hello betty!\n";
    for(int i=0;i<8;i++)
    {
        write(fd,msg,strlen(msg));
    }
    close(fd);
    return 0;
}

3.4 read函数

同样我们也能通过系统接口read对文件进行读写,其原型为:

ssize_t read(int fd, void *buf, size_t count);

其中fd指的是文件描述符,buf为用户缓冲区,而count为期望读的字节数。如果读出成功返回实际读出的字节数,若读出失败则返回-1。

以下我们可以利用read函数对一个log.txt文件进行读出:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
int main()
{
    int fd=open("log.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open fail:");
        return 1;
    }
    char buf[1024]={'\0'};
    ssize_t ret=read(fd,buf,1023);
    if(ret>0)
    printf("%s",buf);
    close(fd);
    return 0;
}

4. 文件描述符——fd

在我们的操作系统中,文件是由我们进程所打开的,存在大量进程就意味着存在大量被打开的文件。为了方便我们对文件进行管理,我们就将每个文件struct file链入我们的双向链表之中。

struct File
{
  //包含了打开文件的相关属性
  //链接属性
};

而一个文件也可能被多个进程所读写,为了让操作系统能够准确识别每个进程对应的文件,我们就一定要让进程与我们的文件建立联系。事实也是如此,我们的进程控制块task_struct中就存在一个指针指向一个名为struct file_struct的结构体,这个结构体中存在一个结构体指针数组struct file*fd_array[]分别存放着着每个文件struct file的地址。这样我们的进程就与文件建立起了联系。

画板

一般我们的指针数组struct file*fd_array[]的0,1,2下标分别对应我们的标准输入流,标准输出流,标准错误流这三个文件,而这些下标就是我们所说的文件描述符——fd。这也解释了我们打开文件的描述符为什么从3开始,并且依次递增。并且,通过对应的文件描述符,进程只需要找到对应的指针数组fd_array就能访问对应的文件,这也是为什么我们文件的系统调用接口的参数一定会有fd的原因。

当然如果我们在中途关掉某个文件,操作系统就会为该下标重新分配对应的文件。

#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
int main()
{
    close(0);
    close(2);
	int fd1 = open("log1.txt", O_RDONLY | O_CREAT, 0666);
	int fd2 = open("log2.txt", O_RDONLY | O_CREAT, 0666);
	int fd3 = open("log3.txt", O_RDONLY | O_CREAT, 0666);
	int fd4 = open("log4.txt", O_RDONLY | O_CREAT, 0666);
	int fd5 = open("log5.txt", O_RDONLY | O_CREAT, 0666);
	printf("fd1:%d\n", fd1);
	printf("fd2:%d\n", fd2);
	printf("fd3:%d\n", fd3);
	printf("fd4:%d\n", fd4);
	printf("fd5:%d\n", fd5);
	return 0;
}

我们也知道,当一个程序运行起来时,操作系统会将该程序的代码和数据加载到内存,然后为其创建对应的task_structmm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。如果与我们的文件管理联系起来,就是一个磁盘文件log.txt加载进内存形成内存文件,最后加入对应双向链表中管理起来。

画板

当文件存储在磁盘上时,我们称之为磁盘文件。而当磁盘文件被加载到内存中后,就变成了内存文件。磁盘文件与内存文件的关系,恰似程序和进程的关系。程序在运行起来后成为进程,同样,磁盘文件在加载到内存后成为内存文件。磁盘文件主要由两部分构成,即文件内容文件属性。文件内容指的是文件中存储的数据,而文件属性则是文件的一些基本信息,包括文件名、文件大小以及文件创建时间等。这些文件属性也被称为元信息。在文件加载到内存的过程中,一般会先加载文件的属性信息。这是因为在很多情况下,我们可能只需要了解文件的基本属性,而不一定立即需要对文件内容进行操作。当确实需要对文件内容进行读取、输入或输出等操作时,才会延后式地加载文件数据。这样的设计可以提高系统的效率,避免在不必要的时候浪费资源加载大量的文件数据。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Betty’s Sweet

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值