【Linux】文件系统和软硬链接

打印 上一主题 下一主题

主题 296|帖子 296|积分 888

目次
文件回首
明白文件
先用和熟悉系统调用的文件操作
重定向
缓冲区的明白
stderr
磁盘文件
看看物理磁盘
磁盘的存储结构
对磁盘存储进行逻辑抽象
软硬链接
见一见软硬链接
软链接特征及用处
硬链接特征及用处


文件回首

看一下这段代码:
  1. #include <stdio.h>
  2. int main()
  3. {
  4.     FILE* pf = fopen("log.txt","w");
  5.     if(pf ==NULL)
  6.     {
  7.         perror("fopen");
  8.         return 1;
  9.     }
  10.     fclose(pf);
  11.     return 0;
  12. }
复制代码
当运行这段代码时,会在当前目次创建一个log.txt文件,那么怎么知道在当前路径下呢,原因是在环境变量中存在pwd,只需要将pwd的内容和文件名粘贴在一起就可以。我们要进行文件操作,条件是我们的步伐跑起来了,文件打开和关闭是CPU在执行我们的代码,因此,打开文件本质是进程打开文件!!那么文件没有被打开的时候,在哪里呢?在磁盘上!进程能打开很多文件吗?可以!系统中可以存在很多进程!很多情况下,在OS内部,一定存在大量的被打开的文件!那么,OS要不要把这些打开的文件进行管理呢?固然要,先形貌,后构造!由此,我们可以预言一下,每一个被打开的文件,在OS内部,一定要存在对应的形貌文件属性的结构体,类似PCB。每打开一个文件,都要创建一个结构体,把这些结构体以链表的情势管理起来,对文件的管理就变成了对链表的增删查改。

当我们创建一个新的文件时,体现的文件巨细为0,

但是这个文件要不要占据磁盘空间呢?要的!文件=属性+内容,上面体现巨细为0实际上是内容为0,我们上面说的每次打开一个文件都会创建一个结构体,这个结构体放的一定是文件的属性。
  1. #include <stdio.h>
  2. int main()
  3. {
  4.     FILE* pf = fopen("log.txt","w");
  5.     if(pf ==NULL)
  6.     {
  7.         perror("fopen");
  8.         return 1;
  9.     }
  10.     fprintf(pf,"helloworld,%d,%s,%lf\n",10,"ghs",3.14);
  11.    
  12.     fclose(pf);
  13.     return 0;
  14. }
复制代码
在上面这段步伐中,使用到了fopen函数,并且以“w”方式打开,对于fopen函数:
   1.如果要打开的文件不存在,就在当前路径下,新建指定的文件
  2.如果要打开的文件存在,默认打开文件的时候,就会默认把目标文件清空
  如果在使用fopen函数时,以“a”方式打开,就会在原有文件内容上追加。
那么,我们之前好像遇到过这样的代码:
  1. echo "hello ghs" > log.txt
复制代码
其中,‘>’叫做输出重定向,其本质就是向文件中写入,把应该向体现器打印的内容打印到磁盘当中,重定向一定是文件操作,

上面的代码中,先对log.txt中写入“hello ghs”,再写入“666666”,但是最终文件中只有“666666”,每次写入内容都是新的,使用‘>’时,都是先清空,再写入,那这样不就是上面以‘w’的方式来把文件打开吗?那按照上面以‘w’方式打开文件的明白,
  1. > 111.txt
复制代码
 这样就可以创建一个文件,由于‘>’会被OS表明为以‘w’方式打开,因此,可以通过‘>’方式新建一个文件。
如果log.txt原本是有内容,那么
  1. > log.txt
复制代码
log.txt会被清空,同样是由于‘>’会被OS表明为以‘w’方式打开,不存在就创建,存在就清空。

别的,当我们使用‘>>’时,这个也会被表明为打开文件,是以‘a’方式打开。

明白文件

a.操作文件的本质:进程在操作文件。
b.文件在没有被打开的时候,是放在磁盘上的,而磁盘本质上是一个外部装备,外设是一个硬件,以是,向文件中写入本质上是向硬件写入。但是,用户没有权利直接写入,由于OS是硬件的管理者,必须通过OS写入,但是我们没有通过OS写入啊,我们用的是fopen、fwrite、fread、fprintf、scanf、printf、cin、cout等进行操作,以是OS必须给我们提供系统调用(OS不信赖任何人),但是,我从来没用过OS提供的系统调用呀?我用的都是C语言提供的,以是我们用的C/C++/...都是对系统调用接口的封装!以是,访问文件,除了使用语言提供的函数,也可以使用系统调用啊
究竟上,C/C++/其他语言访问文件的方式有些不一样!
先用和熟悉系统调用的文件操作

首先是open函数:

open函数的第一个参数是文件名,第二个参数是标记位,返回值是文件形貌符(file descriptor),失败则返回-1,第三个参数是文件的起始权限(如果打开未创建的文件,那么使用第二个open函数;如果打开已创建的文件,需要设置mode参数),我们还不是很明白这个函数,我们先来用一下这个函数:
  1. #include "stdio.h"
  2. #include <sys/types.h>
  3. #include <sys/stat.h>
  4. #include <fcntl.h>
  5. int main()
  6. {
  7.         //system call
  8.         int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);//第二个参数的意思是以写方式打开,不存在就创建
  9.         if (fd < 0)
  10.         {
  11.                 perror("open");
  12.                 return 1;
  13.         }
  14.         return 0;
  15. }
复制代码
在这个步伐中,我们设置初始的权限为0666,但是运行这个步伐后,发现天生的log.txt的权限是rw-rw-r--,和我们设置的不一样,这是由于系统默认权限掩码umask的存在,默认权限掩码是0002,以是,如果想按照我们自己设置的初始权限,先可以把umask设置为0000:
  1. umask(0);
复制代码

那么,在修改了权限掩码之后,有这样一个问题:用系统默认的权限掩码还是自己设置的呢?就近原则!自己没有设置就用系统默认的,有自己设置的就用自己设置的。
除了上面这个问题外,最让我们困惑的实在是上面代码open第二个参数的设置是什么意思?我们先来向这样一个问题,在之前如果我们想给一个函数通报一个标记位,那么就是设置一个int flag;如果想给一个函数通报两个标记位,那么就是设置int flag1;int flag2;int func(...,int flag1,int flag2,..),那如果传10个8个标记位,也要设置10个8个参数吗?显然不太公道!上面open函数的第二个参数但是一个int类型,它有32个bit,以是实在可以用比特位进行标记位的通报,这是OS设计很多系统调用接口的常见方式!这32个比特位实在是一张位图,上面步伐open第二个参数通报的实在是宏,为了更好明白,下面自己来设计一个通报位图标记位的函数:
  1. #define ONE 1         // 1 0000 0001
  2. #define TWO (1<<1)    // 2 0000 0010
  3. #define THREE (1<<2)  // 4 0000 0100
  4. #define FOUR (1<<3)   // 8 0000 1000
  5. void print(int flag)
  6. {
  7.     if(flag & ONE)
  8.         printf("one\n");   //替换成其他功能
  9.     if(flag & TWO)
  10.         printf("two\n");
  11.     if(flag & THREE)
  12.         printf("three\n");
  13.     if(flag & FOUR)
  14.         printf("four\n");
  15. }
  16. int main()
  17. {
  18.     print(ONE);
  19.     printf("\n");
  20.     print(TWO);
  21.     printf("\n");
  22.     print(ONE|TWO);
  23.     printf("\n");
  24.     print(ONE|TWO|THREE);
  25.     printf("\n");
  26.     print(ONE|FOUR);
  27.     printf("\n");
  28. }
复制代码
那么,在明白了这段步伐后,上面open的第二个参数O_WRONLY、O_CREAT是只有一个比特位为1的宏,彼此之间宏值不重复,以是我们现在明白函数的成本就变成了熟悉更多的选项,而大部分的选项都是见名知意的。
再看一个系统调用write:

fd是open的返回值,buf是要写的内容,count是写入的字节数巨细。
  1. #include <stdio.h>
  2. #include <string.h>
  3. #include <unistd.h>
  4. #include <sys/types.h>
  5. #include <sys/stat.h>
  6. #include <fcntl.h>
  7. int main()
  8. {
  9.     umask(0);
  10.     //system call
  11.     int fd = open("log.txt",O_WRONLY | O_CREAT,0666);
  12.     if(fd < 0)
  13.     {
  14.         perror("open");
  15.         return 1;
  16.     }
  17.     const char* message = "hello Linux file\n";
  18.     write(fd,message,strlen(message));
  19.     close(fd);
  20.     return 0;
  21. }
复制代码
添加write这一行代码之后,形成的log.txt文件内容结果是:

然后,将message的内容换位“aaaa”,重新编译运行:

发现在原来的文件基础上重新开始替换,原因是O_WRONLY默认不清空文件,为了实现覆盖写,可以在第二个参数再加O_TRUNC,
  1. int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);
复制代码
上面参数组合的寄义是:写方式打开,不存在就创建,存在就先清空!
   提示:为了证实O_TRUNC的作用,可以预先在log.txt中写入一些内容,同时解释掉write函数,运行步伐后发现log.txt文件被清空了,这就证实了O_TRUNC的作用的作用。
  讲到这里,我们很自然想到上面文件回首中fopen以‘w’方式打开文件也是类似的结果,那么,它和刚才的系统调用有什么关系呢?
我们之前还学过‘a’-追加写入,那么为了达到这样的结果,我们可以增加O_APPEND参数, 
  1. int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
复制代码

那么,它和以‘a’方式打开文件又是什么关系呢?
上面一共提到了需要掌握的四个参数:O_WRONLY(以写方式打开)、O_CREAT(不存在就创建)、O_TRUNC(存在就清空)、O_APPEND(追加写入)。
到现在为止,我们已经学完了open函数的参数、选项、标记位,但是其返回值不绝没有说,那么返回值到底是什么呢?
  1. int main()
  2. {
  3.     int fd1 = open("log1.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
  4.     printf("fd1:%d\n",fd1);
  5.     int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
  6.     printf("fd2:%d\n",fd2);
  7.     int fd3 = open("log3.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
  8.     printf("fd3:%d\n",fd3);
  9.     int fd4 = open("log4.txt",O_WRONLY | O_CREAT | O_APPEND,0666);
  10.     printf("fd4:%d\n",fd4);
  11.     return 0;
  12. }
复制代码
运行这段代码:

打印出来的结果是3、4、5、6。希奇的是,怎么不见0、1、2呢?
   0:尺度输入   键盘
  1:尺度输出  体现器
  2:尺度错误  体现器
   同时,在C语言当中,运行步伐时会默认打开3个文件流,如下图:


它们对应的类型都叫做FILE*,和C语言的fopen返回值类型一样,这说明什么呢?这说明在C语言中,我们把键盘、体现器也当做文件来看的,如果我们想对键盘和体现器操作的话,也可以使用C语言中stdin、stdout、stderr,那么系统中的0、1、2和语言中的stdin、stdout、stderr有什么关系呢?
上面打印的结果没有0、1、2,并不是0、1、2没有用,而是被占用了,那直接用write往1里写是不是也可以呢?1是步伐启动默认打开的,1是体现器,是不是就直接往体现器上打呢?
  1. const char* message = "Hello Linux file!\n";
  2. write(1,message,strlen(message));
复制代码

现在很好奇的是,为什么向一个数字(文件形貌符)里写,就可以向文件里写呢?文件形貌符的本质是什么呢?文件映射关系的数组的下标!
   无论读写,都必须在符合的时候,让OS把文件的内容读到文件缓冲区中。
  那么,open在干什么呢?
   1.创建file
  2.开辟文件缓冲区的空间,加载文件数据
  3.查进程的文件形貌符表
  4.file地址,填入对应的表下标中
  5.返回下标
  write、read函数本质是拷贝函数!
但是,0、1、2分别对应键盘、体现器、体现器,这些可都是硬件啊,宁静凡的磁盘上文件不一样啊,那该怎样明白呢?本质上就要明白一切皆文件!那怎样明白硬件也是文件呢?
硬件重要有键盘、体现器、鼠标、网卡、磁盘等,固然这些硬件各异,但是他们都是IO装备,我们关心的无非就是属性和操作方法,它们的属性都有名字、种别、状态,但是它们的值不一样;别的,对于各种硬件,它们都有自己的读方法、写方法,固然每种装备底层的实现方法肯定不一样,但是把他们的返回值和参数设置成类似的,

在OS内,系统在访问文件时,只认文件形貌符fd!那怎样明白C语言通过FILE*访问文件呢?FILE是一个C语言提供的结构体类型,内里一定要封装文件fd!那我们来证实一下:
  1. int main()
  2. {
  3.     printf("stdout->fd:%d\n",stdout->_fileno);
  4.     printf("stdout->fd:%d\n",stdin->_fileno);
  5.     printf("stdout->fd:%d\n",stderr->_fileno);
  6.     FILE* pf1 = fopen("log1.txt","w");
  7.     if(pf1 == NULL) return 1;
  8.     printf("fd:%d\n",pf1->_fileno);
  9.     FILE* pf2 = fopen("log2.txt","w");
  10.     if(pf2 == NULL) return 1;
  11.     printf("fd:%d\n",pf2->_fileno);
  12.     FILE* pf3 = fopen("log3.txt","w");
  13.     if(pf3 == NULL) return 1;
  14.     printf("fd:%d\n",pf3->_fileno);
  15.     FILE* pf4 = fopen("log4.txt","w");
  16.     if(pf4 == NULL) return 1;
  17.     printf("fd:%d\n",pf4->_fileno);
  18.     return 0;
  19. }
复制代码

从上面的运行结果可知,所有的C语言上的文件操作函数,本质底层上都是对系统调用的封装!
那么现在我们既可以使用系统调用,也可以使用语言提供的文件方法,但是推荐使用语言提供的文件方法,否则如果使用系统调用,代码不具备跨平台性。 
重定向

我们先来看一个函数stat:

stat就是状态的意思,用来获取指定文件对应的属性,可以通过文件形貌符fd获取,也可以通过文件路径获取,重点的是buf这个参数,这是一个输出型参数。
我们之前说过,文件=内容+属性,要么是对文件内容做操作,要么是对文件属性做操作,stat这个函数显着就是对属性做操作, 比如我们想要获取文件的巨细,可以这样做:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #include <unistd.h>
  7. const char* filename = "log.txt";
  8. int main()
  9. {
  10.     struct stat st;
  11.     int n = stat(filename,&st);
  12.     if(n<0) return 1;
  13.     printf("file size:%lu\n",st.st_size);
  14.     return 0;
  15. }
复制代码
我们也可以使用系统调用读取文件里的内容:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #include <unistd.h>
  7. #include <stdlib.h>
  8. const char* filename = "log.txt";
  9. int main()
  10. {
  11.     struct stat st;
  12.     int n = stat(filename,&st);
  13.     if(n<0) return 1;
  14.     printf("file size:%lu\n",st.st_size);
  15.     char* file_buffer = (char*)malloc(st.st_size+1);
  16.     n = read(fd,file_buffer,st.st_size);
  17.     if(n>0)
  18.     {
  19.         file_buffer[n] = '\0';
  20.         printf("%s",file_buffer);
  21.     }
  22.    
  23.     return 0;
  24. }
复制代码
下面我们再来看一段代码:
  1. #include <sys/types.h>
  2. #include <sys/stat.h>
  3. #include <fcntl.h>
  4. #include <stdio.h>
  5. #include <string.h>
  6. #include <unistd.h>
  7. #include <stdlib.h>
  8. const char* filename = "log.txt";
  9. int main()
  10. {
  11.     close(0);
  12.     int fd = open(filename,O_CREAT | O_WRONLY | O_TRUNC, 0666);
  13.     if(fd < 0)
  14.     {
  15.         perror("open");
  16.         return 1;
  17.     }
  18.    
  19.     printf("fd:%d\n",fd);
  20.     fprintf(stdout,"fprintf,fd:%d\n",fd);
  21.     fflush(stdout);
  22.     close(fd);
  23.     return 0;
  24. }
复制代码
在这段代码中,把0-文件形貌符关闭掉了,运行结果:

在这段代码中,如果把2-文件形貌符关闭掉,运行结果:

以是,这里给出一个结论:
   文件形貌符的分配规则: 进程查自己的文件形貌符表,分配最小的没有被使用的fd。
  在这段代码中,如果把1-文件形貌符关闭掉,运行结果:

什么都没打印出来。这是为什么呢?
在我们运行一个步伐时,默认打开尺度输入(stdin->0)、尺度输出(stdout->1)、尺度错误(stderr->2),如果手动把1(尺度输出)关闭,再打开一个文件,此时这个文件的fd就是1了,但是printf和fprintf这两个函数只认stdout,stdout内里封装的数字仍旧是1,但是内核中1号下标不是指向之前的体现器了,而是指向一个新打开的文件了,此时printf和fprintf就会打印到新打开的文件当中了,如下图:

上面这种本来应该打印到体现器上的,却打印到了文件中,这种技能就是之前学习过的重定向,因此,重定向的本质是在内核中改变文件形貌符表特定下标的内容,和上层无关!
上面那段代码中,如果把fflush(stdout);这行代码去掉,天生的log.txt文件中什么都没有,这是为啥呢?在C语言中,stdin、stdout、stderr都是struct FILE*类型的,指向的结构体中除了_fileno外,另有语言级别的缓冲区,printf和fprintf都是写到了语言级别的缓冲区里,然后C语言把stdout内里缓冲区的数据通过1号文件形貌符刷新到内核文件缓冲区里,然后外设才能看到对应的数据。以是,我们现在应该明白,为什么fflush(stdout)通报的参数是stdout了!由于这个刷新根本不是把内核文件缓冲区的内容刷新到外设上,而是把语言级别的缓冲区通过文件形貌符写到对应的内核当中,这是fflush要干的事情!
以是,回归到上面的问题,不加fflush,天生的log.txt文件中什么都没有,这是由于
  1. printf("fd:%d\n",fd);
  2. fprintf(stdout,"fprintf,fd:%d\n",fd);
复制代码
这两行代码固然把内容写到了语言的缓冲区里,但是在我正准备进程return之前要刷新的时候,直接(close(fd))直接把文件形貌符关了,以是根本没有办法通过文件形貌符把语言的缓冲区里的内容刷新到内核中,以是最终就丢失了,就没有写到log.txt中。
现在把close解释掉,

这样不就可以在return的时候把语言缓冲区的数据刷新到内核中了吗!log.txt中就有内容了。

那么我们每次重定向的时候,都要像上面步伐那样写吗?我们熟悉一个新的函数dup2,

这个函数可以在底层帮我们做两个文件形貌符下标对应的数组内容之间值拷贝,让newfd成为oldfd的拷贝,比如:
体现器对应的文件形貌符是1,新打开的log.txt对应的f文件形貌符是fd,现在我的需求是尺度输出重定向(本来体现器打印的内容->log.txt),对应代码是dup2(fd,1),把fd下标数组的内容拷贝到1下标数组里,
  1. const char* filename = "log.txt";
  2. int main()
  3. {
  4.     int fd = open(filename,O_CREAT | O_WRONLY | O_TRUNC,0666);
  5.     dup2(fd,1);
  6.     printf("hello world\n");
  7.     fprintf(stdout,"hello world\n");
  8.     return 0;
  9. }
复制代码

改变上面文件打开方式的代码:
  1. int fd = open(filename,O_CREAT | O_WRONLY | O_APPEND,0666);
复制代码
这就是追加重定向: 

缓冲区的明白

在计算机中,我们既有用户级缓冲区,又有内核级缓冲区,无论哪种缓冲区,都有两种作用:解耦、提高效率。所谓解耦,就是用户只需要把数据交给内核文件缓冲区就行,无需和硬件打交道,不需要关心怎么刷新到外设,这就是用户和硬件解耦;提高效率,一方面指的是提高使用者效率,用户只需要调用printf和fprintf把数据放到语言缓冲区里即可,而无需关注怎么刷新到内核中;另一方面指的是刷新IO的效率,调用操作系统,是有成本的,以是尽量少调用,效率就高了,当多次调用printf和fprintf时,大概数据都临时存在语言缓冲区里,没有刷新到内核,当语言缓冲区的数据积攒到一定量后,只调用一次系统调用,就可以把大量数据刷新到内存里,这样效率就提高了。
以是,我们来总结一下缓冲区的明白:
   1.是什么:缓冲区就是一段内存空间
  2.为什么:给上层提供高效的IO体验,间接提高整体的效率
  3.怎么办:
          a.刷新策略(针对用户级缓冲区,内核缓冲区我们不关心)
                  1.立即刷新(相当于无缓存)。fflush(stdout)-->语言级别;int fsync(int fd)-->系统级别,刷新到外设
                  2.行刷新。体现器,照顾用户的检察风俗。 
                  3.全缓冲。缓冲区写满,才刷新,平常文件。
          b.特殊情况
                  进程退出,系统会主动刷新
                  逼迫
  下面对比了两组代码: 

每打开一个文件都有对应的缓冲区!!
   C语言为什么要在FILE中提供用户级缓冲区呢?
  这里我们需要记住一个结论:为了减少底层调用系统调用的次数,让使用C语言的IO函数(printf、fprintf)效率更高
stderr

我们知道,C语言会默认打开三个流:尺度输入流stdin--0、尺度输出流stdout--1、尺度错误流stderr--2。
我们写的步伐,本质都是在对数据进行处理处罚(计算/存储/...),对此,我们有三个问题,数据从哪里来、数据去哪里、用户要不要看到这个过程。尺度输入流stdin--0是为了从用户那里获取数据,尺度输出流stdout--1是为了用户在计算过程中动态看到结果。由于历史原因,用户不绝需要知道数据从哪来、数据去哪里这两个问题,以是默认打开0和1,方便用户动态获取数据和检察数据。
我们来看一段步伐:


我们可以看到,1和2都会打印到体现器上,说明1和2指向同一个体现器文件,此时,我们再输入一下下令:

我们很好奇,1对应的输出语句已经重定向到了log.txt了,但是2对应的输出语句为啥还是在体现器文件上呢?原因就是,“  >  ”这个符号是尺度输出重定向,只会更改1号fd内里的内容,其对应的过程如下:

那到底为什么会有2呢?我们平常在写步伐时,会有两类消息:一类是正确的,一类是错误的。正确的消息我们往1里打印,错误的消息我们往2里打印,未来我们只需要做一次重定向,就可以把正确的信息和错误的信息在文件层面上就可以分来了。现在,我们看下面的步伐:

这内里有正确的消息,也有错误的消息,调试代码时我们希望看到错误的消息,但是这一大堆消息混在一起我们不好找错误消息,我们可以用"  >  "将通例消息重定向到log.txt中,剩下的消息就是正确的消息:

这样就很显着地看到代码中犯了哪些错误。实在,完备的重定向应该是这样:

实际上,我们也可以将错误消息重定向到另一个文件:

把1和2的消息分开了。体现图如下:

如果我们想把1和2的消息都放在一个文件中,该怎么做呢?

体现图如下:

我们再来看这样几行代码:


运行之后,我们发现其打印结果并没有重定向到log.txt中,实在,perror();的本质是向2打印,而printf()本质是向1中打印,因此,这样就可以通过一次重定向把正确消息放到文件中,而把perror等错误消息打印到体现器上。我们在C++中,可以使用cout和cerr达到这样的结果:

cout就类似于printf,cerr就类似于perror,以是在C++中我们打印错误要用cerr,方便我们把错误信息过滤出来。

以上我们讨论的都是被打开的文件,然而存在很多的文件,被打开的文件只是少量的!没有被打开的文件,在哪里存放着呢? 在磁盘上!我们打开这个文件前,需要在磁盘上找到这个文件,是通过文件路径+文件名找到的,
磁盘文件

看看物理磁盘


这是一张磁盘拆开的照片。发光的一面叫做盘片,伸到内里尖角的叫做磁头,每一面都有一个磁头,
我们知道计算机只熟悉二进制,也就是0和1,0和1在硬件上是被规定出来的,在不同硬件上的规定大概不同(高/低电平,磁铁的南北极)。
磁盘的存储结构

磁盘是一个呆板装备,可以认为磁盘由无数个南北极构成,在磁盘上写0和1本质上是通过磁头改变南北极,磁盘上一圈一圈的是磁道,一个磁道又被分别为一个个扇区,磁盘读写的基本单元是扇区:512字节,4KB,

   怎样找到一个指定位置的扇区?能找到一个扇区,就可以找到任何一个扇区
  a.找到指定的磁头(Header)
b.找到指定给的磁(柱面)(Cylinder)  
c.找到指定的扇区(Sector)
通过这三步找到指定扇区的方法叫做CHS定址法
学到这里,我们就知道为什么盘片是高速旋转,而磁头是左右摆动的?磁头在进行左右摆动时是在探求当前面的哪一个磁道(定位磁道),而盘片高速旋转时,是找到磁道上的某一个扇区(定位扇区)。我们知道,文件=内容+属性,它们实在都是二进制数据,以是,文件就是在磁盘中占据几个扇区的问题
对磁盘存储进行逻辑抽象

既然通过CHS定址法我们已经可以对扇区进行定位了,那为什么还要进行抽象呢?由于OS直接用CHS,硬件改变,OS也要改变,耦合度太高,为了方便内核进行磁盘管理。

那么,详细怎么计算呢?假设一面有1000个扇区,10个磁道,也就是一个磁道有100个扇区,那么,现在拿到了扇区的下标index,index/1000就可以H,然后index%1000=tmp~[0,999],也就是在第H面的第tmp个扇区,然后tmp/100=C就可以确定在哪个磁道,然后tmp%100=S就知道在哪个扇区,通过这样的计算流程,只需要知道磁道的下标,就能确定CHS。这样,文件=很多个sector的数组的下标
一般而言,OS未来和磁盘交互的时候,基本单元是4KB 8个连续的扇区(一个数据块巨细),

当知道块号后,*8就知道这个块第一个扇区的下标,也就知道每个扇区的下标了,再用上面的CHS定址法就行。以是,对于OS而言,未来我们读取数据,可以以块为单元了!!!一个扇区巨细是512字节,一个块有8个扇区,以是一个块是4KB。只要知道一个起始,和磁盘的总巨细,那么有多少块、每个块的块号、怎样转换到对应的多个CHS地址,全都知道了!!实在,上面所说的块号有一个名称叫做LBA(逻辑块地址),这样,就可以得到一个LBA数据,LBA blocks[N],这实在就是先形貌后构造,对磁盘的管理就变成对数组的管理,这样,文件=很多个LBA地址
实际上,磁盘空间还是非常大的,也很难管理,以是就要分区,把一个区管理好,就能把所有区管理好了。什么叫做分区,实在我们只要知道了这个区的起始LBA和结束LBA不就行了吗。分区在我们的电脑上也很常见,我们的C盘/D盘/E盘就是分区之后的结果。

分区后,我们只要记住每个区的起始块号和结束块号是什么,就能确定每个分区的范围。在分完区后,每个区还是有点大,以是我们还要继承分组,比如每个组是10GB,那么就有20个组,只要把每个组管理好,就能把所有组管理好,问题转换为管理好一个组,这实在是分治思想。
我们之前知道,文件=内容+属性,内容是数据,属性也是数据,以是,文件在磁盘中存储,本质是:文件的内容+文件的属性数据。Linux文件系统特点是:文件内容和文件属性分开存储。

在每个组中,Data blocks叫做数据区,它是在一个组中占据空间最大的地区,由一个个4KB的数据块组成,只存储文件的内容。
Block Bitmap叫做块位图,其中纪录了Data Block中哪个数据块已经被占用,哪个数据块没有被占用,一个块4KB=32768bit,如果Data blocks有10w个数据块,位图有4个数据块(13w多个比特位)就能体现这10w个数据块的使用状态。Block Bitmap中比特位的位置,体现块号,比特位的内容,体现该块是否被占用。以是,只要统计Block Bitmap中有多少个0、多少个1就能知道数据区中多少个数据块被占用。
inode Table叫做i节点表,存放文件属性如文件巨细、所有者、近来修改时间等,其基本单元也是块。Linux中文件的属性是一个巨细固定的集合体。实在就是一个struct结构,一般叫做struct inode:
  1. struct inode//文件的属性
  2. {
  3.     int size;   //文件大小
  4.     mode_t mode;//文件权限
  5.     int creater;//文件的创建者
  6.     int time;   //文件创建时间
  7.     ...
  8.     int inode_number;
  9.     int datablocks[N];
  10.     ...
  11. }
复制代码
每个文件的巨细大概千差万别,但是文件的属性巨细是一样的,只是属性的值不一样。在Linux中,struct inode的巨细一般是128字节。inode Table里也是一个个块,一个块巨细是4KB,一个块能存放下4*1024/128=32个struct inode。一个文件对应一个inode, 如果我们要有1w个文件,就需要10000/32=312个块来存文件属性。
inode位图:每个bit体现一个inode是否空闲可用。比特位的位置体现第一个inode(inode number),比特位的内容,体现是否被占用。
但是,struct inode里不包含文件名!那怎样找到这个文件呢?在内核层面,每一个文件都要有inode number!我们通过inode号标识一个文件!可以通过-i指令检察其inode号。

 那么,假设上层能够拿到inode号,先查inode位图,如果比特位为1,说明是这个位置合法的,然后再去inode Table里找到对应inode,然后就找到了inode属性。我们可以通过inode号找到inode属性,现在的问题是,怎么找到对应的文件内容呢?实在,在struct inode中,另有datablocks[N]这个数组,这个数组会包含这个数组占据了哪些数据块,由于每个数据块都有块号,因此,datablocks[N]这个数组就可以存这个块和哪几个块对应,比如,这个文件和块号为0、1、2、3、4、5的数据块对应,那么datablocks[N]这个数组的内容就依次填写0、1、2、3、4、5。以是,只要知道了inode号,就能知道文件的内容和属性。
GDT,Group Descripter Table,块组形貌符,形貌这一个块的基本情况,比如这个块多大,一共多少个inode,一共多少个datablocks,已经有多少个inode被使用了,另有多少个datablocks没有被使用,都会纪录在GDT中。
Super Block,超级块,形貌整个分区的基本情况。你没听错,就是在一个组里的超级块存放了整个分区的情况,比如,一共分了多少个组,每个块组的的基本使用是什么样子,总共有多少block和inode,已经使用了多少block和inode。很好奇的是,Super Block怎么能在块组0里呢?不应该子在最前面单独一块吗?实在,超级块并不是每一个分组都有的,但是也不是在整个分区里只有一份,一般会在2~3个分组里存在,并且这几个分组里的超级块的内容要保持一致,为什么要这么干呢?重要原因是Super Block体现整个分区的使用情况,磁盘是个呆板装备,磁盘通过磁头和盘片定位旋转,万一磁头把Super Block对应的扇区刮花了,数据就乱了,整个分区就挂掉了,那就是几百G不能用了。以是出于效率思量,没须要每个分组都存Super Block,但是它也不止在一个分组里,它在别的分组里另有重要是让我们的文件系统有更好的结实性,万一某个组的Super Block被刮花了,没关系,我们在其他组也能找到Super Block。
   在每一个分区内部分组,然后写入文件系统的管理数据,这个过程叫格式化!!!格式化的本质是在磁盘中写入文件系统!!!
  
至此,我们已经把文件系统的框架搭建起来,然后我们继承讨论细节:
我们探求文件的时候,都必须先得找到文件的inode编号,inode编号是以分区为单元进行分配的,并不是以分组为单元,一个分区内部所有的inode号都不能重复,两个分区之间inode号大概会出现重复,以是inode号不能跨分区访问!!
Super Block和GDT会纪录下来该分区分组的inode的范围,比如,块组0的inode的范围是0-10000,块组1的inode的范围是10001-20000,依次类推,我们某一个分组的inode号是在一个范围内(比如0-10000)。假设现在我有一个inode号=100010,首先要确定在哪个分区,然后拿着10010在0-10000、10001-20000、20001-30000...这些区间去卡,发现10010在10001-20000之间。 
现在假设我们有一个inode编号为50的文件,在这个分区内取探求它落在哪个区间,发现它落在了0-10000这个分组里,然后50-0(0是这个组起始inode),然后再inode Bitmap里从右向左数50个bit,看它是0是1,从而确定这个inode是不是一个合法inode,然后去inode Table里去找第50个元素,从而找到了文件的属性。再比如现在有一个inode编号为10010的文件,发现卡在了10001-20000之间,那就在组1中,然后用10010-10001=9,然后去inode Bitmap去找第9个就可以,确定这个inode是不是一个合法inode后,然后去inode Table里去找第9个元素,从而找到了文件的属性。
datablock也是按照这样的方式分组的,也有start块号和end块号。当inode映射到某一组后,优先在这个组内部使用它的数据块,一般我们不要跨组访问。

inode属性里有datablocks[N]这个数组,这个数组是多大呢?我们先来说一下,之前我们讨论的文件系统是ext2,是一个比较入门级的,实际上另有ext3和ext4,ext2中的datablocks的N是15,这就意味着,这个inode只能映射datablocks中15个数据块,也就是60KB,这是不是不太对呀???怎么大概所有文件这么小???究竟上,datablocks这个数组前12个元素[0,11]是直接一一映射到数据块的,第12号元素不是直接映射,其对应的数据块不生存文件的数据,其对应的数据块中也类似于内里有一个索引数组,再指向其他的很多个数据块,但是这样映射下来也不是很大啊。那么再来看第13号元素,其对应的数据块也是存了索引数组,然后这个索引数组指向的位置也不存数据,也是存索引数组,这样多级映射之后,数据块就很大了,就可以有更大的文件!!!

以上我们都是在已经拿到inode号的基础上,才能找到对应的文件属性和内容,那现在的问题是,怎么拿到文件的inode呢?我们不绝用的是文件名啊!!!并且inode属性是不包含文件名的。以是,这就有点尴尬了。
现在,我们需要先谈一下目次。目次实在也是文件,也有文件属性和文件内容,也有自己的inode,



我们发现,平常文件和目次文件的属性都一样,只是属性值不同。平常文件的内容大概是C代码、C++代码、日记等,那目次文件的内容是什么呢???究竟上,目次的内容是文件名和inode编号的映射关系!!!
因此,当我们需要访问文件时,需要根据文件名在其所在的目次下找到对应的inode编号,然后根据文件名和inode编号的映射关系,就找到了文件名对应的inode号!
了解到这样的知识后,我们可以来表明这样几个问题:
   1.一个目次下不能建立同名文件,由于inode和文件名互为键值,要通过自身能够唯一地找到彼此。
    2.查找文件的次序是根据文件名去找对应的inode编号,然后在所在的分区中确认inode的范围,确定在哪个组里,找到inode Bitmap,确定合法后,再找inode Table,找到对应的属性,再根据inode中属性中的datablock,把对应的数据块全部搞到内存里。
    3.我们进入目次需要x权限,现在这不是我们的重点。当我们把目次的r权限去掉之后,我们可以创建文件,但是不能检察文件。目次的r,本质是是否答应我们读取目次的内容,也就是文件名和inode号的映射关系!目次w,当我们要新建文件时,最后一定要向当前所处的目次内容写入文件名和inode的映射关系。
  现在我们正面回复上面的问题,怎么拿到文件的inode呢?由于我们一定处在一个目次内里,只要拿到了目次的内容,系统就会根据输入的文件名和inode的映射关系找到该文件对应的inode,然后确定在哪个分区,哪个分组,找到所有的inode属性。
   4.怎样明白一个文件的增删查改呢?
  :新建文件,就是在特定的分区申请一个inode号,根据inode确定在哪个分组里,然后再inode BItmap里继承从低向高找哪一个比特位为0,然后在inode Table里去分配一个inode空间,把属性一写,然后再去block bitmap里去申请比特位,把内容写到数据块里,然后把块和inode属性建立映射关系,然后把inode号返回,再建立文件名和inode的映射关系。
  :根据文件名来改。:也是根据文件名来查。
  :只需要找到inode号在inode Bitmap里的位图,由1置0,然后在Block Bitmap里把对应的数据块位图依次清0,就相当于文件被删掉了。因此,删除一个文件并不需要删除文件的属性和内容,只要对应的位图结构由1置0即可。
  现在,另有最后一点问题,我要找到指定的文件,就要找到该文件所在的目次,并把它打开,根据文件名和inode的映射关系,找到目标文件的inode。那么问题来了,怎样找到文件所在的目次?就需要先根据目次的名字找到目次的inode,依次类推,最终找到根目次,而根目次的inode是规定出来的,在我们系统开机的时候就确定了。以是,要想找到一个文件,需要进行逆向的路径剖析,这是由OS自己做的。这就是为什么我们在linux中,定位一个文件,在任何时候,都要有路径的原因!!
那么,逆向的路径剖析这个工作每次都要进行吗?那倒不必!linux系统会对我们常用的路径结构进缓存。
究竟上,我们拿到一个inode号,不是先确定它在哪个分组,更条件得是文件在哪个分区!!!
我们使用的云服务器一般只有一个盘(/dev/vda),vda虚拟出来一个分区vda1,在linux中要访问一个分区实在需要挂载这个分区,就是把磁盘分区和一个目次进行关联,

未来进入一个分区本质就是进入这个目次。
   我们访问的文件,一定直接或者间接带有路径。一个文件其着实访问之前,都是先有目次的!!那么,我只要对比一下路径的前缀就能确定在哪个分区了,文件在哪个目次里,就知道在哪个分区下了!!以是,目次自己除了可以定位文件,还可以确定是在哪个分区下的!!
  Linux内核在被使用的时候,一定存在大量的剖析完毕的路径,要不要对访问的路径做管理呢?先形貌,再构造!使用struct dentry进行形貌。
  1. struct dentry
  2. {
  3.     //路径解析的信息,一个文件一个dentry
  4. }
复制代码
软硬链接

见一见软硬链接


我们看到,软链接后得到的新文件具有独立的inode(1837947),是一个独立的文件。

当我们建立硬链接后,我们发现硬链接得到的文件的inode和被链接文件的inode一样,此外,文件被链接前后,其某个属性由1->2。
软链接特征及用处

通过以上分析,我们可以得到以下结论:
1.软链接是一个独立的文件,由于有独立的inode number;
2.硬链接不是一个独立的文件,由于没有独立的inode number,用的是目标文件的inode;
3.属性中有一列叫硬链接数,是一个引用计数,就是上面由1->2的谁人数。这也是文件的磁盘级引用计数,体现有多少个文件名字符串通过inode number指向这个文件,这也是inode的一个属性!
软链接文件的属性第一个字母是l,体现link;由于软链接是一个独立的文件,文件就有内容+属性,软链接的内容是目标文件所对应的路径字符串,实在,软链接类似windows中的快捷方式。那这样说的话,如果我们把软链接删掉,会不会影响目标文件呢?不会!(由于删除快捷方式不会把软件删掉啊!)那我们把目标文件删掉,快捷方式另有用吗?没用了!删除后,检察软链接会闪烁!

那软链接有什么用呢????

比方,我们在./bin/a/b/c/myls这里有一个可执行文件myls,但是很显着这个可执行文件藏的很深,不容易找到,那么就可以在当前文件对myls做一个软链接,

 
直接运行这个软链接文件就可以运行myls,以是,软链接实在就是快捷方式!!!
再比如,我们有一个很深路径的文件my.conf:

这个文件的路径很深,我们未便查找,以是,我们可以给这个文件加一个软链接,即快捷方式,

这样就可以很方便使用软链接访问这个文件。
在linux库中,我们可以看到:

使用软链接对一些库做了快捷方式,这样,当更新库时,只需要把背面的实际库修改,而无需修改软链接名称,以后可以继承使用这个软链接名称。
硬链接特征及用处

我们上面已经知道,硬链接不是一个独立的文件,那硬链接是什么?我们从上面看到,硬链接和原来的文件对应同一个inode编号,当删除原来的文件后,我们发现这个文件并没有被删除,还可以通过硬链接访问到,引用计数退为1:

实在,硬链接就是一个文件名和inode的映射关系,建立硬链接,就是在指定目次下,添加一个新的文件名和inode number的映射关系!以是,可以有很多个文件名指向同一个inode号。硬链接没有新建文件,因此就不会有新的inode,就不会有对应新的数据块,当有硬链接创建时,对应的引用数据+1。当增加一个硬链接时,引用计数+1,当删除一个inode号对应的文件名时,引用计数-1。上面先建立硬链接然后删除原来的文件,只剩下硬链接,原来的文件仍然可以访问到,这不就是重命名吗!!
通过上面的学习,我们知道,定位一个文件,只有两种方式:
   1.通过路径(软链接)
  2.直接找到目标文件的inode(硬链接)
  但是,无论哪种方式,最终还是要通过inode number找的!
那么硬链接有什么用处呢?

由于目次也是文件,它也有对应的inode。我们发现,新建立的文件的引用计数是1,而新建目次的引用计数是2,这是为什么呢?我们知道,dir和inode是一个映射关系,以是它的引用计数至少是1,同时,任何一个目次内里都有隐藏的.和..,

.体现当前路径,由于它的inode是1837947,和dir的一样,以是.相当于dir的重命名,这就是有两个文件名指向同一个inode,以是引用计数是2。
现在,我在dir内里再建立一个otherdir,


我们发现,dir的引用计数现在变为3,为什么呢?

我们发现,otherdir的..对应inode也是1837947,以是,又有一个文件和这个inode映射,因此,就不难表明为什么引用计数变为3了~这就是为什么cd..可以回退上级目次,由于..指向上级目次,因此,任何一个目次,刚开始新建的时候,引用计数一定是2,目次A内部,新建一个目次,会让A目次的引用计数主动+1。那么根据这个结论,我们还可以知道,一个目次内部的目次数为:A引用计数-2
以是,我们总结一下硬链接的用处:
   1.构建Linux的路径结构,让我们可以使用.和..来进行路径定位。
  那么,在Linux中,可以给目次建立硬链接吗?不可以!我们来看一下:

那这是为什么呢?

假设可以给目次建立硬链接,那如果在lesson23里建立了根目次/的硬链接root.hard,如果某一天要查找test.c这个文件,那从根目次深度优先往里找,不绝找到了root.hard,而它的属性是d,那就会跳到根目次,这样就形成了路径环绕,因此,为了制止形成路径环绕,不可以给路径建立硬链接。
那有人大概有这样的疑问,dir和dir中的.以及和otherdir中的..不也是路径环绕吗?不会的!由于.和..文件名是固定的,所有的系统指令在设定的时候,几乎都知道.和..是干什么的。
   2.硬链接一般用来做文件备份。
  我们还可以建立一个backup目次,在这个文件里放需要备份文件的硬链接,这样即使原文件被删除了,我也可以通过硬链接继承访问这个文件!
到此,我们的文件系统已经学完,我们来总结一下文件就是两种:
   1.打开的文件:和内核、内存有关
  2.没有被打开的文件:和磁盘有关、文件系统有关
  我们现在再来看文件操作,通过调用fopen、open系统调用打开一个文件,第一个参数都是先要给文件路径,根据文件路径(文件名)找到inode编号,在磁盘当中定位分区、确定在哪个分组,然后根据inode编号找到文件属性,然后在内核中创建struct file,把inode和struct file关联起来,此时就有了文件的属性,根据加载进来的inode中的谁人和数据块有映射关系的属性,在指定的分区分组里拿数据块,加载到文件的内核缓冲区里,让用户通过进程的方式、通过文件形貌符的方式从内核拷贝到用户,不就看到文件了吗!厥后对文件进行写入时,使用文件形貌符和FILE*,使用C++的文件流,把数据写到磁盘里,实在就是先把用户级数据处理处罚完,放到语言级别的缓冲区里,通过文件形貌符拷贝到文件的内核级缓冲区,该修改属性就修改属性,该修改内容就修改内容,所谓的刷新就是把属性写回inode,inode一共才128字节,直接全覆盖就行了,然后写回文件的内容,如果少了就开释几个数据块,如果多了就申请几个数据块,重新修改位图,构建映射关系。

另一个问题,我们之前使用文件操作时,会有文本写入和二进制写入,但是在OS上只有二进制写入,那文本写入是什么呢?文本写入实在是语言层的概念,那从磁盘上面的读进来的二进制怎么变成“hello world”这样的文本信息呢?实际上,数据从底层读到缓冲区里,再做语言级的表明就行了!比如,int a =1234567;这个数再大也就占4字节,如果把a直接写入文件,那就是二进制写入,以是占4个字节;如果把1234567转化成“1234567”字符串,这就是以文本写入,那这个转换是谁做的呢?是语言自己的函数在做!比如,fprintf(fp,"hello world:%d\n",a),它就是把原来4字节的a转换成了7字符的字符串“1234567”写入到文件中,以是说文本写入是语言层的概念。再比如,printf("%d",a),本质上是向体现器文件中去打印,打印的实在是7个字符,但是在内存中a是4字节,这实在是printf做的转换!





免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

李优秀

高级会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表