文件描述符/文件系统

文件介绍

✳️
1.文件 = 文件内容 ➕ 文件属性
(如果我今天在磁盘上创建了一个文件,文件里面什么都没写,那么这个文件在磁盘上要不要占空间?要!因为文件除了文件内容,他还有文件属性。包括文件名,文件的大小,文件的权限,文件的所属组,文件的创建时间)
文件属性也是数据,即便你创建空文件,也要占据磁盘空间

2.文件操作= 文件内容的操作 ➕ 文件属性的操作
(我们未来学习的文件操作,无外乎就是对文件的内容操作,要么对文件的属性操作,如fread,fwrite对文件做读写,chomd修改权限,mv重命名对属性做操作)
有可能,再操作文件的过程中,即便改变内容,有改变属性。

3.我们以前读取文件的时候要把文件怎么样?应该是先打开文件!那究竟是在干什么呢?
打开文件不是目的,访问文件才是目的,访问文件的时候,都是要通过fread,fwrite,fputs等代码来完成对文件的操作,一旦通过这些操作来对文件的内容做操作的话,代码执行可是CPU去执行,CPU去执行要去读取文件内容那么接下来打开文件的时候,你是不是一定首先要做到的是首先将文件的属性或内容加载到内存当中。因为CPU要根据fread/fwrite等来做读写,根据冯诺伊曼体系结构决定CPU只能从内存当中做读写!
将文件的属性和内容加载到内存中!—冯诺伊曼决定!
(我们曾经做过操作把一个文件的内容从小写改大写,我肯定是以读写方式打开,然后把文件内容读到内存里,我定一个buffer,把它读到buffer里,然后把所有内容改为大写,然后再把它写回到文件里。所以是不是先把文件内容读到内存里!你有,你把它读到进程缓冲区buffer中,这个现阶段就可以理解为读到内存当中。)

4.是不是所有的文件都会处于被打开的状态?没有被打开的文件,在哪里?
我们的win/Linux会存在成百上千的文件,是不是所有的文件都打开了?绝对不是!那没有被打开的文件在哪里呢?只是在磁盘上静静的存在着!

5.我们应该把文件区分为:(打开的文件)内存文件 和(没被打开的文件,静静的在磁盘上躺着)磁盘文件
(进程对应磁盘上的一个程序不是随时随地都被加载运行的,当被加载到内存里的时候,它在内存里面有一份,在磁盘上也有一份。那么压根没运行,就只是在磁盘上有一份)

我们接下来谈的文件描述符合都是打开的文件(内存文件)。
到下一个主题讲文件系统,软硬链接和inode的时候才谈磁盘文件。

6.通常我们打开文件,访问文件,关闭文件,是谁在进行相关操作?你要学习文件就得知道,操作的对象是谁,文件是什么?你也得知道是谁在操作文件,一定要搞清这两个角色。
是你在操作文件吗?我们很清楚我们想要访问文件,就要用到fopen,fclose,fread,fwrite这些接口…最后是不是你自己的代码?当你把你代码写完了,文件根本没有被访问,而只是你写了一个程序。是个程序没有被执行,那他就是个二进制可执行程序,我们并没有访问对应的文件。
当我们的文件程序,运行起来的时候,才会执行对应的代码,然后才是真正的对文件进行相关的操作。------是进程在对文件做操作!
现在理解原来文件操作在系统角度去理解,是我曾经写的代码变成了进程 ,然后进程被调度的时候,执行到了fread,fwrite等接口才完成对应的操作。当我们执行fopend等时候,就把文件打开了。说白了就是你肯定要把文件加载到内存,要不然我怎么访问呢?

7.学习文件的操作,就转化成了学习:进程 和 打开文件 的关系!

在这里插入图片描述--------------------------------------------------

复习文件的操作(C语言)

--------------------------------------------------
fopen()函数
#include <stdio.h>
FILE *fopen(const char *path, const char *mode);
--------------------------------------------------

✳️1.此时我们打开文件,没有带路径,默认这个文件会在哪里形成呢?当前路径?
什么是当前路径?
我们讲过shell内置命令chdir()函数会更改我们进程所在的路径,所以当前路径指的是什么呢?
所以我们今天一定要将当前路径的面纱揭开!
🌟当前路径:当前进程所处的工作路径!是工作路径!只不过默认情况下是当前程序自己所处的路径
🌟只要你在程序代码里面加chdir(你想到的路径work_path),那么工作路径就会改变成work_path.
🌟所以你不在你的代码不带路径,即不调用chdir()更改路径的函数,那就是默认的当前路径,不一定和你的源代码或可执行程序在同一路径下哟。

✳️2.有“r”方式,叫做只读;“w”方式,叫做只写;“rw”没有这个!;有”r+“和”w+“都叫做读写打开;“a”和“a+”是什么呢?,"a+"只不过是读写操作。
🌟“a”选项是appending,是一个写入操作,只不是写入到文件的结尾,appending相当于新增或者追加,不管怎么说它是写入,只不过写入的时候是写到了文件的结尾。如果文件不存在自己会创建文件并写入。
	a:追加写入,不断的往文件中新增内容 --➡️追加重定向

🌟如果以 “w”的方式打开:则是以文件的开始处做写入,也就是我写的时候是从最开始写;
它也能够如果你的文件不存在会给你创建文件并写入;
它首先做的就是如果文件存在会把文件先清空,清空之后才会从文件开始处写入。如果不存在就创建。

🌟如果以“r”的方式打开:读的话有很多接口fscanf,fread,fgets等接口;
	简单用一下fgets(),从特定的文件流当中获取数据放入到你提供的缓冲区,把读到的内容按行为单位读到你提供的缓冲区。返回值是:成功了返回起始地址,失败了返回NULL。
--------------------------------------------------
	char *fgets(char *s, int size, FILE *stream);
--------------------------------------------------
打样:
char buffer[64];
while(fgets(buffer, sizeof(buffer), fp) != NULL)----➡️从fp中读到放入buffer中
{
	printf("echo:%s",buffer);
}
	
✳️3.关注一下文件清空的问题。
当我们以w方式开文件,准备写入的时候,其实文件已经先被清空了

--------------------------------------------------

int main(int argc), char* argv[])-----➡️命令行参数
{

	if(argc != 2)
	{
		printf("Usage:%s filename",argv[0]);
		return 1;
	}
	/*FILE* fp = fopen("log.txt", "w");*///写入-----➡️w/r都会讲
	FILE* fp =fopen(argv[1], "r");
	if(fp == NULL)
	{
		perrot(“fopen”);----➡️perror会讲 
		return 1;
	}
	char buffer[64];
	while(fgets(buffer, sizeof(buffer), fp) != NULL)----➡️从fp中读到放入buffer中
	{ 
		printf("echo:%s",buffer);
	}
	/*printf("pid: %d",getpid());*/
	
	/*const char* msg = "hello 104"
	int cnt = 1;
	while(cnt < 20)//循环打印,也就相当于循环式向一个文件当中写入多条记录
	{
	fprintf(fp, "%s: %d\n", msg,cnt++)//向fp当中写入,输出我们的msg,为什么	用fprintf呢?因为是想多写一些消息*/
	/*其实换成fwrite也可以*/
	}
}
--------------------------------------------------
fprintf()函数
int fprintf(FILE *stream, const char *format, ...);
--------------------------------------------------

请添加图片描述

回归理论

✳️
a.当我们向文件写入的时候,最终是不是向磁盘写入?— 是。

b.磁盘是硬件吗?—是的。

c.只有谁有资格向硬件写入呢?–操作系统!

d.能绕开操作系统吗?—不能!因为操作系统是软硬件资源的管理者,你对人家硬件做任何的操作,你都不能绕开操作系统,若你绕开操作系统,操作系统就不清楚硬件,你能绕别人也就能绕,所有人都绕了,那操作系统清不清楚磁盘正在被谁访问?现在访问的有效空间有多少?已经被剩下的是哪些?那操作系统能知道吗?不能知道!不能知道让我还怎么去对硬件做管理。
也就决定了所以的上策访问文件的操作,都必须贯穿操作系统。

e.操作系统是如何被上层使用的?—必须使用操作系统提供的相关调用!

f.如何理解printf
显示器是硬件,我们printf最终打印到硬件上了,打印到硬件上是不是你调了程序打印到你的硬件上,可是是不是你程序显示到我们显示器上呢?并不是,显示器也是硬件,它不能绕过操作系统。你必须得通过接口来访问显示器
printf函数内部一定封装了系统调用接口

g.不管学什么语言,你用到了文件接口都是语言给你提供的,但是万变不离其中,所有语言提供的接口,你为什么没有见过系统调用呢?因为这所有的语言都对系统接口做了封装,所以看不到底层接口的差别。

h.为什么要封装?
C语言有一套文件操作叫做fopen,fclose,fread,fwrite;Java、C++等都有自己的文件操作,但实际上底层都是一样的,但为什么要进行封装呢?
1.原生系统接口使用成本比较高!
2.直接使用原生接口不具备跨平台性(你说所有语言不管C/C++等,可以不提供封装吗?我就用操作系统接口,不要扯调用系统接口难,可以!但是系统接口是由操作系统提供的,那么就带来一个问题,你如果使用原生接口,你只能在这一个平台上跑。不同平台上暴露出来的接口,包括细微差别完全不一样,操作系统是不同人去设计的。如果直接使用原生系统接口,一定会导致语言不具备靠平台性!)

i.封装是如何解决跨平台性的呢?
大家学过多态,在软件上呢,实际上我们可以让我们上层的接口,在访问的时候,可以根据具体对象访问实现不同的方式。比如fopen经过多态,将接口重新设计,就可以有win平台和Linux平台…
C语言穷举所有的底层接口 + 条件编译!C语言库是别人给你编好了的,win、Linux等C语言文件操作接口都给你实现一份,然后加上条件编译来识别你的平台,是别到是什么平台就把另外的平台裁掉。所以最后在封装基础上你在不同平台上都是该系统调用的接口。但在上层用户来看都是fopen,fread,fcloose等,这是目前C语言解决的方案

g.
我们现在用的C语言库提供的文件访问接口,但是还有系统调用,他们两个具有上下层关系。
我们可以肯定的是C语言一定会调用系统调用接口,只不是哪些接口我们还没有学。
这也是为什么我们必须学习文件级别的系统调用接口。( 这些接口更加贴近操作系统,因为它是系统调用,我们只要把它学习到了,理解语言很多层面上的接口就很简单了;另外这些系统文件接口学会了,往后再学习其他语言的时候,虽然学的时候是陌生的,但是你上层原理再怎么变,我下层的东西我心里很稳因为我知道你底层全都是用的系统接口。那么上层我就只需要关心你语言的特性就行了,这就是以不变应万变,也就是存在的意义。)
请添加图片描述

见一见猪跑

open()/close()/write()/read()

在C语言上打开文件是fopen,但在系统上打开文件是open();

----------------------------------------------------------------------
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);

参数解释:
第一个参数 pathname:可以理解为要打开的文件名,不要带路径的,和C语言上差不多。
第二个参数 flags:你打开文件名要传入的选项(小专题会阐释清楚)O_RDONLY, O_WRONLY, or O_RDWR,O_APPEND,O_CREAT、O_TRUNC(文件清空选项)
第三个参数 mode:C语言当中Linux下叫文件不存在,文件不存在怎么办呢?我就直接创建它嘛,创建它怎么办呢?创建它是不是得有权限,我们有没有给我们的文件设置权限呢?所以这个参数是关于权限的。

返回值: 
返回值非常特殊,不再是我们用的FILE*了,这里变成了int,它呢代表是一个打开一个文件,创建一个file descriptor(文件描述符)如果成功了就会将文件描述符返回,-1表示失败。



close()函数
#include <unistd.h>
int close(int fd);
就是你把你要关闭的文件的文件描述符交给我,我就能帮你关闭。

write()函数
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数解释:
第一个参数 fd:向特定的文件描述符
第二个参数 buf:你要写入的时 候缓冲区的起始地址
第三个参数 count:你要写入缓冲区的大小

其实并没有C语言好用,因为它的参数是void*,而C语言的接口都是char*,没有C语言格式化写入很痛苦!

read()函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count); 
(ssize_t是有符号整数)

注意:
read的参数是void* 的,是与类型无关的参数,他不关心读上来的是二进制,但是我们得关心,我们想当作字符串就得预留位置给‘\0’!

介绍函数作用:
从特定的文件描述符当中,将数据从文件当中读到你所指定的缓冲区当中,count代表缓冲区的大小,一般是你期望读多少子节。


返回值:
返回的是你实际上读了多少子节


----------------------------------------------------------------------
✳️我们在学什么的时候看见纯大写加下划线的?没错就是宏!open需要的那些选项都是宏。
读方式、写方式、读写方式、添加、不存在就创建它
但是这个flag是一个整数标记为呀。我们在C语言是怎么去传标记位呢?我一次想传多个标记为呢?传5个标记在C语言上就带5个参数呗flag1、flag2.....那我要传20个呢?那我有时候还只要传3,4个呢不是浪费?有人说传可变参数,但调用和编写成本高。有人说数组,数组也可以但非常麻烦。
所以我们这传的标记为无非就是有/没有,你别忘了你一个整数是有32个比特位的!所以我们操作系统传标记为选项的时候,它有个非常明显的特征。
🌟系统传递标记为,使用位图结构来进行传递的。
每一个宏标记,一般只需要有一个比特位是1,并且和其他宏对应的值不能重叠。
我们来写一个代码。实现一下!(领悟一下,不一定和系统一样)
#include <stdio.h>

#define PRINT_A 0x1 //0000 0001
#define PRINT_B 0x2 //0000 0010
#define PRINT_C 0x4 //0000 0100
#define PRINT_D 0x8 //0000 1000
#define PRINT_DFL 0x0

void Show(int flags)//我想让Show根据你传入不同的选项(flag)从而打印出不同的语句
【
	/*按位与(&)都为1才会是1*/
    if(flags & PRINT_A) printf("hello A\n");
    if(flags & PRINT_B) printf("hello B\n");
    if(flags & PRINT_C) printf("hello C\n");
    if(flags & PRINT_D) printf("hello D\n");

    if(flags == PRINT_DFL) printf("hello Default\n");
}
int main()
{
    printf("PRINT_DFL:\n");
    Show(PRINT_DFL);
    printf("PRINT_A\n");
    Show(PRINT_A);
    printf("PRINT_B\n");
    Show(PRINT_B);
    printf("PRINT_A 和 PRINT_B\n");
    /*想要满足多个条见用 按位或(|)*/
    Show(PRINT_A | PRINT_B);-----➡️想打印A 和 B
    printf("PRINT_C 和 PRINT_D\n");
    Show(PRINT_C | PRINT_D);-----➡️想打印C 和 D
    printf("PRINT all:\n");
    Show(PRINT_A | PRINT_B | PRINT_C | PRINT_D);---同理

    return 0;
}
----------------------------------------------------------------------
umask(0);-----➡️按照我用户自己想要的设置权限来
int fd = open("log.txt", O_RDONLY);----//首先读一个文件,那他肯定是存在才去读对吧
/*int fd = open("log.txt",O_WRONLY | O_CREAT,0666);*/
/*int fd = open("log.txt",O_WRONLY | O_CREAT);*/
----------------------------------------------------------------------
✳️1.
//我们在命令行参数看到我们创建的log.txt权限是乱码,是什么原因:原因是你要新建一个文件的话,其中这个文件是要受到Linux权限的约束的,所以你新建的文件默认的权限是什么你得告诉Linux,所以我们要打卡一个我们曾经并不存在的一个文件
我们不能用两个参数的open,而是用三个参数的open。用mode代表你文件的权限。
那么我们就设置一下呗。

✳️2.
我们默认给log.txt的权限是666,但我们在命令行参数上看到的是664这又是为什么?
因为我的shell默认设置的权限掩码是0002,你要666不能给你,你必须得在你给的默认权限下再把我的权限掩码去掉。
那我们非得创建666怎么办?我们可以用到系统接口umask(),没看错,umask不仅仅是一个命令,它也是一个系统级接口。
那我们可以吧权限掩码设为0,设为0后就有点像就近原则,我的文件默认创建文件时,本来要听操作系统,umask()就说你别听操作系统给你瞎扯,你听我的,我离你更近,这是用户要求的你就别用操作系统的默认umask了,你就用我给你设置好的。
----------------------------------------------------------------------
umask()函数
mode_t umask(mode_t mask);
----------------------------------------------------------------------
if(fd < 0)
[
	perror("open"):
	return 1;
}
printf("fd: %d",fd);
int cnt = 0;
char buffer[128];
/*const char* str = "hello 104\n";*/
ssize_t s = read(fd, buffer, sizeof(buffer) - 1);//因为想整体作为一个字符串所以预留一个位置给'\0';
if(s > 0)
{
	buffer[s] = '\0';/*如果我们读了5个字符,那么buffer里面那几个字符的下标是01234所以5就是‘\0’的位置*/

//我有没有像行读取?没有。我有没有像fscanf一样做格式化读取?没有。系统接口有吗? 没有!这个功能只能由你C语言去完成。所以按行读等乱七八糟接口都是C语言做的处理!
	printf("%s",buffer);
}
/*while(cnt < 5)------➡️向文件写入5行信息
[
	write(fd, str,strlen(str));
	/*这里不需要strlen(str)不需要+1,\0是C语言的认识,但是我文件系统不认你的*/
}*/

close(fd);

return 0;

详细介绍一下文件系统基本接口

open更多选项-OTRUNC/OAPPEND

1.C语言在“w”方式打开文件的时候,文件是会被清空的!
(今天也说了文件当中,我们用的是系统接口,C语言写的底层调的就是open,但是我们刚刚做了实验用系统接口的open对已经写入的文件,继续写入的时候并没有清空,而是从头的开始位置吧以前内容覆盖,曾经的数据还有一部分保留
这是为什么呢?)
所以没有清空是因为我们打开文件还有一个选项就是O_TRUNC(清空选项)
O_TRUNC:如果文件存在或者打开是为了写入,那么它的作用是为了把这个文件进行清空的!所以默认的文件系统接口是不会清空的。
(所以我们在用C语言的时候仅仅是调了“w”的方式,你就以写的方式打开了,打开之后不存在你还创建它,甚至存在你还能清空,实际上它对应的底层调用的接口是open,且调用的选项就是那几个。)

2.C语言中我们还可以用“a”方式打开文件,追加功能!
而今天用文件系统接口想要追加的话,要在open的选项上加伤O_APPEND,则会以追加方式打开文件。
所以我们之前在C语言层面,只用"a"实现的功能,在系统底层用O_APPEND这一组选项来完成文件的打开

我们以前学习的文件呢,它的文件接口是C语言提供的,但系统级别的接口呢本来就是被文件接口做的封装,或者说是我们C语言文件接口的底层实现。我们在C语言文件接口"w","a"等就是系统接口上对应的一个个选项,所以系统接口当然麻烦!还是C语言用的舒服。

接下来谈谈fd返回值的问题(引入文件描述符)

我们open()函数的返回值fd,成功了会返回文件描述符(fd > 0),失败会返回-1(fd <0);

❓为什么是从3开始返回文件描述符?0 1 2去哪里了?
✳️0,1,2被默认打开了!
0:标准输入,键盘
1:标准输出,显示器
2:标准错误,显示器
我们曾经学过:stdin、stdout、stderr这三个东西是我们C语言提供的三个文件指针,对应的底层就是三个文件流
我们用的fd是谁的概念呢?是操作系统接口的概念。stdin、stdout、stderr对应的是C语言的。我们曾经说过C库函数 和 系统接口 (系统接口认的是你上面的文件描述符,C语言认的是你说的stdin、stdout、stderr)既然C标准库调的是系统接口,那么是什么关系呢?
那么我又要问同学:FILE*–>我们把它叫做文件指针–>FILE是什么呢?FILE是C库提供的结构体!那么内部一定封装了多个成员。
C标准库一定是调了系统接口,对文件操作而言,系统接口只认fd文件描述符。
既然你FILE是个结构体封装了多个成员,而且你C语言调printf、fwrite等底层你都是调的系统write等接口,所以你必须得是文件描述符,因为你必须得用操作系统 ,必须得通过贯穿操作系统来访问硬件。
所以你必须得,你用的是FILE结构体必定封装了fd
所以今天就告诉你了FILE是一个结构体,因为底层系统接口只认fd文件描述符,你C语言用的是FILE*,但我底层之人fd,所以必定封装了fd!

❓那么我应该如何证明呢?

// ✳️1.先验证0,1,2就是标准IO
 47	    //char buffer[1024];
 48	    //ssize_t s = read(0, buffer, sizeof(buffer)-1);---我们通过0号文件描述符就可以读!不是非得用你的stdin或者scanf等方式读!
 49	    //if(s > 0)
 50	    //{
 51	    //    buffer[s] = '\0';
 52	
 53	    //    printf("echo: %s", buffer);
 54	    //}
 55	    //const char *s = "hello write\n";
 56	    //write(1, s, strlen(s));------对显示器写入虽然可以用你的printf,但我们也可以用write,向1号文件描述符,所以照样能像显示器写入!
 57	    //write(2, s, strlen(s));------同理1号文件描述符

 58	    // ✳️2.验证012和stdin,stdout,stderr的对应关系,因为stdin等是指针所以就可以访问它的成员!
 			fileno就是FILE所封装的文件描述符。
 59	    //printf("stdin: %d\n", stdin->_fileno);--得到0
 60	    //printf("stdout: %d\n", stdout->_fileno);--得到1
 61	    //printf("stderr: %d\n", stderr->_fileno);--得到2

✳️我们回答了:函数接口的对应
fopen/fclose/fread/fwrite—>open/close/read/write
数据类型的对应
FILE*–>FILE
fd
✳️我们就想说一个结论:我们用的C语言接口它一定封装了系统调用接口!

✳️所以为什么从3号开始呢?因为0、1、2所对应的标准输入、标准输出、标准错误已经被默认打开了。所以只能从3给了。
请添加图片描述

❓0,1,2,3,4…你见过什么样的数据,是这个样子的?其他的数字为什么不行?(设计原理的介绍)
我们在数组下标见过!
我们用返回fd的函数都是系统接口,操作系统提供的返回值!(那是不是操作系统里面本身就有的。)

✳️我们目前学到的知识是:进程 和 内存文件的关系–>最终都是在内存里面维护的—>被打开的文件都是在内存里的
一个进程可不可以打开多个文件?当然可以,所以在内核中,进程 比 打开文件的比例是1:n的。所以系统在运行中,有可能会存在大量的被打开文件!
那么OS要不要对这些被打开的文件进行管理呢(这么多文件,你打开的是哪个文件?你不用了什么时候去释放它?在哪里给他开空间?这些都是操作系统要管的)?当然要管!
那么操作系统如何管理这些被打开的文件呢?—先描述再组织!

✳️所以一个文件被打开,在内核中,要创建该被打开的文件的内核数据结构–先描述!
struct file----(相当于如果我们在内核当中打开了对应文件,在内核当中系统会为文件创建struct file结构)
{
//包含了我想看懂的大部分内容 ➕ 属性
struct file* next;
struct file* prev;
}
所以你打开一个文件有就一个struct file,如果你打开10几个文件,就会有10几个struct file,并且该结构用链表的形式链接起来了!-----再组织!

✳️所以对被打开文件的管理,就被转化成了对链表的增删查改!

✳️系统当中被打开多少个文件不重要,所有被打开的文件在内核当中我都有对应的struct file,然后我们还有被打开的进程,进程的task_struct,哪些打开的文件和我的进程有关系呢?(我们前面说过进程和文件打开的比例是1:n)
那么进程如何和打开的文件建立映射关系呢?
所以在内核当中,此时task_struct内部里面包含了struct files_struct* fs结构体指针,其中files_struct* fs不就是多个文件的结构体嘛。
然后files_struct在内核当中,files_struct里面包含了一个数组
struct files_struct
{
struct file* fd_array[]-----➡️文件指针数组,里面全都是放的struct file*的指针!它不就是一个将来要被打开的一个文件吗?如果有所指向就能直接指向对应的文件结构。如果没有指向那么数组里面的指针就是为NULL。
那么我们就将进程和文件关系建立的映射关系就好了。
//…
}
所以一个进程将来想访问某一个文件,是不是只需要知道该文件在这张映射表当中的数组下标

✳️我们调的open/read/write/close要么是得到fd,要么是用fd。
那么此时我们在系统当中要打开一个文件怎么办呢?肯定是先给你创建一个struct file,在当前进程的文件描述符表里面分配一个没有被使用的下标,然后把struct file结构的地址传给数组对应的下标,然后把下标的位置(fd,如3号文件)返回给用户。
后面用户在通过调用read/write一定是传入了对应的fd,那么它是一个特定进程的,那么我只要找到特定进程的文件描述符表,然后根据传入的fd文件描述符,找到它在对应数组的下标位置,就可以对该文件做相关操作了!

✳️所以文件描述符为什么是int fd,本质是因为他是一个数组下标,系统当中使用指针数组的方式,来建立进程和文件对应的关系,将文件描述符返回给用户,上层用户就可以根据后续接口继续传入文件描述符,来索引我们对应的指针数组,找到对应的文件!

✳️所以我们学的所有文件操作,不管你是C/C++等最本质的就是在系统层面上全部都是文件描述符,没有例外!这就是进程 和 文件 的关系与语言没有半毛钱关系!这也是文件 和 进程的对应关系

❓有一个东西不对呀!我们之前谈的0、1、2–>stdin、stdout、stderr–>键盘、显示器、显示器,这些都是硬件呀!难道也是上面讲的struct file来标识对应的文件吗???
肯定的呀!因为从stdin对应的0,stdout对应的1.stderr对应的2就可以看出来。0、1、2正常被使用,那么肯定也是我们对应的struct file结构找到索引表并找到下标索引到文件对象。----如果想要解释清楚这个问题就不得不再谈谈Linux下一切皆文件的概念了!
请添加图片描述

如何理解Linux下一切皆文件!

❓有一个东西不对呀!我们之前谈的0、1、2–>stdin、stdout、stderr–>键盘、显示器、显示器,这些都是硬件呀!难道也是上面讲的struct file来标识对应的文件吗???
肯定的呀!因为从stdin对应的0,stdout对应的1.stderr对应的2就可以看出来。0、1、2正常被使用,那么肯定也是我们对应的struct file结构找到索引表并找到下标索引到文件对象。----如果想要解释清楚这个问题就不得不再谈谈Linux下一切皆文件的概念了!

一切皆文件这个话题应该不陌生,我们之前是有提过的。今天就要将它理解掉。

✳️如何使用C语言,实现面向对象(类)。C语言没有class不能将成员方法和成员变量放在一起。C语言只有struct
struct file
{
//对象的属性(C语言结构体里不能有函数方法不像C++,那如何实现对象呢?)
//函数指针
//void read(int fd,…)----❌这样是错误的
但若是void (read)(int fd,…)----✅函数指针是可以的
所以将来C语言实现类,成员方法要模拟的话就是函数指针,它也是一个类型,可以被定义的
}
如果我将来有void read(struct file
filep,int fd, …)//write等
{
//逻辑代码
}
将来就用指针指向所对应的函数
然后我将来用struct file定义一个对象:struct file f;
f.read(&f,…)
到时候就可像面向对象的方式来进行编写代码了

C++类里面成员函数是包含一个当前对象this指针的东西不就是上面模拟实现的struct file* filep嘛。既然可以做到,就可以知道C++根据C衍生出来的,也不是平白无故衍生的,是从大量工程实践得来的。

✳️我们在计算机里面有各种硬件,比如说键盘、显示器、磁盘、网卡等其他硬件,这些设备统一叫做-----外设----也就是IO设备。
以磁盘为例,磁盘有自己的一套读写方法:read_disk();write_disk()。你可以想想根据冯诺伊曼,磁盘是一个外设,要么从磁盘里读到内存里,要么叫把数据从内存写到磁盘里。方法有很多,但他一定有自己的一套方法。同理其他外设。
对于显示器来讲,我们可以向显示器文件当中写入,但我从来没有说从显示器里面读呀?没有读没关系!我把你的读函数设为NULL不就好了嘛!那么你不就相当于有读方法吗?那么同理键盘没有写方法。
根据我们的常识,不同的设备,对应的读写方法一定是不一样的。
操作系统说你要打开磁盘是吗?可以呀。再Linux当中我可以抽象出一种文件对象struct file,它里面包含两套东西:文件属性➕文件方法;
struct file
{
//文件属性
//文件方法
int (*readp)(…)
…等函数
}
操作系统说你现在不是要打开文件打开磁盘吗?你打开之后我给你在内核当中创建struct file然后用readp指针指向你底层read_disk()方法等等其他函数方法。
所以其他外设打开方法都是同样的流程。
struct file相当于OS内的内存文件系统,我们惊奇的发现在这层软件之上,在往上不就是file,在往上不就是进程来了,进程来了那么进程和文件的关系一堆逻辑不就来了吗?
所以个进程找到对应的文件file,然后struct file再往下找到一个硬件设备,便可以调用硬件的读写方法了!不就可以对你硬件进程操作嘛!
我们在struct file这一层看到的都是struct file文件对象,我们以统一的视角看到所有的设备,在往上一层看,我们就可以把下面看成一切皆文件!

✳️也就是说你未来想要打开一个文件,你打开吧,打开之后,你把你的各种读写方法,属性等给我,我在内核里给你这个硬件创建号struct file,创建struct file给你初始化的时候,吧函数指针指向你对应的函数方法。然而我在内核当中永远存在的是struct file,然后我操作系统把所有的struct file通过链表链接起来。
所以对操作系统站在用户角度来看,一个进程看待所有的文件都以统一的视角来看!
所以当我们访问一个file的时候,这个file具体指向底层哪个文件对应的设备,完全取决于它对应的底层读写方法指向哪个硬件提供的方法。
这不也是多态吗?上层使用同一个对象,指针指向不同的对象,就可以调用不同的方法,其实就是多态前身。我们学过多态,运行时多态,不就是用的虚标,虚函数指针嘛,那不就是我们的函数指针嘛!
所以在往更上面看,就到了进程task_struct 和 struct file,即进程 和 文件的关系,struct file指向的对应的硬件设备是谁完完全全取决于底层的硬件是怎么设计的。就通过操作系统通过一个软件层做了一个封装,达到了这样的效果(所有软件底层的差异,都可以通过添加一层软件层,来屏蔽掉底层软件的差异,这是一个非常重要的计算机学科的设计理念!)。具体硬件读写方法是驱动干的。对操作系统而言不管你是什么方法,反正你们都是外设,只要是外设你们就得提供读写方法,最终给你在我的内核里抽象成struct file就都是文件了,具体文件内的函数指针对应的是哪个设备,就找到你这个设备,即相当于调用你该设备的方法。此时就完成了一个叫一切皆文件。
我们把设计出struct file来标识一个一个文件的这种内存文件系统叫做VFS(Vitural FIle System虚拟文件系统)
请添加图片描述

✳️我们一开始是在回答文件描述符的值为什么是0、1、2、3…这种的;因为文件描述符的值在内核当中属于进程文件的对应关系,它是用数组来完成映射的,0、1、2、3、4…就是该数组的下标。我们这里用的0、1、2…是系统提供的返回值,然后我们的read、write等系统调用接口都必须得用0、1、2、3… 这些文件描述符来找到底层对应的struct file结构进而访问底层对应的读写方法,然后包括相关的属性,缓冲区等。
然后我们也说了C语言的FILE肯定是封装了fd在其结构体里面,所以最终我们也就回答了stdin、stdout、stderr和我们0、1、2是一一对应的关系。因为这三个系统默认给我们打开了,那么创建的文件描述符是从3开始!

✳️我们现在看到open立马就在脑海里这个open打开的文件在内核里立马给他创建struct file,初始化它内部的属性,还有函数指针指向对应的方法,然后呢是我这个进程打开的那么就要把我这个struct file文件的地址填到我这个进程对应的文件描述符表里面分配一个数组里面没有被占用的下标,然后你把文件描述符即下标你给我返回。

✳️所有软件底层的差异,都可以通过添加一层软件层,来屏蔽掉底层软件的差异,这是一个非常重要的计算机学科的设计理念!
第一个我们以前学习的进程地址空间(也就是虚拟地址),这是在进程和内存资源之间添加了一层软件层,最后带来的结果就是让所有进程都看到的是统一的进程地址空间。如果在今天学的一切皆文件,那么在那里是不是可以叫一切皆地址空间,我们进程访问所有资源,只要找你的地址空间就行了,这难道不就是一层软件虚拟吗。包括我们现在学习的struct file东西,以及未来将要学习的IP协议,其实是同样的。
我们在语言层学习到的有没有这个概念呢?是有的,比如说在设计的时候都会设计一个基类,接口类,这些不都是顶层的封装,用接口类的方式来屏蔽顶层不同的对象差别吗?多态本质不就是在顶层用同样一种对象,具体指针指向不同对象时可以调用不同的方法。多态和基类最大意义是在写接口上,我如果有了一套继承体系的话,未来我想把我的接口呈现给外部,其实所有接口的参数只要设计成父类的指针就可以了,说白了我想把我的类封装成一个方法让别人去调,不用面向对象的方式,就把我的基类作为函数的参数对外呈现一种方法,至于你外部传来的子类一、二…等我不管,我就能自动调用不同的方法,所以最大的好处就是统一的基类指针作为参数。
这不就是做了一层软件封装吗?底层各种子类都有差别化,但在基类都以同一种类来描述对象,以后设计接口时就只要把接口设计成基类,作为我们对象指针就好了!
继承之下本身就是把我们形成对象级别的软件层状结构,而软件层状结构解决的问题是用来进行所谓的…
我们写的子类是会有不同的,以前理解继承多态就是共性放在顶层,不同的子啊子类当中实现,但是从设计理念基类就是一层软件层。
所以很多语言都尊重这套体系,因为类当中既有方法又有属性,便于我们各种软件层状设计。它是历史发展的必然,每次说用指针的方法比较麻烦,能不能更容易点呢?就设计出来了面向对象的语言,天然就已经能够对属性和方法做封装,并不是为了封装而封装,它是封装之后方法和属性就有了内部的相关性后,模块与模块之间,对象与对象之间便与建立层状结构,所以在语言层面倒逼程序员在无意识情况下照样写出操作系统类似方法的代码。

探索应用特征

文件描述符的分配规则

❓为什么还有规则,不是我们说好了0、1、2先是默认占用了,然后从3开始给我们的吗,这还有什么分配规则?

来看看下面代码!

/*close(0)*/
close(1);
 11	    //根据fd的分配规则,新的fd值一定是1
 		//我们现在看到open立马就在脑海里这个open打开的文件在内核里立马给他创建struct file,初始化它内部的属性,还有函数指针指向对应的方法,然后呢是我这个进程打开的那么就要把我这个struct file文件的地址填到我这个进程对应的文件描述符表里面分配一个数组里面没有被占用的下标,然后你把文件描述符即下标你给我返回
 12	    int fd  = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
 13	    if(fd < 0)
 14	    {
 15	        perror("open");
 16	        return 1;
 17	    }
 18	
 19	    // printf->stdout->1->虽然不在指向对应的显示器了,但是已经指向了log.txt的底层struct file对象!
 20	    printf("fd: %d\n", fd);
 21	
 22	    //为甚必须要fflush? 这个就和我们历史的缓冲区有关了
 23	    fflush(stdout);
 24	
 25	    close(fd);
 26	

✳️所以我们现在有一个0、1、2默认被打开,你新打开一个文件默认给你分的,就是3,如果你把0关掉就是给你0,把1关掉就是给你1等等以此类推(但是你把1关掉就是关掉你进程显示器,而我们用printf打印是向我们显示器去打印的,所以你在命令行不就没有显示了)
所以分配规则:从头遍历数组,找到一个最小的,没有被使用的下标,分配给新的文件!返回给用户

✳️我们close(1)按照分配规则,被新分配的文件描述符肯定是1,这没问题。我们printf/cout虽然往1号显示器里面打,虽然你1被关了,但是又被我们log.txt打开了,相当于我们1不再指向显示器了,但是已经指向了log.txt的底层struct file对象,那么我们所谓的结果没有被打印出来,是不是应该在我们的log.txt里面!可是我们看了,log.txt里面并没有!这个问题只能后面说了!(因为没有被fflush(stdout)刷新,这就又有一个问题了为什么要fflush()一下??这个和我们历史上缓冲区有关系了!)
不对呀我们,我们自己代码里面调用的不就是printf吗?printf本来应该往显示器里面打印,但是现在不往显示器里面打印了,直接写到了文件里面,同学们这叫什么?----➡️重定向!(所以也就看到了重定向和缓冲区的身影)
请添加图片描述

重定向

重定向原理
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
	perrot("open");
	return 1;
}
fprintf(stdout, "打开文件成功,fd: %d\n",fd);//--将后面一串数据显示到stdout里面

✳️我们将1号文件描述符提前关掉,发现显示器上面什么都没有,因为我把1关了呀,把1关了按照我们的猜测,当我们紧接着再打开一个文件,分配的文件描述符一定是1(由我们上面说的分配原则得知)。我们把1关掉了,现在新的文件的文件描述符一定是1。你现在fprintf不往显示器上打我理解了,但你肯定要往新建的文件里面打呀!因为我们1号下标指向的是显示器文件,现在1号下标指向的是新建的文件log.txt。可是我们赶忙去看log.txt,但是也没有呀!上面也讲过了如果加上fflush(stdout)刷新一下;log,txt就有内容了!那么为什么要刷新一下呢?现在没办法解释,要先将缓冲区讲了之后才能理解!(后面解释)

✳️我们要关注的点已经出来了,log.txt确实被分配的文件描述符是1,也承认你fprintf确实是要往标准输出显示器上打印,但本来应该要往显示器打印,缺最终向指定文件打印。
我们本来是要将数据写到显示器文件上面的,显示器也是文件,所以向显示器上面写和向文件里面写时没有区别的。所以你本来是准备向显示器文件写的,后来你变成向特定文件写入----------➡️这叫做重定向!无意间摸到重定向原理。

✳️PCB代表一个进程没问题,PCB里面呢有一个指针指向file_struct,然后file_struct里面包含了一个file数组,里面是一个个file对象。当我们打开进程时,都有标准输入、标准输出、标准错误文件file。每一个文件被打开了都要在内核当中维护一个file对象。
那如果我此时把1号关掉,就相当于数组下标为1的地址不再是指向显示器file的文件了,也就是该指针无效了,然后你又打开一个文件log.txt,然后他要去遍历数组里面去找没有被使用的下标,那就是1,然后就把log.txt的file地址填入进去,然后把1返回给用户int fd = open(…)。
在我们没有做一系列操作前,我们标准库里面有一个stdout它就是一个FILE对象它里面封装了fd,天然就是1。此时在应用层就有两个东西,stdout和log
.txt文件描述符都是1。你当然可以继续使用你的fd操作,但你也拦不住别人,比如fprintf(stdout,…)它往stdout里面打,虽然底层做了各种关闭,让1号下标去除和显示器file的关联,然后再把log.txt的地址填进去,返回1给用户。
但你所有的这些操作对上层用户来讲,它fprintf()知道吗?它不知道。这个stdout这个可怜的struct FILE对象C标准库里面的stdout,它知道吗?他不知道。他被狸猫换太子了,他并不知道!stdout依旧认为我可以像1里面去写入,写就写呗很快乐的就像我们1号文件描述符去写入了。可是当他写入时,它的数据被送到哪里了,一旦它写了,这就是系统里面的工作了,和你库就没关系了,你把数据交给1号你就完了。所以底层在写时它把数据在做刷新写的时候,就写到了log.txt的文件。其中底层所做的任何差异,在open它看来就是打开文件,我们不关心它了。但是最关键的是在我们对应的stdout看来,你底层所做的任何动作,我是不知道的,我也不关心,反正我只对1号负责,所以我只关心把数据写到1里面,至于你最后送到哪里我完全不关心。
所以换句话说,我们将来要重定向,上层只认0、1、2、3…这样的fd编号,我们可以在OS内部,通过一定的方式,调整数组的特定下标的内容(就是file
的指向问题)就可以完成重定向操作!

✳️所以对我们来讲,所有的重定向,你上层你只是0、1、2、3…等数字,但是 这个里面具体内容指向哪个file你完全不用关心,也不是你应该关心的,只要我能够在操作系统内把你的数组下标内容调整一下,本来指向这个文件,改成指向哪个文件 (无非就是改变地址嘛),改完之后,我们在操作系统内部调整它,我们就可以在上层之情的情况下直接狸猫换太子,完成重定向的操作了!这就是重定向本质。
我们操作里面是先close(1),再调用open()打开一个文件,那么此时给文件的分配的文件描述符就是1,所以我们先close再open的动作其实就是先把1号文件描述符的对象设置为空,然后新的文件对象地址填进去。但是你做这些动作对C库里面的stdout而言,他说我在哪,我被做了什么?可是底层对应的映射关系一旦重新建立,好吧stdout照样不知情,它照样像1号文件描述符里打,它只认1号文件。所以他就向1里面显示了!这就叫做重定向原理。

重定向具体操作

重定向原理清楚了,谁来做这个工作呢?什么叫谁来做这个工作,如果你要让我们进行重定向,你总不能让像你那样让用户先关闭1先关闭谁,然后再去重定向吧。这会不会有点太丑陋了。所以具体操作就要对应到接口层面上了!

✳️上面的一对数据,都是内核数据结构,只有谁有权限?OS---->必定提供接口

dup2()【重定向,系统调用】
dup2()函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
返回值解释:
成功的话会返回一个新的文件描述符是,失败了的话会返回-1

描述dup2:
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
1.是在讲整数互相拷贝吗?上面好像是在讲两个描述符,说的是拷什么呢?
拷的不是fd整数,两个拷来拷去没有任何意义,
就是在内核层面上吧oldfd和newfd里面的内容作拷贝,也就是拷贝file里面的内容,而不是整数!也就是拷贝file*指针。所以拷过来拷过去,最终我们得到的就是标准输出、标准输入、标准错误,先打开文件各种文件地址的拷贝。

2,按照接口描述来看,谁是谁的一份拷贝?
比如你要把new拷给old或是old拷给new,最终既然是拷贝,那是不是两个文件描述符对应的地址指向的文件,最终只能指向同一个文件,最终拷下来是new还是old?
读描述makes newfd be the copu of old
不就是说把旧的拷贝给新的吗!
所以最终只剩下oldfd!
重定向的原则呢:就是把前面一个参数的内容写入至后者,也就是说最终和oldfd指向的内容是同一个。

3.参数应该怎么传?----下面给出解释(把第一个参数内容写到第二个参数里面辅助记忆)

4.打样
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
{
	perrot("open");
	return 1;
}
/*我们把1号和fd号两个不相关的文件描述符进行了重定向,本来应该向显示器里面打印的,现在显示到文件里*/
dup2(fd, 1);//-------就不需要我们close(1)这种丑陋的操作了!
fprintf(stdout, "打开文件成功,fd: %d\n",fd);//--将后面一串数据显示到stdout里面
理解dup2()参数传入

✳️现在呢我stdout对应的是1,
现在能我想输出重定向,我想把本来显示到显示器上打内容,显示到文件log.txt 里面。在最开始来讲,我们的标准输入、标准输出、标准错误都是有各自的指向。
1.复制什么呢?(上面解释了)
2.参数怎么传?(下面解释!)
假设我们新打开的log.txt对应的文件描述符是fd,我们要把本来显示到显示器文件上的内容显示到log.txt里面参数怎么传,谁是谁的一份拷贝?

✳️现在有一个进程,它对应打开的标准输入、标准输出、标准错误,然后自己又打开了log.txt一切是我们非常常规的动作,接下来我们要做一个输出重定向爱!在很常规的情况下log.txt对应的文件描述符是不是fd是3,stdout的fd是1。所以我想输出重定向。重原理上来讲,我想输出重定向到哪里呢?我本就是想向显示器去打印的,现在我想让你往log.txt里面去打印,1还是1,3还是3,fd都不变,我们要重定向的本质就是把file* array[]文件数组里面下标内容做改变!
所以呢,我们现在是要把3(新的fd)的内容拷贝到1里面,还是1里面内容拷贝到3(新的fd)里面呢?
我们是一个指针,把一个内容拷贝到另一方,最终的本质就是我把我拷给你,意思就是我指向哪里,你也就得指向哪里。换句话说我们重定向最后想要的是1好文件描述符,不要指向所谓的显示器文件了,你帮我指向一个新打开的文件。我们要的是把1里面内容指向新的文件file地址。
说白就是改变1里面的内容,3被打开,那么3里面的内容指向的是新打开的文件file地址。
所以我们要把3(新的fd)里面的内容拷贝到1里面!
如果我们要把新来的fd是3里面内容拷贝到1里面,这不是两个空间吗?那么最后请问数组里面两个下标最终指向的内容和谁里面保持一致的呢?—新的fd 3一致!
都和fd里面的内容一样!

✳️函数参数怎么传呢?
所以又根据上面函数解释
int dup2(int oldfd, int newfd);要将旧的拷贝给新的!
所以应该这么传!
dup2(fd, 1);
所以oldfd是我们上面说的新来的fd 3;newfd是上面说的1;

理解文件关闭

一个文件你打开它,你关闭的时候,你想想,当你关闭这个文件的时候,销毁工作是你该管的吗?销毁工作是你这个进程该管的吗?你该关心吗?你不应该关心。你进程一关,你只要把你文件描述符表关掉就行了。至于这个文件怎么样,是人家操作系统管理的事,再结合一个文件是可以被多个进程打开的关系。想到智能指针,我们以前讲智能指针标定一块内存,它被多少人指向,有一个概念叫做“引用计数”。那么每一个文件对象里面,它里面有一个计数器count,代表这个文件被引用的次数。你打开文件是操作系统给你打开的,你打开只是把这个文件地址填到你struct file* array[]数组里面,然后你就对文件file里面的count做++了。所以关闭文件你只需要把数组指向设置为无效,然后你紧接只需要把count做–就完了,至于对象什么时候关闭,是人家操作系统关心的事情。操作系统遍历,咦?你的引用计数怎么是0啦?给你关掉!此时文件才是关闭。所以个文件做到被打开多次非常简单!在底层就是由引用计数的方案,填写自己地址进数组里面。

追加重定向又怎么干呢?

如果说重定向当中呢,我们单纯的重定向呢就是重定向并对我们文件做清空。
追加重定向仅仅是在该文件打开的时候open()函数的选项里面添加O_APPEND。这便是追加重定向。

输入重定向呢?

说白了就是以前从我们的键盘里面去读,现在变为从我们的文件里面去读。

int fd = open("log.txt", O_RDONLY);
/*int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);*/
if (fd < 0)
{
	perrot("open");
	return 1;
}
dup2(fd 0);//----经过dup2()就实现输入重定向了!那么log.txt内容就会被读取到buffer里面。
char buffer[128]
while(fgets(buffer, sizeof(buffer), stdin))
//以前我们读取内容是从键盘里面去读的,stdin去读,执行程序后,他会卡在那里等你去输入
[
	printf("%s\n", buffer);
}

缓冲区的理解

什么是缓冲区

1.什么是缓冲区?
有缓冲的样例是在我们写进度条的时候见过一次。
✳️缓冲区的本质就是一段内存
(不过这个内存究竟在哪里呢,是后话了。)

为什么要有缓冲区

2.为什么要有缓冲区?
我们举个例子:你想把你读的10本书从云南农业大学送到北京邮电大学。
现在有两种做法:一种是你自己送过去,经过一系列交通到目的地,自己送过去最大的成本在于,耽误你的时间
第二种做法:你学校有个顺丰快递点,他的学校也有一个快递顺丰点。有着两个快递点,你填个单子就回宿舍做自己事情了,快递员通过一系列过程数据到另一个顺丰。你的朋友接到快递电话,取走就完事了。
那么我们这个顺丰包括他如果想给你写东西,也叫个顺丰就行了。其实大家都知道写入的 时候,单纯在这你配送和快递员配送看来在路上该花多长时间还是得花多长时间。只不过顺丰目前来看最大价值于解放你的时间!
也就是说让你能够以不让自己深度去参与配送,交给顺丰自己立马返回做你自己的事情。
所以顺丰存在的价值就等同于我们即将讲的缓冲区,你把你的书,其实说白了就是数据,你是一个进程,你把你的数据交给顺丰这个缓冲区,然后立马返回了,是解放进程的时间,不要让进程在这么长时间里面去等,所以进程立马就返回了,解放的是你的时间,那么你的进程是不是就可以继续往后执行你的代码了,那么配送的过程做他的事情。
✳️第一个意义:解放使用缓冲区的进程时间
(在计算机内存和外设当中,当你把数据从内存写到外设是非常浪费时间的!)

张三说你上次送的书特别好,你能不能多送我几本?你说行。一会儿送这个书,一会儿送那个书,又一会儿送其他书。反正想起来送什么就去送了。当顺丰收到你的第一本书时候,顺丰会不会立马给你派送?与此同时只有你一个人要派送吗?总之顺丰不会傻到只有你一本书我就给你去派送。顺丰会说,你写吧,反正又不影响,你交给我,我呢可能积累到一定的量,比如说我一次派送1本性价比不高,当我积累到500本书的时候,再集体派送。
也就是以前一本书要花费一次在路上时间,那么现在500本书每一本书都要花费一次路上时间。
那么我现在500本书整体打个包,再整体写到我们的外设里面,那么其中我派送500本书只要花费一次的时间
✳️第二个意义:缓冲区的存在可以集中处理数据刷新,减少IO的次数,从而达到提高整机的效率的目的。(我们现阶段意义应该是第一个意义,而不是第二个)

✳️如果我告诉你那些接口并没有把数据直接写到外设里面,而是写到缓冲区里面,那么对你来讲,带来最大的意义就是相当于你调用某个函数时,你调用的时间会非常短,所以整个进程跑起来的效率会特别高
请添加图片描述

缓冲区在哪里?(通过验证推理得)

3.缓冲区在哪里?
缓冲区多了,用户层有缓冲区,操作系统内部也有缓冲区,你具体指的是哪个?包括我们们自己写的char buffer[]也是缓冲区!

我们用代码验证一下。

printf(" hello printf"); // 向显示器里面打,stdout -> 1, 
fflush(stdout)//---强制让他刷新 
const char *msg = "hello write";
write(1, msg, strlen(msg)); 
sleep(5);

✳️我们发现有/n的话:显示器显示hello printf和 hello write正常;

✳️但是我们去掉/n后:hello write立刻显示出来,但是hello printf过了一会儿才显示
我想说:printf它内部就是封装了write(能理解吗?肯定能理解呀!你printf写再多都是往stdout里面打)
我们来说一下为什么hello printf过一会儿显示:我们曾经写进度条的时候说过,这里的打印出来的hello printf其实是在缓冲区里面,没有被立即刷新出来,所以slepp的时候并没有被显示出来。
什么意思呢?意思就是说printf现在不带\n了,它的数据没有立即刷新,原因在于它有缓冲区,要么在sleep(5)秒期间执行完了,那么printf在做什么呢? 数据在哪里呢?所以我们当时说它是有缓冲区的。不带\n就是说它数据没办法立即刷新饿了,所以我们当时做了一个工作叫fflush(stdout)强制刷新一下,然后发现就都打印出来了。没有问题,这也是我们之前讲过的。
❓那么你printf没有立即刷新的原因?
是因为有缓冲区的存在。(是因为数据被暂存在了缓冲区里面,sleep结束这个进程退出,数据才被刷新出来)
但我们看到write(1, msg, strlen(msg)); 这行代码可是立即刷新出来的!而你又告诉我printf里面是封装了write的
❓那么这个缓冲区在哪里呢????
❓那么这个缓冲区不在哪里呢??
printf是库函数,write是系统调用接口,所以无论你有没有\n,hello write立马就给你显示出来了,那么此时缓冲区一定不在write内部!
如果缓冲区在write内部,是该函数提供的,那么直接决定了,所有printf你底层调了write嘛,那么最终你这个数据hello printf,是不是立马就要刷新出来。但是与我们实验结过矛盾,所以缓冲区一定不在write里面!
我们曾经谈论缓冲区不是内核级别的,如果是的话,我们应该看到的结果应该都是立即被刷新显示出来。
✳️那么今天只能告诉大家,这个缓冲区只能是C语言提供的。
(因为我调用C接口时,它数据是会在因为缓冲问题而不会立刻刷新,而底层write的数据立刻刷新,说明了我们的数据根本就不会直接由printf立马调用write去写入,而是把数据先暂存在某一个地方,等到合适的时候,再把数据通过write在刷新到显示器上。)

✳️只能由C语言提供,那么现在问题就是,我们的缓冲区是一个语言级别的缓冲区。
我说一下你这样理解:因为write怎么调,有没有\n他都会立即刷新,换而言之,你printf想把数据立刻刷出来,它必须调用write。
换句话说它不想把数据立刻刷新出来,它暂时肯定没有调wrie(),没有write()的时候,那么在sleep()期间printf调完了,没有调wrire(),那么在sleep期间,数据在哪里呢?那么一定在某个缓冲区里面,只是当进程退出的时候,它才调对应的write() ,那么缓冲区一定在printf()内部。
但是我们发现,好像每个函数好像都有比如fpritnf(),fputs()都是有这种特性的。你不信你可以试试fprintf(stdout,“hello fprintf”)/fputs(“hello fputs”,stdout);
发现fprintf、fputs的实验结果和printfI()一摸一样!
你这几个接口底层封装了write(),只要你调了write()那么数据就一定立马刷新出来。因为我们已经测试过了,write()无论带不带\n,数据都立马刷新。
换句话说只要你们几个调了write(),数据一定显示出来,换句话说你们几个函数调完没有显示出来,一定是没有调write(),你内部没有立刻调write()怎么办呢?当你在sleep()5秒期间没有调write()这个数据在哪里呢?缓冲区一定是语言级别的,如果是系统级别的此时这个数据 会立即刷新出来的!
下面正式谈谈缓冲区在哪里呢?

✳️你有没有发现一个问题,我们上面3个接口printf、fprintf、fputs是C语言提供的,他们都有一个隐形的公共参数,他们都有stdout(printf有,只不过没显示写出来罢了) ,那么这三个接口都有参数stdout。
既然他们都有一个参数stdout,意味着什么呢?意味着stdout它自己是FILE*类型,而FILE是一个结构体,那么该结构体里面一定会封装很多成员属性。
成员属性除了我们经常说的fd,该结构体FILE里面还有语言级别的缓冲区!

✳️换而言之,FILE结构体里面它会有封装的缓冲区。
所以很多数据是这么操作的,FILE里面封装了一个fd,另一个是char buffer[1024];我们代码里里面调用了很多的printf()、fprintf()、fputs()等等,当我们 要把我们的数据写入时,并不是你直接调用系统接口write()直接写到系统里面然后刷到硬件上。
它调用这些接口的时候,是把数据直接写到了封装的buffer里面,相当于是缓冲区里,写到缓冲区里,当缓冲区里面的数据量积累到一定程度,那么此时他会定期的,通过fd调用write把数据刷新到我们对应的内存当中。
那么什么意思呢?意思就是说 ,我们printf()等接口的数据不是直接写到我们的操作系统里面,而是先写到自己语言级别的缓冲区里面!这东西就是我们的顺丰,是为了能够调用这些接口时,你只要把数据放在这里,积累到一定的量之后,那么再定期向我们的硬件设备做刷新。

✳️其中这样可以调用函数时效率变高了,因为这样没有把数据直接还调系统调用写到内存里在写到我们的硬件上,没有这样的工作了。你只需要把数据房子啊你的缓冲区里就可以了!
第二个他可以集中化的进行刷新,可以减少我们和外设包括操作系统交互的次数,从而达到提高整机效率的目的。
换句话说,数据一开始是先放到缓冲区里面然后定期去刷新。不要担心,内心里先不要有任何比如说具体该怎么做呢 这些疑惑或困惑,稍后我们回来实现它,我们自己封装一个FILE,自己封装一个printf、fprintf等甚至fflush()函数。
所以对我们来讲,缓冲区里的数据,本质是由谁提供的呢?是由C语言提供的。那么这里会存在两个问题 。
第一个问题就是:你说刷新就刷新?我什么时候刷新呢?我数据放到缓冲区里我怎么去刷新呢?这里就会牵扯到刷新策略的问题!(这些接口都是把数据暂存在缓冲区,然后定期调用write去刷新的,那么这里就牵扯到一个刷新策略的问题,怎么去刷新他呢)
第二个问题:如果在刷新之前关闭fd会有什么问题(我说的是关闭文件描述符,不是关闭文件FILE)?来我们代码做一下实验。换句话说你刚刚说了printf、fprintf等函数接口数据没有\n,那么数据会暂存在stdout它内部的缓冲区里,函数内部没有调用write()那么数据就还在缓冲区里,如果我们直接调write()它是直接刷新了,现在我们调printf等函数时,直接关闭close(1),那么你不是sleep()结束后,进程退出后你再刷新吗?好呀你刷个试试,我把你内部文件关掉了!

printf(" hello printf"); // 向显示器里面打,stdout -> 1, 
const char *msg = "hello write";
write(1, msg, strlen(msg)); 
/*close(1)*/
/*close(stdout->fileno);*/fileno相当于就是文件描述符了!
sleep(5);
close(stdout->fileno);

好了我们close(1)试了之后,我再来试试close(stdout->fileno),fileno不是你自己里面的文件描述符嘛,我把你这个关了,甚至我把这个关闭放在slepp()执行的后面。在slepp期间,这个数据没有被刷新出来,它只能在我们自己对应的stdout定义的缓冲区里面,我把你fileno关掉,那么进程在后续退出时,你能刷新出来吗?
不会被刷新出来!
因为我进程当后续退出时,想刷新了,可是你把文件描述符给我关了,所以后面我write()函数调用,就调用失败了!
这又证明了,printf等函数里面的一开始的数据根本就没有被写到操作系统里面,而只是写在自己的缓冲区里面。因为后续你还要用fileno来写入你的操作系统,我不让你玩了,我把你关掉,你写吧,所以当然不会显示了。

✳️你说缓冲区是在FILE结构体(C语言)里面,而我们每一次打开一个文件都要有一个FILE*(C语言)返回,是不是每一个文件都有一个fd和属于他自己的语言级别缓冲区?是的!
所以你觉的fopen干了些什么?fopen打开文件会返回FILE*,那它的FILE在哪儿呢?是不是它肯定创建一个FILE对象,然后把它地址返回。

✳️1.我们已经通过现象推测出来缓冲区在哪里的问题
2.基于推测我们得到缓冲区刷新的过程,实际上是用户层面上先写到缓冲区,然后在合适的时候通过fd(因为fd是一个Linux下内核结构file对象,file结构体里面会有读写的方法!)调用write()接口写到操作系统给你刷新出来。
3.我们做了打开一个文件强制刷新如何?我们提前关闭文件描述符又如何?现象,也观察到了现象。
4.又告诉到家,也见过了FILE内部的缓冲区结构(Linux课件里面有)但我们没有读源代码,不关心它。反正我们知道FILE里面有缓冲区就够了,我们调用的所有C语言接口如printf、fprintf、fputs等接口都是要传入FILE*结构体,会先写到FILE结构体里的缓冲区。

缓冲区刷新策略

你刚刚说printf等接口一开始的数据是先写入到FILE结构内部的缓冲区里面,然后定期去调用write()刷新,那是怎么个定期刷新法?我们的刷新策略又是怎么样的呢?
也就是说我们现在有数据了,数据已经放在了用户层的缓冲区里,那什么时候刷新呢?

❓什么时候刷新呢?
1.常规情况:a.无缓冲(立即刷新)
b.行缓冲(逐行刷新),也就是说你有一行内容了我再给你把这一行刷新出去,说白了把缓冲区的数据包括\n之前的数据刷新出去—一般是显示器文件
c.全缓冲(缓冲区写满再刷新)----一般普通磁盘文件

2.特殊情况:
a.进程退出(一个进程在退出时,它的资源各方面要退出了,它有时候有的接口如C语言有很完整的解决方案,它一定要强制的让我们在退出时把数据全部刷新到系统里面,这是特殊情况)
b.用户强制刷新(你别跟我扯什么立即刷新和行刷新,我fflush你立即给我刷新出去就完事了)
所以常见的刷新策略就是上面那些!

✳️我们要谈论的就是:我们现在套一套,反正就是printf、fprintf等函数一开始把数据写到缓冲区,你说定期刷新,那它什么时候刷新呢? 如果对应的文件是显示器,那么它的刷新策略就是行刷新或者是;全缓冲刷新策略一般是对应我们的块设备对应的文件如磁盘文件(比如说你今天open打开log.txt,这是一个磁盘文件,按照正常情况它的刷新策略就变成全缓冲,也就是说行缓冲你写一行缓冲区没满,但我是行缓冲我也必须给你刷新出去;如果你是全缓冲,我们数据被写满了,数据才会刷到磁盘上,这对应的就是普通磁盘文件)请添加图片描述

奇怪的问题

	const char *str1 = "hello printf\n";
    const char *str2 = "hello fprintf\n";
    const char *str3 = "hello fputs\n";
    const char *str4 = "hello write\n";

    //C库函数
    printf(str1); //?
    fprintf(stdout, str2);
    fputs(str3, stdout);

    //系统接口
    write(1, str4, strlen(str4));

    //是调用完了上面的代码,才执行的fork
    fork();

我们正常执行该程序结果是:
hello printf
hello fprintf
hello fputs
hello write
这没问题
但是我们在命令行加入输出重定向即:#./myfile > log.txt
然后打印log.txt即#cat log.txt
发现结果是
hello write
hello printf
hello fprintf
hello fputs
hello printf
hello fprintf
hello fputs
请解释该现象!

✳️首先我们往显示器打印,打出的四行结果是符合预期的,也很理解,因为向显示器写入的时候字符都是\n所以无论是C库函数,还是调用系统接口在打印的时候,数据都能立即刷新出来,因为面对的stdout这样的设备,我们可以把它称之为显示器文件,所以前面三个函数肯定是先写到缓冲区里面,但是字符串里面有\n那么由于显示器文件遵循行刷新策略就会立即刷新,调用系统接口write()能够立即刷新这个很好理解。
第二个实验:当我们把它重定向到log.txt的时候 ,重定向它内部是做了dup2()的,当然也不仅仅只有dup2(),但是我们现在知道他肯定做了dup2()的,本来应该显示到文件描述符1的内容也就是显示器的内容,经过重定向写到了log.txt文件里。
因为重定向了,我们前面讲了缓冲区刷新策略:如果是显示器文件则是行刷新;
如果是普通磁盘文件那就是全缓冲刷新策略,也就是写满才缓冲
我发现我们重定向之后数据是7条,首先是printf、fprintf、fputs各出现两次,他们三个一共是6次,但是write依旧只有一个。
那么造成这种现象的一定跟缓冲区有莫大关系!
这也就证明了write()压根就不关心你所谓的缓冲区影响。
现在问题变成了为什么我调C语言的库函数,经过从定向到文件里,最后这三条消息却在文件里打印了两次呢?
答案是一定也跟fork()创建子进程有关系!如果你把fork去掉,那么都是跟实验一一样只有4条消息。
这里怎么理解7条呢?是这样理解的:你们三个把数据写到缓冲区里,因为已经重定向到log.txt,数据不会立即刷新了,虽然你又\n,因为我磁盘文件遵从的是全缓冲刷新策略,而不是你的行缓冲刷新策略。所以这三条消息暂存在了stdout(FILE对象)对应的缓冲区里面,存在里面没有刷新。
当我们调用fork()的时候,大家都知道,就要发生创建子进程。当然我们写的代码fork()之后,立马就是父子进程就都得退出了。
父子进程退出之后,就面临一个问题,父子进程就要刷新缓冲区了!
我们要强调两个细节:
1.刷新的本质:把缓冲区的数据写入到我们的操作系统内部,然后情况缓冲区;
2.这里的缓冲区是自己的FILE内部维护的,数据父进程内部的数据区域!
所以当你调fork()的时候,数据没有从缓冲区刷新出去,当我们fork()时,不管是父子谁先进行刷新清空,代码父子进程共享,数据父子进程要以写时拷贝的方式各自形成一份。
数据区域本来属于你父进程,你也要对数据做清空,所以要发生写时拷贝,那么父子进程各自有一份。然后各自将自己的缓冲区刷新出去。
最终就有了我们的这个7条消息的现象!
(我们再讲一下为什么write在第一条:因为write()没有缓冲区,它是直接写到操作系统,然后交给文件,它是最早写进log.txt的所以第一行是write)

实现一下:my_read/my_write等等

我还是不太理解怎么办?尤其是应用
✳️让同学们理解原理,样例代码,不代表全部的标准的实现!
1.模拟实现一下自己封装C标准库

typedef struct _MyFILE{
 17	    int _fileno;
 18	    char _buffer[NUM];
 19	    int _end;
 20	    int _flags; //fflush method---➡️标记刷新策略
 21	}MyFILE;
 22	
 23	MyFILE *my_fopen(const char *filename, const char *method)
 24	{
 25	    assert(filename);
 26	    assert(method);
 27	
 28	    int flags = O_RDONLY;
 29	
 30	    if(strcmp(method, "r") == 0)
 31	    {}
 32	    else if(strcmp(method, "r+") == 0)
 33	    {}
 34	    else if(strcmp(method, "w") == 0)
 35	    {
 36	        flags = O_WRONLY | O_CREAT | O_TRUNC;
 37	    }
 38	    else if(strcmp(method, "w+") == 0)
 39	    {}
 40	    else if(strcmp(method, "a") == 0)
 41	    {
 42	        flags = O_WRONLY | O_CREAT | O_APPEND;
 43	    }
 44	    else if(strcmp(method, "a+") == 0)
 45	    {}
 46	
 47	    int fileno = open(filename, flags, 0666);
 48	    if(fileno < 0)
 49	    {
 50	        return NULL;
 51	    }
 52	
 53	    MyFILE *fp = (MyFILE *)malloc(sizeof(MyFILE));
 54	    if(fp == NULL) return fp;
 55	    memset(fp, 0, sizeof(MyFILE));
 56	    fp->_fileno = fileno;
 57	    fp->_flags |= LINE_FLUSH;
 58	    fp->_end = 0;
 59	    return fp;
 60	}
 61	
 62	void my_fflush(MyFILE *fp)
 63	{
 64	    assert(fp);
 65	
 66	    if(fp->_end > 0)
 67	    {
 68	        write(fp->_fileno, fp->_buffer, fp->_end);
 69	        fp->_end = 0;
 70	        syncfs(fp->_fileno);
 71	    }
 72	
 73	}
 74	
 75	
 76	void my_fwrite(MyFILE *fp, const char *start, int len)
 77	{
 78	    assert(fp);
 79	    assert(start);
 80	    assert(len > 0);
 81	
 82	    // abcde123
 83	    // 写入到缓冲区里面
 84	    strncpy(fp->_buffer+fp->_end, start, len); //将数据写入到缓冲区了
 85	    fp->_end += len;
 86	
 87	    if(fp->_flags & NONE_FLUSH)
 88	    {}
 89	    else if(fp->_flags & LINE_FLUSH)
 90	    {
 91	        if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n')
 92	        {
 93	            //仅仅是写入到内核中
 94	            write(fp->_fileno, fp->_buffer, fp->_end);
 95	            fp->_end = 0;
 96	            syncfs(fp->_fileno);
 97	        }
 98	    }
 99	    else if(fp->_flags & FULL_FLUSH)
 100	    {
 101	
 102	    }
 103	}
 104	
 105	void my_fclose(MyFILE *fp)
 106	{
 107	    my_fflush(fp);
 108	    close(fp->_fileno);
 109	    free(fp);
 110	}

int main()
[
MYFILE* fp = my_fopen();
if(fp == NULL)
[

}


const char* s = "hello my file\n";
my_fwrite(fp, s, strlen(s));

my_fclose(fp);
}

重定向

对原先自己写的shell加上重定向操作

重定向,之前不是这么用的啊?
我们之前是这么用的呀:ls -a -l > log.txt,命令行中的重定向实现的细节

我们运行我们自己写的myshell,发现ls -a -l > log.txt并不能运行,那是因为我们自己的shell并不认识重定向符号。那么怎么办呢?

ls -a -l > log.txt:我们知道这个命令左半部分进行重定向,左半部分可以称为完整的命令,而>右侧是一个文件,本来左侧那个程序执行完应该是显示到我们的显示器上 。而现在我们要把显示的结果写到文件里,这就是我们要所说的重定向。
所以我们自己写的shell在对输入的一行字符串作分析的时候。所以我们在获取用户的输入之后,以及我们在进行对用户的指令做提取和分析之前。我们要想明白有可能数据是这个样子的:ls -a -l > log.txt;也有可能是这样的:cat < file.txt;也有可能是这样的:ls -a -l >> log.txt(追加);当然也有可能单纯的就是ls -a -l。
我们在获取一个完整的字符串指令以及分析的时候,我们对下面所有的分析说白了就是,无外乎就是将我们所做的前半部分的整条指令做分析,后半部分是你不应该关心的。因为你是指令分析,你管你的指令就行了,你管我重定向和你有什么关系。
所以对于我们来讲所先要做的是:对该字符串是否重定向这个方面,要做一个相关的分析。既然要做一个对它是否重定向相关的分析呢,我们要达到什么样的结果呢,就以输出重定向委例子。我们今天只写输出重定向,输入重定向和追加重定向其实也讲了,就只是标记位和读写方式的问题。我么讲好逻辑,下面可以自己再去想着写。

我们可以这么干:如果用户输入的是ls -a =l > log.txt,我们此时可以想办法把它的>符号看成’\0’,也就看成了ls -a -l \0 log.txt。也就是说呢,我们有一个指针指向头,另一个指针指向’\0’的位置。
也就是一旦把>符号写成’\0’,那么前半部分继续执行我们之前写的指令分析,后半部分, 我们负责打开文件和做重定向相关的工作。原理就是这么简单!

我们接下来要做的就是:CheckDir(command_line),检查我们是否有重定向的行为,我们将command_line传进去。我么函数CheckDir是要有一个返回值的,至于什么意义呢我们稍后会讲。
我们要保证是否你传说中的重定向的操作呢?我们只关系command有没有>、<、>>等符号。

#define NONE_REDIR -1//默认没有重定向情况
#define IPUT_REDIR 0//---输入重定向
#define OUTPUT_REDIR 1//----输出重定向
#define APPEND_REDIR 2//----追加重定向

//------➡️下面的宏函数技巧值得学习!
#define DROP_SPACE(s) do [
 	while(isspace(*s)) s++; }
 while(0)//为了避免用户在>符号后面输入多个空格符号,然后我用此函数找对应的后半部分指令

char* redir_flag = NONE_REDIR;
char* filename = NULL
void CheckDir(char *commands)
{
    assert(commands);
    //我们要保证是否你传说中的重定向的操作呢?我们只关系command有没有>、<、>>等符号。
    //[start, end)---➡️我们地址➕长度,指向的应该是'\0'的位置。end应该是指向结尾
    char *start = commands;
    char *end = commands + strlen(commands);
    // ls -a -l > log.txt
    while(start < end)
    {
        if(*start == '>')-----➡️如果此时是>0不应该结束而是要检查是否为>>的情况
        {
            if(*(start+1) == '>')//----➡️检查是否为>>情况
            {
                //ls -a -l>>  log.txt --追加
                *start = '\0';
                start += 2;
                g_redir_flag = APPEND_REDIR;
                DROP_SPACE(start);
                g_redir_filename = start;
                break;
            }
            else{//-----➡️只能是>输出重定向的情况。
                //ls -a -l > log.txt -- 输出重定向
                *start = '\0';//---➡️因为在遍历的时候,start一直在做++所以当遍历到>的时候start是指向>的就将指令分为前后部分,所以我们就找到前半部分指令
                start++;
                DROP_SPACE(start);//---➡️是为了忽略用户输入的多个空格符,找到后半部分指令
                g_redir_flag = OUTPUT_REDIR;//----➡️确定重定向类型,标记为输出重定向
                g_redir_filename = start;//---➡️指向后半部分指令的头
                break;//----➡️表示我们当前将命令分开成功
            }
        }
         else if(*start == '<') // ------➡️输入重定向
        {
            *start = '\0';
            start++;
            DROP_SPACE(start);
            g_redir_flag = INPUT_REDIR;
            g_redir_filename = start;
            break;
        }
        else 
        {
            start++;
        }
        
    }

 pid_t id = fork();
        if(id == 0)
        {
            int fd = -1;
            switch(g_redir_flag)
            {
                case NONE_REDIR:/----➡️如果不是重定向什么都不干,正常后面操作就行了
                    break;
                case INPUT_REDIR://----➡️输入重定向
                    fd = open(g_redir_filename, O_RDONLY);//---➡️把后半部分指令的文件打开
                    dup2(fd, 0);//---➡️cat < log.txt
                    //---➡️从标准输入中读,变为我们后半部分指令(log.txt)的文件中读
                    break;
                case OUTPUT_REDIR:
                    fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_TRUNC);
                    dup2(fd, 1);
                    break;
                case APPEND_REDIR:
                    fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_APPEND);
                    dup2(fd, 1);
                    break;
                default:
                    printf("Bug?\n");
                    break;
            }
 ✳️后面正常执行我们子进程做的。程序替换工作;
 程序替换是不会影响我们子进程打开的文件!程序替换只影响该进程的代码和数据。
 因为程序替换是只会重建页表的映射关系。
 我们曾经打开的文件以及维护文件描述符表都叫做内核数据结构,同PCB一样不受程序替换的影响!
 
          }

小型完整版的自己写的shell

#include <stdio.h>
#include <string.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <ctype.h>

#define SEP " "
#define NUM 1024
#define SIZE 128

#define DROP_SPACE(s) do { while(isspace(*s)) s++; }while(0)

char command_line[NUM];
char *command_args[SIZE];

char env_buffer[NUM]; //for test

#define NONE_REDIR  -1
#define INPUT_REDIR  0
#define OUTPUT_REDIR 1
#define APPEND_REDIR 2

int g_redir_flag = NONE_REDIR;
char *g_redir_filename = NULL;

extern char**environ;

//对应上层的内建命令
int ChangeDir(const char * new_path)
{
    chdir(new_path);

    return 0; // 调用成功
}

void PutEnvInMyShell(char * new_env)
{
    putenv(new_env);
}

void CheckDir(char *commands)
{
    assert(commands);
    //[start, end)
    char *start = commands;
    char *end = commands + strlen(commands);
    // ls -a -l
    while(start < end)
    {
        if(*start == '>')
        {
            if(*(start+1) == '>')
            {
                //ls -a -l>>  log.txt --追加
                *start = '\0';
                start += 2;
                g_redir_flag = APPEND_REDIR;
                DROP_SPACE(start);
                g_redir_filename = start;
                break;
            }
            else{
                //ls -a -l > log.txt -- 输出重定向
                *start = '\0';
                start++;
                DROP_SPACE(start);
                g_redir_flag = OUTPUT_REDIR;
                g_redir_filename = start;
                break;
            }
        }
        else if(*start == '<')
        {
            // 输入重定向
            *start = '\0';
            start++;
            DROP_SPACE(start);
            g_redir_flag = INPUT_REDIR;
            g_redir_filename = start;
            break;
        }
        else 
        {
            start++;
        }
    }
}

int main()
{
    //shell 本质上就是一个死循环
    while(1)
    {
        g_redir_flag = NONE_REDIR;
        g_redir_filename = NULL;

        //不关心获取这些属性的接口, 搜索一下
        //1. 显示提示符
        printf("[张三@我的主机名 当前目录]# ");
        fflush(stdout);

        //2. 获取用户输入
        memset(command_line, '\0', sizeof(command_line)*sizeof(char));
        fgets(command_line, NUM, stdin); //键盘,标准输入,stdin, 获取到的是c风格的字符串, '\0'
        command_line[strlen(command_line) - 1] = '\0';// 清空\n

        //2.1 ls -a -l>log.txt or cat<file.txt or ls -a -l>>log.txt or ls -a -l
        // ls -a -l>log.txt -> ls -a -l\0log.txt
        CheckDir(command_line);

        //3. "ls -a -l -i" -> "ls" "-a" "-l" "-i" 字符串切分
        command_args[0] = strtok(command_line, SEP);
        int index = 1;
        // 给ls命令添加颜色
        if(strcmp(command_args[0]/*程序名*/, "ls") == 0 ) 
            command_args[index++] = (char*)"--color=auto";
        // = 是故意这么写的
        // strtok 截取成功,返回字符串其实地址
        // 截取失败,返回NULL
        while(command_args[index++] = strtok(NULL, SEP));

        //for debug
        //for(int i = 0 ; i < index; i++)
        //{
        //    printf("%d : %s\n", i, command_args[i]);
        //}
    
        // 4. TODO, 编写后面的逻辑, 内建命令
        if(strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL)
        {
            ChangeDir(command_args[1]); //让调用方进行路径切换, 父进程
            continue;
        }
        if(strcmp(command_args[0], "export") == 0 && command_args[1] != NULL)
        {
            // 目前,环境变量信息在command_line,会被清空
            // 此处我们需要自己保存一下环境变量内容
            strcpy(env_buffer, command_args[1]);
            PutEnvInMyShell(env_buffer); //export myval=100, BUG?
            continue;
        }

        // 5. 创建进程,执行
        pid_t id = fork();
        if(id == 0)
        {
            int fd = -1;
            switch(g_redir_flag)
            {
                case NONE_REDIR:
                    break;
                case INPUT_REDIR:
                    fd = open(g_redir_filename, O_RDONLY);
                    dup2(fd, 0);
                    break;
                case OUTPUT_REDIR:
                    fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_TRUNC);
                    dup2(fd, 1);
                    break;
                case APPEND_REDIR:
                    fd = open(g_redir_filename, O_WRONLY | O_CREAT | O_APPEND);
                    dup2(fd, 1);
                    break;
                default:
                    printf("Bug?\n");
                    break;
            }
            //child
            // 6. 程序替换, 会影响曾经子进程打开的文件吗?不影响
            //exec*?
            execvp(command_args[0]/*不就是保存的是我们要执行的程序名字吗?*/, command_args);
            exit(1); //执行到这里,子进程一定替换失败
        }
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0)
        {
            printf("等待子进程成功: sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
        }
    }// end while
}

标准输出和标准错误

我们曾经学了,它们都对应着我们显示器设备,那么他们有什么区别呢?

在C++中呢,cerr表示往我们标准错误去打,错误打印
cin表示读;cout表示写,打印

在C语言中,printf,fprintf,fputs都是向标准输出stdout里面打印
而stderr表示向标准错误里面去打

	//stdout
    //printf("hello printf 1\n");
    //fprintf(stdout, "hello fprintf 1\n");
    //fputs("hello fputs 1\n", stdout);

    stderr
    //fprintf(stderr, "hello fprintf 2\n");
    //fputs("hello fputs 2\n", stderr);
    //perror("hello perror 2");


    cout
    //std::cout << "hello cout 1" << std::endl;

    cerr
    //std::cerr << "hello cerr 2" << std::endl;

✳️然后由上面的代码我们在命令行输入#./a.out > stdout.txt,也就是将上面的代码形成可执行程序后,将打印的内容重定向指向指定的文件中。
然后我们会发现显示器只显示:
fprintf(stderr, “hello fprintf 2\n”);
fputs(“hello fputs 2\n”, stderr);
perror(“hello perror 2”);
std::cerr << “hello cerr 2” << std::endl;
而下面的不显示:
printf(“hello printf 1\n”);
fprintf(stdout, “hello fprintf 1\n”);
fputs(“hello fputs 1\n”, stdout);

再然后我们在命令行输入#cat stdout.txt发现只显示
printf(“hello printf 1\n”);
fprintf(stdout, “hello fprintf 1\n”);
fputs(“hello fputs 1\n”, stdout);
std::cout << “hello cout 1” << std::endl;

虽然标准输入和标准输出是两个对应同一个设备。我们的task_struct里面的文件描述符表有0、1、2对应着标准输入、标准输出、标准错误 。我们用的所有printf等向stdout里面打,都是向1文件描述符里面打;若是向标准错误打就是向2文件描述符里面打。
对应的都叫显示器 ,但是他们两个并不连着。先当于大家都打印到了一个显示器上,但依旧是通过不同的文件描述符打印的。大家互不干扰!
所以当你重定向的时候,你只是对1号文件描述符重定向,与我2号有什么关系呢?一点关系也没有!此时这就叫做标准错误。

✳️那么此时我就是想要向你标准错误里面显示呢?甚至你显示的内容交错的,有向1打的,也有向2打的,那我们怎么分开呢?
此时就是:#./a.out > stdout.txt > stderr.txt
此时就会发现显示器什么都没了,分别打印到了stdout.txt和stderr.txt里面。
所以这个命令作了两次重定向,第一个重定向将我们的标准输出重定向,第二次把2标准错误重定向。
则这叫做吧向1和向2打印的内容分开去打印!

✳️有何意义呢?
可以区分哪些是程序日常输出;哪些是错误;
(我们在打印信息的时候,其实信息会有不同的重要级别的。比如有些就是常规的:打开文件成功,创建对象成功,这些都是正常信息。
但有时侯我们的程序有些错误,它可能还能继续往后面跑,你让它继续跑;也有可能一些致命错误,程序直接退出了,那么我就应该吧这些错误信息,通过2号文件描述符,通过cerr/stderr的方式,将错误打印出来。方便我后续对我代码中的常规信息和错误信息作分离和分析!)
这些都是日志等级!

✳️若是不想区分开,就是想一股脑打的打到一个文件里呢?
命令:#./a.out > all.txt 2>&1;
(首先你是作重定向的,你会打开all.txt,然后它对应的文件描述符是3,我们前面才讲过重定向,所以在程序的内部在做重定向分析的时候,它把本应该显示到1里面的内容显示到3号文件描述符的文件里 ;然后呢2>&1是把1里面的内容拷贝到2里面.因为2本来就是指向显示器的,现在把也改为指向新的文件,所以它们最后指向同一个文件,都向同一个文件里面打印了。)

✳️perror()函数

errno:C语言有个全局的变量,记录最近一次C库函数调用失败的原因;

FILE* fd = fopen(log.txt, O_RDONLY);----➡️必定会失败,因为当前目录没有该文件

void MyPerrot(const char* info)
{
	fprintf(stderr, "%s: %s\n",info, strerror(errno))
	//➡️就这一行命令,向标准错误打印:你自己的自定义信息和系统根据错误码描述的默认信息
}
if(fd == NULL)
{
	MyPerror("open");
	/*perror("open");*/
}

文件系统

引出磁盘物理结构

❓上面我们学到的所有的东西,全部都是在内存中吗??
我们曾经讲过一些东西,就是同学们我们的文件要被访问必须得将它们调入到内存当中,然后将其打开。所谓文件被访问打开,本质上是进程在打开和访问。我们所谓的文件和进程之间有对应关系,操作系统中进程和文件的关系是1:n的比例 。文件越来越多,操作系统要管理,所以每个文件都必须要有struct file,先描述再组织嘛对不对!
这些讲的都是在内存中。

❓那是不是所有的文件都被打开了?
解答:并不是!被打开的文件属于一个要进程被访问的文件,并不是所有的文件都被打开了。
被打开的文件与打开了的文件相比是毛毛雨。没有被打开,没有被读写和访问的文件才是宿命。所以我们之前研究的是进程打开的文件,是要由操作系统去管理的文件的动态特性的。

✳️但是我们还有大量的文件,就在磁盘上,静静的躺着。这批文件非常多,杂,乱。
那你为什么要让这些文件躺在你磁盘上浪费你空间呢?你不要把它删了不就完了吗? 为什么还要留在磁盘上呢?
很简单,因为它随时可能被打开,从来永远不会被打开和访问的文件是垃圾文件,早被清理掉了。有可能被访问的文件必须得保留下来。
所以文件非常多非常乱,是不是有点像楼下的快递点,每一个快递都是将来要被买东西的人随时去取,但是这个快递的人什么时候来,快递点的老板并不知道。
所以所谓的磁盘级别的文件管理,本质工作和快递点的老板做的工作是一样的。 我们现在要做的是什么呢?把所有的文件按照我们的要求,把文件归置好。我们看到的所有快递,都是有编号的,老板按照他的要求摆好。快递点老板存在的最大意义是能够快速的存取快递!
所以对磁盘上的文件做管理,本质上就是对磁盘这块大空间,如何合理划分让我们快速定位,查找到文件,乃至后面相关的访问操作。

✳️我们把做这部分工作的模块叫做文件系统!
我们要理解它,就要把视角从内存中移开。我们的视角应该是要迁移到磁盘上!
所以我们以前讲的是内存级别的文件,是被打开的文件,它应该属于在我们现在要谈的文件系统之后。所以你想打开就打开吗?文件在哪里?好你说再当前路径,那当前路径又在哪里嘛?在磁盘的什么位置?你怎么知道!你说操作系统给你管,操作系统说吧文件随便放在哪里,操作系统自己都不知道自己在哪儿。所以必须得再系统层面上把文件管理好。

你知道磁盘是什么样子的吗?磁盘的内部是什么样子的吗?你知道磁盘内部存储单元和各种组件是什么样子的吗? 你知道操作系统如何看待磁盘吗?你知道分区在做什么?所以必须得吧这些理解和实验证明才能理解!
请添加图片描述

磁盘的物理结构

对文件系统我们有时候很难想象,因为跟硬件关系很大。

磁盘是我们电脑上的唯一的一个机械设备,指的是存储结构当中。

目前,同学们笔记本不用磁盘了,而是SSD。它是固态硬盘,它内部虽然也是各种硬件设备,但固态硬盘里面没有一些机械结构。所以相对而言对我们同学用起来主要特点是效率会高很多。固态硬盘完全是另一种存储方案和磁盘存储方案差别很大。它单价上要比磁盘贵很多。
SSD虽然是固态硬盘,其一,从耐用性是不如磁盘,SSD一般都有自己的读写上限,比如几万次,短的两三万,长的四五万,但磁盘可以读写很长时间。其二,磁盘有自己主要特点,单价上便宜。便宜的话呢所以再企业的后端虽然笔记本上基本不用了,但是公司内部服务器基本都是磁盘。
磁盘在互联网非常重要,几十万服务器,没块服务器配上8/10块磁盘,若用20万台,则真的是很大价钱。更重要的是磁盘有自己的使用寿命,短的两三年,长的四五年,所以又是大价钱!

请添加图片描述
磁盘有很多盘面,光盘的面和磁盘很像。磁盘的结构里面有盘片,光盘是一面是光的,另面不是。但是磁盘的盘片两面都是光的。磁盘里面有一摞盘片,每个盘片都有两面,两面都可以放数据。
磁盘里面还有一个马达,马达一旦转起来,盘片就会高速旋转。
与此同时磁头和这个盘片并没有挨着,将来我们的计算机寻址的时候,数据放在盘片上(2进制)。
然后呢,一方面,盘片在高速旋转,磁头在摆动,就可以完成。磁头摆动决定的是访问盘片的内侧还是外侧;磁盘旋转决定的是,内圈当中的哪一个位置,通过旋转来完成。磁盘旋转的转速慢的有7200转,快的有万转。转速完全取决你的马达有多快,还有你自己在进行定位寻址的速度有多高。大家可以想象一下,将来寻址会特别忙,盘片在疯狂旋转,磁头在来回摆动,其实就是在这个时候写的。
怎么写到下面?记住了一个面一个磁头!有一摞磁头!数据就在面上存储着的。关于怎么存的,下面有存储结构来说。
下面除了我们说的马达,磁头,盘片,还有电路板。电路板能够接收到驱动程序给他发布的磁盘协议,有的控制磁头摆动,有的访问磁盘内部某些数据的,反正所有电路合起来可以称作四幅系统,四幅系统是他的硬件结构。然后这些事大概结构。
磁盘目前制作工艺非常高,磁头和盘面不敢挨着,碰着就会有火星。磁盘机械结构➕外设决定了磁盘一定是很慢的(针对于CPU,内存来比较)。一般磁盘盘面可以存很多二进制。因为是机械结构,很多老的台式机用的还是磁盘,或者磁盘➕SSD混盘。我们开机的时候会有特别大的声音,即便是风扇转了,磁盘转声也很大。磁盘盘面非常光滑,磁盘依旧有自己的排布方式的。因为磁盘和磁头挨着特别近,所以磁盘里面不能有任何灰尘。

磁盘存储二进制数据:磁性的东西是会有N和S两个极。我们都知道电脑是存储二进制的,但不要只能为二进制就是存0/1。0/1又是什么呢?0/1其实也是被我们计算机渲染过的概念,就是给他的定义。
其实0/1有很多的情况,根据你的硬件设备不同,它的含义和底层实现是不一样的。比如说有的设备,入内存它把电子信号存储起来,可以根据有无电流来表示0/1;还有一些比如是电路脉冲,我们的光电信号,他们在传播的时候,我们也可以用有无,也可以用疏密和高低电频来区分0/1。
那么磁盘是一个磁性的设备,那么我们就可以根据N/S来区分0/1。0/1是一个概念,但是不同的硬件,对于底层的0/1设备的存储方案不一样。
纽扣式的磁铁一面时南极,一面是北极。然后磁盘如何理解呢?我们可以把纽扣式的N/S结构,把它想象成一个特别小的东西放在磁盘上,然后磁盘上有无数个小的N/S构成。
所以我们磁头在进行写入时,它做的工作,是在改变南北极,就是在改变0/1;

所以我们自己建立的文件、目录结构,将来文件读写的时候,最终你的文件数据就在这个盘面上!反正知道是根据0/1数据去存储就够了。
在这里插入图片描述

磁盘的存储结构

在这里插入图片描述
盘片旋转的时候,磁头在来后寻址,最终就找到磁盘上的某个数据。
下面告诉大家,磁盘可以被划分为一堆一堆的同心圆,以上帝视角俯视这一摞磁盘,它存储数据的时候呢,是把数据数据存储在同心圆上的,我们把同心圆称作“磁道”,可以看到磁道内圈比较短,外圈周长比较大。但是呢以圆心为起点,可以吧它再进行划分。
我们真正存储数据不是以磁道为单位的,而是是小段的跟扇子一样区域。这个区域呢我们称之为“扇区”。
磁盘上存储的基本单位是“扇区 ”。我们将来写数据,不是以0/1序列为单位的,内存读写大小叫字节。磁盘也有读写大小,叫扇区。
磁盘上存储的基本单位是扇区大小常见为512字节。
我们要知道,一个盘片是有背面的,一摞盘片都是由这么一面面盘片构成。所以我只要先把这一面搞清楚。所以对我们来讲,我们其中数据是在扇区上存储的。
所以将来磁头在进行定位的时候,磁头找的是某一个面 的 某一个 磁道 的某一个 扇区。也就是说我们将来要读写磁盘的时候,我们比如说要写上一块数据是512字节,我呢磁头先要确定是在哪一面,确定好一面后,里面有这么多磁道,同心圆式的散开 ,无数的同心圆。你要的是哪个磁道,然后磁道有一圈之后,在通过盘片旋转找到对应的扇区。
所以我们找到某一个面是由:哪一个磁头决定!
哪一个磁道,就是哪一个柱面!(解释柱面:因为一个一个同心圆叫做磁道,但是我有一摞这么一样的同心圆,叠加起来立体化不久形成了一个柱面吗?)
所以柱面和我们理解成的磁道是一个概念,说白了,你是哪个磁道就是你里圆心的半径是多少。
其实你是哪个磁道也差不多是由磁头决定的。所以磁头确定的你是哪一面,然后继而确定你是这面当中哪个磁道(距离圆心的位置)。
某一个扇区呢,它是一个磁道上的一段扇区,它是由盘片旋转决定的!
所以磁头决定两个东西,你是在哪一盘片上的哪一个磁道,而你在磁道道哪一个位置是由盘片旋转决定的。
所以磁头一般在旋转的时候,定位的时候,它先将自己的磁头移动到指定的磁道上,然后悬浮不动,等你盘片旋转。如果你的访问的扇区在某一个位置,如果磁头对准了那一段扇区就开始读写了。
上面就是磁盘存储结构。所以对我们来讲,我们一般把磁盘当中的某一个盘片的的某一面的某一个磁道的某一个扇区,我们能找到就能找到特定的位置了。
换而言之,只要我们能找到磁盘上的盘片的盘面,柱面(磁道),扇区就找到了一个存储单元用同样的方法,我们可以找到所有的基本单位。

所以我们文件系统:什么文件,对应了几个磁盘快。(就相当于,你建一个文件,相当于是10kb,你10kb可能用了20个扇区,你用了哪几个20个扇区呢?我只要能知道你文件对应的是哪几个磁盘块,然后采用上面的方法不就把文件数据全读到了吗?)
请添加图片描述
我们一般把磁头叫做Head;磁柱(柱面)叫做Cylinder;扇区叫做Sector;所以呢我们这种在物理上,查找某一个扇区的寻址方式叫做“CHS地址”(也就是在那个盘片,哪个磁头,哪个扇区上)

也是我们现在已经知道了,磁盘长什么样子的,也知道了内部结构是这个样子的,然后我们最终找到磁盘当中的任意一个位置,磁盘读写单元是一个扇区512kb,然后我们要找的话,就只要找到你对应的是哪个盘片的哪一面的哪一个柱面的哪一个扇区,然后我们最终到确定的位置。
所以现在的问题是对我们来讲呢我们把这种定位磁盘当中最小基本单元的地址叫做“CHS地址”。
所以只要你随便给我“CHS地址”,我呢就能够直接帮你找到磁盘当中对应的扇区!数据给你拿出来 ,这就是磁盘的存储结构!

有人可能会问,外部的扇区比内部的扇区宽很多,问它们的容量都是512吗?是的,它们的容量都是512,它是通过我们的扇区的二进制序列NS结构或01结构通过设置不同的密度来实现的。当然也可以做成不一样的,技术达到就行!

磁盘的逻辑抽象结构

我们现在已经知道了,磁盘会把数据放在它的盘片上的。
我们应该知道磁带,磁带上面也是有数据的,我们把磁带拉出来,拉成一条直线的样子,所以盘片也是圆状的,也将它拉成直的。
我们把盘片想象成为线性的结构,请添加图片描述
它的每一个单元存储着对应的就是扇区512kb,那么其中这么长的一段东西,我们可不可以把它当作数组呢?入struct disl【100000】,所以一瞬间对磁盘的管理,定位成一个sector扇区,只要找到下标就行了
所以对磁盘管理,转化成了对数组的管理!我们把它叫做:先描述,再组织!
我们把这一摞的盘片想象成500GB大小的数组,那么接下来我们怎么能够转化成对应的找到磁盘的地址呢。
现在我们找到一个扇区,只要找到下标就可以了。这种下标对应的地址我们叫做“LBA”,这是操作系统用磁盘的时候,认为磁盘基本单元的地址,也叫做逻辑块地址。
所以你未来想要向磁盘当中写入,只需要告诉我,比 如有一批内存,内存里面有数据,想往我们对应的磁盘当中写入。对于内存呢只知道有一个地址叫做LBA地址,然后它的数据要写到磁盘的某一个位置,那应该怎么做呢?
它肯定要首先吧LBA地址转化成CHS地址,转化好,配合CHS地址写到对应的位置,就完成了写入!
所以对我们来讲,我们现在的问题就转化成,所谓LBA地址转化成CHS地址,那应该怎么转呢?试试讲讲

逻辑上把磁盘想象成数组,假设磁盘 一共有两片,一共四面的数据大小,假设LBA地址=1234,此时4面的数据大小为4000 。所以数组的前1000个区域属于第一面,1000-2000属于第二面;2000-3000属于第三面;3000-4000属于第四面。所以数组当中的每一个区域代表4个面的第几个面。
所以1234怎么办呢?1234/1000 = 1,说明在第一面,那么磁面H就为1了;
然后1234%1000 = 234,假设一面呢有20个磁道,然后234 / 20 =11,那就找到了11号磁道,也就是Cylinder柱面(磁道)= 11;
又假设每一个磁道有20个小弧段,所以 234 % 20 = 14,所以2个磁盘被拉成一段数组,又因为2个磁盘有4个面,所以一段大数组被划分为4个区域(0、1、2、3),然后一个区域又被划分为20个小段数组,一小段数组又有20个基本单元的小数组,所以Sector扇区 =14;所以我们将LBA = 1234的地址,就将它转化成了我们磁盘CHS地址lC = 11 ; H = 1 ; S = 14;就能将数据写到磁盘上!

磁盘的基本单元是扇区,每一个扇区是512个字节,但是有一个问题,操作系统说:磁盘你划分的空间有点小呀,每一次我只能访问512字节是不是,你是一个机械磁盘,每次让我搬来搬去的效率太差了。
所以操作系统又进一步,将数组再做抽象,然后以八个扇区为单位;也就是将数组中最后被划分为最小单元的一个数组,将其连续8个为一个单元。
所以最后以4KB为基本单位IO了。即4KB为最基本的单位。
说白了操作系统存储数据的时候,以4KB为单位读取。

此时又有一个问题,磁盘500个GB太大了,怎么办呢?将其拆分成小的区域,是不是只要把一个个小区域(100GB),然后将100GB拆成小的单元,假设这个小单元有10GB,是不是只要把10GB的空间管好,100GB的空间就能管好?因为管理方法可以ctrl C和ctrl V的。我把你拆成小的单元,500GB我管不好,那我把你拆成小块,你100GB管好了,你500GB不就自然管好了?
所以把500GB拆分成100GB的小区域,就叫做分区的过程!这么一个管理方式就是分而治之!

假设磁盘被拉成直的,大小为500GB,想象成一个以4KB为单位的数组。我们现在可以估算一下500 * 1024(MB)* 1024(KB) / 4 =131072000;所以block disk_array[131072000]这么一个数组的大小。所以对500GB做管理,变成了对这么大一个数组做管理!
里面有很多细节我不关心,但我就是把你抽象出来了。把你数组的下标地址,转换成对应的CHS地址,在磁盘当中定位我们的扇区把数据读取到内存里。
现在你就能为这个数组就是磁盘。下标为0就有可能是第一面的一个扇区以此类推。
只不过我以前物理看上去是圆的,但现在我就把你拉长,想象成一个大数组,操作系统访问你的时候以基本单位就行了。

✳️磁盘的基本单位是:扇区(常规字节512字节),文件系统访问磁盘的基本单位是:4KB。
为什么要这样呢?–➡️1.提高IO效率(512字节比较少,如果我一次能够读取上8个扇区,对应4KB的空间呢,也就是一次IO能够读取到4KB的数据,那这样我的效率就会比较高,因为一次读取4KB,我操作的也就是4KB ,曾经写入的时候也是按4KB去写。换句话说我读取的时候,只要经过一次的磁盘寻址,磁头来回在我们的柱面当中去找,然后磁头对应的是我们的那个盘片,盘片在旋转时候确定是哪个 扇区,这是机械式工作,次数越少越好。如果以前你按512字节读4KB,你得读8次。但是现在呢我只要做一次就可以了。所以整体IO数据效率高了。寻址的过程是最耗费时的!)
2.不要让软件(OS)设计和硬件(磁盘)具有强相关性,换句话说,就是解藕合!(就以缓冲区为例,你要将书交给你朋友,你自己去送,这样你既要干自己的事情,还要干顺丰的事情。有缓冲区的存在你只要将你的书交给顺丰,剩下工作你就别管了。所以同学把书给快递点,然后快递点把书送出去,你和快递员认识吗?你和顺丰老板认识吗?完全不关心,你们各自做自己事情,这就相当于你和快递小哥解藕。如果操作系统当时也按512字节为单位的数据读取,因为磁盘规定的基本大小为这么大,那我么操作系统也这样读。这样可以!但是这往往不是最优解。你如果说让操作系统读取扇区512字节,把操作系统和硬件规定成一样的,并且所有读写磁盘的操作都交给操作系统去做,这样可以吗?可以。但是万一硬件变了呢?硬件一变,你操作系统是不是就要改?所以不要让软件和硬件强相关。所以你硬件512字节或者你将来变了,但不管我操作系统只设计成我要的4KB为基本单位。这叫做解藕合。)

我有一个问题,既然操作系统把数据读到内存是按4KB的,那么内存申请空间的时候也是要按照4KB?是的,所以呢我们这里有个概念,我们在讲多线程的时候会提到。我们把磁盘上对应的4KB大小的数据,我们称之为页针。我们把用来装4KB的内存空间叫做页框。

这里500GB太大了,举个例子我让你去管全中国大学的宿舍你好管吗? 大有大的难处。所以我们这里有500GB空间太大了,所以此时将500GB的空间划分一下。所以我们将500GB划分成100、150、50、100、100GB。我管500GB太难管,但我管100GB是不是好管,换而言之 只要管理好这100GB不就管理好500GB吗?是的。
磁盘大小不就是容量的差别吗?是不是只要把1GB管理好了,方法和管理的数据复制和粘贴不就好了吗?

✳️所以只要管好局部就能管好全部!
但是100GB还是太大了,我们把100GB再次划分,又拆成了很多个组,把很多组有划分成小组,那么我只要把小组管理好了,那么100GB所构成的一块块都管理好了吗?把100GB管理好不就是,把管理经验复制和粘贴到其他模块当中吗?不都管理好了吗?
我们对磁盘一个分区内部,拆成一个个的组,只要把一组管好不就管理好了一个分区。---------➡️对磁盘组进行管理
所以对如何管理文件,变成了对一个小组的数据管理。

我们在win上看到一个个C区,D区等其实只有一块盘,只不过把我们的盘分区了。
请添加图片描述

如何对磁盘组进行管理

一个磁盘有好几个分区,一个分区呢比如是100GB,一个分区里面一般开始有一个Boot Block(一般计算机在刚开始启动的时候,就是你按电源 ,就要找你主板上的base IO system,它是一个硬件,里面有大改500多个字节的存储空间,里面就存储了我们的磁盘设备,然后启动后,就要去找计算机里面操作系统在什么地方。所以一旦启动后就要去读一个分区里面Boot Block当中一个机器的开机信息,开机信息包括把一块物理磁盘划分了几个区域,也就包含了分区表这么一个东西。同时告诉我们操作系统这个软件在什么地方 。所以读取这么一小块数据,就能找到操作系统并加载操作系统,俗称开机,所以Boot Block与开机有关。但是Boot Block不是我们的重点。)
然后一个个分区里面呢又分成了一个个小组,也就是0~n个组,即Block group。假设这个分区有100GB,假如它分了20个小组,那么每个小组5GB。
然后这么一个小组又分成5组,那么最后一组的大小就为1GB。所以最后对整个磁盘做管理,就得把分区管好;你想吧分区管好,就得把块组管好;最后就得是把1GB的空间管理好。
所以我们以前要管理500GB,但现在我只要把1GB管好了,就管好了全部。这种思想就是分而治之。

下面正式来谈1GB空间如何来管理。

✳️曾经讲过,文件 = 内容 ➕ 属性。(内容是数据,属性也是数据)–都是数据===那么都要存储!
那么Linux采用的是将内容和属性的数据分开存储的方案!(就是说内容上呢你存你的内容,属性上呢你存你的属性。反正分开存就行了。)
我们会将内容放入block俗称4KB的空间。我们把文件的属性呢,放在inode中,inode就是磁盘上另一份空间。就是说我能存你的数据放在block中,那么我inode也能够存储在磁盘另一份空间里,大小一般为128字节。将来128字节里面,一个扇区里面512字节,存上4个inode,
说白了就是inode存储的是文件的属性!
要解释一个东西:内容这个东西是可以不断增多的,但属性值呢是稳定的。因为你要什么属性,比如文件名、文件大小、文件权限等,后面无论你怎么改,无非就是改属性里面数字大小。但是我们属性不会增多了,该是多少还是多少。所以inode是定长。

✳️现在隆重介绍 Data blocks:那么这个Data blocks呢,它呢可能就占了1GB空间的80%,那么80%的空间是Data blocks,也就是8000MB。
在这么一个Data blocks块组内,它4KB为单位就会有几千个block。每一个都会有自己的编号。这个编号呢不就是数组下标0、1、2…这样往下排嘛。所以这么一个4KB的block块大小主要做什么呢?
所以Data blocks是以块为单位,进行文件内容的保存。
如果你够4KB你存4KB,如果你不够,那么磁盘上存的时候,操作系统照样给你按4KB写,哪怕你只存储一个比特位,我也照样按4KB给你写;如果你最终文件是9、10KB ,系统照样也得按4KB的倍数给你,即3个4KB给你存储数据!换句话说这里是以块为单位给你存储数据的。

✳️inode table:以128字节为单位,进行inode属性的保存!换句话说呢,这个inode table其中里面会包含大量的inode空间。
比如说inode table里面也有一段空间,只不过里面的空间呢保存的是一个一个inode大小为128字节。
那么这么一个128字节的空间保存的什么呢?主要用来保存文件的属性。你甚至可以想象成我们可以定义一个struct
struct inode
{
int id;//自己的编号
mode_t node;
user name;
data d;

}
然后我们定义一个struct inode i - {};然后就把 i 以二进制写入到磁盘里面就可以了;

✳️换句话说我们把内容和属性分开存,inode table这里是所有我分组当中文件的属性的集合;Data blocks是文件的内容集合。

❓其中对于inode而言呢我有一个小问题,我们每一个文件,怎么标定文件的唯一性?
那么我们inode属性里面,有一个叫inode编号。inode编号是在我们组当中,因为inode编号不是在块组内分配的,而是在分区层面上分配的,所以在分区内部inode是具有唯一性的,你现在理解成是全局的,我的一个inode和其他inode都不一样。

✳️一般而言一个文件一个inode,相当于一个文件只能有一个inode。(当然一些文件没有inode后面说)
一个文件一个inode编号。

实验证明:
我们在命令行:#touch file;#mkdir dir;
然后#ll -i此时i就是inode编号意思,然后可以在文件权限前面看到一串数字就是编号,也就是文件的唯一标识符。请添加图片描述
我们现在已经知道Data blocks是所有文件的数据集合,它是以Block group块为单位4KB大小;inode Table是所有文件的属性集合,它是以inode128字节大小为单位,只要我找到inode号,就能在inode Table里面定位。

✳️Block Bitmap:我们有很多的Block group数据块,数据块的大小为4KB。假设我一共有1000个数据块,你怎么知道哪些数据块是否被使用了?你怎么知道在你小分区里面1000块里面哪些已经被申请,哪些是可以被覆盖呢?
若只能通过去遍历的话,遍历还只是一方面。它也不好处理。那么我们就可以用位图去做,我们就可以用1000个bit比特位。我以8个为例:0000 0000假设从右往左分别为0、1、2…就对应Data blocks里面0、1、2…编号的一块一块。若0号位为0说明没被使用可以被申请,若为1说明已经被占用。说明用比特位的内容来代表数据块是否被占用,用比特位的位置来代表哪个一个数据块。
在这一大堆数据块里面,哪些被占,哪些没被占?很简单, 我把你的block弄到内存里,做一个位图级别的遍历和统计,就能能知道哪些数据块被占和没被占。

✳️inode Bitmap:判断inode块是否被占用!比如你有1000个数据块,对应的是800个文件(一个文件可能占多个块),800个文件就一定会存在800个inode,800个inode哪些被使用了?哪些没被使用?你怎么知道?这就有了inode Bitmap,它的作用和Block Bitmap差不多一样。

❓请问一个块组Block group里面:有多少inode,起始的inode编号,有多少个inode被使用,Data blocks里面多少block被使用,还剩多少,你的总Block group大小是多少?
这些信息很显然计算太麻烦了,就有了Group Descriptor Table。我们上面问的所有信息,不就是这个抽象出来的块的属性吗?(你不是有n个Block group块组吗?你是几号块组呢?你的块组里面BitMap有多少个?inode有少个,block有多少了?inode使用率是多少?block使用率是多少?毕竟你inode是全局的,在这个分区里面,你inode起始编号是多少,然后根据inode值➖起始编号就能得到在这个分区里面是几号inode。但是呢对我们来讲上面的属性是在inode Table和Data blocks是体现不出来的)----------➡️Group Descriptor Table

✳️所以必须得有个Group Descriptor Table—GDT(块组描述符)。

✳️当我知道了我是Block group块组里面第几号块,我里面Data blocks里的blocks申请了多少,我的inode有多少?起始indoe编号是多少?这些都可以通过Group Descriptor Table—GDT得知。
可是你一个分区里面有少个Block group块组呢?每一个块的inode是多少,使用率是多少等等这些属性是不是也的要被管理起来呢?
----------------➡️Super block!

✳️SB(Super block):就是我们文件系统的顶层数据结构了!(它表示的就是:整个分区的一共有多少个Block group块组,每一个Block group块组的inode使用情况是什么,每一个Block group块组使用情况是多少,整个Block group块组是多大,整个分区从磁盘的几号到几号是我的区域 ,整个分区它是什么样的文件系统,文件系统总类是什么就全部把这些属性写到了Super block里面,所以它是文件系统的顶层数据结构。所以Super block管理的是我们宏观上整个分区,然后GDT管理的是整个分区内的一个Block group块组)

❓按你这么说Super block怎么能放在和GDT一个组里面呢?不应该放在和Block group里面吗?就相当于Boot Block负责启动,Super block负责整个文件系统的快组的分布情况。那他怎么会在每一个块组内部内部设计一个Super block的结构呢?
不要多想,理论上应该按照你们说的那样,但是不知道有没有经历过,经常异常关机后,win启动后告诉你:检查到你文件系统有所损毁,是否执行修复操作,你直接yes。过一会儿你会发现你文件系统恢复出来饿了。
其实这里每一个Block group不是所有的Block group都有Super block。可能在我们分的5个块组里面只有两三个Superblock,而且完全一样。主要是为了做备份!
万一我在读取文件系统属性从某一个位置读,时间久了这个区域坏掉了,坏掉了怎么办?万一只有一个Super block,整个分区全都弄掉了。不敢这么干!
你说你块组1GB出现问题,我能忍。但是你整个这么一个分区坏掉了,那坏了全都找不到了。
所以得在每一个常见的块组内做我们的备份!仅此而已。所谓的备份不就是拷贝回来,重新做读写就完事了。

❓还有一个问题,我们说 文件 = 内容 ➕ 属性,Linux采用文件内容和属性分开存储。我们每一个文件匹配一个inode。所以一个文件如何和属于自己的内容关联起来呢???
按照你刚刚的说法,这里inode Table里面有一个文件的inode,这个inode有编号比如是1234,我只要有这个编号就能找到文件和属性。可是Data block里面没有编号呀,你不是说里面存的是数据呀,它里面没有任何编号。我怎知道哪些block是属于这个文件的呢?(当你想要稳定性的同时一定要在效率上做取舍!跑的稳的话就必须得跑得慢很少又快又稳。)

✳️我们现在知道的是,我只要知道inode编号就能找到inode。数据块呢是独立在Data blocks里面存的。我找文件肯定是既找属性也要找内容。那怎么找呢?
在对应的一个inode里呢,构成了struct inode
struct inode
{
//文件的所有属性
blocks[15]—是一个数组,数组里面会涉及[0,11]直接保存的就是该文件对应的blocks编号
[12,14]对应的下标会在下面讲干什么的!

}
所以我只要找到文件的indoe,然后也确定在某一个确定的 Block group里面后,在inode Table里面找inode,找到之后就找到blocks[15],然后里面是文件内容blocks的编号,即指向我们Data blocks里面的blocks某些块,就能直接找到该文件所对应的内容!


❓但是又有一个问题,你现在[0,11]保存的是编号,我们12 * 4(每一个块大小为4KB) = 48KB,所以Linux下面最多只能保存48KB的文件吗?,很显然并不是!

✳️不要认为某些特殊情况,Data blocks里面的每个块大小为4KB,它也可以保存其他块的编号!
假设你一个块大小为4(KB) * 1024 = 4096Byte ,然后4096 / 4 =1024 。所以我们此时一个block4KB也能直接保存其他的数据块编号 。
换而言之我们接下来也照样,可以用我们对应说的[12,15]下标呢,来指向一个Data block,但是这个data block不保存有效数据,而保存该文件所使用的其他块的比那好。(实际上涉及到,我的[12,14]中12直接指向的是一个块,但他这个块里面对应的不是直接的文件数据,而是做一个类似于二级索引,这个块里面可以保存4KB的其他它块的编号大小!比如一个块4KB ,它能保存1000多个块编号,进而可以让它在自己块内找到其他的块,这里相当于用一个二级索引的方式。你可以再算算,相当于1000多再乘以4KB,不够的话再用三级索引。反正呢,我们可以用不断添加数据块的方式来进行定义更大的文件。目前Linux做到二级索引够多了,了解就行了!)

✳️我们不仅仅直接用blocks[15]里面的[0,11]直接来映射找到对应的blocks的编号,我们也可以根据[12,14]来进行二级三级等映射更多的块!
总之indoe属性呢会保存我们对应的文件内容的blokcs的编号,所以文件的内容和属性就关联起来了。所以我们找一个文件的inode,就可以找到这个文件的所有内容了。

❓文件名算文件的属性吗?算!但是inode里面并不保存文件名!!!
所以linux下,底层实际上是通过inode编号来标识文件!

✳️要找到文件,就一定要找到inode编号!
(因为我只要知道了inode编号,我呢就一定能确认inode在一个分区内的那一个小组,比如第一个Block group用的是0到100000,以此…,所以我可以根据inode编号确定你在哪个组,然后在组内根据你的inode编号➖起始编号,确定你的inode是谁,inode知道了你的文件所有属性和数据都能找到。属性拿到了inode里面属性blocks[15]就可以映射到Data blocks内的一块块blocks编号确定文件的内容!所以我们找到一个文件inode,就找到了所有)

❓谁来帮我去找inode编号呢?
Linux下一切皆文件,那么目录是文件吗?是的!如果目录是文件,那么文件= 内容 ➕ 属性。 既然目录是文件,那么它对应的属性必须得有自己的inode,我们目录里面对应的内容是什么呢?
即目录的Data blocks数据块放什么呢?我们曾经讲过目录权限的问题,进入一个目录需要一个X权限!
我们创建一个权限是W权限,查看文件是R权限。

❓既然目录这个货也是文件的话,目录文件也是文件,它也有自己的inode,也有自己的数据块,那么它数据块里面放的是是什么呢?
✳️目录的内容放的是文件名 和 inode编号的映射关系!
(所以目录也是一个文件,它也有自己属性,目录文件保存的是什么呢?保存的是文件名和inode编号之间的关系!)

❓文件名和inode编号是数据吗?
–是的,是数据!那么最终就保存到了目录的内容当中!!
那么它是一个目录的数据,我们其实目录里面最终保存的是目录的属性➕目录的内容,那么它的内容就是它里面的文件名与其对应的inode编号关系!

❓所以Linux同一个目录下,可以创建多一个同文件名吗??
不会!
✳️文件名本身就是一个具有key值的东西
(也就是说在一个目录里面,我获取一个文件名,那么文件名和它的inode编号一定是一对一的关系,不存在多个同文件名)

❓当我们创建一个文件,操作系统做了什么?
操作系统在创建一个文件时,它需要在特定的一个分区的特定块组,先查找一个inode Bitmap,查到一个没被使用,首先将这个比特位由0置1,然后在inode Table里面自己创建的文件属性值写到对应的inode Bitmap的位置的编号里面。其一这个inode绝对没有被占用,其二谁写的,用户名,时间等等都做好。当前文件是空文件,所以不会使用到Datablocks,那么inode里面的block[0,15]全部置为0;当你进行写入的时候,系统会帮你查找Block Map里面哪些块没有被使用,然后把你要写的数据写到块里面,然后填上它和inode之间的关系。
所以创建文件时候,操作系统给我们做的是修改ionode Bitmap,将inode Table里面找到对应的inode节点,然后向他里面去写入对应属性,并分配对应的数据块,把数据写到数据块里面,同是修改Block Bitmap,并且同时建立inode和blocks的映射关系,返回该文件的inode;

✳️创建一个文件时,你一定在一个目录下!
所以当我们把一个文件创建好之后怎么办呢?然后我们拿到新建的文件inode,拿到inode之后怎么办呢? 将创建的文件的文件名和inode编号把这两数据找到自己所处的目录,该目录也是一个文件,也有自己的内容和属性。根据目录的inode找到目录的Data block将对应的文件名和inode编号关系写入到目录的数据块当中!
这也就解释了inode为什么不保存我们所谓的文件名。因为文件名在我们的目录里面。

❓请问删除一个文件,操作系统做了什么呢?
我肯定知道自己在什么目录下,找到自己目录,然后找到目录的inode,然后找到目录对应的block,block里面有文件名和inode编号对应关系
首先文件名在该目录下是唯一性,根据文件名做查找,找到目录里面的一个文件名和inode对应的关系,然后找到inode编号,此时找到inode编号,根据他所处的 Block group,然后只要把该文件对应的inode Bitmap由1置0,把你这个文件对应的block Bitmap也由1置0,此时就完成了文件删除。
有人说只要在目录下解除文件名和inode之间的映射关系就可以了,这个说法不太准确,映射关系肯定会去掉,但肯定要把文件对应的位图inode Bitmap和block Bitmap都要做修改,这样文件才被删除,必须要这么做,这样才可以二次使用!
我们会发现一个现象,我们拷贝一个电影的时候,4个G高清的,拷的时候我花了一两分钟,但是删除的时候,只花了一秒钟不到,其实在做对应的删除时候,它只是把对应的标记该文件属性和内容的位图结构由1置0就相当于删除了,然后最后在该文件所处的目录当中把该文件的文件名和inode映射关系删掉,就彻底删除了。

❓Linux有没有真正的清除数据呢?
没有真正清除数据,当我们把一个文件对应的inode Bitmap和block Bitmap对应的位置由1置0,这个文件就删掉了。
我们最害怕的是,后续你依旧在创建文件,继续在创文件,然后向文件里继续写入,当你在不断新创建文件时,一定会带来结果,你曾经刚刚被释放的inode Bitmap和block Bitmap对应的位置由0置1,然后inode的属性从新覆盖了。
换而言之,我们把Linux文件删掉之后,我们能恢复这个文件吗?可以,因为删掉的时候数据根本没有被清除。
恢复就是只要你能帮我找到曾经你删掉的文件的inode值就行了,然后根据inode把inode Bitmap和block Bitmap对应的位置由0恢复成1。然后根据inode编号在inode Table里面找inode属性,找到里面的block[0,15],然后根据映射关系找到block Bitmap,将其由0置1,就恢复了。

❓我知道自己所处的目录,就能知道目录的inode吗?
如果我知道自己目录名,不就知道我的inode吗?答案是:不能!
你如果知道你自己的目录的话,便会有对应的inode没问题。但是你只有目录名,所以你得去查你的父目录里面保存的是你目录名和inode映射关系。所以按照这个思路都要根据根目录去找。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
✳️1.我们使用命令#ls,它一定是找到该目录的inode编号,根据inode编号找到inode,inode里面有属性,属性里面有对应inode 和 数据块的映射关系。找到数据块只把文件名列出来。

✳️2.我们使用命令 #ls -l,ls找到了目录的数据块,也就是说目录的内容有了,内容里面有文件名和inode编号的映射关系,继续拿着每一个文件的indoe编号去查找每一个文件 inode,将文件自己的属性拿出来,此时拼接文件名就有了#ls -l之后的信息。
✳️3.我们输入命令#stat ➕具体文件名,可以读到文件的更多属性。

这就是在了解文件系统后给我们带来的好处。

软硬链接

✳️创建链接的命令是“ln”,也就是link简称;
1.#ln -s my,txt my.txt.soft. ------➡️-s表示软的意思,也就是创建了一个软链接
在这里插入图片描述
2.#ln my.txt my.txt.hard-------➡️表示创建了一个硬链接

在这里插入图片描述
✳️软硬链接的区别:
软链接是一个独立文件,有自己独立的inode和inode编号。而硬链接不是一个独立文件(那是什么?)它和目标文件使用的是同一个inode!(软链接的inode是一个新的inode。而硬链接用的是被链接的文件的inode值)

✳️软链接其实是一个独立的文件,那从哪里可以看出来呢? 因为软链接有自己的独立inode编号。
软链接通常用来作为我们对应某一个文件的链接,来帮我们进行对应的文件的快速访问。(我们有一个可执行程序,我们想运行该程序,但是该程序的路径太深了,假设原来是./d1/d2/d3➕mytest.exe才可以跑,但是我们#ln -s ./d1/d2/d3mytest.exe my.exe 此时我们就创建了一个软链接,接下来想运行的话就./my.exe直接就能跑了!)
所以软链接相当于Linux下的快捷方式。(今天我们讲的是软链接可以链接可执行程序,未来我们可以链接某些头文件,链接某些库文件,就不用让我们冗余的再到某些地方找这些库,那样就太麻烦了)

✳️硬链接是什么:如果my.txt.hard对应的inode还是目标文件的inode,那么ln 在创建所谓的硬链接的时候,它本还是执行的该文件my.txt,my.txt本来就存在了,它inode就是那个值,那你后面硬链接的也是那个inode值。该inode值的文件本来就存在,那么请问你这my.txt.hard干什么了呢?
软链接可是创建了新的文件,该文件是被分配了inode编号。然后就得有inode的属性和数据块等等。但是硬链接干了什么?其实非常简单,所谓的硬链接就是单纯在Linux指定下的目录下,给指定的文件新增 文件名和indoe编号的映射关系。

✳️那怎么去删除链接呢?
建议用"unlink":#unlink my.txt.soft;便可以删除;
那么unlink可不可以删除不是软硬链接的文件呢?答案是可以的!可以用来删除文件,但一般它是用来删除链接。

我们说了硬链接相当于在该目录,给指定的文件新增文件名和inode的映射关系。换句话说,两个文件名映射的是同一个inode值。但是会发现有一个数字发生变化了,就是硬链接数子发生了改变都为2,如果我继续.#ln my.txt my.txt2.hard,链接数就变为3了
在这里插入图片描述在这里插入图片描述
✳️什么是硬链接数?
inode编号不就是一个“指针”的概念吗?我们会在各种教材都会有,尤其是看文件系统相关的,操作系统相关的书。它数据明显在磁盘上,说定义一个指针指向什么东西。我们今天说了inode属性里面会保存一个数组blocks[0,15]是数据块的编号,也有可能会说保存的是数据块的指针。所以只要是能够标定某一个资源的,都能称为指针,
硬链接本质就是该文件inode属性中的一个“计数器”,count;标识有几个文件名和我的inode建立了一个映射关系。
简而言之,就是有几个文件名 指向 我的inode(文件本身!)
对我们来讲,当有一个人指向我,那我就引用计数++ ,再有人指向我,我就再++,我们把它叫做硬链接数。

❓我们创建软链接的时候,它也指向我们的目标文件呀,但为什么没有变化?
答:首先软链接文件是一个独立的文件,因为它独立的inode。既然它的引用计数没有变化,就证明软链接不是单纯的拿文件名和你的inode建立映射关系, 那要不然和硬链接就没有区别了。
它是独立文件,它有自己的独立inode。那么我们对应的软链接不是一个文件吗?
那么软链接的文件内容是什么?—➡️保存的是指向的文件的所在路径。(你想看是直接看不到的,是系统级别的)

我们现在知道硬链接的本质就是给指定文件添加文件名 来喝inode建立映射关系。
❓那么硬链接有什么用呢???
我们现在创建一个文件和一个目录,为什么文件被创建出来,默认的硬链接数是1,而目录的默认硬链接数是2???
文件默认的硬链接数是1,那肯定呀。如果是0的话不就意味着当前没有文件名和你文件的inode建立映射关系。那么你这文件在用户层面上没法被使用,属于被删除文件。默认的是你的文件名和你的文件inode建立映射,那么你文件名本身指向你的文件,所以就是1.
目录默认是2,首先肯定是自己的目录名和自己目录inode就有一个映射关系,就有一个了。另一个呢?任何一个目录里面,都会存在隐藏文件:一个点和两个点。其中呢一个点".“表示的是当前目录,它也是指向目录的inode所以又有一个了。所以加起来就有两个了!所以经常我们执行可执行程序:./程序名,为什么要用”.“呢。因为其中“.”就是用来表示当前路径。
那么两个点”…“是表示的是上级目录!那么两个点”…"的inode值与上级目录的inode是一样的!
❓为什么文件被创建出来,默认的硬链接数是1?
因为,普通文件的文件名,本身就和自己的inode,具有映射关系,只有一个!

✳️我们能够通过系统的一个硬链接数,我不进入你这个目录就能估算出你这个目录里面大概会有多少目录,引用计数-2那么就知道它里面的目录数了。

✳️刚创建目录的时候目录默认的硬链接数是2,当该目录里面创建了越来越多的目录则其硬链接数就会越来越多!因为每个在他目录下创建的目录都有一个符号:两个点"…"表示上级目录。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值