深入理解Linux系统内存中文件结构以及缓冲区,模拟实现c语言库文件接口
目录
一、文件的理解
二、文件操作
1.Linux系统中文件接口:
1.1.open
1.2.write
1.3.read
三、文件描述符
四、重定向的理解
五、缓冲区
1.语言层缓冲区
2.系统层缓冲区
3.缓冲区刷新策略(语言层)
六、c文件接口的模拟实现
1.mystdio.h
2.mystdio.c
一、文件的理解
在Linux系统中,文件被存储在磁盘上。磁盘是计算机重要的存储工具,属于外设,它既是输入设备也是输出设备,而且是永久性储存的机械设备。
从广义上来讲,Linux系统中⼀切皆⽂件(键盘、显⽰器、⽹卡、磁盘……这些都是抽象化的过程),也就是对于这些硬件的管理信息,都被抽象并储存在文件中。
因为这些设备都有相同的属性,比如读,写等,所以可以屏蔽底层的差异把它们都抽象成文件就让系统方便管理的多,这样就实现了c++中多态的效果。
注意:文件=内容+属性,所以一个0KB的空文件也占磁盘空间!
二、文件操作
对文件进行操作时第一步必然就是打开文件,因为显示器,键盘等等都是文件,所以我们要把数据打印到显示器上,或者把从键盘输入数据都需要打开文件。
但是在我们在写程序的时候自己从没打开过这些文件,依然能够实现对应的输入输出操作。这其实是因为在程序启动时系统就默认打开三个输⼊输出流,分别是stdin(标准输入流),stdout(标准输出流),stderr(标准错误流)。
关于文件操作在c语言中我们学过fopen打开文件,fread,fwrite读写文件。而我们也知道c库中这些函数都是官方写好的一下比较常用的接口来提高我们代码编写效率。而对于涉及硬件的这些接口它底层必然用到了系统接口,比如文件操作相关接口。
1.Linux系统中文件接口:
常用接口:
1.1.open
以上图片是截取了man手册中关于open使用的部分信息。
注意使用它需要包含的三个头文件。
- 返回值:open失败返回-1,成功则返回文件描述符通常记作fd,关于fd先错误的想象成 “区分文件的标志”,到下文会详解。
- 第一个参数pathname需要传入要被打开的文件路径。
- 第二个参数flags需要传入标志位,从而让系统知道文件的打开方式。
- 第三个参数mode需要传入这个文件需要被设置的起始权限,注意:需要以0开头表示八进制。当然这个参数也很少用到。
- 返回值:
标记位:巧用了位图的思想,比如我们把W = 1(0000 0001)来表示写文件,C = 2(0000 0010)表示没有此文件就创建该文件,T = 3(0000 0011)表示把先该文件清空再写入 ,如果传入参数flags = C | W | T,到了函数内部如果C | flags为真则如果文件不存在就创建,如果W | flags为真就以写的形式打开文件文... ... 这样传入一个参数就达到了传入多个参数的效果。W,C,T只是为方便临时举的例子,下面我们来看Linux系统提供的标志位参数。
Linux文件系统提供的标志位常用参数有:
- O_RDONLY: 以只读方式打开
- O_WRONLY: 以只写方式打开
- O_CREAT : 若⽂件不存在,则创建它。需要使⽤mode选项,来指明新⽂件的访问权限O_TRUNC:先清空再写入
- O_APPEND: 追加写入
1.2.write
- 返回值:操作错误返回-1,否则返回写入成功的字节数。
- 第一个参数fd,传入文件描述符。
- 第二个参数buf,传入需要写的数据区域的指针。
- 第三个参数count,传入需要写入的字节个数。
1.3.read
- 返回值:操作错误返回-1,否则返回读取成功的字节数。
- 第一个参数fd,传入文件描述符。
- 第二个参数buf,传入需要读取到的数据区域的指针。
- 第三个参数count,传入需要读取的字节个数。
三、文件描述符
文件描述符fd简单一点来说就是数组下标,是一个什么样的数组,数组元素是什么我们来具体来看。
首先在系统中所有进程用到的所有文件都会被加载到内存中,这些文件通常会以链表或其他结构组织起来。而每个进程pcb中会存在一个files_struct结构体来储存该进程用到的文件信息,而在files_struct中存在一个数组,数组元素是该进程用到的文件信息,通过该信息可以找到对应的文件。如下:
所以我们用一个fd就可以锁定确定的文件。
注意:在程序运行时系统默认给我们打开的文件stdin,stdout,strerr分别对应文件描述符表中的0,1,2下标。
进程中新打开的文件会从文件描述符标的0下标位置依次往后找到被关闭的文件并把新文件的信息储存到其中,如果没有就插入到数组尾部。
四、重定向的理解
重定向本质就是文件描述符表中文件信息的相互覆盖,打个比方:文件描述符表下标 a储存f1的文件信息,下标b储存文件f2的文件信息,把b下标的信息覆盖到a下标位置,那么原来对f1文件操作就会变成对f2文件操作。注意:b下标储存的还是f2。
以上操作我们通常使用dup2接口来实现:
它需要传入两个参数,作用是把oldfd位置的内容覆盖到newfd位置。
因为操作系统对文件操作只认fd(下标),无论是c库封装的FILE本质还是通过fd来确定文件。比如printf只认fd = 1(对应的是标准输出),那么我们可以实现这样的重定向操作:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log",O_CREAT | O_WRONLY, 0664);
if(fd<0) return -1;
dup2(fd,1);
printf("hello linux!\n");
close(fd);
return 0;
}
注意:标准输出流stdout(fd=1),标准错误流stderr(fd=2),它们虽然下标不一样,但对应文件描述符表的文件信息是一样的都是显示器。
正如我上面所说的措辞:fd并不是区分文件的标志。
那为什么要把同一个文件写在一个进程的两个文件描述符中呢?这是为了方便把错误信息单独分离出来。
五、缓冲区
缓冲区分为语言层的和系统层的,像c语言就有自己的缓冲区。通常我们所说的缓冲区指的是语言层的缓冲区。
1.语言层缓冲区
在认识缓冲区之前我们先想象一个场景,假设一辆公交车在司机在候车区看到1个人需要乘车就马上拉走一个,拉完再回来拉,每次几乎只拉一个人而路途又很长,那样的话可以想象效率是多么的低,解决方法就是公交车会在候车区停留20分钟到30分钟,等乘客足够多的时候一次性拉走,这样一来效率就高了很多。
像printf,sacnf......这些函数是调用了操作系统指令才得以实现的,如果每读写1个数据都做一次系统调用,操作系统在执行的时候会被频繁的打断,效率变得很低。所以有了缓冲区的出现,让需要调用操作系统的数据先放在缓冲区,到最后一次性的执行。
数据:乘客
缓冲区:候车区
操作系统:公交车
注意:关闭文件起到了刷新缓冲区的作用,这也是打开的文件一定要关闭的原因之一。
2.系统层缓冲区
当语言层缓冲区刷新之后,数据并不会马上写入磁盘,而是放到了系统的缓冲区,系统缓冲区的作用是减少磁盘的随机读写,增加顺序读写从而提高读写效率。因为读写到一起的都是相关性强的数据,等再次被读的时候就可以一起被读出来。
3.缓冲区刷新策略(语言层)
在c语言中被写到显示器文件的数据是按行刷新,也就是遇到换行符“\n”就进行刷新,因为我们通常在阅读时候都是一行一行读的。而被写到其他文件的数据都是等缓冲区满的时候才刷新,也就是全刷新。最后在程序结束后缓冲区也会刷新。
我们可以做以下验证:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello linux");
sleep(3);//休眠3秒
return 0;
}
在执行以上代码我们会明显感觉到先执行sleep(3)才打印出hello linux,这是因为printf的信息一直没有刷新到缓冲区,直到程序退出后才刷新。
注意:语言层缓冲区刷新到系统后就当做已经完成读写了,不要纠结系统的缓冲区是否刷新。
我们再看以下示例:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg1), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
输出结果(写到显示器):
当我们重定向到其他文件时(写到其他文件):
如果你能解释以上输出结果,那么恭喜你,已经掌握了缓冲区的刷新策略。
首先printf,fwrite是c语言库接口,执行这两条语句后数据被加载到缓冲区,而write是系统调用,数据被直接加载到系统。
写到显示器是行刷新策略,所以都被刷新到系统了。尽管后面fork创建了子进程,子进程什么也不做也会刷新缓冲区,但缓冲区已经空了,所以结果如上。
写到其他文件是全部刷新策略,write写的内容直接到系统了,所以只需要刷新printf,fwrite写的内容,父进程刷新一次,子进程刷新一次,所以打印了两份内容。
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!
六、c库文件接口的模拟实现
1.mystdio.h
#include<stdio.h>
#define N 128
typedef struct myFILE
{
int fd;
char buffers[N];//模拟缓冲区
int buffSize; //缓冲区有效字符个数
int flag;
}myFILE;
myFILE* myfopen(char* ,char* );
void myfwrite(char* ,size_t ,myFILE* );
void myfclose(myFILE* );
void myfflush(myFILE* );
2.mystdio.c
#include "mystdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
myFILE* setFILE(int _fg, int _fd)
{
myFILE* fp = (myFILE*)malloc(sizeof(myFILE));
if(fp == NULL) return NULL;
fp->fd = _fd;
fp->flag = _fg;
fp->buffSize=0;
memset(fp->buffers, 0, N);
return fp;
}
myFILE* myfopen(char* str,char* way)
{
int fd = -1,flag = 0;
if(strcmp(way,"w")==0)
{
flag = O_CREAT | O_WRONLY | O_TRUNC;
fd = open(str, flag, 0664);
}
else if(strcmp(way,"r")==0)
{
flag = O_RDWR;
fd = open(str, flag, 0664);
}
else if(strcmp(way,"a")==0)
{
flag = O_CREAT | O_WRONLY | O_APPEND;
fd = open(str, flag, 0664);
}
else
{
//more
}
if(fd < 0)
{
printf("myfopen error\n");
return NULL;
}
return setFILE(flag, fd);
}
void myfwrite(char* str,size_t sz,myFILE* fp)
{
memcpy(fp->buffers+fp->buffSize,str,sz);
fp->buffSize += sz;
if(str[sz-1]=='\n')
myfflush(fp);
}
void myfclose(myFILE* fp)
{
myfflush(fp);
close(fp->fd);
free(fp);
fp = NULL;
}
void myfflush(myFILE* fp)
{
write(fp->fd,fp->buffers,fp->buffSize);
fp->buffSize = 0;
}