访问文件
文件 = 文件内容 + 文件属性(例如:文件名称、文件拥有者、修改时间)
在系统里新建一个空文件也要占磁盘空间
但文件在磁盘放着,磁盘是硬件,只有os能访问磁盘
所以访问文件,本质是进程调用系统接口访问文件
linux文件权限
什么是文件权限
权限就是是一件事情是否允许被某个用户"做"
即权限 = 用户 + 事物属性
linux的文件权限 = 用户 + 文件属性
用户有三种:拥有者、所属组、other
文件属性也有三种:读、写、执行
如何查看linux文件的权限
ll可以查看当前目录下文件的属性详情,里面就包含了文件的权限
文件权限:-rw-rw-r--
1 第一列的第一个字符,表示该文件的文件类型
css
d:目录
-:普通文件
p:管道文件
b:块设备/磁盘设备
c:字符设备,例如:键盘或显示器
l:链接文件,例如快捷方式
2 剩下的固定9个字符,3个字符为一组,分别表示拥有者、所属组、other
rwx的位置是固定的,如果要表示没有对应权限,用-表示
3 -rw-rw-r--
表述文件的权限:
该文件是普通文件
拥有者具有读权限、写权限,没有可执行权限
所属组具有读权限、写权限,没有可执行权限
other具有读权限,没有写权限和执行权限
4 由于linux权限可以用固定的9个字符表示,且每个字符是二态的,要么有要么没有,
所以可以用三个8进制数来表示文件的权限。0是8进制的前缀,例如:
0777:换成二进制就是 111 111 111,表示拥有者、所属组、other都拥有读、写、执行权限
umask权限掩码
linux默认:一个目录被创建,起始权限是从777开始;
一个普通文件被创建,起始权限是从666开始。
但最终权限 = 起始权限 & ~umask
凡是在权限掩码中出现的权限,都不能出现在最终权限中
umask是1个三位八进制数,以0002为例: 000 000 010,表示创建文件时,other的写权限要被去掉
语言对文件系统调用接口的封装
1 系统调用接口要懂系统 ------ 难度较高,语言会对这些接口做封装,让这些接口更好用
不同语言,有不同的文件访问接口,但封装的都是系统调用接口
2 跨平台 ------ 一份代码,放在linux上运行,也能放在windows上运行
如果语言不提供对文件系统调用接口的封装,所有访问文件的操作,都必须使用os的接口,
windows和linux的文件系统调用接口是不一样的!!!
而用户要访问文件,一旦使用系统接口编写文件相关代码,就无法在其他平台中直接运行。
C文件访问接口
ini
// 以写入方式打开,当前路径指的是进程的cwd
FILE *fp = fopen("log.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
const char *s1 = "hello fwrite\n"; // '\0'结尾是c语言的规定,文件不需要遵守!文件只保存有效数据
fwrite(s1, strlen(s1), 1, fp);
const char *s2 = "hello fprintf\n";
fprintf(fp, "%s", s2);
const char *s3 = "hello fputs\n";
fputs(s3, fp);
fclose(fp);
return 0;
arduino
void usage(const char* proc)
{
cout << "Usage:"<<proc << " filename" << endl;
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
usage(argv[0]);
return 1;
}
FILE* fp = fopen(argv[1],"r");
if(fp == NULL)
{
perror("fopen");
return 2;
}
char buffer[1024];
//fgets是c语言函数接口,会自动在字符结尾添加'\0'
while(fgets(buffer,sizeof(buffer)-1,fp) != NULL)
{
fprintf(stdout,"%s",buffer);
}
return 0;
}
linux文件类系统调用接口
open和close
const char* pathname:要打开的文件路径
int flags:选项,打开文件时可以传递多个选项,表明要如何打开文件(例如读方式打开)
常用的选项有:O_CREAT(文件不存在就创建文件)、O_TRUNC(打开文件后先清空文件内容)
O_WRONLY(以只写方式打开文件)、O_RDONLY(以只读方式打开文件)、O_RDWR(读写方式打开文件)
O_APPEND(追加)
mode_t mode:8进制文件权限,代表创建新文件时,文件的权限是什么
返回值:成功返回文件描述符 >= 0,失败返回-1
perl
int main()
{
//以只读方式打开
int fd = open("log.txt",O_RDONLY);
if(fd < 0){
perror("open error");
return 1;
}
printf("open success,fd:%d\n",fd);
//关闭文件
close(fd);
return 0;
}
scss
//以只写方式打开,且打开文件时清空文件内容,相当于fopen的"w"
int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC);
if(fd < 0){
perror("open error");
return 1;
}
//关闭文件
close(fd);
open传递多个标志位的原理
flags的选项,本质是位图的某一个比特位,
传递时可以和其他比特位组合,实现不同的功能。
arduino
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
//0000 0000 0000 0000 0000 0000 0000 0000 32个比特位,如果第一个比特位设置了,就打印1
//用int中不重复的一个bit,就可以标识一种状态
#define ONE 1 //0000 0001
#define TWO 2 // 0000 0010
#define THREE 4 //0000 0100
#define FOUR 8 //0000 1000
void test(int flag)
{
if(flag & ONE) printf("状态ONE\n");
if(flag & TWO) printf("状态TWO\n");
if(flag & THREE) printf("状态THREE\n");
if(flag & FOUR) printf("状态FOUR\n");
}
int main()
{
test(ONE|FOUR);//0000 0001 | 0000 1000 -> 0000 1001
return 0;
}
read和write
从文件描述符中读取最多count个字符,放到buf中
返回实际读到的字节数/字符数
ini
int fd = open("log.txt",O_RDONLY);
if(fd < 0){
perror("open error");
return 1;
}
char buffer[1024];
ssize_t s = read(fd,buffer,sizeof(buffer));
if(s > 0)
{
buffer[s] = '\0';
}
cout << buffer << endl;
close(fd);
scss
//该进程创建的文件,权限掩码都为0
umask(0);
//O_TRUNC:打开文件时,清空文件
//O_APPEND:向文件结尾追加
int fd1 = open("log1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); //相当于 fopen("log.txt","w")
int fd2 = open("log2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//相当于 fopen("log.txt","a")
string str = "hello world\n";
write(fd1,str.c_str(),str.size());
write(fd2,str.c_str(),str.size());
close(fd1);
close(fd2);
fd与FILE
FILE里封装了文件描述符fd,在系统层面只认fd
ini
umask(0);
int fd1 = open("log1.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd2 = open("log2.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd3 = open("log3.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd4 = open("log4.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
cout << fd1 << fd2 << fd3 << fd4 << endl;
以fd的0、1、2为例:
c语言默认会打开3个文件流:stdin、stdout、stderr 都是FILE*类型
0 ------ 标准输入
1 ------ 标准输出
2 ------ 标准错误
c
fprintf(stdout,"hello\n");
const char* str = "hello\n";
//1 - 标准输出
write(1,str,strlen(str));
arduino
char buffer[1024];
//实际读到的byte数
ssize_t s = read(0, buffer, sizeof(buffer));
buffer[s] = '\0';
cout << buffer;
c
cout << stdin->_fileno << endl;
cout << stdout->_fileno << endl;
cout << stderr->_fileno << endl;
写一个cat命令
arduino
void usage(const char* proc)
{
printf("Usage:%s filename\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 2)
{
usage(argv[0]);
return 1;
}
int fd = open(argv[1],O_RDONLY);
char buffer[1024];
ssize_t s = read(fd,buffer,sizeof(buffer));
buffer[s] = '\0';
write(1,buffer,strlen(buffer));
close(fd);
return 0;
}
文件描述符fd
文件描述符是什么
进程要访问文件,必须先打开文件!
一个进程可以打开多个文件,一般而言:
进程:打开的文件 = 1:n
一个文件要被访问,前提是加载到内存,才能被访问!!!
如果多个进程都打开文件,系统中会存在大量被打开的文件!!!
os要把这些被打开的文件管理起来 ------ 先描述,再组织
在内核中,os要为了管理每一个被打开文件,构建struct file结构体,
每打开一个文件,就要创建struct file对象
arduino
struct file{
struct file* next;
struct file* pre;
//包含一个被打开文件的几乎所有内容(不仅仅属性)
//文件属性从哪里来 ------ 磁盘
}
一个进程可能打开多个文件,怎么把该进程和打开的文件关联起来呢?
在内核中,为了维护进程和被打开文件的关系,每个进程各自私有一个指针数组
数组的元素是被打开的文件的地址,而文件描述符fd ------ 就是该数组的下标
文件描述符分配规则
c
close(0);//本质是将进程文件描述符表中下标为0的元素置NULL
int fd = open("log.txt",O_WRONLY|O_TRUNC|O_CREAT);
cout << fd << endl; //0
int fd1 = open("log1.txt",O_WRONLY|O_CREAT|O_TRUNC);
cout << fd1 << endl; //3
int fd2 = open("log2.txt",O_WRONLY | O_CREAT | O_TRUNC);
cout << fd2 << endl; //4
从头扫描文件描述符表fd_array[],找到最小的、没有被占用的位置,将下标/文件描述符返回
open打开文件,系统做了什么
1 在内核中创建一个struct file对象(如果该文件没有被打开)
2 从头遍历进程的文件描述符表中,找到第一个NULL位置,把struct file对象的地址填进去,
同时把对应的数组下标fd返回给用户
重定向原理
进程被创建,它的文件描述符表的0 1 2 下标的值,会填写标准输入、标准输出和标准错误对应的struct file地址
给进程默认关联打开的三个文件
在打开文件之前,把文件描述符1关闭了 close(1)
然后打开一个新文件时,根据文件描述符分配规则,将新打开的文件对象地址填入 fd_array[1]
上层printf、cout等输出函数,只负责把内容写入给stdout(fileno =1),但下层的1已经指向log.txt
重定向的本质:在os内核更改fd对应的文件指向
arduino
close(0);//关闭标准输入
int fd = open("log.txt",O_RDONLY);
char buffer[1024];
fgets(buffer,sizeof buffer,stdin);//本来从标准输入/键盘读取,变成从文件中读取,按行读取
printf("%s",buffer);
int dup2(int oldfd, int newfd);
简单来说:让newfd指向的文件,和oldfd指向的文件保持一致
scss
int fd = open("log.txt",O_RDONLY);
dup2(fd,0);//此时0也指向fd对应的文件
close(fd);//fd是要手动关的
char buffer[1024];
ssize_t s = read(0,buffer,sizeof(buffer));
if(s > 0)
{
buffer[s] = '\0';
write(1,buffer,strlen(buffer));
}
else{
cout << "读取文件描述符0的内容失败"<<endl;
}
linux下一切皆文件
广义上的文件
狭义上的文件:普通磁盘文件
广义上的文件:几乎所有外设,都可以称为文件
站在系统角度,可以Input读取,或者能够Output写入的设备就叫文件
理解一切皆文件
一切皆文件是linux的设计哲学
linux是用c写的!!!
类:包含成员属性和成员方法
c结构体怎么包含成员方法 ------ 函数指针
arduino
struct file{
mode_t mode;//权限
long size;
int user;
int (*read)(int fd,void* buffer,int length); //读方法
int (*write)(int fd,void* buffer,int length);//写方法
}
所有外设的核心访问函数,都可以是read和write,
因为read->Input,write->Output。在冯诺依曼体系,对外设的操作无非是IO
当打开一个磁盘文件时,在内核里创建一个struct file对象,让read指针和write指针指向磁盘文件的读写方法
当打开显示器(默认就打开了),在内核里创建struct file对象,让read指针和write指针指向显示器的读写方法(涉及到驱动开发)
于是linux下有一切皆文件的理念!!!
VFS :虚拟文件系统
缓冲区
什么是缓冲区
就是一段内存空间
arduino
//用户定义的用户层缓冲区
char buffer[1024];
int a;
scanf("%s %d",buffer,&a);
为什么要有缓冲区
1 写透模式WT:直接将数据写到硬件里
2 写回模式WB:先将数据写到缓冲区,缓冲区积累足够多的数据后,再一次性把数据刷新到硬件上
冯诺依曼体系结构里IO的效率是很低的,尽量减少IO次数,就需要使用缓冲区
缓冲区的刷新策略
1 立刻刷新
2 行刷新(遇到\n就把\n以前的数据刷新)
3 满刷新(把缓冲区写满才刷新)
4 特殊情况:
(1) 用户强制刷新,例如:fflush
(2) 进程退出
所有的设备,倾向于全缓冲,因为缓冲区满了才刷新,就需要更少次的io操作
其他刷新策略,是结合具体情况做的妥协,例如:
显示器:直接给用户看的,一方面要照顾效率,一方面要照顾用户体验
极端情况,可以自定义规则的 ------ 立即刷新
往磁盘文件里写时,用户不需要立刻看到文件内容,当进程退出或强制刷新时,写到文件里就可以了,因此磁盘文件采用全缓冲
c语言库的缓冲区
c
int main()
{
//c库函数
fprintf(stdout,"hello fprintf\n");
const char* str = "hello fputs\n";
fputs(str,stdout);
//系统调用
const char* ss = "hello write\n";
write(1,ss,strlen(ss));
fork();//最后调用,上面函数已经被执行完了
return 0;
}
重定向后,log.txt里,c库函数打印的内容有2份
fputs等c库函数,是先把数据拷贝到c标准库提供的缓冲区中
c标准库再按照刷新规则,将数据刷新到os内核中 -> 调用write接口
fputs等c库函数,其实是拷贝工作,将数据拷贝到缓冲区,按照刷新规则,调用write接口写入到os内核中
c
//c库函数
fprintf(stdout,"hello fprintf\n");
const char* str = "hello fputs\n";
fputs(str,stdout);
//系统调用
const char* ss = "hello write\n";
write(1,ss,strlen(ss));
//fork之后的return 0:父子进程各自退出
//进程退出时,数据要被强制刷新
//刷新缓冲区,也是一种写入,清空缓冲区的数据,会发生写时拷贝
//所以缓冲区里的数据,会出现两份
fork();//fork之后,上面的函数已经执行完了,不代表数据已经刷新了
//3 之前谈的"缓冲区",一定是由c标准库维护的
1 如果向显示器打印,刷新策略是行刷新,最后执行frok时,一定是数据已经刷新了
2 如果进行了重定向,向磁盘文件打印 ------ 满刷新.
上面函数执行完了,但数据没有刷新,还在当前进程的c标准库中的缓冲区里
3 c标准库里缓冲区的数据,也是父进程的数据!!!当fork()创建子进程时,父子进程的数据采用写时拷贝
4 进程退出时,数据是要被强制刷新的,即清空缓冲区,发生写时拷贝,父子各自执行一次,
所以上面的c库函数在文件中会打印2份.
5 c标准库的FILE给我们提供的用户级缓冲区
/usr/include/libio.h 一旦拷贝到file里的内核缓冲区,该数据就不属于进程了,属于内核数据,os会刷新
自己设计一个用户层缓冲区
使用c库函数所有IO接口fputs、fclose、fread、fwrite、fflush等,都会涉及FILE*,
因为无论读、写、刷新,文件描述符和缓冲区数据都在FILE*里.
ini
struct MyFILE
{
int fd;
char buffer[1024]; // 缓冲区
int end; // 缓冲区结尾
};
//fopen模拟实现
MyFILE* myfopen(const char* pathname,const char* mode)
{
MyFILE* fp = NULL;
if(strcmp(mode,"r") == 0)
{
int fd = open(pathname,O_RDONLY);
if(fd >= 0)
{
fp = (MyFILE*)calloc(1,sizeof(MyFILE));
fp->fd = fd;
}
}
else if(strcmp(mode,"w") == 0)
{
int fd = open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd >=0)
{
fp = (MyFILE*)calloc(1,sizeof(MyFILE));
fp->fd = fd;
}
}
else if(strcmp(mode,"a") == 0)
{
int fd = open(pathname,O_WRONLY|O_CREAT|O_APPEND);
if(fd >= 0)
{
fp = (MyFILE*)calloc(1,sizeof(MyFILE));
fp->fd = fd;
}
}
return fp;
}
arduino
void myfputs(const char* str,MyFILE* fp)
{
int preLength = fp->end;
strcpy(fp->buffer+fp->end,str);
fp->end += strlen(str);
if(fp->fd == 1 || fp->fd == 2)
{
//标准输出/标准错误的刷新策略
const char* flag = strrchr(str,'\n');//从尾部开始找'\n',找到返回地址
if(flag != nullptr)
{
//在str中,'\n'对应的下标
int target = flag - str;
//因为str的内容被拷贝到了fp->buffer的尾部,所以在fp->buffer中,'\n'的下标是target + preLength
//将fp->buffer[target+preLength]以前的数据全部刷新
write(fp->fd,fp->buffer,target+preLength+1);
syncfs(fp->fd);//把数据从内核刷新出去
strcpy(fp->buffer,fp->buffer + target + preLength + 1);
fp->end = strlen(fp->buffer);
}
}
}
scss
void myfflush(MyFILE* fp)
{
if(fp->end > 0)
{
write(fp->fd,fp->buffer,fp->end);
syncfs(fp->fd);//把内核数据刷新
fp->end = 0;
}
}
void myfclose(MyFILE* fp)
{
myfflush(fp);
close(fp->fd);
free(fp);
}
scss
MyFILE *fp1 = (MyFILE*)calloc(1,sizeof(MyFILE));
fp1->fd = 1;
myfputs("hello\n world1",fp1);//先打印出hello
sleep(2);
myfputs("hello\n world2",fp1);//过2秒,打印出world1 hello
sleep(2);
myfclose(fp1);//再过2秒,打印world2
缓冲区的刷新策略,是在c标准库中的代码逻辑实现的
stdout和stderr
c
cout << "hello cout 1 " << endl;//stdout -> 1
cerr << "hello cerr 2" << endl;//stderr -> 2
const char* str1 = "hello 1\n";
write(1,str1,strlen(str1));
const char* str2 = "hello 2\n";
write(2,str2,strlen(str2));
文件描述符 1 和 2 都指向显示器文件,但是是不同的显示器文件。
重定向只会改变1号文件描述符,把标准输出的内容重定向到其他文件,追加也一样
./myproc 2>err.txt
把本来应该显示到2号文件描述符的内容,显示到err.txt中
把本来应该显示到2号文件描述符的内容,写到文件描述符为1指向的文件中
机械硬盘
基本认识
内存 ------ 掉电易失存储介质
机械硬盘 ------ 永久性存储介质,断电后也不会丢失数据
机械硬盘是一个外设,在冯诺依曼体系中承担输入输出设备的角色,也是唯一的机械设备(慢)
机械硬盘的物理结构
机械硬盘包括磁盘盘片、磁头、伺服系统(硬件电路)、音圈马达.....
3D NAND技术发展迅速,SSD取代HDD硬盘明年可待-CFM闪存市场 (chinaflashmarket.com)
机械硬盘运转的基本过程
通电后,盘片在主轴马达的带动下进行快速旋转.
盘片转的同时,磁头进行左右摇摆(线性马达控制)来寻址
有一摞盘片,每一个面都有一个磁头
盘面上存储数据,计算机只认识二进制,本质存二进制
内存可以用有电和没电区分0/1,磁铁是两态的 -> 有南极和北极
有无数的小磁铁颗粒形成盘面.
向磁盘写入本质就是:改变磁盘上的南极北极
磁头来回在读写磁盘时,磁头上是一些电子信号,可以在磁盘的特定位置,产生放电行为,改变磁盘某些区域的正负性,从而向磁盘写入二进制。
笔记本最好不要开机状态来回搬运,因为磁盘盘片在高速旋转,磁头悬浮在盘片之上读取/写入,磁头上下震动,磕到盘片,容易丢失数据
机械硬盘存储规则
磁盘盘片有存储数据的规则,分门别类,方便快速找到某个区域
(1)盘面由一系列同心圆构成,数据并不是在磁盘的任意位置做写入的,
只能写在这些同心圆之上,这些同心圆叫磁道
(2)磁道一圈容量太大,可能只用磁道的一部分,所以以圆心为起点,将磁盘划分成多个块
一圈叫做磁道,一小块叫扇区
(3) 因为磁盘有一摞盘片,半径相同的多个磁道 -> 柱面(与磁道等价)
(4) 扇区是磁盘存储数据的基本单位!!!一般是512字节,所有的扇区都是512byte,它是通过密度来实现的,外部的密度低,内部的密度高
在物理上,怎么把数据写到扇区里?
先确定在哪一个磁道 ------ 也就是确定同心圆
然后确定在哪一个磁头 ------ 也就是确定要写到哪一个盘面
最后就能确定在哪一个扇区
CHS寻址:
makefile
C:Cylinder同心圆
H:Head磁头
S:Sector扇区
有了CHS,就能找到任意一个扇区,那么所有的扇区都能找到了
机械硬盘的逻辑结构
磁盘可以抽象成一个数组
磁盘访问的基本单位是扇区(512byte),每一个元素大小是512byte
要访问某一个扇区,只要知道该扇区对应的数组下标,在os看来就是在访问数组的一个元素 ------ LBA (Logical bload address)
例如:
os访问磁盘上的某个扇区,用的是LBA。要通过LBA转换成CHS寻址,才能访问具体的扇区
抽象过程
将数据存储到磁盘 -> 将数据存储到该数组
找到磁盘特定扇区的位置 -> 找到数组特定的位置
对磁盘的管理 -> 对数组的管理
空间太大,不好管理 ------ 分区
对磁盘的管理->对一个小分区的管理
但是一个分区仍然很大,继续对分区做拆分!!!
例如:将c盘拆分成多个块组
boot block
每个分区前面都有一个boot block ------ 启动块,用于开机时,加载操作系统
计算机开机时,开机程序、对应开机的一些属性信息,是在每个分区的开始,都存在一份
只要一份就够了,但每个分区都有,是为了备份
有时候计算机启动不了,windows会蓝屏并说正在修复/恢复,此时可能是某个分区的boot block出问题,要把其他分区的boot block写到这个分区的boot block
Superblock
提供了整个分区的概览,是整个分区的属性集。例如:在当前的分区,有多少个块组,哪些块组满了,哪些块组没满,总共有多少个数据块可以被写,有哪些数据块已经被占
因为在物理结构上,磁盘的磁头,可能会和盘片有碰撞,导致物理上数据出现丢失
盘片被刮花,此时分区的使用情况可能会乱。所以要给整个分区的属性集做备份,块组里通常都会备份Superblock
Data blocks
Data blocks是多个4KB大小的集合
虽然磁盘的基本单位是扇区512byte,但是linux的文件系统模块和磁盘进行IO的基本单位是4KB!!!也就是8*512byte
哪怕向磁盘写1byte的数据,读取1byte数据,也必须一次写入/读取4KB大小的数据
所以4KB一般也称为block大小,磁盘也被叫做块设备
inode Table
文件 = 内容 + 属性
linux在存储时,将内容和属性是分开存储的.
Data blocks里保存的是文件的内容,而inode就是特定文件的属性
inode是一个大小为128byte的空间,保存对应文件的属性
每一个inode里,都要有一个inode编号
一般而言,一个文件,一个inode,一个inode编号
inode Table:该块组内,所有文件的inode空间的集合,就是多个128byte空间的集合
将来创建一个文件,首先在inode table里,申请128byte的大小,把文件属性写进去,
内容在DataBlocks里申请n个数据块,把内容写进去
Block Bitmap
假设有1万个DataBlock,怎么知道哪些dataBlock已经被占用了,哪些没有被占用呢?
Block Bitmap是标识Data blocks使用情况的位图
假设data blocks里有10000+个block,那么block Bitmap就有10000+个比特位,和特定的blocks一一对应
比特位为1代表block被占用;比特位为0代表没有被占用.
inode Bitmap
inode Bitmap是标识inode Table使用情况的位图
假设inde Table有10000+个inode节点,inode Bitmap就有10000+个比特位,比特位和特定的inode一一对应
其中比特位为1,代表该inode被占用,否则代表可用.
Group Descriptor Table
代表这个块组的使用情况/属性信息
例如:这个块组多大,有多少个inode,已经占用了多少个,还剩多少,还有多少个block等信息
通过inode找到文件内容
一个文件有多个block
(1) 哪些block属于同一个文件? 分割分分割 (2) 只要通过文件对应的inode编号,就能找到文件的inode属性集合
可是,文件的内容如何找到?
arduino
struct node{
//文件大小、inode编号等
int blocks[15];//和他同一个块组的块的编号
}
如果这个文件特别大,要使用非常多的块
data blocks中,不是所有的数据块,只能存文件数据,也能存其他块的块号
例如:block[12] block[13] block[14],存的是其他块号
用内容块去索引文件的块号
格式化
将磁盘的某个分区分隔成上面的内容,并且写入相关的管理数据,整个分区就被写入了文件系统信息,就是格式化.
目录的dataBlocks
目录也是文件,它的dataBlocks里存的是 文件名和inode编号的映射关系
在目录里创建文件/删除文件必须要有w权限
因为目录也是文件,数据块放的是文件名和inode编号的映射关系
必须要有写权限,才能把文件名和inode的映射关系写入或删除
显示目录里的文件名和属性要有r权限
文件名只能从目录的datablock里读取,
同时要拿到文件更多属性,要用inode编号找到inode,才能把文件的属性显示出来
创建文件,系统做了什么?
touch test.txt
文件大小是空的
1 根据文件系统,找到文件目录所在的分区,确定保存新文件的块组
2 遍历inode bitmap,找到第一个为0的比特位,把0置1,同时也拿到对应下标,分配inode编号
3 在inode Table中,根据之前遍历位图的下标,把文件的属性写进去,例如:inode编号、文件权限等,另外因为是空文件,把inode里的block[15]全部清0,
4 返回inode编号,再将用户的文件名和系统返回的inode编号建立映射关系,写到目录的数据块中
删除文件,系统做了什么?
1 找到目录对应的Data blocks,以文件名作为key值,找到inode编号
2 找到文件的inode,将inode bitmap特定的比特位,由1置0
3 将该文件对应的内容块在blockBitmap中由1置0
4 在目录中将文件名和inode编号的映射关系去掉
查看文件内容,系统做了什么?
1 找到目录对应的data Blocks,根据文件名找到要查看文件的inode编号
2 通过inode编号找到文件inode
3 在文件inode的映射数组int block[]里,找到文件内容的数据块所在下标位置
4 将文件的data blocks里的数据块返回给用户
磁盘还有空间,创建文件失败
inode固定,datablock是固定的
1 inode没了,数据块还有
2 数据块没了,inode还有
软硬链接
软链接
类似于windows的快捷方式,有独立的inode
ln -s 目标文件 软链接文件
软链接的文件内容,是指向文件对应的路径
硬链接
和目标文件共用inode
ln 目标文件 硬链接文件
创建硬链接,就不是真正的创建新文件
本质是在指定目录下,建立了文件名 和 指定inode的映射关系 ------ 仅此而已
文件的属性里有一个硬链接数,一个inode编号可能和多个文件名相关联,只有把这些文件名全部删除,才能把对应的inode删除
这个硬链接数,是一个引用计数,标识该inode与多少个文件关联
当我们删除文件时,不是将文件的inode直接删除,而是先将引用计数--
当引用计数= 0时,该文件才真正删除
创建一个普通文件,引用计数是1
创建一个目录,引用计数是2
我们进入到dir目录中,隐藏文件 . 就是dir的硬链接