文件系统
- 前言
- 1. 回顾关于C文件部分函数
- 2. 一些文件知识的共识
- 3. 相对路径
- 4. fwrite中的'\0'
- 一、文件描述符fd
- 1. 概念
- 2. 系统调用
- ① open 和 close
- ② write
- ③ read 和 lseek
- 3. 缺省打开的fd
- 二、重定向
- 1. 原理
- 2. 系统调用dup2
- 3. stdout和stderr的区别
- 4. 进程替换和原来进程文件
- 三、一切皆文件
- 四、文件缓冲区
- 1. 认识缓冲区
- 2. 缓冲区刷新方式
- 3. 小结
- 五、文件系统
- 1. 硬件
- ①磁盘
- ②存储构成和CHS寻址方式
- ③磁盘——逻辑结构
- ④磁盘寄存器
- 2. 文件系统 —— ext2
- 五、软硬连接
- 1. 认识软硬链接
- 软链接
- 硬链接
- 2. 实际应用
- 六、打开的文件和文件系统的文件关联
前言
1. 回顾关于C文件部分函数
C语言中关于文件的博客:函数和C文件操作和部分函数 下面在复习两个函数
写文件 (fwrite) 和读 (fread) 文件:(被注释的是写文件的方法)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//FILE *fp = fopen("myfile", "w"); 写文件
FILE *fp = fopen("myfile", "r"); //读文件
if(fp == NULL)
{
perror("fopen");
exit(EXIT_FAILURE);
}
char buf[1024]; //把内容读到这个数组中
const char *msg = "hello Linux!\n";
while(1)
{
size_t s = fread(buf, 1, strlen(msg), fp); //读取内容的大小是 第二个参数 * 第三个参数
if(s > 0)
{
buf[s] = 0;
printf("%s", buf);
}
if(feof(fp))
break;
}
//写文件
//const char *msg = "hello Linux!\n";
//int conut = 5;
//while(conut--)
//{
// fwrite(msg, strlen(msg), 1, fp); //fread和fwrite的返回值,是实际写的块个数,如果完整的写完了,就返回的是第三个参数值
//}
fclose(fp);
return 0;
}
输出到显示器(fprintf、fwrite和printf):(其中参数是stdout或者stderr)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
printf("pid: %d\n", getpid());
const char *msg = "hello Linux!\n";
fwrite(msg, strlen(msg), 1, stdout);
fprintf(stdout, "%s fprintf:stdout\n", msg);
fprintf(stderr, "%s fprintf:stderr\n", msg);
return 0;
}
运行结果:
stdin && stdout && stderr:
C语言默认打开的三个输入输出流,上面的实验也证明了,stdout和stderr都可以向屏幕输出。stdin是用来进行输入。
并且类型都是FILE*
stdin ------- 标准输入 —— 键盘
stdout ------- 标准输出 —— 屏幕
stderr ------- 标准错误 —— 屏幕
所以可以直接使用标准输入输出函数
2. 一些文件知识的共识
- 文件 = 文件内容 + 文件属性
对文件的操作 = 对文件内容的操作 + 对文件属性的操作- 文件:分为打开的文件与未被打开的文件 —— (下面会根据这两种分类进行详细介绍)
- 一个进程可能会打开多个文件,多个进程可能也都会用到同一个文件,而且OS中有很多进程。所以就需要进行管理。
- 文件也必须先描述再组织。(下面详细介绍)
根据上面所述:所以需要对文件进行管理,因此引出文件系统。
3. 相对路径
问题:fopen用相对路径打开文件是如何找到路径
#include <stdio.h>
int main()
{
FILE *fp = fopen("myfile", "w"); //以写的方式打开文件
while(1);
fclose(fp);
return 0;
}
查看上述代码执行的进程信息:
进程的文件创建路径就是与cwd有关。接下来进行测试
接口:chdir更改当前工作目录
头文件:#include <unistd.h>
函数声明:int chdir(char *path);
参数:所要更改的目录,相对、绝对都可以
返回值:成功返回0。失败返回-1,并且错误码被设置
更改后的代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
chdir("/home/kpl_2023/linux/basisIO");
FILE *fp = fopen("myfile", "w"); //以写的方式打开文件
while(1);
fclose(fp);
return 0;
}
小结: 使用相对路径创建文件受cwd影响
4. fwrite中的’\0’
//fwrite '\0'
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, fp); //测试的目的在这里
fclose(fp);
return 0;
}
strlen加不加1的运行结果:
小结:
- 字符串以\0结尾只是语言方面设置的标记
- 对于OS和一些文本编辑器而言,这个标记位就是多余的
一、文件描述符fd
1. 概念
文件操作符本质就是数组的下标。这个数组就是文件描述符表。通过这个数组下标就可以实现对该文件的控制
文件如何被先描述再组织呢?
答:在进程中PCB中有个结构体files_struct
指针,指向进程打开的文件描述符表。文件描述符表是一个数组,每个下标对应一个文件描述符fd(fd中的内容就是一个结构体指针),而所指向的结构体,包含了位置,基本属性,权限,大小,文件打开模式、文件的内核缓冲区,struct file *next
这种指针(每个文件描述符结构体是用链表链起来的),引用计数等
结构图:
2. 系统调用
在前文也说过系统调用和库函数之间的关系:
- 上面介绍的一些关于文件的函数,例如
fopen fclose fread fwrite
等,都是C标准库中的函数 —— 库函数(libc)open close read write
都属于系统提供的接口 —— 系统调用
在说初始进程的操作系统部分时,提到了这张图。通过观察下面这张图,所以f#系列的函数底层对系统调用一定进行了封装,为了便于开发
① open 和 close
open:
头文件:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
函数声明:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
函数参数:
1. pathname:文件路径
2. flags:文件打开方式(以下几种常见方式)
(1)、O_CREAT:文件没有就创建
(2)、O_TRUNC:文件打开就清空
(3)、O_APPEND:文件以追加的形式打开
(4)、O_WRONLY:文件以只写的方式打开
(5)、O_RDONLY:文件以只读的方式打开
(6)、O_RDWR:文件以读和写的方式打开
注:多种方式可以用 | 相连
3. mode:文件打开的权限,一般文件设置666,目录设置777
返回值:
1. 打开成功,返回fd(所打开的文件描述符)
2. 打开失败,返回-1,并设置错误码
close:
头文件:
#include <unistd.h>
函数声明:
int close(int fd);
参数:
fd:文件描述符
返回值:
1. 关闭成功,返回0
2. 关闭失败,返回-1,并设置错误码
umask: 权限掩码
头文件:
#include <sys/types.h>
#include <sys/stat.h>
函数声明:
mode_t umask(mode_t mask);
参数:
mask:八进制位掩码值
返回值:
总是成功,返回上一个掩码值
简单使用:
//open
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
umask(2); //掩码修改
int fd = open("myfile.txt", O_WRONLY | O_CREAT, 0666); //以写的方式打开文件,如果文件不存在创建文件
if(fd < 0)
{
perror("open"); //如果打卡失败,打印错误
exit(1);
}
close(fd);
return 0;
}
② write
头文件:
#include <unistd.h>
函数声明:
ssize_t write(int fd, const void *buf, size_t count);
参数:
1. fd:文件描述符
2. buf:要写入文件内容的指针
3. count:写入文件内容的大小
返回值:
1. 写入成功,返回写入文件的字节数,0表示什么也没写
2. 写入失败,返回-1,错误码被设置
简单使用:
//write
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main()
{
umask(2); //掩码修改
int fd = open("myfile", O_TRUNC | O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open"); //打印错误
exit(-1);
}
const char *msg = "hello write!\n";
write(fd, msg, strlen(msg)); //msg:缓冲区首地址。 strlen(msg):本次读取期望写入多少个字节的数据。 返回值:实际写了多少字节的数据
close(fd);
return 0;
}
③ read 和 lseek
read:
头文件:
#include <unistd.h>
函数声明:
ssize_t read(int fd, void *buf, size_t count);
参数:
1. fd:文件描述符
2. buf:存放读取内容空间的指针
3. count:读取的字节数
返回值:
1. 成功,返回读入的字节数,0意味着读到了文件尾
2. 失败,返回-1,并设置合适的错误码
lseek:
头文件:
#include <unistd.h>
#include <sys/types.h>
函数声明:
off_t lseek(int fd, off_t offset, int whence);
参数:
1. fd:文件描述符
2. offset:移动相对于whence偏移量offset的位置
3. whence:固定位置。(三个可选的位置:SEEK_SET(文件开头), SEEK_CUR(文件当前位置), SEEK_END(文件末尾位置))
几种常用:
1. lseek(fd, 0, SEEK_SET); //移动文件指针到开始
2. lseek(fd, 0, SEEK_CUR); //移动文件指针到当前位置
3. lseek(fd, 0, SEEK_END); //移动文件指针到结束位置
返回值:
1. 成功:返回距离文件开头的字节数
2. 失败:返回-1,错误码被设置
简单使用:
//read
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MAX_SIZE 1024
int main()
{
int fd = open("myfile", O_CREAT | O_TRUNC | O_RDWR, 0666);
if(fd < 0)
{
perror("open");
return -1;
}
//向文件写入
const char *msg = "hello write\n";
write(fd, msg, strlen(msg));
//这里需要使用系统调用lseek。因为经过上面的写入操作,文件指针指向了结束位置。
//如果直接读取会导致什么都读不到,所以要使用系统调用lseek把文件指针移到开始位置
lseek(fd, 0, SEEK_SET); //移动文件指针到开始的位置
char buf[MAX_SIZE] = {0};
read(fd, buf, strlen(msg));
printf("read: %s\n", buf);
close(fd);
return 0;
}
运行结果:
因为msg字符串中有\n,然后我们printf的时候又加了\n所以会换行两次
3. 缺省打开的fd
在上文提到C语言默认打开三个输入输出流,但是这并不是C语言的特性,而是操作系统的特性,进程会默认缺省打开三个文件描述符(三个流)。分别是标准输入(0)、标准输出(1)、标准错误(2)。对应的物理设备是(一般是这样):键盘,显示器,显示器。
三个流:
typedef struct _IO_FILE FILE;
extern struct _IO_FILE *stdin;
extern struct _IO_FILE *stdout;
extern struct _IO_FILE *stderr;
这三个流在底层封装了文件描述符
#include <stdio.h>
int main()
{
printf("stdin->fd : %d\n", stdin->_fileno);
printf("stdout->fd : %d\n", stdout->_fileno);
printf("stderr->fd : %d\n", stderr->_fileno);
return 0;
}
运行结果:
系统调用write和read的第一个参数就是文件描述符。
验证:
从文件描述符0(也就是stdin)读入数据,再向文件描述符1(stdout)和2(stderr)中写入从0读入的数据
预期结果:从键盘读入一段数据,在显示器中打印两端读入的数据
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define MAX_SIZE 1024
int main()
{
char buf[MAX_SIZE] = {0};
read(0, buf, MAX_SIZE - 1);
//在读取数据时,通常会将读取的数据存储到一个缓冲区中。在这段代码中,定义了一个大小为MAX_SIZE的缓冲区buf,
//为了确保在读取数据时不会发生缓冲区溢出,需要在读取数据时保留一个字节用于存储字符串结束符’\0’。
//因此,在读取数据时使用MAX_SIZE-1,以确保在读取数据后能够在缓冲区末尾添加’\0’
write(1, buf, strlen(buf));
write(2, buf, strlen(buf));
return 0;
}
运行结果:符合预期,输入一次hello IO 之后打印出来两次
结论: stdin、stdout、stderr三个流中对应的文件描述符分别是0、1、2
二、重定向
1. 原理
先来看一段代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
close(1); //关闭文件描述符1
int fd = open("myfile", O_WRONLY | O_CREAT, 0666);
if(fd < 0)
{
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
运行结果:
前文提到OS会默认打开三个流,0、1、2。这里我们把文件描述符1关闭了,所以本来应该输出到显示器的内容,输出到myfile文件中。—— 这就是输出重定向
底层:
stdout只是上层的概念表示1号文件描述符,底层文件描述符里的内容可能会发生变化。
所以打开文件本质就是给它在文件描述符表中遍历找一个空位置,并将所打开的文件指针放入,而该位置的下标就是所打开的文件描述符。
2. 系统调用dup2
头文件:
#include <unistd.h>
函数声明:
int dup2(int oldfd, int newfd);
参数:
1. oldfd:当前使用的文件描述符
2. newfd:调用接口成功使用的文件描述符
返回值:
1. 成功,返回newfd
2. 失败,返回-1,错误码被设置
简单使用:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("myfile", O_TRUNC | O_CREAT | O_RDWR, 0666);
dup2(fd, 1); //将fd中所指向文件的指针覆盖到1号文件描述符中
printf("hello Linux\n");
close(fd);
return 0;
}
实验结果:
如果文件指针在多个文件描述符中,控制引用计数即可。如果觉得重定向后原来的文件描述符多余,可以关闭
3. stdout和stderr的区别
先看一段代码:
#include <unistd.h>
#include <string.h>
int main()
{
const char *msg = "hello Linux\n";
write(1, msg, strlen(msg)); //stdout
write(2, msg, strlen(msg)); //stderr
return 0;
}
运行结果:
通过前面的学习,可以理解1和2两个文件描述符的是向显示器输出的。
拓展:
将执行的结果重定向到文件myfile中:
发现一半显示在屏幕上,另一半重定向到了文件当中
原因:当使用 > 符号将输出重定向到文件时,只有标准输出流(stdout)的内容会被重定向到指定的文件中,而标准错误流(stderr)的内容仍然会显示在终端上。这样做为了让用户能够及时看到程序的错误信息,而不会导致混淆
当然stdout和stderr两个流也可以重定向同一个文件中。两种方法
./test 1 > myfile 2>> myfile
注:>>要与前面挨着,哪怕换成>也是要与前面挨着。 这里使用了>>(重加重定向)因为再使用输出重定向会清空前一个重定向的内容
./test 1 > myfile 2>& 1
注意>&要与前面挨着不能有空格
-./test 1 > myfile
执行后,已经完成重定向动作,1中的内容已经是myfile文件的指针了。使用2>& 1
这个就是把1的内容写到2里面
4. 进程替换和原来进程文件
进程替换,不会对进程的文件进行替换。
进程替换:替换页表中虚拟地址和物理地址的联系并且在内存中加载相应进程的内容,是与mm_struct对象有关。而文件是另一部分files_struct相关。
三、一切皆文件
显然在这里就可以体现出了,继承和多态的思想
所以我们在调用系统调用read这样的接口时候。表面可能只是传了fd之类的参数。但是实际调用是由进程来实现的。(进程是我们对计算机操作的主要手段)
ssize_t read(int fd,...)
{
task_struct -> files -> fd_asrray[fd] -> f_ops -> (*write)()
然后根据初始化是write指针指向那个硬件的方法,就是那个方法
四、文件缓冲区
1. 认识缓冲区
先看两段段代码:
- 描述:只使用系统调用向1号文件描述符写入,然后关闭1号文件描述符
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *str = "hello write";
write(1, str, strlen(str));
close(1);
return 0;
}
运行结果:成功打印
- 描述:只使用C接口向1号文件描述符写入,然后关闭1号文件描述符
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *fstr = "hello fwrite";
printf("hello printf");
fprintf(stdout, "hello fprintf");
fwrite(fstr, strlen(fstr), 1, stdout);
close(1);
return 0;
}
运行结果:什么都没有输出
观察上面两个例子,发现了奇怪现象,第一段代码使用系统调用write结果正常,而第二段代码使用C式接口却什么都没有输出。
原因:
- 在进程控制讲exit和_exit时讲到了,C缓冲区不在内核空间,在用户空间。
- 在测试C接口和系统调用时,我都没有在字符串中加\n,代表我没有主动刷新缓冲区
- C语言的缓冲区不在内核中,在程序关闭前要刷新缓冲区时,但是前面把1号文件描述符关闭了,C语言的缓冲区刷不到内核中,所以看不到写入的结果,而使用系统调用write是直接写入到内核级的缓冲区,哪怕关闭了1号文件描述符也不会影响
注:
- C的写接口在底层必然封装了系统调用的写接口(eg:write)
- 目前我们认为只要数据刷新到内核了,数据就可以到硬件了
底层:
2. 缓冲区刷新方式
回顾一个刷新缓冲区的接口fflush:
头文件:
#include <stdio.h>
函数声明:
int fflush(FILE *stream);
简单使用:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("hello printf");
fflush(stdout);
close(1);
return 0;
}
运行结果:
缓冲区刷新方式:
- 无缓冲 —— 直接刷新
- 行缓冲 —— 不刷新,直到碰到\n才刷新 —— 一般常用在显示器
- 全缓冲 —— 缓冲区满了,才刷新 —— 一般常用文件写入
注:进程退出的时候也会刷新,所以不局限上面情况
测试以下缓冲区大小,也就是全缓冲:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
FILE *fp = fopen("myfile", "w");
const char *msg = "hello Linux";
int cnt = 1;
while(cnt < 1000)
{
fprintf(fp, "%d %s", cnt, msg);
cnt++;
}
fclose(fp);
return 0;
}
测试结果可以自己测试一下,缓冲区的内容还是挺大的。
测试:刚刚说到写到普通文件中的内容都是全缓冲,我们用行缓冲测试
#include <stdio.h>
#include <string.h>
#include <unistd.h>
//需要三秒把内容都写到文件
int main()
{
FILE *fp = fopen("myfile", "w");
const char *msg = "hello Linux\n";
fprintf(fp, "%s", msg);
sleep(1);
fprintf(fp, "%s", msg);
sleep(1);
fprintf(fp, "%s", msg);
sleep(1);
return 0;
}
运行结果:前面几秒都没有显示内容,可见向文件写是全缓冲
通过上面的实验得出的结论,来进行接下来的测试:
描述:使用C接口和系统调用向1号文件描述符写入,并在最后fork()
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
//系统调用
write(1, str, strlen(str));
fork();
return 0;
}
运行结果:出现问题了,为什么直接运行和写入到文件中的结果不一致
解释:
- 向屏幕显示的行缓冲变成了向文件写入的全缓冲
- 创建了子进程,发生了写时拷贝。注:创建十个文件,那就有十个缓冲区。
- 缓冲区也是数据,在程序关闭时会自动刷新,而无论那个进程先刷新都会发生更改,所以就会出现拷贝,所以也会拷贝两份
3. 小结
用户级缓冲区存在的意义:
- 解决用户的效率问题。
- 配合格式化
C缓冲区在FILE结构体中,其中包含缓冲区字段和维护信息
C接口和系统调用的底层关系:
五、文件系统
以上的内容介绍,可以说都是围绕被打开的文件。那还有没有被打开的文件,这一部分主要理解文件在磁盘上如何存储的。
Linux的文件在磁盘中存储,文件的内容和属性是分开存储的
文件 = 文件内容 + 文件属性 -> 在磁盘上存储文件 = 存文件的内容 + 存文件的属性。
- 文件的内容是按数据块存储的
- 文件的属性存储在inode(是一个一般128字节的数据块)中
1. 硬件
①磁盘
是电脑中唯一的机械设备,也是一个外设
盘面:存储二进制信号。表面光滑实际不是
磁头:每个盘面都要有一个磁头,一一对应的关系。两者不接触
注:
- 主轴旋转是定位扇区的过程,磁头臂(摇头臂)来回摆动是定位柱面(磁道)的过程
- 在软件设计上,一定要有意识的将数据放在一起。因为磁盘作为一个机械设备,运动越少效率越高。
②存储构成和CHS寻址方式
存储构成:
磁道:就是一个盘面的任意同心圆
柱面:所有盘面上下相同位置的磁道构成的立体圆柱
扇区:磁盘被访问的最基本单元 —— 512字节 / 4KB。最外一层同心圆的扇区和最里面一层的同心圆扇区数量是一样的,外层的0、1序列稀疏,里面0、1序列稠密就可以。当然现在也可以让最外层的磁道划分更多的扇区,算法会发生变化
CHS寻址方式:如何找到数据或把数据存储到磁盘,首要解决的就是地址问题
- Header —— 先定位磁头(也就是确定盘面)
- Cylinder —— 定位磁道
- Sector —— 定位扇区
③磁盘——逻辑结构
以前的英语听力使用磁带,我们都知道磁带延展开,那在逻辑上我们看它就是线性的。磁盘也是同样的道理。
抽象一个线性磁盘:—— 对于磁盘的建模
本质就是基于扇区的数组
LBA地址(逻辑扇区地址):任意一个扇区都有下标,属于该扇区的唯一标识。
只要有LBA地址,都可以通过计算得出CHS地址。
④磁盘寄存器
磁盘寄存器(端口/串口):用于快速接收地址
2. 文件系统 —— ext2
上面我们抽象一个线性磁盘。但是一般来说,磁盘容量很大,所以要进行分区管理。(eg:电脑上的C、D盘)。主要采取的就是分治思想
- 格式化:每个分区在被使用之前,都必须提前将部分文件系统的属性信息提前设置进对应的分区,方便后续分组等工作。
- Linux文件在磁盘中存储,是将属性和内容分开的,在这里得到证明
Data Blocks:存的就是文件内容
inode Table:存的就是文件属性,这个属性不包含文件名。- 在Linux中标识文件的方式是inode编号
- Data Blocks:一般而言每个块只存放自己的数据
上文说到inode存的是文件属性:inode和数据块
删除数据:
删除一个文件,只需要在对应的inode Bitmap和Block Bitmap把映射数据的块由1置0,把对应的inode也由1置0即可。
删除 == 允许被覆盖
认识:
文件名不属于inode内的属性。关于文件的增删查改都是通过inode进行的。上文也说到Linux中标识文件的方式是inode。
但是这里就有个问题,使用者操作的一直都是文件名,但是前面的认识又说标识文件的方式是inode。下面进行解释
目录:
目录也是文件,有自己的inode,同时也有自己的属性和内容。
- 目录中存放的内容:文件名和对应的inode的映射关系。有点像kv结构,所以在同一目录下不能创建同名文件
- 目录权限
- 目录下,没有w权限,无法创建文件
- 目录下,没有r权限,无法查看文件
- 目录下,没有x权限,无法进入目录
所谓的w权限,其实就是添加对应的文件名与inode的映射关系
所谓的r权限,就是查看文件名对应的映射
x权限,就是可以在进入目录前做一下判断,如果没有该权限,就限制更改环境变量
3. 文件可以通过命令获取其inode,但是目录的inode怎么获取,只有向上递归,一直到根目录。根目录的inode是确定的。同时这样无疑效率慢,所以也有dentry缓存,将经常访问的缓存起来
注:stat [文件名]
也可以查看文件的属性
五、软硬连接
1. 认识软硬链接
软链接
软链接:是一个独立的文件,具有独立的inode,它的数据块中保存的是指向文件的路径(相当于Windows快捷方式)。
注:至于数据块里没有保存指向文件的inode而是路径,是因为inode有一定的限制,而直接使用路径可以指向任意位置的文件,甚至跨文件系统。
使用:
ln -s [指定文件] [目标文件]
对普通文件建立和删除软链接:
- 对普通文件建立软链接
- 删除软链接 —— 两种方式
第一种:
第二种:unlink [目标链接]
- 如果该文件已有软链接,但是该文件被删除
对目录软链接:(这种操作,就介绍着玩,接下来这个就套娃了)
软链接是独立的文件:
硬链接
不是独立的文件,因为没有独立的inode。
本质就是在特定的目录的数据块新增文件名和指向文件的inode编号映射关系
每一个inode内部都有一个计数器,有多少文件名指向我
目录里面保存的是:文件名——inode编号的映射关系
文件名1——inode 1111
文件名2——inode 1111
- 所以:(建立普通文件硬链接)
- 删除操作
rm和unlink都行,上面说软链接时已经演示注:rm删除原指向的文件,硬链接依旧可以使用
建立目录硬链接(不可行)
不可行原因:
当使用find命令找文件时,此时有个路径下有个根目录的硬链接,此时查找时,就会陷入套娃。
注:.和…存在的原因是因为Linux系统在底层做了一定的工作,所以不会被影响
对硬链接或者文件其中任意一方修改,都会影响另一方
2. 实际应用
软链接:可以用来创建快捷方式,方便用户访问文件。跨越不同的文件系统,可以指向任意位置的文件。指向目录,可以创建循环链接。
硬链接:通常用来进行路径定位,可以进行目录间切换。不能跨越不同的文件系统。用来节省存储空间,因为多个文件名指向同一个数据块。
六、打开的文件和文件系统的文件关联
认识1:
页框和页帧:
物理内存的页框和磁盘的页帧都是用来管理内存的单位
- 物理内存的页框和磁盘上的Data block块(也就是页帧)是对应的关系。当操作系统需要将物理内存中的数据换出到磁盘上时,会将数据存储到磁盘上的Data block块中。当需要将数据从磁盘换入到物理内存时,操作系统会将磁盘上的Data block块读取到物理内存的页框中,实现数据的交换和管理。
物理内存的页框和磁盘上的Data block块是一一对应的关系,用来实现虚拟内存的管理和数据交换。
在在物理内存和磁盘上划分4KB的理由:
- 硬件:减少IO次数 —— 减少访问外设的次数
- 软件:基于局部性原理,预加载机制。
当然也不必担心浪费或者说要很大的空间需要频繁IO,因为OS中还有slab分派器和伙伴系统算法
认识2: OS如何管理内存?
真理:先描述再组织
- 先描述:一般描述物理内存,肯定要有地址,状态,大小,引用计数等。因此这里使用一个结构体管理:
struct page{ //page页必要的属性信息 }
- 再组织,在Linux系统中有个数组
struct page array[SIZE]
每个page管理4KB的内存,所有的page结构体由这个数组组织在一起,这个数组所占内存并不大,因为page结构体中采用联合体的形式。所以对内存的管理变成了对数组的管理,存储page数组的大小是固定的,所以可以通过地址计算出页号,同时反过来,通过页号也能计算出地址。
- 我们要访问内存,只需要找到这个对应的4KB的page,就能在系统中找到对应堆的物理页框。所以申请内存的操作,都是访问内存的page数组
认识3: 在Linux中,每一个进程打开的每一个文件都要有自己的inode属性和自己的文件页缓冲区。
图解:
- 上图的树是基数树,再画个图介绍原理,可以发现通过地址有序地对page对象进行排列
认识4:
- 加载文件系统
- 物理内存到磁盘