出身寒微,不是耻辱。能屈能伸,方为丈夫。
一、缓冲区(语言级:IO流缓冲,内核级:块缓冲)
1.观察一个现象
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <string.h> 4 int main() 5 { 6 //C接口 7 printf("hello printf\n"); 8 fprintf(stdout,"hello fprintf\n"); 9 fputs("hello fputs\n",stdout); 10 11 //系统接口 12 const char* msg = "hello write\n"; 13 write(1,msg,strlen(msg));//不要把\0带上 14 15 //fork(); 16 17 18 return 0; 19 }
1.
如果没有fork创建子进程的步骤,无论是运行进程还是将运行结果重定向到log.txt文件,两者输出结果都是相同的,均为4条打印信息
2.
若具有了创建子进程的步骤,运行进程后显示到显示器上的结果是4条信息,但如果重定向到log.txt文件中,就变为7条信息,并且可以看到C函数的打印信息被重复打印了两次,而系统调用write接口打印的信息只在log.txt中打印了一次。
3.
可以猜测到的是,log.txt文件中的C函数打印两次,一定和C语言函数有关,并且和创建子进程也有一定的关系,和进程有关,那是不是和写时拷贝有一些关系呢?这些都是我们的猜测,下面来系统的学习一下缓冲区的相关知识。
2.理解缓冲区存在的意义(节省进程IO数据的时间)
1.缓冲区就是一段专门用来作缓存的一块内存空间。
2.
在平常生活中,如果我们想要给远方的朋友送一些东西的话,我们为了节省时间去做其他的事情,一般都会选择快递的方式来邮递东西,如果时间比较紧的话,也会选择顺丰快递来帮我们邮递快递。
快递行业存在的意义,实际上就是为了节省发送者的时间。
3.
在上面例子当中,发送者代表内存,接收者代表磁盘,发送的东西就是数据,顺丰就是缓冲区,我们依靠内存中的进程来将数据写入到磁盘的文件中。
4.
但是我们知道,如果直接将内存中的数据写到磁盘文件中,非常的消耗时间,因为磁盘是外设,外设和内存的速度相比差距非常大,一旦开始访问外设,读取数据的效率就会非常低(讲冯诺依曼那里我们说过),这个时候在内存中就会开辟一段空间,这段空间就是缓冲区,进程会将内存中的数据拷贝到缓冲区里,最后再从缓冲区中将数据输入到磁盘外设里。
5.缓冲区的意义实际上就是为了节省进程进行数据IO的时间。
6.
进程将内存中的数据拷贝到缓冲区,这句话可能有些晦涩难懂,但实际上这个工作就是fwrite做的,与其说fwrite函数是写入到文件的函数,倒不如理解成是拷贝函数,将数据从进程拷贝到“缓冲区”或者“外设”中!!!
3.语言级缓冲区的刷新策略(三种策略,两种特殊情况)
1.
当发送者将快递给到顺丰之后,快递什么时候开始发货,快递公司有自己的规定和策略,可能等到快递数量达到什么样的程度之后,统一开始发货。
2.
所以进程在将数据拷贝到缓冲区之后,缓冲区将数据再刷新到磁盘中,这个过程中缓冲区也有自己的规定和策略,下面我们来谈谈缓冲区的具体刷新策略是什么。
3.
如果有一块数据想要写入到外设中,是一次性将这么多的数据写到外设中效率高,还是将这么多的数据多次少批量的写入到外设中效率高呢?答案显而易见,当然是前者,因为外设的访问速度非常的慢,假设数据output到显示器外设的时间是1s,那么可能990ms的时间都在等待显示器就绪,10ms的时间就已经完成数据的准备工作了,所以访问一个外设是非常辛苦的。
4.
缓冲区一定会结合具体的设备,定制自己的刷新策略:
a.立即刷新 — 无缓冲
b.行刷新 — 行缓冲 — 显示器
c.缓冲区满刷新 — 全缓冲 — 磁盘文件
无缓冲:一般情况下,立即刷新这样的场景非常少,比如显示错误信息的时候,例如发生标准错误的时候,编译器会立即将错误信息输出到显示器文件上,也就是外设当中,而不是将信息先存放到缓冲区当中,应当是立即刷新到显示器文件中。
行缓冲:之前写的进度条小程序,带\n时数据就会立马显示到显示器上,而不带\n时,就只能通过fflush的方法来刷新数据。上面我们所说的缓冲区数据积累满之后在刷新,本身就是效率很高的刷新策略,那为什么显示器的刷新策略是行缓冲而不是全缓冲呢?是因为显示器设备太特殊了,显示器不是给其他设备或机器看的,而是给人看的,而人的阅读习惯就是从左向右按照行来读取,所以为了保证显示器的刷新效率和提升用户体验,那么显示器最好就是按照行缓冲策略来刷新数据。
全缓冲:全缓冲的效率毫无疑问是最高的,因为只需要等待一次设备就绪即可,其他刷新策略等待的次数可就不止一次了,在磁盘文件读写的时候,采用的策略就是全缓冲。
5.
两种违反刷新策略的特殊情况:
a.用户强制刷新(fflush)
b.进程退出时,一般都要进行缓冲区刷新
4.语言级缓冲区在哪里?(C语言FILE结构体里包含fd和语言级缓冲区)
1.
上面这种现象一定和缓冲区有关,但从现象可以知道缓冲区一定不在操作系统内核中,因为如果在内核中,hello write也应该打印两次。
所以我们之前所谈到的缓冲区,都指的是用户级语言层面给我们提供的缓冲区!!!
2.
这个缓冲区在stdout、stdin、stderr,而这三个流都是FILE*类型的,不管是printf隐式调用stdout,还是fprintf显示调用stdout,都要传文件指针stdout给操作函数,在FILE结构体中不仅有封装的文件描述符fd,例如标准输入,输出,错误对应的FILE结构体中封装的fd是012,FILE中除fd外,实际上还包括了一个缓冲区!!!
3.
所以在我们强制刷新缓冲区时,调用fflush( )或fclose( )时候,必须传文件指针,因为FILE里面有缓冲区。
4.
下面是路径下/usr/include/libio.h文件中的struct _IO_FILE结构体,这个结构体实际上就是struct FILE结构体,只不过在/usr/include/stdio.h文件中做了类型的重定义,重定义为FILE类型。
5.
int _flags代表缓冲区的刷新策略,int _fileno代表文件描述符fd,中间一大堆的char*指针维护的内存空间就是进程IO数据时相关的缓冲区。
6.
所以,我们以前进行的所有的C语言操作,fgets、fputs、fprintf函数实际都是把数据先写到文件指针所指结构体内部的缓冲区里。
5.用已学知识来解释刚开始的现象(系统调用没有语言级缓冲区,缓冲区刷新就是对数据修改,什么数据被修改就拷贝什么数据,所以写时拷贝后就会出现两份语言级缓冲区的数据。)
1.
如果没有进行重定向,也就是没有将数据写到文件里,而是写到显示器上,那么缓冲区策略就是行刷新,在fork之前,三条C函数已经将数据打印输出到显示器上了,因为输出的字符串末尾有换行符,执行了行刷新策略,所以在FILE内部,也就是进程内部,实际上就不存在对应的数据了。
2.
下面代码的运行结果便可以看出,没有语言级缓冲区的write系统调用,在调用时根本不用依靠什么缓冲区刷新策略,这些策略压根不用看,直接打印到显示器即可。
而具有缓冲的C函数在调用时,字符串末尾不加换行符\n,无法满足显示器文件的行刷新策略,所以不会立即将数据刷新到显示器,只有在进程退出的时候才会将数据刷新到显示器上。
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <string.h> 4 int main() 5 { 6 //C接口 7 printf("hello printf"); 8 fprintf(stdout,"hello fprintf"); 9 fputs("hello fputs",stdout); 10 11 //系统接口 12 const char* msg = "hello write\n"; 13 write(1,msg,strlen(msg));//不要把\0带上 14 15 sleep(5); 16 // fork(); 19 return 0; 20 }
3.
如果进行了重定向,数据写入的对象由显示器文件改为普通文件,那么缓冲区的刷新策略也就由行缓冲变为全缓冲,所以即使C函数打印的三条字符串带了\n,也不会被立即刷新到普通文件中,因为这点儿字符串不足以将stdout中的缓冲区写满,数据也就不会被立即刷新,而是存放在stdout输出流中的缓冲区里面。
然后继续向下执行fork时,创建了子进程,紧接着就是进程退出(具体哪个进程先退出我们不关心,这是操作系统的事情),只要是进程退出,那就需要进行缓冲区刷新,也就是将数据从缓冲区里拿到外设中,这不就是对数据进行了修改吗?这个时候就会发生写时拷贝,什么数据被修改就会拷贝什么数据,所以物理地址空间中就会有两份语言级缓冲区的数据,等到父子进程都退出的时候,这两份数据就都会被刷新到外设的磁盘文件中了,所以在文件中就会有两份C函数打印的数据,因为C函数具有语言级的IO流缓冲区。
4.
至于write系统调用没有被打印两次,是因为write并没有语言级别的缓冲区,只有内核缓冲区,所以write直接在内核中将数据传输到磁盘文件就OK。
6.自己写一份代码来模拟封装C语言缓冲区(加深对于C语言缓冲区和内核缓冲区的理解)
1.
下面的代码最精华部分在于mystdio.c源文件里面,通过自己封装的FILE_结构体,fopen_,fwrite_,fclose_,fflush_可以更加清楚的了解到,C语言的IO函数在被调用时,对数据操作的细节和流程,以及当满足刷新策略时,fwite函数是怎么做的,fclose实际内部隐式的包含了fflush,清空缓冲区时利用了惰性释放的方式,这些代码让我们真正从原理上理解了C语言的缓冲区在数据IO时,具体是怎么做的,以及FILE结构体是如何封装的。同时也让我们看到了系统调用write可以直接将数据写到内核缓冲区。
2.
虽然我们所写的肯定不如库函数所写的,但是原理是相同的,只要理解了原理,那么我们的目的就达到了。
1 #include "mystdio.h" 2 3 int main() 4 { 5 FILE_ *fp = fopen_("log.txt","w"); 6 if(fp == NULL) 7 { 8 return 1; 9 } 10 const char *msg = "hello linux\n"; 11 fwrite_(msg, strlen(msg), fp); 12 13 fclose_(fp); 14 return 0; 15 }
1 #include "mystdio.h" 2 3 FILE_ *fopen_(const char *path_name, const char *mode) 4 { 5 int flags = 0; 6 int default_mode = 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 umask(0000); 20 21 int fd = 0; 22 if(flags & O_RDONLY) fd = open(path_name ,flags); 23 else fd = open(path_name, flags,default_mode); 24 25 if(fd < 0) 26 { 27 const char *error_msg = strerror(errno); 28 write(2, error_msg, strlen(error_msg)); 29 return NULL; // 打开文件失败,返回空指针 30 } 31 FILE_ *fp = (FILE_ *)malloc(sizeof(FILE_)); 32 assert(fp);// malloc申请空间必须成功 33 34 fp->flags = SYNC_LINE;// 默认设置为行刷新 35 fp->fileno = fd; 36 fp->capacity = SIZE; 37 fp->size = 0; 38 memset(fp->buffer, 0, SIZE);//将所有字节初始化为\0 39 40 return fp;// 返回FILE_*指针 41 42 } 43 void fwrite_(const void *ptr, int num, FILE_ *fp) 44 { 45 //1.将数据写入到语言级缓冲区里 46 memcpy(fp->buffer + fp->size, ptr, num); 47 //加fp->size的原因是因为打开文件的方式有可能是追加。 48 //这里不考虑缓冲区溢出的问题,如果你想考虑可以通过realloc的方式来解决, 49 50 fp->size += num;//更新FILE_中的buffer当前使用量 51 52 //2.判断是否满足刷新策略,如果满足那就刷新,不满足就不刷新 53 if(fp->flags & SYNC_NOW) 54 { 55 write(fp->fileno, fp->buffer, num); 56 fp->size = 0;//相当于清空缓冲区,下次写入时直接覆盖原有缓冲区内容 57 //惰性释放 58 } 59 else if(fp->flags & SYNC_LINE) 60 { 61 //暂时不考虑abc\ndef这种情况,处理这种情况可以利用for循环遍历,记录\n位置并将\n之前的数据刷新到磁盘外设文件中。 62 if(fp->buffer[fp->size-1] == '\n') 63 { 64 write(fp->fileno,fp->buffer,fp->size); 65 fp->size = 0;//清空缓冲区 66 } 67 } 68 else if(fp->flags & SYNC_FULL) 69 { 70 if(fp->size == fp->capacity) 71 { 72 write(fp->fileno, fp->buffer, num); 73 fp->size = 0;//清空缓冲区 74 } 75 } 76 } 77 void fflush_(FILE_ *fp) 78 { 79 //fflush做两件事情,1.用户缓冲区数据->内核 2.内核数据->外设 80 81 //系统调用write可以直接将数据写到内核缓冲区里。 82 if( fp->size > 0 ) write(fp->fileno, fp->buffer,fp->size); 83 //实际上write可以将任何数据直接写到内核缓冲区中。 84 fsync(fp->fileno); //将内核缓冲区数据强制性刷新到外设里 85 fp->size = 0;//清空缓冲区 86 } 87 void fclose_(FILE_ *fp) 88 { 89 //fclose关闭文件,需要先进行语言级缓冲区刷新,然后再关闭文件描述符 90 fflush_(fp); 91 close(fp->fileno); 92 }
1 #pragma once 2 3 #include <string.h> 4 #include <stdlib.h> 5 #include <assert.h> 6 #include <errno.h> 7 #include <stdio.h> 8 #include <sys/types.h> 9 #include <sys/stat.h> 10 #include <fcntl.h> 11 #include <unistd.h> 12 13 #define SIZE 1024 14 #define SYNC_NOW (1<<0) 15 #define SYNC_LINE (1<<1) 16 #define SYNC_FULL (1<<2) 17 typedef struct FILE_ 18 { 19 int flags;//刷新策略 20 int fileno;//文件描述符 21 int capacity;//buffer总容量 22 int size;//buffer当前使用量 23 char buffer[SIZE]; 24 }FILE_; 25 26 FILE_ *fopen_(const char *path_name, const char *mode); 27 void fwrite_(const void *ptr, int num, FILE_ *fp); 28 void fflush_(FILE_ *fp); 29 void fclose_(FILE_ *fp);
下面是Makefile文件内容
1 main: main.c mystdio.c 2 gcc $^ -o $@ -std=c99 3 .PHONY:clean 4 clean: 5 rm -f main
3.
上面代码默认的语言级刷新策略是行缓冲,下面做一些实验来验证我们对于缓冲区的理解。
> log.txt --- 清空文件内容 while :; do cat log.txt ; sleep 1; echo "#################"; done --- 每隔1s查看log.txt文件内容的监控脚本并打印一行分隔符 ctrl + r --- 命令的快速提取
4.
字符串末尾没有\n时,不满足刷新策略,只有等到调用fclose时,数据才会被刷新到log.txt文件上,所以现象应该是前9秒log.txt文件中什么都没有,最后一秒log.txt文件中直接出现10行hello linux内容。
1 #include "mystdio.h" 2 #include <stdio.h> 3 int main() 4 { 5 FILE_ *fp = fopen_("log.txt","w"); 6 if(fp == NULL) 7 { 8 return 1; 9 } 10 int cnt = 10; 11 const char *msg = "hello linux"; 12 while(cnt--) 13 { 14 fwrite_(msg, strlen(msg), fp); 15 sleep(1); 16 printf("count:%d\n",cnt); 17 } 18 19 fclose_(fp); 20 21 return 0; 22 }
5.
当打印的字符串有\n时,满足行缓冲刷新策略,则会出现每隔1s,log.txt文件内容会多一行hello linux,因为循环10s,每次都会向log.txt文件中写一行hello linux,所以log.txt文件中hello linux的行数应该是逐渐增加的。
6.
如果当cnt等于5时,我们强制刷新一下文件指针fp,则缓冲区的数据会立马被刷新,所以我们看到的现象应该是前5秒log.txt中什么都没有,然后第5秒时,log.txt直接出现5行hello linux,接下来的4秒什么都没有,等到第10秒时,log.txt直接出现10行hello linux内容。
7.用户级缓冲区和内核级缓冲区的联系(用户级缓冲区在struct FILE结构体,内核级缓冲区在struct file结构体。)
1.
write写入接口,实际上并不是直接将数据写到磁盘中,而是将数据写到内核缓冲区里面,而且fflush也不是将数据刷新到磁盘里,而是将数据从语言级缓冲区刷新到内核缓冲区里,这个内核缓冲区就在OS中的struct file结构体里面,最后由操作系统自主决定将内核缓冲区的数据刷新到磁盘上。
2.
我们上面所谈到的刷新策略都是FILE结构体里面的刷新策略,而内核缓冲区的刷新策略是非常复杂的,不像我们上面所说的那样简单,因为操作系统需要兼顾整个内存的使用情况,来决定是否进行内核缓冲区的刷新,然而这却是非常复杂的。
3.
所以C函数打印的一个字符串,首先需要被拷贝到FILE中的用户级缓冲区里,然后通过系统调用write再将数据从FILE缓冲区中刷新到file结构体中的内核级缓冲区,最后再由操作系统自主决定将内核级缓冲区的数据刷新到外设物理媒介上。
4.
内核缓冲区刷新数据到磁盘上,这个过程和用户毫无关系。
5.
系统调用接口fsync可以用来同步文件内核状态到存储设备中,说白了就是强制刷新内核缓冲区的数据到磁盘(物理媒介)上
6.
fwrite将数据拷贝到用户级缓冲区,write将数据拷贝到内核级缓冲区,本质上fwrite和write函数都是拷贝函数,fsync将数据从内核缓冲区写入到磁盘外设中。
真正意义上的fflush不仅要将数据从用户缓冲区依靠write拷贝到内核缓冲区,还要将数据从内核缓冲区依靠fsync刷新到外设中。
所以即使hello linux后面没有带\n,也就是数据会被拷贝到用户级缓冲区里面,但只要我们调用了fflush_,数据就会从用户级缓冲区里被最后输入到外设磁盘文件log.txt中,并且会一条一条的增加到log.txt文件中。
1 #include "mystdio.h" 2 #include <stdio.h> 3 int main() 4 { 5 FILE_ *fp = fopen_("log.txt","w"); 6 if(fp == NULL) 7 { 8 return 1; 9 } 10 int cnt = 10; 11 const char *msg = "hello linux"; 12 while(cnt--) 13 { 14 fwrite_(msg, strlen(msg), fp); 15 sleep(1); 16 fflush_(fp); 17 // if(cnt == 5) fflush_(fp); 18 printf("count:%d\n",cnt); 19 20 } 21 22 fclose_(fp); 23 24 return 0; 25 }
二、文件系统
1.什么是文件系统?(组织和管理磁盘上的所有文件的软件机构)
1.
之前所谈论的情况都是进程和被打开文件的关系,如果一个文件没有被打开呢?
没有被打开的文件,只能静静的在磁盘上放着,磁盘上面有大量的文件(大多数文件没有被打开),这些大量的文件需要被静态管理起来,方便我们随时打开!— 这就是文件系统
2.磁盘中没有被打开的文件,会按照一定的格式和布局在磁盘中进行保存的。
2.磁盘的物理结构(固态硬盘 && 机械硬盘)
1.
目前的笔记本上很少见到磁盘,因为现在的笔记本效率都很高,它里面装载的不再是机械硬盘HHD,而是固态硬盘SSD,固态硬盘的价格大约是机械硬盘的两倍或者还要更多,但是固态硬盘的读取和写入速度完胜机械硬盘,非常快。
2.
磁盘是计算机中的唯一的一个机械机构!,正由于硬盘(固态硬盘和机械硬盘)是外设,所以硬盘的访问速度会很慢。
3.
企业中,磁盘依旧是存储的主流,SSD非常快,但是SSD的造价太高,性价比不高,而磁盘的存储容量要比SSD更大,SSD的读写次数有限制,在高并发的情况下容易被击穿从而导致数据丢失,所以企业中更偏向于使用磁盘来进行数据的存储。
4.
有些公司会对SSD进行优化,搞一个高并发集群,定期将数据从SSD传输到磁盘上,以前的笔记本用的是混盘,就是磁盘和固态硬盘混合式存储,一般先将数据存到固态硬盘上,然后再将数据转移到磁盘上进行存储,但现在的笔记本采用的都是双固态硬盘式存储。但企业中最主流的存储方式就是磁盘式存储。
5.
磁盘中的盘片不仅仅只有一片盘片,有可能是一摞盘片,盘片的上下两面都是光滑的并且都可以用来存储二进制数据,而且每一面上都有一个磁头,假设盘片有5片,那总共就有10个盘面,也就有10个磁头,在磁盘主轴中有马达来迫使盘片高速旋转(每秒可以达到上万转),并且磁头部分也会有马达来控制磁头进行左右摆动,硬件电路组成磁盘的伺服系统,可以给磁盘发送二进制指令,让磁盘定位或寻址盘片的某个特定区域,然后再从中进行读取数据。
6.
磁盘的密封性要求特别高,在磁盘组装时,是需要在真空实验室里进行组装的,磁盘这个设备不能拆,拆开后磁盘就会立马报废,一旦进入灰尘磁盘立马就会报废,灰尘对于高速旋转的盘片来讲,就相当于空中飞行的波音747撞到了一只小鸟,飞机立马损坏,磁盘会立马报废。
磁头和盘面是没有接触的,两者之间的距离非常的近,在盘片进行高速旋转的时候,磁头会漂浮起来,所以磁盘必须方式抖动,因为一旦发生抖动,磁头就有可能碰到盘面,这时候盘面就会被磁头刮花,盘面上存的都是二进制数据,一旦刮花这些二进制数据就会发生丢失,这时候系统很有可能出现一堆奇奇怪怪的问题,蓝屏黑屏什么的。但企业中的机房不会出现这种问题,磁盘都被放在主机架上,静静的在那里放着,所以企业中的环境用磁盘来存储是非常适合的,笔记本中用磁盘就非常不合适了,因为笔记本需要来回移动,很容易发生抖动。
7.希捷,西部数据,东芝,日立这些公司都是磁盘技术这个行业中的翘楚。固态硬盘和机械硬盘的区别(7大区别,简单易懂)(转载自知乎博主源字节1号的文章)
8.
计算机中,一般用控制单元的带电或失电状态来表示二进制0和1,网络中用信号的有无或信号的疏密来表述二进制0和1,磁盘的盘片表面充满了磁化后的基本单元,这些基本单元拥有N级和S级,NS就可以用来表示二进制0和1,通过磁头的放电技术将磁盘中的NS级互换来完成二进制数据的写入。
9.
下面是关于磁盘销毁的文章,大家如果有兴趣可以看一看,我还是推荐大家看一看的,因为磁盘销毁还是挺有意思的。
3.磁盘的存储结构(磁头、柱面(磁道)、扇区)
1.
磁盘寻址的时候,基本单位不是bit,也不是byte,而是扇区,扇区大小一般是512byte,因为bit或byte太小了,如果每次的基本单位是bit或byte的话,读取100MB的数据,需要太多太多的IO次数,而磁盘又是外设,IO的时间又很长,所以需要的时间太多了,效率低到无法想象,所以磁盘在划分的时候,以扇区为单位,最少读取单位都是512字节的数据,所以磁盘存储结构就会被划分为一个个的块状儿结构,一块存储大小为512字节,在LInux中的文件类型有一种叫做块设备类型,磁盘就是典型的块设备文件。
2.
在物理层面看来,扇区越靠近外侧,弧度越大,物理长度越长,但为什么和内侧的存储大小一样呢?其实越靠近中心,byte位会越密集,越靠近外侧,byte位会越稀疏,所以由于密度的不同导致每一个扇区的存储空间大小是一样的。
3.
在磁盘单面上,只要定位了扇区就可以在这个扇区上完成二进制数据的读写了,定位扇区首先需要定位扇区在哪个磁道上面,最后需要定位在具体磁道的哪个具体扇区上,每个磁道上的扇区都有自己的编号,而每个磁道的周长又是不一样的,所以根据这些特征我们是可以找到盘片上具体的某个扇区的,接下来就可以在扇区上存储二进制数据了。
4.
磁头来回摆动的时候,就是用来确认此时在哪一个磁道上面,磁头会从最外层磁道和最内层磁道之间的所有磁道上面进行来回摆动,一旦摆动到某个要求的磁道上面(例如1号磁道什么的),磁头就会停下来,而在盘片高速旋转的时候,其实就是让磁头来定位扇区,在磁头和盘片的共同运动下,就可以精准的确定具体的磁道和扇区了。
磁头的摆动动作如下面动图所示,来回在磁道之间进行摆动
5.
将所有盘片的相同的同心圆的磁道,抽象成一个整体,这个整体就是柱面,但实际上柱面就是磁道,一般的教科书只会讲磁头、磁道、扇区,但其他市面上的数据可能讲的是磁头、柱面、扇区,但这两个其实是等价的,不用过多担心。
6.
盘片是具有多个的,所以磁头也是具有多个的,在寻找扇区时,多个磁头共同进行左右摆动,并且在盘片高速旋转下,每个不同的磁道都会形成不同的柱面。
7.
所以在磁盘中定位一个扇区,需要先定位在哪一个磁道,也就是在哪一个柱面上,然后定位磁头,也就是定位在盘面的哪一个面上,最后在定位哪一个扇区上。
8.
能够定位任意一个扇区,同时也就意味着能够定位任意多个扇区,也就可以进行多个扇区的数据存储了。磁头用来确定扇区具体在哪个盘面上,柱面可以用来确定具体在哪个磁道上,最后的扇区S就是在具体的盘面中的具体的某个磁道上的某一个位置,这个位置抽象来看就是数组下标(抽象的过程下面会讲)。
4.磁盘的逻辑结构(高度抽象化过程)
1.
小时候的英语听力磁带,他是圆形的吧,当我们将其中的磁性塑料带子抽出来的时候,它就由圆形变为线性结构,同样在计算机中也就是软件层面,为了方便管理磁盘,将磁盘的圆形物理结构抽象为线性数组结构。
2.
又由于磁盘以扇区为单位,自然可以更加形象的将数组称之为sector array[ ]数组,每一个元素都是一个扇区,然后继续完成抽象,将盘面、磁道(柱面),扇区都抽象到这个线性数组结构中,所以磁盘上的扇区在软件层面就变成了一个个的数组下标,在OS中,将下标对应的地址称为LBA逻辑块地址。
3.
之前谈到过物理层面定位某个扇区运用的算法是CHS定位法,那么LBA如何转到CHS定位呢?其实很简单,只需通过一些计算方式就可以进行转换,下面图片中的计算方法是捏出来的数据,方便大家理解LBA转到CHS定位的过程。
H磁头用来判断是哪个扇面,C柱面用来判断是哪个磁道,S就是确定在具体扇面的具体磁道中的具体某个扇区,这些工作在软件层面都可以解决,解决的过程其实就是LBA转到CHS的过程。
4.
为什么OS要进行磁盘的逻辑抽象呢?直接用CHS定位不行吗?
其实有两个原因,第一点是便于管理:在软件层面OS只要管理一个线性数组就可以了,而在物理层面管理一个三维立体结构可不是轻松的。最重要的第二点是:不想让OS的代码和硬件强耦合,因为如果强耦合,底层换了存储设备由SSD换成HHD,OS的代码就失效了,而如果进行逻辑抽象的话,底层无论更换任何存储设备,在操作系统看来都不过只是一个线性数组结构罢了,适应性很强。
5.磁盘管理的分治思想(文件系统的基本单位:块(4KB大小),页帧,页框)
1.
虽然对应的磁盘访问的基本单位是512字节,但依旧很小,如果从外设将一个文件的数据搬到内存中,假设这个文件大小是4KB空间大小,那么就要经历8次IO的过程,一次IO读取一个扇区内的数据,每次访问磁盘都得等磁盘就绪,等磁盘就绪实际上就是OS给了磁盘某个扇区的地址,然后盘片疯狂旋转,磁头高速来回摆动,帮助我们确定扇区的地址在哪里,在这个时间过程中,进程被迫等待,状态由R状态变为S状态,也就是阻塞状态,等到磁盘就绪,操作系统将磁盘对应文件数据加载到内存里,此时OS才会唤醒进程,进程才可以访问磁盘上的文件,所以当进程访问磁盘上的文件时,需要的时间会比较长,原因就是时间都花在等待磁盘确定扇区地址上了。
2.
由于OS一次读取一个扇区的效率太低,所以OS内的文件系统定制的进行多个扇区的读取,也就是一次读取的数据大小不再是512字节,而是以1KB,2KB,4KB为基本单位进行数据的读取,4KB(8个扇区为基本单位)是文件系统里最最常用的基本单位,10个文件系统里有9个是以4KB为基本单位进行IO的。
在这种情况下,哪怕你只想读取或修改磁盘某个文件中的1个比特位的数据,也必须从磁盘中将4KB的数据load到内存里进行读取或修改这一个比特位,并且刷新到显示器上,如果有必要,再将数据写回到磁盘中,
3.
至于为什么选择4KB为基本单位,之前的计算机科学家做过测试,发现以4KB作为IO的基本单位,性能是最好的,所以文件系统就采用了4KB了,这背后都是有论文作为依据的。
早些年诞生一项理论,叫做局部性原理,这项理论证明,当计算机访问某些数据时,极大可能访问到它周围的数据,所以在进程IO数据时,多加载一些数据是有助于提高操作系统的效率的,并且在一定程度上减缓了数据多次IO的过程。顺序表相比链表优势便在于数据更加集中,缓存时数据的命中率更高。
所以多加载数据的原因就是,达到预加载数据和以空间换时间的目的!!!
4.
所以真实的内存被划分是以4KB作为基本数据大小的空间,16G内存的笔记本中大约有4194304个基本数据大小的空间,这些空间叫做页框,就是内存中一个个的块。
磁盘中的文件,尤其是可执行文件,实际上也是按照4KB大小划分为一个个的块,可执行文件中的一个个块叫做页帧。
所以从磁盘中加载数据到内存时,就是分为一个个的块进行加载,将页帧的数据加载到页框里,这就是文件系统和内存管理之间的耦合,他们都是以4KB为大小进行划分的。
5.
所以现在重新看待磁盘时,它的基本单位就不再是扇区了,而是一个个的块,每个块大小是4KB,随便查看某个文件的IO Block都是4096字节,也就是IO数据时的块大小,也是页帧大小,所以4KB是最常见的块大小。
6.
管理最核心的思想就是分治,国家管理各个省,企业管理各个部门,大学管理各个学院,处处可见分治思想的管理方式,管理磁盘当然也不例外。
但计算机与现实不同的地方在于,每个省、每个部门、每个学院的管理策略之间都是有差别的,而计算机不用担心这一点,磁盘分区的各个区的管理策略都是相同的,只要把管理方法ctrl+c,ctrl+v就可以解决其他区的管理问题了,区域中细分的组之间的管理策略也是如此,所以在管理时,我们只要管理好细分的组便可以管理好磁盘这一大块空间了。
我们的笔记本只有一块硬盘,那些什么C盘、D盘、E盘等,都是一块硬盘做出来的分区。
下图中的数据都是随便写的,是为了方便大家理解。
7.
不同的文件系统的分组大小是不一样的,这完全取决于文件系统,文件系统太多了,不同文件系统的处理方式都是不一样的,但是处理的核心思路是一样的,都是分治思想。
6.深度剖析块组Block group
1.
计算机在通电之后,首先进行通电自检,通过硬件的方式检测相当多的硬件的健康状态,如果磁盘出故障,操作系统就无法正常加载到笔记本中,笔记本就会直接黑屏,无法正常开机,然后OS就会读磁盘,从特定的盘符,比如C盘或者Linux的根目录开始进行读取,从0号柱面,0号磁头的1号扇区,这个扇区中加载了分区表和操作系统所在的位置。
操作系统开机、通电、启动的相关信息都在Boot Block启动块区域中。
(Boot Block引导块区域的知识听一下就好,不重要)。
2.
如果有个盘不想用了,可以格式化这个盘,格式化就是清空盘的所有数据,重写文件系统,Super Block超级块保存一整个分区中的信息,例如区中有多少个组,起始组和结束组的地址是多少,使用的组有多少,没有被使用的组有多少,使用率是多少,整个分区的健康状态,这些信息都存储在Super Block里面。
在分组里面,有一个区域不是必须的,这个区域就是Super Block,如果有那就挺好的,没有也不影响。
但为什么有多个组中都有Super Block呢?Super Block存的是整个分区的信息,这些信息很重要,放着多个组里是为了备份,说白了就是分担风险,如果某天你的常用块组中的Super Block的数据坏掉了或丢失了,不用担心,OS会将其他组中的Super Block的数据拷贝到常用块组中的坏掉的Super Block里面,这样就完成了数据的恢复,文件系统依旧可以正常使用。
3.
我们知道文件=内容+属性,Linux的文件属性和文件内容是分批存储的。
inode块用于保存文件属性,inode块大小一般是128或256字节,inode块具体大小和文件系统的版本有关系,ext2/3版本的inode大小是128,ext4版本的inode大小是256字节,并且一个文件一个inode,但文件名不在inode块中存储。
data block用于保存文件内容,data block的大小随着文件类型的变化而变化,文本文件和电影文件的文件内容当然相差很大。
4.
每一个文件都有自己的inode,所以inode块为了区分彼此,使得每一个inode都有自己的ID,ls -li便可以显示每个文件的inode编号。
inode table保存了分组内部所有的可用(已经使用+未被使用)inode,inode table块大小就是可用的inode的个数乘上128字节或者256字节,所以创建文件的第一件事情就是在inode table里面找到一个没有被使用的inode,然后将文件的所有属性填充到inode里面。
data block保存的是分组内部所有文件的数据块,以4KB为单位进行存储。
总结:文件的所有属性放在inode table当中的某一个inode里面,文件的所有数据放在data block里面的某一个或多个数据块里面。
所以在linux创建一个文件时,首先就是需要在inode table里面找没有被使用的inode,然后将文件属性放入,然后在data block里面找没有被使用的数据块,然后将文件内容放入。
5.
在创建一个文件时,避不开的话题就是查找(在inode table里面查找没有被使用的inode,在data block里面查找没有被使用的数据块)。
那么自然就引出来了inode bitmap也就是inode对应的位图结构,每一个inode table中的inode都与位图结构中的一个比特位对应,比特位的位置和inode table中inode的位置一一对应,比特位的内容代表inode是否被占用,1代表inode被占用,0代表inode未被占用。
block bitmap代表数据块的位图结构,比特位的位置和data block数据块的位置一一对应,比特位的内容代表数据块是否被使用,数据块被占用,对应的比特位由0置1,数据块被释放,对应的比特位由1置0。
6.
group descriptor table简称GDT,代表块组描述符,存储块组的宏观的属性信息,例如data block的使用情况,inode bitmap剩余多少未被使用的inode等等信息。
7.
查找一个文件的时候,统一使用的是inode编号,在一个分区的所有组里面,inode的编号形式是统一的,所以inode可以跨组,但inode不可以跨区,不同区的inode编号形式是不同的,inode中不仅包含文件属性,实际上还包含了数据块数组,用于查找文件内容。
不同的文件系统里面,数据存储的方案是不太一样的,下面假设数组大小就是15,数组里面放的就是data blocks数据块的编号,如果每个下标只存储一个数据块的内容,那么这一个inode对应的文件数据大小实在是太小了,所以某些数组下标内容中存储的数据块中,不会再存储文件数据内容,而是存储其他的数据块id,以此来建立一级索引,这样保存的文件数据就会很大了,如果你觉得一级索引不太够,也可以建立二级、三级索引,这样blocks数组就可以存储特别特别大的文件数据内容了。
8.
有了上面的知识储备,建立一个文件就特别好办了,首先在inode bitmap和block bitmap中查找未被使用的inode和数据块,然后在inode table中的inode中存储文件属性,然后根据inode中的数据块数组,耦合到data blocks块,然后将文件内容存储到data blocks块中的数据块里面,最后block group中的所有块区域就都被调用起来了。
9.
删除文件采用的是惰性删除的方式,找到文件inode对应的inode bitmap中的比特位,然后将这个比特位由1置0,然后再找到block bitmap中数据块对应的比特位,也将它置为0,这样就完成了文件的删除。
恢复文件其实就是将inode bitmap和block bitmap中文件的inode和数据块对应的比特位由0置为1即可。
如果我们不小心将某个文件删除了,接下来最好什么都不要做,如果一旦你又创建了某些文件什么的,那么这个文件的inode很有可能复用原来删除文件的inode,直接破坏了原有的inode table中inode和数据块的映射关系,这个时候文件就真正的被删除了,无法恢复。
10.
然而在linux上,我们一直使用的是文件名,而不是文件的inode。
目录也是一个文件,目录的数据块放的就是目录下所有文件的文件名与inode和数据块的映射关系。
所以在ls查看目录下的文件信息时,实际上就是将当前路径下目录的数据块内容提取出来,然后再根据inode的映射关系和文件名将具体的文件的属性和数据提取出来,显示到显示器上。
所以在一个目录下创建和读取文件都是取决于目录的rw权限,因为创建和读取文件都是在访问目录的数据块内容。