缓冲区
缓冲区本质就是一段内存
如果要现在的快递行业极大地节省了我们的时间;如果要寄快递,只需要将快递放到寄存地点,物流就会帮我们将其送达目的地
其实物流同样也存在于操作系统中;比如我们向文件中写东西,大的角度来看是从内存将内容写到磁盘中,其底层是进程将数据写到文件中,其过程是:先将数据拷贝到缓冲区,缓冲区再将数据拷贝到文件中,缓冲区的存在极大地减少了数据写到文件的时间;这也与缓冲区的结构有关
缓冲区刷新的策略
如果存在一块数据,可以一次性全部都写入到外设中;也可以多次批量写入到外设中;缓冲区会根据具体的外设,制定相应的刷新策略
立即刷新也就是无缓冲
行刷新-行缓冲对应的外设就是显示器
当缓冲区已经满时进行刷新-全缓冲
其实还存在两种刷新方式:用户强制刷新;进程退出时都会进行缓冲区刷新
缓冲区到底在哪呢?上面代码的打印结果与缓冲区有关,从结果来看,其一定不存在于内核之中;上面学习的三个标准输出流,输入流,错误流其类型是FILE*,对应的结构体FILE包含着文件描述符,并且还包括缓冲区
现在来解释上面打印结果差异的原因:打印到显示器上面时,stdout默认是行刷新,在创建子进程之前,三条库函数调用已经将数据打印到显示器上,结构体FILE中已经不存在对应的数据;重定向到文件中时,缓冲区采用的是全缓冲,库函数调用接口,虽然含有\n,但是不足以将stdout缓冲区写满,数据并没有被刷新,创建子进程时,stdout属于父进程父进程,接着就是进程退出,父进程或子进程退出时,一定要进行缓冲区刷新,也就是进行数据修改,所有发生了写时拷贝,后一个进程退出时也会打印一次;系统调用之所以没有没打印两次,是因为write中并没有FILE结构体,所有也没有所谓的缓冲区
模拟实现缓冲区
mystdio.h
1 #pragma once 2 3 #include<assert.h> 4 #include<stdlib.h> 5 #include<errno.h> 6 #include<string.h> 7 #include<unistd.h> 8 #include<sys/types.h> 9 #include<sys/stat.h> 10 #include<fcntl.h> 11 12 #define SIZE 1024 13 #define SYNC_NOW 1 14 #define SYNC_LINE 2 15 #define SYNC_FULL 4 16 17 typedef struct _FILE{ 18 int flags;//刷新方式 19 int fileno;//文件描述符 20 int cap;//总容量 21 int size;//当前大小 22 char buffer[SIZE]; 23 }FILE_; 24 25 FILE_ *fopen_(const char*path_name,const char*mode); 26 void fwrite_(const void*ptr,int num,FILE_ *fp); 27 void fclose_(FILE_*fp); 28 void fflush_(FILE_*fp);
mystdio.c
1 #include"mystdio.h" 2 3 FILE_*fopen_(const char*path_name,const char*mode) 4 { 5 int flags=0; 6 int defaultmode=0666; 7 if(strcmp(mode,"r")==0) 8 { 9 flags|=O_RDONLY; 10 } 11 else if(strcmp(mode,"w")==0) 12 { 13 flags|=(O_WRONLY|O_CREAT|O_TRUNC); 14 } 15 else if(strcmp(mode,"a")==0) 16 { 17 flags|=(O_WRONLY|O_CREAT|O_APPEND); 18 } 19 else 20 { 21 22 } 23 24 int fd=0; 25 if(flags&O_RDONLY) 26 fd=open(path_name,flags); 27 else 28 fd=open(path_name,defaultmode); 29 30 if(fd<0) 31 { 32 const char*err=strerror(errno); 33 write(2,err,strlen(err)); 34 return NULL; 35 } 36 37 FILE_*fp=(FILE_*)malloc(sizeof(FILE_)); 38 fp->flags=SYNC_LINE; 39 40 fp->fileno=fd; 41 42 fp->cap=SIZE; 43 44 memset(fp->buffer,0,SIZE); 45 46 return fp; 47 } 48 49 void fwrite_(const void*ptr,int num,FILE_*fp) 50 { 51 //写入到缓冲区 52 memcpy(fp->buffer+fp->size,ptr,num); 53 fp->size+=num; 54 55 //判断是否需要刷新 56 if(fp->flags&SYNC_NOW) 57 { 58 write(fp->fileno,fp->buffer,fp->size); 59 fp->size=0;//清空缓冲区 60 } 61 else if(fp->flags&SYNC_FULL) 62 { 63 if(fp->size==fp->cap) 64 { 65 write(fp->fileno,fp->buffer,fp->size); 66 fp->size=0; 67 } 68 } 69 else if(fp->flags&SYNC_LINE) 70 { 71 if(fp->buffer[fp->size-1]=='\n') 72 { 73 write(fp->fileno,fp->buffer,fp->size); 74 fp->size=0; 75 } 76 } 77 else 78 { 79 80 } 81 } 82 83 84 void fflush_(FILE_*fp) 85 { 86 if(fp->size>0) 87 write(fp->fileno,fp->buffer,fp->size); 88 89 fsync(fp->fileno);//强制要求刷新 90 fp->size=0; 91 } 92 93 void fclose_(FILE_*fp) 94 { 95 fflush_(fp); 96 close(fp->fileno); 97 }
总结
当我们向文件中写入时,肯定不是直接将内容写入到文件中,其中还包含着许多步骤;首先将内容拷贝到库所提供的缓冲区中fwrite(),也就是FILE中的缓冲区,紧接着文件结构体struct_file通过调用其函数指针将内容拷贝到内核缓冲区write();最后由操作系统决定按照什么样的刷新策略将内容写到文件中
理解文件系统
如果一个文件没有被打开呢?磁盘中存在着许多没有被打开的文件,这些文件又该如何进行管理呢?
文件系统就是为了管理这些没有被打开的文件,在学习文件系统之前,先来了解磁盘的结构
磁盘
物理结构
磁盘是计算机中唯一的一个机械结构,同时也是外设
结构包括盘面和磁头,盘面的两面都有磁头;盘面通过马达控制旋转,磁头也通过马达控制左右摇摆,两者之间是没有任何接触的
存储结构
磁盘被划分为多个同心圆,每个同心圆称作磁道也称作柱面,同时每个磁道又被分为多个圆弧,每个圆弧称作扇区,数据就存储在每个扇区中,并且每个扇区存储的大小都是512 byte
在盘面上进行寻址时,磁头来回摆动确定在哪个磁道上,紧接着盘面旋转再确定在哪个扇区上,这种方法称为 CHS定位法
逻辑结构
磁盘物理结构上是圆盘形状,可以将其想象成线性结构,就像磁带一样,卷起来是圆形的,扯出来就是线性结构的
把磁盘从逻辑上看作是一个数组sector arr[n+1],每个元素是一个扇区,对磁盘的管理,转化为对数组进行管理;转化为逻辑结构之后,再进行寻址就变得简单很多,只需要知道这个扇区的下标就能定位到该扇区,在操作系统中,称这种地址为LBA地址
优点:
方便管理
避免代码和硬件强耦合
进一步理解磁盘读写数据
虽然磁盘每次访问的基本单位都是512byte,但相对来说还是太小,如果文件的大小是4字节或8字节,那么要访问磁盘8次或者16次,效率太低;所以操作系统每次访问时,会进行多个扇区的读写,以4字节作为基本单位
文件系统
文件系统为了对磁盘,进行管理,先进行分区,就相当于将每个同心圆分隔出来;再进行分组,每个同心圆中有不同的区域,每个区域所存储的内容也不同
文件=内容+属性
Linux的文件属性和文件内容是分批存储的;inode是用来存储文件的几乎所有属性,出来文件名,每个文件对应一个inode,大小固定,inode为了进行彼此区分,都有自己的ID;data block存储着文件内容,随着应用类型的大小在变化
Super Block:保存的是整个文件系统的信息,并不是所有块组中都有,主要是为了备份
Group Descriptor Table:对应分组的宏观属性信息
Block Bitmap:数据块对应的位图,位图中的比特位位置和当前data block对应的数据块位置是一一对应的
inode Bitmap:inode对应的位图结构;位图中比特位位置和当前文件对应的inode的位置是一一对应的
inode table:保存了分组内部所有的inode(已经使用或未被使用的),如果先要添加一个文件,首先要到该组中找到一个未被使用的inode进行文件属性的存储
data blocks:保存分组内部所有文件的数据块
当我们查找某个文件时,统一使用的是:inode编号,先到inode Bitmap中查看该文件是否存在,如果存在,可以读取文件的属性;如果要读取文件的内容又该如何呢?
inode的数据结构中存储着自身的编号,大小,还有数组,当读取文件内容时,可通过该数组到data Blocks中找到对应的扇区,便可对文件内容进行读写操作
文件删除操作就较为简单,只需要修改Block Bitmap和inode Bitmap即可
所有的文件都放在目录中,目录也是文件,也有自己的属性和内容,在Linux的操作中,对文件或目录从来没有使用过inode,而且文件名也不存储在inode中,所以目录中的数据块存储的是什么呢???
其实目录中的数据块存储的是当前目录下的文件名于inode的映射关系,这也就是解释了为什么在目录中新增文件必须有写入权限,因为新增文件时,需要在目录中写入文件名与inode的映射关系
软硬链接
观察上面所创建的软硬链接,可以发现:软链接具有独立的 inode可以作为独立文件,而硬链接却是和文件共用一个 inode,既然如此,那么创建硬链接有何用处呢???
首先创建硬链接,根本没有创建新文件,所使用的还是原本文件的内容和 inode,所以创建硬链接的目的就是在指定路径下,新增文件名和 inode编号的映射关系
图解如下
inode编号结构中存在着引用计数,记录着硬链接数,也就可以解释图中,为什么文件和硬链接的计数为2,因为两者全都指向了同一个 inode编号;所以只有当一个文件的硬链接变为0时,这个文件才真正被删除
硬链接的作用如此,那软链接呢???
如果将文件删除,结果会怎样呢?
从图中可以看到,软链接此时已经出问题,而硬链接正常;如果再重新创建一个新文件会发生什么?
由此可见,软链接是只认识文件名,在系统中只能通过特定路径进行查找文件,就类似快捷方式,方便用户使用
观察下列指令
创建一个文件,硬链接数为1,因为其本身的文件名和自己的inode具有映射关系,但是空的目录为什么硬链接数为2呢??
试着进入目录中,看看是否有新的发现
进入目录中发现,里面还存在着一个名为.的隐藏文件,而且它的inode编号还是和目录的inode的编号一样,这就可以解释为什么一个目录中的硬链接数为2;不过,还可以目录中还存在着一个名为..的文件,它的作用是什么呢???接下来慢慢揭晓
在目录yjm中创建一个空目录dir
图中可以发现,创建完新目录后,原本目录中的硬链接就变成了3,进入新目录中,发现文件名为..的文件inode编号与原目录的inode一样,所以这也就解释为什么硬链接数变化的原因;目录中文件名为.的文件表示当前文件,文件名为..的文件表示上级文件
关于软硬链接的内容已经结束,还剩最后一个疑问
为什么不允许给目录进行硬链接,却可以进行软链接?
原因很简单,如果给根目录进行硬链接,然后通过根目录寻找次链接,便会无限循环,出现问题