
个人主页:小则又沐风
个人专栏:<数据结构>
<竞赛专栏>
<Linux>
座右铭
路虽远,行则将至;事虽难,做则必成
目录
前言
我们的进程的知识的讲解搞了一段落了,现在我们将会学习一下有关文件的一些操作,我们知道的是
在我们的进程中我们会打开一些文件,至于打开文件的底层逻辑到底是什么?文件是怎么被找到的,文件是存储在哪里的?
这些的知识在本章节中都会具体的讲解.
今天我们讲解的内容是通过C语言的文件操作我们来认识一下在Linux下文件操作
C语言的文件操作
我们在C语言学习的时候我们就接触到了有关文件操作的一些接口了
下面我们先来复习一下.
cpp
#include <cstdio>
#include <stdlib.h>
int main()
{
FILE *f = fopen("text.txt", "w");
if (f == nullptr)
{
printf("open error\n");
exit(1);
}
fprintf(f, "hello\n");
fclose(f);
FILE *fp = fopen("text.txt", "w");
if (fp == nullptr)
{
printf("open error\n");
exit(1);
}
fprintf(fp, "world\n");
return 0;
}
需要知道的是如果我们的当前的路径下是没有这个text.txt的文件的话,我们的进程会在当前的路径下建立出一个这样的文件的.现在我们运行起来看看效果是什么样子的

我们可以看到这个文件中的第一条写入的信息被我们覆盖了,这是因为我们打开文件的时候选择的是w操作,这个就是会覆盖之前文件的内容的,但是如果我们想要对文件的内容进行追加的话,我们打开文件的时候的操作就需要的是a操作了
cpp
#include <cstdio>
#include<iostream>
#include <stdlib.h>
int main()
{
FILE *f = fopen("text.txt", "w");
if (f == nullptr)
{
printf("open error\n");
exit(1);
}
fprintf(f, "hello\n");
fclose(f);
FILE *fp = fopen("text.txt", "a");
if (fp == nullptr)
{
printf("open error\n");
exit(1);
}
fprintf(fp, "world\n");
fclose(fp);
return 0;
}

以上就是写操作了,下面就是C语言的读操作
cpp
#include <cstdio>
#include<iostream>
#include <stdlib.h>
int main()
{
FILE *fil=fopen("text.txt","r");
char buf[128];
while(1)
{
ssize_t n= fread(buf,1,sizeof(buf),fil);
if(n>0)
{
buf[n]=0;
std::cout<<n;
printf("%s\n",buf);
}
if(feof(fil))
{
break;
}
}
fclose(fil);
return 0;
}
现在我们在text.txt加一点内容


以上就是我们的在C语言中常用的文件操作了
但是呢我们早早的就说了在Linux下一切都是文件
那么也就是说我们的显示器,键盘都是一个个的文件了
怎么证明???
我们平常怎么向显示器打印信息的?
fprintf(stdout,"hello\n");
这个stdout就是我们的显示文件了
那么接下来我们就来了解一下在Linux下的IO系统了
系统⽂件I/O
首先在了解这个东西之前我们直接来看一下在Linux下我们是怎么打开文件的
int open(const char *pathname, int flags, ...
/* mode_t mode */ );
我们看到的是首先我们需要传入的是文件的路径,这是非常容易理解的
那么这个flag是一个怎么样的一个参数呢?
这个参数的实际含义是我们将要打开的文件是用什么样子的方法来打开的
参数:
O_RDONLY: 只读打开
O_WRONLY: 只写打开
O_RDWR : 读,写打开
这三个常量,必须指定⼀个且只能指定⼀个
O_CREAT : 若⽂件不存在,则创建它。
需要使⽤mode选项,来指明新⽂件的访 问权限
O_APPEND: 追加写
那么这么多的参数种类,在通常的情况下我们打开文件的话,是需要多种的方式组合的
例如:
我们用写的方式打开文件,但是如果文件不存在的话我们就创建它
那么这样的话,我们的参数就是一个这样的O_CREAT| O_WRONLY
其实这些的打开的方式的本质就是一个宏定义而已,他们的实质就是一个个的标志位
可以联想一下我们之前讲解进程调度的时候说的比特位图
在介绍这个参数的时候我们来学习一个知识点
也不算是一个知识点就是⼀种传递标志位的⽅法
传递标志位的⽅法
我们来进一步的了解一下这种的标志位是怎么实现的
cpp
#include<iostream>
using namespace std;
#define ONE 0x0001
#define TWO 0x0010
#define THREE 0x0100
#define FOUR 0x1000
void fun(int flag)
{
if((flag&ONE))
{
printf("flag has ONE\n");
}
if((flag&TWO))
{
printf("flag has TWO\n");
}
if((flag&THREE))
{
printf("flag has THREE\n");
}
if((flag&FOUR))
{
printf("flag has FOUR\n");
}
}
int main()
{
fun(ONE);
cout<<endl;
fun(ONE|FOUR);
cout<<endl;
fun(ONE|THREE|FOUR);
cout<<endl;
return 0;
}

那么原理就是简单的位运算了
如果在相应的位置上有1的话就代表的是这个功能是打开的,那么|的作用就是在相应的位置上添加上1
那么我们就可以理解上面的flag的大概的原理了
回归到我们的正文中,那么第三个的参数是什么含义呢?
在上面我们简单了进行了讲解,这个就是我们如果是一个创建一个文件的话,这个mode就是我们创建出一个文件的权限
那么我们来实践一下
cpp
#include<iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd=open("text.txt",O_CREAT|O_WRONLY,0666);
if(fd==-1)
{
perror("open");
}
close(fd);
return 0;
}

至于为什么文件的权限不是666
这是因为有mask值的存在
文件描述符
我们可以看到这个open的返回值是一个整数值
其实这个整数值就是文件描述符
但是有个疑问就是为什么一个小小的整数就可以代表出一个文件呢?
下面我们就来详细的来了解一下进程是怎么打开文件的
大家都知道的是进程是由自己的task_struct
在这个结构体中,其中有一个指针是一个flie_struct类型的.这个指针指向的内容就是一个flie_struct的结构体
在这个结构体中有什么信息呢?其中就有一个类似于一个数组一样的指针数组,在这个数组中,一个个的指针是指向相应的文件结构体的,然后我们可以通过数组的下标进行对指针指向的内容进行访问,
那么这一个个的数组下标就是我们说的文件描述符
我们可以看下面的图片来进一步的了解

下面我们来看下面的情况
cpp
#include<iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
int fd=open("text.txt",O_CREAT|O_WRONLY,0666);
if(fd==-1)
{
perror("open");
}
cout<<"fd is ->"<<fd<<endl;
close(fd);
return 0;
}

我们打开的文件为什么他的文件描述符是三呢?
为什么不是0呢???
我们知道的是数组的其实的下标就是0,按道理说打开的文件的描述符应该是从零下标开始的
这是因为0 1 2都被占用了.
我们的程序时默认打开了三个文件,分别就是键盘,显示器,显示器
所以上述代码的fd是3
那么文件描述符是怎么进行分配的呢?
我们来看下面的代码
cpp
#include<iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
close(0);
int fd=open("text.txt",O_CREAT|O_WRONLY,0666);
if(fd==-1)
{
perror("open");
}
cout<<"fd is ->"<<fd<<endl;
close(fd);
return 0;
}

可以看到的是新打开的文件的描述符变成了0
这就揭示了这个文件描述符的分配的机制了
是从小到大开始查找最小的分配的
在文件描述符的基础上理解重定向
我们在上面的了解中我们知道了显示器的占用的文件描述符是一个1,至于那个2的话就是错误信息的输出
前提是操作系统不认识什么文件的本质,他只知道文件描述符为1的就是显示器了
那么我们把这个1号的位置占用了不就是实现了把从显示器输出改成了我们指定的文件了???
我们尝试一下实现我们的代码
cpp
#include<iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
close(1);
int fd=open("text.txt",O_CREAT|O_WRONLY,0666);
if(fd==-1)
{
perror("open");
}
cout<<"fd is ->"<<fd<<endl;
cout<<"现在我就是显示器了"<<endl;
close(fd);
return 0;
}

显而易见的重定向的输入就是这样的原理,现在我们实现的是>的功能
怎么实现追加的>>功能呢?
我们只需要改变就是打开文件的方式加上一个追加
下面介绍一个函数
int dup2(int oldfd, int newfd);
这个函数的作用就是让newfd也指向oldfd的文件
如果newfd被占用的话就先把他关闭
缓冲区
什么是缓存区呢?我们在向文件中出入信息的时候我们并不会直接输入到文件中的,只会输入到文件相对应的缓存区中,当满足缓存区的刷新条件的时候才会把缓存区的内容刷新到文件中,
那么为什么需要这个文件的缓存区呢???
假设不存在这个文件的缓存区那么我们向文件中输入的信息,会不断需要进行系统的调用来读写文件,这样一定会大幅度的cpu的调度的.
就像我们之前列举的例子一样,我们更倾向于把垃圾存储一定的数量之后才下楼去扔垃圾
这也是这样的道理的
所以缓存区刷新的条件是什么呢???
大概的是分为下面的几个条件
标准I/O提供了3种类型的缓冲区。
• 全缓冲区:这种缓冲⽅式要求填满整个缓冲区后才进⾏I/O系统调⽤操作。对于磁盘⽂件的操作通 常使⽤全缓冲的⽅式访问。
• ⾏缓冲区:在⾏缓冲情况下,当在输⼊和输出中遇到换⾏符时,标准I/O库函数将会执⾏系统调⽤ 操作。当所操作的流涉及⼀个终端时(例如标准输⼊和标准输出),使⽤⾏缓冲⽅式。因为标准 I/O库每⾏的缓冲区⻓度是固定的,所以只要填满了缓冲区,即使还没有遇到换⾏符,也会执⾏ I/O系统调⽤操作,默认⾏缓冲区的⼤⼩为1024。
• ⽆缓冲区:⽆缓冲区是指标准I/O库不对字符进⾏缓存,直接调⽤系统调⽤。标准出错流stderr通 常是不带缓冲区的,这使得出错信息能够尽快地显⽰出来
除了上述列举的默认刷新⽅式,下列特殊情况也会引发缓冲区的刷新:
-
缓冲区满时;
-
执⾏flush语句;
-
进程结束
那么我们来看看具体的实例:
cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
// close(1);
int fd = open("text.txt", O_CREAT | O_WRONLY, 0666);
if (fd == -1)
{
perror("open");
}
printf("我开始了\n");
printf("hello world");
sleep(3);
close(fd);
return 0;
}
你可以在你的电脑下运行起来,可以看到地是开始语句是直接打印出来的,但是这个hello world是停顿了好久的,它刷新到显示器地原因就是进程结束了
上面的示例有一点不明显我们来看看下面的代码
cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
//close(1);
int fd = open("text.txt", O_CREAT | O_WRONLY, 0666);
if (fd == -1)
{
perror("open");
}
printf("我开始了\n");
printf("hello world\n");
close(fd);
return 0;
}

缓存区的代码是直接刷新到显示器上的
cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
close(1);
int fd = open("text.txt", O_CREAT | O_WRONLY, 0666);
if (fd == -1)
{
perror("open");
}
printf("我开始了\n");
printf("hello world\n");
close(fd);
return 0;
}

但是当我们重定向到文件中的时候这些信息没有出现在文件中,这就是因为我们的内容还是在缓存区中的,并没有刷新.(文件的缓存区刷新的条件是全缓存)
所以这时候想要看到内容的话我们需要自己来刷新一下缓存区的内容
cpp
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
close(1);
int fd = open("text.txt", O_CREAT | O_WRONLY, 0666);
if (fd == -1)
{
perror("open");
}
printf("我开始了\n");
printf("hello world\n");
fflush(stdout);
close(fd);
return 0;
}

我们知道C语言自己封装的文件操作,我们可以笃定的是他一定封装了fd的
那么我们来简单的模拟实现一下
简单的模拟实现C语言的接口
首先我们先定义一个结构体相当于是file结构体
在这个结构体中包含刷新条件,缓存区,内存大小,文件描述符等等
并且我们简单的指明我们将会实现的函数接口
cpp
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int fdnm;//fd
int size=0;//size;
char outbuffer[SIZE];
int flag;//刷新的方式
int cap;
};
typedef struct IO_FILE myfile;
myfile* myopen(const char* filename,const char *mode);
int mywrite(const char* ptr,int num, myfile* fp);
void myfllush(myfile*fp);
void myclose(myfile* fp);
那么下面就是实现一下函数接口了
首先实现这个open函数我们显示要知道open的模式,并且用相应的方式打开文件,并开辟出空间初始化我们的文件结构体
cpp
myfile *myopen(const char *filename, const char *mode)
{
int fd = -1;
if (strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if (strcmp(mode, "w") == 0)
{
fd = open(filename, O_WRONLY | O_CREAT, 0666);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(filename, O_WRONLY | O_APPEND | O_CREAT, 0666);
}
if (fd < 0)
{
return nullptr;
}
myfile *fp = (myfile *)malloc(sizeof(myfile));
if (fp == nullptr)
{
return nullptr;
}
fp->cap = SIZE;
fp->flag = FLUSH_LINE;
fp->fdnm = fd;
return fp;
}
然后就是刷新的函数接口了,这个函数的实现是相对简单的
首先的是刷新的条件是我们自己定义的缓存区中是含有内容的.
然后我们需要把我们自己缓存区中的内容放到真正的缓存区中,然后我们将对应的缓存区中的内容进行刷新
然后把我们自己的缓存区中的内容大小清零
cpp
void myfllush(myfile *fp)
{
if(fp->size>0)
{
write(fp->fdnm,fp->outbuffer,fp->size);
fsync(fp->fdnm);
fp->size=0;
}
}
这个写内容的函数在这里只简单的实现一下判断行刷新的情况
我们首先需要将将要写入的内容写到我们的缓存区中(假设缓存区的内容没有满,并且可以容纳我们将要写的内容)
然后我们需要判断的就是我们的刷新的模式是不是行刷新,然后并判断我们的内容中有没有换行符
符合上述的条件的话,我们就调用我们自己的刷新函数
cpp
int mywrite(const char *ptr, int num, myfile *fp)
{
memcpy(fp->outbuffer + fp->size, ptr, num);
fp->size += num;
if (fp->size > 0 && fp->flag == FLUSH_LINE && fp->outbuffer[fp->size - 1] == '\n')
{
myfllush(fp);
}
return num;
}
关闭文件的话就是我们首先在关闭文件之前我们需要判断这个文件的缓存区中是否有内容,如果有内容的话我们需要把这个内容刷新一下,然后关闭我们的文件
cpp
void myclose(myfile *fp)
{
if(fp->size>0)
{
myfllush(fp);
}
close(fp->fdnm);
}
下面是测试代码
cpp
#include "my_file.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
myfile *fp = myopen("./log.txt", "a");
if(fp == NULL)
{
return 1;
}
int cnt = 10;
while(cnt)
{
printf("write %d\n", cnt);
char buffer[64];
snprintf(buffer, sizeof(buffer),"hello message, number is : %d", cnt);
cnt--;
mywrite(buffer, strlen(buffer), fp);
myfllush(fp);
sleep(1);
}
myclose(fp);
}