目录

一、什么是文件

如何在系统角度理解文件?

什么是一切皆文件

二、复习一下C的接口

perror

fwrite、fprintf、fputs

要不要+1的问题

①w模式

②a模式追加

fgets按行读取

深入理解cat之类的命令

总结

三、直接使用系统接口

1.open

如何给函数传递标志位?

1.第一个open方法

选项O_CREAT

选项O_WRONLY

2.第二种open的使用方式

open中传入标记位

设置权限掩码

2.close

3.write

选项O_TRUNC

追加模式的参数

4.read

O_RDONLY

四、分析系统接口的细节,引入fd(文件描述符)

第一个问题:0,1,2去哪里了呢?

fd是什么呢?

第二个问题:为什么是这样的数据

五、文件周边概念

文件描述符fd的理解

fd的分配规则

输出重定向的原理

输入重定向的原理

追加重定向的原理

重定向的系统调用

重新理解重定向 ​​​​​​​

输出重定向

追加重定向

如何理解一切皆文件?(理性)

缓冲区

什么是缓冲区?

为什么要有缓冲区?

这个缓冲区在哪里? 谁提供?

缓冲区在哪里?

六、我们自己设计一下用户层缓冲区

七、minishell中支持重定向

1.close关闭fd之后文件内部没有数据

2.1,2 stdout stderr的区别

perror()

strerror

八、理解文件系统

文件系统的背景知识

磁盘文件

磁盘结构

磁盘的存储结构

inode vs 文件名

九、软硬链接

软链接

特性

硬链接

十、动静态库

创建静态库

怎么用这个库?

1.将库添加到系统路径中

2.硬使用这个库

创建动态库

1.硬编译

动态库的加载

2.建立软链接


一、什么是文件

如何在系统角度理解文件?

在磁盘上建立一个大小为0的文件,占不占空间?

文件=内容+属性

即便是内容为0,磁盘上也会存储文件的属性。(文件的属性也是数据!)

文件的所有操作无外乎只有两种

①对内容(往文件中写helloworld)②对属性(更改文件的权限等等)

(对文件的内容的操作也可能会影响文件的属性,比方说文件的大小发生了变化)

文件在磁盘(硬件)上放着,我们访问文件,先写代码->编译->exe->运行->

访问文件本质是谁在访问文件呢?

是进程在访问文件。进程访问文件是需要通过接口(语言层面的接口C语言,C++)访问的。

要向硬件中写入,只有谁有权力呢?

操作系统。

那如果我们普通用户也想写入呢?

就必须让操作系统提供接口(文件类的系统调用接口),但为什么我们之前没听过?

因为文件类的系统调用接口比较难以写,所以我们的语言(C,C++)上对这些接口做一下封装,为了让接口更好地使用

每一种语言都会对这些接口进行封装,导致了不同的语言有不同的语言级别的文件访问接口(都不一样),但是,都因为封装的是系统接口,

为什么要学习OS层面的文件接口呢?

理由①这样的接口,在Linux上只有一套

为什么这样的接口只有一套呢?

因为你选择的操作系统只有一个。你属于安卓就是安卓,属于Linux就是Linux。

理由②这样的代码具有跨平台性。使用C++的平台都可以使用C++的文件接口,使用Java的平台都可以使用Java的文件接口。

如果语言不提供给文件的系统接口的封装,是不是所有访问文件的操作都必须直接使用操作系统的接口。(Windows的系统接口和Linux的系统接口种类,参数等等都是不一样的)

而使用语言的客户,要不要访问文件呢?

当然要。

一旦使用系统接口编写所谓的文件代码,这份代码就无法再其他平台当中直接运行了,那么这份代码就不具备跨平台性!

那C语言C++是如何做到跨平台性的呢?

把所有的平台的代码都实现一遍,封装好。

然后再采用条件编译的方式,在便器的时候实现动态编译。

显示器是硬件吗?

显示器既然是硬件,printf向显示器打印,为什么不觉得奇怪呢?

printf向显示器打印,也是一种写入,和磁盘写入到文件,没有本质区别!

C/C++程序,默认会打开三个文件流

标准输入、标准输出,标准错误

什么是一切皆文件

Linux下一切皆文件

键盘、显示器可以被看做文件吗?

键盘和显示器可以被看做是文件。但是我从来没有打开过键盘和显示器啊,但是依旧能够进行scanf,fgets,printf,cout。

上面的我们的c/c++中的文件指针是FILE*类型的,c/c++程序在编译的时候,把打开的代码都已经内置到你的代码里面了,这就是默认情况下会打开这些流。

标准输入->默认设备键盘

标准输出、标准错误->默认设备显示器

Linux认为:一切皆文件

感性认识:

对于文件(记事本,文件夹等等)而言(先不考虑文件的属性),无外乎两种类别的操作,就是读和写(read write)。

显示器:printf/cout本质上对应的是一种写入(write)。

键盘:scanf/cin本质上就是一种读取(read)。把输入的内容给显示器一份(让你看见)也给程序一份

站在你写的程序的角度,我们的程序必须加载到内存,站在你写程序的角度就是站在内存的角度,键盘将我们输入的数据交给内存,系统将内存中的数据刷新到显示器或者写入到硬盘当中,也就是分别对应着input和output。(也就是I/O的动作)

所以软件的行为,是可以被转化为是硬件的行为的。

那么如果就是一个普通文件,我们使用fopen/fread去读取它,它就被读取到了我们的程序(进程)的内部(内存),我们再使用fwrite将数据写入到文件中

从普通程序读取到进城内部(内存)就是input

从内存中写入文件中就是output

什么叫做文件呢?

站在系统的角度,能够被input读取,或者能够被output写出的设备就叫做文件!

狭义上的文件:普通的磁盘文件(.txt .doc文件等等)

广义上的文件:显示器,键盘,网卡,声卡,显卡,磁盘,几乎所有的外设都可以称之为叫做文件。(这些设备都具有上面可以被读或者被写的特点)

(狭义上的文件是我们今天谈论的广义上的文件的子集)

二、复习一下C的接口

man 3 fopen

w:清空文件或者创建新的文件,往文件的开头写

a:往文件的末尾追加新的内容(让我们的文件中追加更多的内容)

这里我们就想到了输出重定向和追加重定向!

fopen第一个参数打开的文件路径和文件名

第二个参数是打开的模式

perror

输出错误信息

#include <stdio.h>
int main()
{//我们当前的目录是没有这个文件的FILE*fp= fopen("log1.txt","r");//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== nullptr){perror("fopen");return 1;}
}

w方式的话,如果要写入的文件不存在,会直接创建一个文件 ,在哪里创建呢?

在当前路径。(Linux是这样的)

这个当前路径是跟着可执行程序走的!

但是不同的平台是不同的,比方说Mac平台上的clion是在这里的

其实这个程序根本就不是你的程序创建的文件,是当你运行起来,形成一个进程的时候,进程创建的文件!

(是进程通过你的系统接口创建的!)

我们不妨创建一个打开文件的进程来查看一下我们上面的说法

#include<stdio.h>
#include <unistd.h>
int main()
{FILE *fp=fopen("log.txt","w");if(fp==NULL){//sterroeperror("fopen");return 1;}//进行文件操作fclose(fp);while(1){sleep(1);}return 0;
}

我们将我们上面的这个测试代码运行起来,然后再用另外一个终端查找我们刚刚的进程./test的PID为18947

我们查看一下这个进程18947

这个就是我们18947进程的信息

从上面的图中我们观察到这个exe所指向的路径其实就是我们当前进程的对象

所以进程通过这个exe就能够知道我们当前进程执行的是哪一个程序!!

上面的cwd是我们这个进程的所指向的工作目录

cwd

进程的内部属性(current work directory)

也就是我们的当前进程的工作目录!!

将cwd所指向的路径,和我们上面传入的文件拼接起来,形成完整的路径名称

当一个进程运行起来的时候,每个进程都会记录当前所处的工作路径!

当你打开一个文件的时候,创建的文件就是在我们当前的路径下,也就是我们进程所处的路径下。

所以形成这个文件最终是怎么形成的呢?

所以我们进程内部会直接用这个进程的cwd也就是当前的工作路径,然后将文件名test拼接起来形成我们的exe找到执行的文件

所以为什么我们上面形成的log.txt会在和我们的程序相同的路径下,是因为我们的当前进程的工作路径也在这个路径下,所以也生成在这个位置!!

(进程具有确定性,一般我们将程序部署到系统当中了,路径一般就不变了,比方说你安装了一个程序带D盘,那么路径一般就是在D盘了)

fwrite、fprintf、fputs

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{FILE*fp= fopen("log.txt","w");//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== NULL){perror("fopen");return 1;}//进行文件的操作const char* s1="hello fwrite\n";//二进制的方式写入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;
}

要不要+1的问题

上面的strlen(s1)也就是写入的字符串大小,我们需不需要+1,也就是将\0的大小也计算进去?

上面这里要不要+1?

这里不要+1

因为这里\0是C语言的规定,文件要遵守吗?

文件保存的是有效数据!这里的\0并不是有效数据,仅仅是标定字符串结尾的标识符,不用+1

下面就是我们+1之后的测试

我们发现我们的文件中出现了乱码!!!

反而我们上面之前的没有+1的代码运行之后生辰多个文件是正常的!!

①w模式

执行下面的测试代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{FILE*fp= fopen("log.txt","w");//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== NULL){perror("fopen");return 1;}//进行文件的操作const char* s1="hello world\n";//要不要+1//二进制的方式写入fwrite(s1,strlen(s1),1,fp);//关闭文件fclose(fp);return 0;
}

原来我们的文件中是这样的

现在,我们看到了仅仅剩下了hello world,上面文件中的原先全部字符都不见了

这里我们就说明w是先将文件打开的时候,将该文件的内容清空,然后才写入内容!!

比方说我们下面就是用echo helloworld>log.txt往我们的log.txt中写入了helloworld

然后我们如果仅仅是>log.txt,我们发现我们的log.txt文件中的全部内容都被清空了,这跟我们上面所说的前将文件全部清空然后再开始写入的原理是一样的!

②a模式追加

测试代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{FILE*fp= fopen("log.txt","a");//append:追加//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== NULL){perror("fopen");return 1;}//进行文件的操作const char* s1="hello world\n";//要不要+1//二进制的方式写入fwrite(s1,strlen(s1),1,fp);//关闭文件fclose(fp);return 0;
}

fgets按行读取

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main()
{FILE*fp= fopen("log.txt","r");//append:追加//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== NULL){//strerrorperror("fopen");return 1;}//按行读取char line[64];//这里的fgets是c语言提供的接口,这里的fgets中的s是string的意思,会自动在字符的结尾添加\0while(fgets(line,sizeof (line),fp)!=NULL){//将读到的行显示出来//当然我们可以通过printf的方式打印出来
//        printf("%s",line);//当然我们也可以通过标准输出的方式打印出来//往显示器上写,stout就是标准输出fprintf(stdout,"%s",line);}//关闭文件fclose(fp);return 0;
}

深入理解cat之类的命令

从传入的文件名中读取文件

我们知道cat有两个参数,就是打印文件中的内容

#include <stdio.h>
#include <unistd.h>
#include <string.h>//test 一个文件名
int main(int argc,char *argv[])
{if(argc!=2){printf("argv error!\n");return 1;}//argv[1]就是我们传入的参数FILE*fp= fopen(argv[1],"r");//append:追加//如果打开失败了,就将打开失败的原因以文本的形式返回if(fp== NULL){//strerrorperror("fopen");return 2;}//按行读取char line[64];//这里的fgets是c语言提供的接口,这里的fgets中的s是string的意思,会自动在字符的结尾添加\0while(fgets(line,sizeof (line),fp)!=NULL){//将读到的行显示出来
//        printf("%s",line);//往显示器上写,stout就是标准输出fprintf(stdout,"%s",line);}//关闭文件fclose(fp);return 0;
}

这里它就会将我们传入的参数作为文件进行读取, 并且循环打印出里面的内容。

如果我们将rm test cat将其重命名为cat,就实现了我们Linux中的cat的打印的效果

总结

fopen以w方式打开文件,默认先清空文件(注意:在fwrite之前)

fopen以a方式打开文件,追加,不断向文件中新增内容

上述代码演示了文件的读和写的一般操作。

我们的c语言默认会打开三个标准的输入输出流:

stdin        键盘

stdout     显示器

stderr      显示器

这里的标准输入,标准输出,标准错误,在c语言中,我们都当成FILE*来看待

上面的代码中我们就是使用过了stdout

​​​​​​​

对于这个FILE*本身,其实我们并不清楚。

我们在下面会进行解释

三、直接使用系统接口

上面我们使用的是C库函数

那么系统调用接口与我们上面的库函数时什么关系呢?

系统调用接口是我们系统提供的接口,C库函数时语言层面提供的接口,系统调用的接口明显在语言的接口下面。

语言的调用必须调用底层的系统调用

C语言的库函数

fopen fclose fread fwrite

系统接口

open close read write

我们可以理解为fopen封装了open,以此类推

1.open

man 2 open

打开,或者可能创建一个文件

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);

第一个参数(pathname)是打开的文件路径+文件名

flags是选项,也就是手册中如下之类的

如何给函数传递标志位?

如何给函数传递标志位?

我们在哪里见过一个东西是纯大写的,用_隔开的?

宏定义!

我们c/c++中的一个标记位,比方说上面的flag只能够传递一种状态

但如果我们想要在c语言中传大量的选项呢?

标记位表示的是是或者否的意思,

一个整数就是32位,

一个标记位就是一个比特位,就可以表示大量的标记位

有一个数据结构就称为位图

#include <stdio.h>
#include <unistd.h>
#include <string.h>//用int中的不重复的一个bit就可以表示一种状态
//0000 0000
//第一个比特位为0或者1,表示one是否要打印
#define ONE 0x1
//0000 0010也就是第二个比特位
#define TWO 0x2
//0000 0100也就是从右往左第三个比特位
#define THREE 0x4
//我们想让我们的flag一次性传入三个标志位
void show(int flags)
{if(flags & ONE) printf("hello one\n");//0000 0011 &0000 0001if(flags & TWO) printf("hello two\n");if(flags & THREE) printf("hello three\n");
}int main()
{show(ONE);printf("-----------------------------------\n");show(TWO);printf("-----------------------------------\n");show(ONE|TWO);//000 0001|0000 0010=0000 0011,也就是将one和two全部都打印printf("-----------------------------------\n");show(ONE|TWO|THREE);//如果是0000 0111也就是将one,two,three全部都打印出来return 0;
}

​​​​​​​​​​​​​​

这就是系统传递标志位的一种方案。

我们这里就可以理解上面的open中的flags传递标志位的方法了

1.第一个open方法

int open(const char *pathname, int flags);

选项O_CREAT

选项O_WRONLY

以只写的方式打开

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd =open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);return 0;
}

我们发现我们的系统其实并不会帮助我们自动创建,我们还要添加一个O_CREAT选项

你在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能要做非常多的动作!

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd =open("log.txt",O_WRONLY|O_CREAT);if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);return 0;
}

OK现在我们创建成功了

但是我们发现这个文件的权限并不可用

这个接口只有在读取的时候用。

2.第二种open的使用方式

open中传入标记位

这里我们就是仿照上面的选项标记位来标记我们的参数

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);return 0;
}

这里的权限怎么少了一个呢?

我们观察到我们下面的权限仅仅是-rw-rw-r--并不是我们预期的-rw-rw-rw-

因为系统还有创建的默认权限umask权限(默认是0002)(也就是将0002的权限给减掉了)

umask设置系统的权限掩码

设置权限掩码

在进程的上下文中,设置进程的权限掩码。

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{//将权限掩码设置为0umask(0);int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);return 0;
}

现在我们的权限就是我们设置的666了(也就是我们上面所期望设置的-rw-rw-rw)。

其实我们文件已经存在的时候,使用具有两个参数的open函数就可以了

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);
//    int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-int fd=open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);return 0;
}

这里我们看到我们上面创建成功的时候open的返回值是3

那么为什么是3呢?

在下面一个目录四会讲解

2.close

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);
//    int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-int fd=open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}//open successprintf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

3.write


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-
//    int fd=open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}const char*s="hello write\n";//将hello write写入文件中要不要将strlen+1?//不用,这里我们写入文件中就是文件了,不再符合C语言中的那一套标准了!,上面已经解释过了!write(fd,s,strlen(s));//open successprintf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

这个是我们log.txt最初的状态

我们修改一下代码再往log.txt中写入googby


#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd =open("log.txt",O_WRONLY|O_CREAT,0666); //rw-rw-rw-
//    int fd=open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}const char*s="goodby";//将hello write写入文件中要不要将strlen+1?//不用,这里我们写入文件中就是文件了,不再符合C语言中的那一套标准了!,上面已经解释过了!write(fd,s,strlen(s));//open successprintf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

为什么我们的文件是覆盖式地从开头开始写的,并不是清空再开始写的呀?!

选项O_TRUNC

我们在写一个选项O_TRUNC

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd =open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); //rw-rw-rw-
//    int fd=open("log.txt",O_WRONLY);if(fd<0){perror("open");return 1;}const char*s="hello\n";//将hello write写入文件中要不要将strlen+1?//不用,这里我们写入文件中就是文件了,不再符合C语言中的那一套标准了!,上面已经解释过了!write(fd,s,strlen(s));//open successprintf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

我们看到我们的文件先被清空然后重新写入了 hello

追加模式的参数

也就是我们c语言中的a选项追加模式,其实就是将我们上面的O_TRUNC换成了O_APPEND

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);
//    int fd =open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); //rw-rw-rw-
//    int fd=open("log.txt",O_WRONLY);int fd =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-if(fd<0){perror("open");return 1;}const char*s="hello write\n";//将hello write写入文件中要不要将strlen+1?//不用,这里我们写入文件中就是文件了,不再符合C语言中的那一套标准了!,上面已经解释过了!write(fd,s,strlen(s));//open successprintf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

我们观察到我们的hello write就被追加到我们的原先的hello之后了

4.read

​​​​​​​​​​​​​​​​​​​​​

O_RDONLY

以只读的形式打开

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd =open("log.txt",O_RDONLY,0666); //rw-rw-rw-if(fd<0){perror("open");return 1;}char buffer[64];memset(buffer,'\0',sizeof (buffer));//fread在调用我们的read的时候才会在我们的最后加上\0read(fd,buffer,sizeof(buffer));printf("%s\n",buffer);printf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

我们看到我们的read确实读取到了我们的log.txt中的内容,

四、分析系统接口的细节,引入fd(文件描述符)

上面的代码,如何深入理解呢?

打开一个文件,默认的文件描述符是3

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-if(fd<0){perror("open");return 1;}printf("open success fd:%d\n",fd);//关闭我们的文件close(fd);return 0;
}

我们观察到我们的文件描述符的返回值是3

我们继续测试下面的代码,我们不妨试试看打开多个文件

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{umask(0);int fd1 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-printf("open success fd1:%d\n",fd1);int fd2 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-printf("open success fd1:%d\n",fd2);int fd3 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-printf("open success fd1:%d\n",fd3);int fd4 =open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666); //rw-rw-rw-printf("open success fd1:%d\n",fd4);//关闭我们的文件close(fd1);close(fd2);close(fd3);close(fd4);return 0;
}

这时我们观察到的是3,4,5,6

第一个问题:0,1,2去哪里了呢?

在c语言中,我们讲过,系统会默认打开stdin,stdout,stderr三个默认的文件,他们都是FILE*类型的,每一个文件都对应着一个文件描述符。

0标准输入

1标准输出

2标准错误

可以从标准输出流中打印

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{//fprintf往显示器上打印fprintf(stdout,"hello stdout\n");const char *s ="hello 1\n";//向1里面打印write(1,s,strlen(s));
}

可以从标准输入流中读取

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{int a=10;//scanf里面默认就有stdinfscanf(stdin,"%d",&a);printf("%d\n",a);
}

read的返回值是你实际读到的字节数,其中的参数size_t count中是你期望读取的字节个数

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{char input[16];//从标准输入中读取//从0中读取//这里的0就是stdin也就是标准输入流size_t s=read(0,input,sizeof (input));if(s>0){//手动添加\0input[s]='\0';printf("%s\n",input);}
}

这里的0,1,2都是int类型的和stdin,stdout,stderr是等价的。

那么他们之间是什么关系呢?

这里的FILE是什么意思呢?

这里的FILE是一个结构体struct!

谁提供的呢?

C标准库!

只要是一个结构体,一般内部就会有多种成员!

C文件的库函数 内部一定要调用系统调用!

在系统角度,认FILE还是认我们上面写的fd?

系统只认识这里的整数fd。

既然FILE是一个结构体,系统只认fd,那么我们的FILE结构体里面一定封装了fd!

上层将fd封装了,变成了FILE,底层用的还是fd

stdin,stdout,stderr内部有没有fd呢?绝对有!那么怎么证明呢?

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int main()
{
//    stdin,stdout,stderrprintf("stdin:%d\n",stdin->_fileno);printf("stdout:%d\n",stdout->_fileno);printf("stderr:%d\n",stderr->_fileno);return 0 ;}

也就是说我们的c语言将我们的系统的0,1,2文件封装进了stdin,stdout,stderr

stdin,stdout,stderr都是结构体指针,其内部的_fileno属性就是我们对应的0,1,2!!!

fd是什么呢?

所以,fd是什么呢?

进程要访问文件,必须先打开文件!

一个进程可以打开多个文件吗?

一般而言:(进程:打开的文件)=(1:n)

所以正如文件被打开的目的就是被访问,文件想要被访问,前提是加载到内存当中,才能被直接访问!

第二个问题:为什么是这样的数据

既然一个进程比上其打开的文件数目是1:n的,那么如果是多个进程都打开自己的文件呢?

系统中会存在大量的被打开的文件!

所以OS要不要将如此之多的文件也管理起来呢?

必须管理!

那么怎么管理呢?

先描述,再组织!

所以在我们的内核当中(操作系统内部),如何看待打开的文件?

OS内部要为了管理每一个被打开的文件,要先构建内核的结构对象struct file

是一种内核数据结构

里面包含了文件的所有内容和属性

(文件的打开时间,修改时间,所属组,所有者等等)

struct file
{struct file *next;struct file *prev;//包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性,还包含文件的权限,连接属性,缓冲区等等)}

创建struct file的对象,充当一个被打开的文件。

如果有很多呢?

再用双链表组织起来!

所以一个进程的pcb只要找到上面那个打开的文件的链表的头部,那么就能找到全部的文件了!

进程和文件的对应关系!

进程和文件的对应关系是一对多的,

在内核当中为了维护进程和文件的关系,会有一个数组(struct_file*array[32])(指针数组,数组中的类型都是struct_file*) ,第一个数组元素[0]指向第一个文件,第二个元素[1]指向第二个文件,以此类推。系统就可以通过数组进行哈希索引,找到对应的文件。

fd在内核当中,本质是一个数组下标!!

在内存当中的 文件双向链表末尾添加我们新的打开的文件,并且将其地址放到我们上面的struct_file*array数组中,然后我们的操作系统就可以通过fd这个数组指针,从这个指针数组中的对应位置的元素查找到我们文件存储的地址!!,然后就可以读取到我们的对应的打开的文件!!

文件对象里面,包含了文件的所有内容

这个struct_file*array就是文件映射表,也可以被称为文件描述符表

(文件描述符的本质是数组下表)

那么我们到底应该用语言级别的io接口还是系统调用接口呢?

对好用语言级别的接口,因为语言级别的文件接口具有跨平台性!

那我们为什么要学系统级别的io接口呢?

我们为了更好地理解系统底层的运行原理。

有一些类似于套接字只有系统调用接口才能够完成

文件分为两类:

第一类:被进程打开的文件(open属于系统调用),这里的文件可以被称为内存文件。

第二类:没有被打开的文件(就静静地躺在磁盘上,磁盘上保存的文件包含了文件=(内容+属性))称为磁盘文件。

一旦操作系统有打开大量文件的情况,操作系统就必须将这些文件管理起来(也就是你可以不要,但我不能没有)

然而这些文件又是在内存当中,所以我们就需要先描述再组织

struct file是一种内核数据结构,里面包含了文件的所有内容和属性

(文件的打开时间,修改时间,所属组,所有者等等)

文件 里面的属性从哪里来呢?

文件天然在磁盘上,本身就有属性,属性从磁盘中被载入,比方说文件的大小,等等,填充到结构体当中。

下面的图中的结构都是在内核当中的

启动程序->进程->tack_struct

下面的file *fd_array[]就是文件映射表,也可以被称为文件描述符表

​​​​​​​

fopen->open->fd->FILE->FILE*

fwrite()->FILE*->fd->write->write(fd,……)->自己执行操作系统内部的write方法->就能找到进程的task_struct->*fs->files_struct->fd_array[]->fd_array[fd]->struct file->内存文件被找到了!->操作!

五、文件周边概念

文件描述符fd的理解

Linux进程默认情况下会有3个缺省打开的文件描述符,

分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);close(fd);return 0;
}

文件描述符是3,我们在上面的部分已经做过解释,这里不再赘述

这里我们尝试将0号文件关闭掉

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//将0号文件描述符关掉close(0);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);close(fd);return 0;
}

我们观察到我们的新的打开的文件的文件描述符竟然变成了0号文件!

我们再试试看将2号文件关闭掉

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//将2号文件描述符关掉close(2);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);close(fd);return 0;
}

我们观察到我们的新的打开的文件的文件描述符竟然变成了2号文件!

fd的分配规则

这里fd的分配规则是:

最小的,没有被占用的文件描述符,

换句话说,操作系统帮我们维护文件映射符表,也就是操作系统会默认分配最小的,没有被占用的文件描述符。

所以我们上面的0号文件被关闭了,我们的新生成的文件描述符就是0号

这里我们再试试看将1号文件关闭掉

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//文件描述符的分配规则,最小的,没有被占用的文件描述符close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);close(fd);return 0;
}

为什么我们将1号文件关闭了,文件就不打印了?!

根据我们刚刚所说的规则,如果如果我们close(1),那么下一次打开的时候fd的值一定是1,也就是最小的,没有被占用的文件描述符,那么我们下面的文件打印怎么没有消息呢?

我们下面将close注释掉,再看看结果!

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//文件描述符的分配规则,最小的,没有被占用的文件描述符//close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);
//    close(fd);return 0;
}

我们发现我们的printf又能够正常打印了!

printf默认往stdout打印这个stdout默认是FILE[fileno=1]打印,可以1原本是一个显示器,但是我们现在的1对应的是我们log.txt

但是我们发现我们的log.txt中有内容了!

这里我们将close放开,然后使用flush刷新一下!

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{//文件描述符的分配规则,最小的,没有被占用的文件描述符close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);//将内容刷新出来fflush(stdout);
//    stdout->_fileno==1;close(fd);return 0;
}

这里我们将close放开,我们发现我们的log.txt也有内容了!

这里我们进一步进行实验

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{//文件描述符的分配规则,最小的,没有被占用的文件描述符close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}
//都应该是往显示器(标准输出打印的)!printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);fprintf(stdout,"hello fprintf\n");const char*s="hello fwrite\n";fwrite(s,strlen(s),1,stdout);fflush(stdout);
//    stdout->_fileno==1;close(fd);return 0;
}

我们发现所有的内容都被打印到了log.txt文件中

我们再将close(1)注释掉,再试试看

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{//文件描述符的分配规则,最小的,没有被占用的文件描述符
//    close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}//都应该是往显示器(标准输出)打印的!//但是下面的都写入(显示)到了log.txt文件中//这是为什么呢?printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);printf("fd:%d\n",fd);fprintf(stdout,"hello fprintf\n");const char*s="hello fwrite\n";fwrite(s,strlen(s),1,stdout);fflush(stdout);
//    stdout->_fileno==1;close(fd);return 0;
}

此时我们所有的内容都被打印到显示器上了!

也就是说这个1本来是显示器,如果你把这个1给关闭掉了,按照我们上面的最小的,没有被占用的编号规则,我们新的打开的文件编号就是1,然后我们原本应该被打印到显示器上的内容都被打印到了我们的文件当中!

这个功能叫做输出重定向!!!

输出重定向的原理

当我们默认打开时系统会默认打开三个文件,Linux一切皆文件,键盘,显示器,显示器可以被称作文件,可以为这些硬件同样可以向硬盘一样被操作系统描述为struct file

这三个文件就是标准输入,标准输出,标准错误,分别对应键盘,显示器,显示器

这三个都是struct file对象(变量)

站在操作系统的角度,一个进程默认要打开这三个文件,就将0号描述符填入标准输入文件的地址,1号描述符填入标注输出文件的地址,2号描述符填入标准错误文件的地址。

操作系统就会默认给进程打开这三个关联的文件。

以上的内容都是操作系统为我们做的,不是c语言!

FILE *stdin=fopen("显示器","w");  ->open->1

stdout->FILE*->FILE->fileno(1)

printf,fprintf(stdout,"……")

在c语言层面,我们全部都用的是stdout,也就是1,关于1指向的是什么文件,并不重要

当操作系统又要打开一个文件,也就是我们上面的log.txt

但是很不幸,我们上面假设将1号文件描述符给关闭了,也就是将1号的标准输出指向的地址设置为null,也就是底层已经没了,但是我们上层的stdout中保存的数字还是1

当我们再open的时候,打开的就是1,也就是我们的log.txt文件

因为按照我们上面的最小的,没有被占用的文件描述符就是1,所以这个1中的地址null就被我们的操作系统重新指向了log.txt(原本是标准输出)

所以我们后面的printf都是往1号文件描述符指向的对象写,也就是内容不会流向我们的标准输出文件了,而是输出到我们的log.txt文件中!!

所以重定向的本质,其实是在操作系统OS的内部,更改fd对应的内容的指向!!

(和我们的c语言并没有半毛钱关系!这里的0,1,2都是有操作系统帮助我们做的)

输入重定向的原理

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{char buffer[64];fgets(buffer,sizeof (buffer),stdin);printf("%s\n",buffer);return 0;
}

我们输入了1234hello

然后它读取并且打印了1234hello

读取是从标准输入中读取的

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{int fd=open("log.txt",O_RDONLY);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);char buffer[64];fgets(buffer,sizeof (buffer),stdin);printf("%s\n",buffer);return 0;
}

同样我们可以读取到我们的输入的内容

这里我们将0号文件关闭再试试看

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{close(0);int fd=open("log.txt",O_RDONLY);if(fd<0){perror("open");return 1;}printf("fd:%d\n",fd);char buffer[64];fgets(buffer,sizeof (buffer),stdin);printf("%s\n",buffer);return 0;
}

这里我们将0号文件关闭了,也就是stdin,按照我们上面的最小的没有被占用的文件描述符开始分配的规则,所以我们的0号文件变成了log.txt

所以本来应该从键盘中读取的内容,现在变成了从文件(log.txt)中读取

也就是我们的输入重定向!!!!

追加重定向的原理

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{close(1);//先清空,再打印int fd=open("log.txt",O_WRONLY|O_TRUNC|O_CREAT);if(fd<0){perror("open");return 1;}fprintf(stdout,"you can see me,success\n");return 0;
}

这样的话就仅仅剩下我们刚刚的you can see me,success

如果我们想要追加呢?

追加重定向就是修改一下这里的标志位改成append

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{close(1);int fd=open("log.txt",O_WRONLY|O_APPEND|O_CREAT);if(fd<0){perror("open");return 1;}fprintf(stdout,"you can see me,success\n");return 0;
}

我们就成功追加了一行

you can see me,success

重定向的系统调用

man dup2

int dup2(int oldfd, int newfd);

If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.

If  oldfd  is  a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.

拷贝的是什么?

dup2要拷贝,拷贝的是进程的文件描述符的里面的内容进行拷贝,也就是对其指向文件的指针进行拷贝!

将new拷贝给old还是将old拷贝给new?

dup2()  makes  newfd  be the copy of oldfd, closing newfd first if necessary, but,note the following:

将oldfd拷贝给newfd!!

拷贝最终和谁一样?

既然new是old的拷贝,也就是将old对应的文件描述符对应的指针拷贝给new,也就是最终和new一样,也就是newfd曾经的指针指向的内容就没有用了,就被关闭掉了。

1里面本来保存的是显示器里面的地址,也就是让1里面的地址不再指向显示器了,而是指向log.txt,也就是让我们1中保存的地址变成log.txt的地址

也就是我们需要将3的内容拷贝到1的里面,所以最终这两个文件描述符的内容和3中的内容一致,也就是log.txt的内容

oldfd copy to newfd->最后和谁一样?oldfd

1:newfd

3:oldfd

也就是我们的输出重定向的参数在传递的时候为dup(3,1)

也就是将3中的地址拷贝到我们的1的文件标识符所指向的地址!

重新理解重定向

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main(int argc,char *argv[])
{if(argc!=2){return 2;}int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);if(fd<0){perror("open");return 1;}fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
}

我们看到我们传入的参数就被打印出来了

输出重定向

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main(int argc,char *argv[])
{if(argc!=2){return 2;}int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);if(fd<0){perror("open");return 1;}//将本来应该显示到显示器上的内容显示到文件当中dup2(fd,1);fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
}

这就将我们刚刚的参数打印到文件当中了

追加重定向

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main(int argc,char *argv[])
{if(argc!=2){return 2;}//int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC);int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND);if(fd<0){perror("open");return 1;}//将本来应该显示到显示器上的内容显示到文件当中dup2(fd,1);fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件
}

我们就成功追加了一个hahahaha在原先的文本后面

这里我们增加了一个close(fd)看看会有什么变化

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main(int argc,char *argv[])
{if(argc!=2){return 2;}int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
//    int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND);if(fd<0){perror("open");return 1;}//将本来应该显示到显示器上的内容显示到文件当中dup2(fd,1);fprintf(stdout,"%s\n",argv[1]);//stdout->1->显示器文件close(fd);
}

我们发现我们的log.txt中确实有我们的参数

为什么我们之前的代码需要flush才能够刷新出来,我们这里dup2不需要刷新为什么就能够打印出来?

(可以在看了之后的缓冲区作解答)

因为我们的数据其实是被写到缓冲区中了,你如果先close,那么文件描述符已经关了,那么进程退出再刷新,那么文件描述符都没有了,根本没有办法刷新。所以必须先刷新再关闭。

如何理解一切皆文件?(理性)

如何理解一切皆文件?

一切皆文件是Linux的设计哲学,体现在操作系统的软件设计层面的!

Linux是用c语言写的,如何用c语言实现面向对象,甚至是运行时多态?

C++中的类其实就是C语言中的结构体,也就是类:

其中有

1.成员属性

2.成员方法

C语言中有没有一种属性将一堆属性和方法放在一起呢?

有struct!

也就是我们现在面向对象的起点

struct中可以包含成员属性,但是在纯C语言中可不可以包含成员函数呢?

不好意思,没有!!

在struct内部是不存在所谓的成员方法的!

那么我们今天非得想要在struct内部包含成员方法呢?

那我们只能是定义一个struct file结构体

struct file{int size;mode_t mode;int user;inr group;……
//函数指针!!!int (*readp)(int fd,void*buffer,int len);int (*readp)(int fd,void*buffer,int len);……
}

所以我们C语言虽然不支持成员方法,但是我们可以保存函数指针,然后调用对应的方法!

然后我们的C语言就可以封装一个类!

如何重新理解文件?

磁盘,显示器,网卡,键盘,显卡……

底层不同的硬件,一定对应的是不同的操作方法!

(访问显卡和显示器的方法显然不一样!)

但是上面的设备都是外设!(冯诺依曼操作系统)

所以每一个设备的核心访问函数,都可以是read和write

为什么?

因为read代表的是i,外设代表的是o

也就是说所有的设备都可以有自己的read和write,但是代码的实现一定是不一样的!

(函数的接口的名称可以是一样的,但是函数体的实现逻辑是不一样的)

磁盘,显示器等等都有各自的read和write的方法

每一个硬件都必须要提供各自的读写方法。

但是这并不重要。

所以操作系统设计了一个struct file,当我们打开一个所谓的磁盘文件的时候,我们的内核中创建一个结构体,让其读和写的指针指向磁盘的读和写的方法。

如果打开显示器,就再创建一个struct file,让其读和写的指针指向显示器的读和写的方法!

然后为了先描述再组织,我们就将所有的struct file链接起来,变成一个双向链表,要使用哪一个设备的读和写就调用哪一个struct file

所以在这一层网上,就没有任何硬件的差别了,看待所有的文件的方式,都统一成为了struct file,所以在Linux下,就变成了一切皆文件。

这里的双向链表将struct file链接起来的就是由我们的操作系统维护的

下面的不同设备的struct file中的读和写的具体的方法,就是我们的驱动开发。

我们将Linux的这一套文件系统就称为VFS(virtual file system也就是虚拟文件系统) 。

缓冲区

什么是缓冲区?

什么是缓冲区?

缓冲区就是一段内存空间(我们上面写的char buffer[64],scanf(buffer))

这个空间谁提供?(用户(我们上面写的char buffer[64],scanf(buffer))?语言?系统OS?)

为什么要有缓冲区?

在用户层面是为了方便,那么在语言和系统层面呢?

为什么要有缓冲区?

假设你在云南大学,你的好朋友在北京邮电(地理位置非常远)。

你和你的朋友的关系特别好,想要将一些书给你的朋友。

你可以乘火车,飞机,或者其他的交通工具,将这些书给你的朋友。

你直接将这些书给你的朋友,就是写透模式(WT)!

这种方式有一个问题,就是成本非常高,也就是非常慢。

所以你们学校中有一个机构,就是快递发送点,也就是顺丰快递,你的朋友那边也有顺丰快递的发送点。

你现在只需要将你的快递交给顺丰快递,你的发送任务就完成了!因为你相信顺丰,因为顺丰会非常快并且安全地交给目的地。

用这样的顺丰模式,我们就称作写回模式(WB)!

最大的优势就是快速(你不用自己跑过去了),成本低!

然后你到了顺丰,发现顺丰是将你的快递先暂存一下,然后将七八个人的全部的快递一起配送过去。反正就是积累足够多的数据,然后再发送出去。

这个顺丰就是一个典型的缓冲区,只要顺丰不跑路,你的快递是不会丢的!(计算机是不会抱着你的64字节跑了的!)

缓冲区存在的意义就是加速,提高整机的效率。

你的朋友就是磁盘,你的要寄的东西就是数据,你把你的数据交给缓冲区,写了一大批数据,然后将全部的数据一次性刷新到磁盘上。

效率会有所提高,但是主要是为了提高用户的响应速度。

磁盘是一个物理设备,是一个机械设备。

在内存级别的操作往往是微秒级别的,然而磁盘的操作级别往往是毫秒级别的,相比之下,就非常慢了(1微秒=1/1000毫秒)(之间的差别是10^3,差了快整整1000倍,也就是你的朋友一个月20000块,你的月薪20块的水平!相差非常大!)必须要有缓冲区的存在!

这个缓冲区在哪里? 谁提供?

这个缓冲区在哪里? 谁提供?

顺丰什么是有将顺丰快递发出去呢?顺丰的发送策略是什么呢?

比方说顺丰一共有6个快递架,那么一个快递架摆满了,就发送,这是一个策略

或者全部6个快递架装满了再发送。

这里的发送策略也就是相当于我们的缓冲区刷新策略

一般情况

1.立即刷新

2.行刷新(行缓冲!)(以\n为代表)(将包含在缓冲区之前的数据全部都刷新出去\n之前的内容全部刷新出去,\n之后的数据并不会刷新出去)

3.满刷新(全缓冲)(必须将缓冲区写满了,才能将数据刷新出去!)

特殊情况:

1.用户强制刷新(fflush)(顺丰倒闭了,但是还是将最后残留的快递全部都发送出去。不满足刷新条件,还是被强制刷新。 )

2.进程退出(进程一般退出的时候,一般都会将缓冲区中的内容刷新到操作系统内部)

缓冲策略是一般情况+特殊情况的!

缓冲区就是一段内存空间,缓冲区存在的意义就是为了提高整机的效率

(数据并不会立即写入到目标设备,并不是直接往磁盘上写的,数据先写入缓冲区中,然后按照一定的时间进行刷新)

(谁提供的缓冲区,就由谁来维护,谁定义的刷新策略,谁就来维护)

关于缓冲区的认识:

什么时候用行刷新,什么时候用满刷新?

一般而言:行缓冲的设备文件--显示器,全缓冲的设备文件--磁盘文件

为什么这两个设备之间会有差别呢?

所有的设备,永远都倾向于全缓冲!

因为缓冲区满了,才刷新,也就意味着,需要更少次的IO操作,更少次的外设的访问,也就是说:提高了效率!

那刷10次也是1000个数据,刷1次也是1000个数据的话,为什么就提高了效率呢?

和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最耗费时间的!比方说你去问老师问题,你一次性将问题全部问完,老师一次性全部解答,肯定比你一次次问问题,然后老师看到你的消息,再一次次地回答你的问题更加快。

所以准备IO的时候是最耗费时间的!(多次沟通的效率是非常低的)

其他刷新策略是,结合具体情况做的妥协!

显示器:因为显示器是直接给用户看的,一方面要照顾效率,一方面要照顾用户体验!

(如果你的显示器,很少的数据不刷新,一刷新就刷新一大批,那么用户得体验是非常不好的!)显示器一行一行刷新才符合人的阅读习惯!

极端情况下:你是可以自定义规则的!

磁盘为什么是全刷新的?

磁盘上写东西的时候,用户并不会马上看到,这是我们更多考虑的是效率,而不是用户体验,所以我们往磁盘中写的时候,就是满刷新。

所以我们得到的结论是:显示器是行刷新,其他的设备比方说磁盘是满刷新的。

缓冲区在哪里?

缓冲区在哪里? 

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{//C语言提供的fprintf(stdout,"hello fprintf\n");printf("hello printf\n");const char*s="hello fputs\n";fputs(s,stdout);//os提供的,上面的这些接口底层都调用的是writeconst char *ss="hello write\n";write(1,ss,strlen(ss));//注意,我们是在最后调用的fork,上面的函数已经被执行完了fork();//创建子进程}

正常的话,应该是打印四条消息,因为我们的fork创建的子进程是在最后创建的,所以我们正常的上面四条都会打印

将我们的log.txt中的内容清空

>log.txt

我们将这个文件的打印重定向到log.txt文件中

./test >log.txt

我们发现我们的打印的变成了7次??

我们将fork注释掉,再看看结果

./test log1.txt
#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{//C语言提供的fprintf(stdout,"hello fprintf\n");printf("hello printf\n");const char*s="hello fputs\n";fputs(s,stdout);//os提供的,上面的这些接口底层都调用的是writeconst char *ss="hello write\n";write(1,ss,strlen(ss));//    fork();//创建子进程}

还是4次!这一定和我们的fork有关!!

为什么只有write打印了一次?其他的都打印了两次?

(我们c语言的文件操作函数都打印了两次?)

回答我们上面的奇怪现象的问题

同样的一个程序,

向显示器打印,输出4行文本

向普通文件(磁盘上),打印的时候,变成了7行

其中①我们的C语言的IO接口是打印了2次的

②系统接口,只打印了一次,和向显示器打印一样!

(上面的函数已经执行完了,并不代表进程的数据已经刷新了!)

为什么fork函数会产生这样的问题呢?

那我们之前在学fork相关的知识的时候,什么东西可能会被拷贝一份呢?

没错,就是写时拷贝!

上面的测试表明,这个fork并不会影响系统接口只会影响c语言的这批接口。

如果有所谓的缓冲区,那么我们之前所谈的“缓冲区”应该是由谁维护的呢?

首先,肯定不是由我们维护的,那么是C标准库还是OS呢?

我们曾经所谈的缓冲区,绝对不是由操作系统提供的!!

如果是OS统一提供,那么我们上面的代码,表现应该是一样的!

那就只能是由C标准库提供的!

C标准库会为我们的进程开辟一段空间,这里我们拿fputs举例,你把你的数据写入了C标准库提供的缓冲区当中。

写到这里之后呢,由C标准库按照一定的时间刷新到操作系统内核当中(调用write接口)。

所以进程把数据写入缓冲区当中,就可以返回了,置于什么时候刷新,就不用这个进程操心了!

fputs所做的工作与其说是写入,不如说是将数据拷贝到缓冲区当中。

那如果你调用的是write的话,数据就是直接写入系统内核的,没有写入缓冲区中!

1.这些函数都是向显示器打印的,所采用的的刷新策略都是行刷新的

那么我打印的时候,我们所打的字符串是带有\n的,

那么fork的时候,一定是函数执行完了,并且是数据已经被刷新了的!

所以你接下来执行的fork就没有意义了,因为上面的数据已经刷新到显示器上了!

2.如果你对应的程序进行了重定向,也就是比方说本来要向显示器上打印,但是现在变成了要向磁盘文件打印,那么隐形的刷新策略变成了全缓冲!(上面我们的操作就是将原本的要打印到显示器上的操作重定向到文件当中)

那么我们上面在字符串中写的\n就没有意义了!是不会刷新的!而是放入缓冲区中的。

fork的时候,一定是函数执行完了,然后fork,但是数据还没有刷新!

这些数据在当前进程对应的C语言标准库对应的缓冲区中!

所以我们的父子进程就往我们的缓冲区中刷进了两份相同的内容,然后全部都重定向进入了我们的文件中

那这部分数据是不是父进程的数据呢?

这个缓冲区的空间是标准库给你的,你讲数据拷贝到这个缓冲区当中,这个缓冲区就是你这个进程的,专门给你用的,也就是专门给父进程用的。

这段数据还在父进程的上下文当中。

那么当fork函数执行之后,父子进程分别执行自己的代码,当然在我们上面的代码中,就是父子进程分别各自退出 。

在我们上面所说的特殊的刷新策略中,我们的进程在退出时会发生强制刷新。

刷新是不是写的过程呢?

是!你将这段数据写入到显示器文件中了。

那么就会发生写时拷贝,子进程中也会有一份相同的数据。

所以,在强制刷新了之后父子进程都会刷新这份数据

C标准库提供给我们的都是用户级缓冲区!

那是不是还存在着一个内核级缓冲区?

是的!在后面说

父进程在fork前是一个char * buffer ="hello world"

那么父进程要输出的时候,会将父进程的数据拷贝到子进程,子进程中也会有一份hello world

下面我们在fork之前先fflush强制刷新一下

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{//C语言提供的fprintf(stdout,"hello fprintf\n");printf("hello printf\n");const char*s="hello fputs\n";fputs(s,stdout);//os提供的,上面的这些接口底层都调用的是writeconst char *ss="hello write\n";write(1,ss,strlen(ss));//fork之前,强制刷新!fflush(stdout);//注意,我们是在最后调用的fork,上面的函数已经被执行完了fork();//创建子进程}

然后运行我们的test文件,并且将其打印的结果重定向到log.txt当中!

./test >log.txt

现在又变成了4条!

fflush是将数据强行刷新到缓冲区当中,所以在fork之后并不会发生写时拷贝,因为数据已经刷新到缓冲区当中了!

为什么我们这里刷新的时候只刷新stdout就行了?(不需要加上缓冲区文件吗)

其是因为这个stdout中封装了一号文件,也就是我们的标准输出文件

我们C语言中打开文件FILE*fopen(const char *path ,const char *mode)

struct FILE是一个结构体,其内部封装了fd,但是只封装了fd吗? 

并不是!

那么除了fd还有什么呢?

除了我们所谓的struct FILE还包含了文件fd对应的语言层面的缓冲区结构!

//在/usr/include/libio.h
struct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
//缓冲区相关
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. */char* _IO_buf_base; /* Start of reserve area. */char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base; /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno; //封装的文件描述符
#if 0int _blksize;
#elseint _flags2;
#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

C语言中打开的文件FILE,也就是文件流

在C++中的cout<< cin>>中的cout或者是cin是什么呢?

是一个类!

cout cin->类->必须包含

1.fd也就是文件描述符

2.必须包含buffer

operator<<进行的重载就是将内容拷贝到buffer中。

strcuct file
{//有自己的内核缓冲区
}

一旦拷贝该数据,这个数据已经属于内核了,不在属于用户进程了。

六、我们自己设计一下用户层缓冲区

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#define NUM 1024struct MyFILE_{//文件描述符int fd;char buffer[NUM];int end;//当前缓冲区的结尾
};
//struct在c语言中是没法省略的,在c++中是可以省略的
typedef struct  MyFILE_ MyFILE;
//打开哪一个文件
//每一个接口都要有*MyFILE,因为我们的数据就在里面
MyFILE *fopen_(const char*pathname,const char* mode)
{//断言assert(pathname);assert(mode);MyFILE * fp=NULL;if(strcmp(mode,"r")==0){} else if(strcmp(mode,"r+")==0){}else if(strcmp(mode,"w")==0){int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);//打开成功if(fd>=0){fp= (MyFILE*)malloc(sizeof (MyFILE));memset(fp,0,sizeof (MyFILE));fp->fd=fd;}}else if(strcmp(mode,"w+")==0){}else if(strcmp(mode,"r+")==0){}else if(strcmp(mode,"a")==0){}else if(strcmp(mode,"a+")==0){}else{//什么丢不做}return fp;
}
//将message中等数据写入缓冲区中
//是不是应该是C标准库中的实现!
void fputs_(const char *message,MyFILE*fp)
{assert(message);assert(fp);//向缓冲区当中进行写入,从原来的buffer的起始位置+end也就是在原有数据之后再往后写//abcde\0的\0位置往后写//string拷贝会不会在我们的字符串最后再加上\0?//会的!这是c语言的接口,回给你加上的strcpy(fp->buffer+(fp->end),message);//调节我们的文件指针,向后移动fp->end+=strlen(message);//for debugprintf("%s\n",fp->buffer);//暂时没有刷新,刷新策略是由谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作//这里效率提高,体现在哪里呢? 因为C提供了缓冲区,那么我们就通过策略,减少了IO的执行的次数(不是数据量)if(fp->fd==0){//标准输入}else if(fp->fd==1){//标准输出//碰到\n就刷新,不碰到\n就不刷新if(fp->buffer[fp->end-1]=='\n'){fprintf(stderr,"fflush:%s\n",fp->buffer);write(fp->fd,fp->buffer,fp->end);fp->end=0;}}else if(fp->fd==2){//标准错误}else{//其他文件}
}void fflush_(MyFILE*fp)
{//将fp的内容刷新出来assert(fp);//有数据需要刷新!if(fp->end!=0){//暂且认为刷新了//其实是将数据写入了内核里面write(fp->fd,fp->buffer,fp->end);//将数据写入到磁盘syncfs(fp->fd);fp->end=0;}
}void fclose_(MyFILE*fp)
{assert(fp);//刷新fflush_(fp);//系统调用close(fp->fd);free(fp);
}
int main()
{close(1);MyFILE*fp=fopen_("./log.txt","w");if(fp==NULL){printf("open file error");return 1;}fputs_("one:hello world\n",fp);sleep(1);fputs_("two:hello world\n",fp);sleep(1);fputs_("three:hello world",fp);sleep(1);fputs_("four:hello world\n",fp);sleep(1);fputs_("five:hello world\n",fp);sleep(1);fclose_(fp);
}

 这里为什么会有上面的现象呢?

因为我们这里只有在one,two,four,five的时候对我们的缓冲区进行了刷新,也就是说在three的时候没有刷新,所以我们的three依然留在缓冲区中,所以我们的four再刷新的时候,会将three和four一同刷新出来!

因为我们这里调用了printf,所以会将结果打印到屏幕上

cat log.txt

然后我们观察到我们的log.txt当中同样也有我们刚刚打印在屏幕上的内容

因为我们将1号文件关闭了,所以我们新打开的文件就是1号文件,也就是stdout,所以我们的自己写的文件都会打印到这个新打开的log.txt当中

如果我们将主函数修改成这样呢?

int main()
{
//    close(1);MyFILE*fp=fopen_("./log.txt","w");if(fp==NULL){printf("open file error");return 1;}fputs_("one:hello world\n",fp);fork();fclose_(fp);
}

编译完上面的程序后我们再指向下面的代码,就是将我们的./test打印出来的内容重定向到log.txt中

./test>log.txt

log中同样也有两条数据!原理我们在上面也已经解释过了。

原因也就是和我们之前解释的一样(这里我们再放一遍解释)

如果你对应的程序进行了重定向,也就是比方说本来要向显示器上打印,但是现在变成了要向磁盘文件打印,那么隐形的刷新策略变成了全缓冲!(上面我们的操作就是将原本的要打印到显示器上的操作重定向到文件当中)

那么我们上面在字符串中写的\n就没有意义了!是不会刷新的!而是放入缓冲区中的。

fork的时候,一定是函数执行完了,然后fork,但是数据还没有刷新!

这些数据在当前进程对应的C语言标准库对应的缓冲区中!

所以我们的父子进程就往我们的缓冲区中刷进了两份相同的内容,然后全部都重定向进入了我们的文件中

七、minishell中支持重定向

这是我们之前写的minishell

Linux【编写一个简单的shell】_桜キャンドル淵的博客-CSDN博客

下面我们在2.1部分将我们重定向功能增加一下。

#include <string.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
//系统相关的头文件一般放在最后
#include <sys/wait.h>
#include <sys/types.h>
#include<sys/stat.h>
#include <fcntl.h>
//定义一个大小为1024个字节的缓冲区
#define NUM 1024
//第一一个用于打散之后的字符串的个数的大小
#define SIZE 32
//宏定义分隔符
//由于我们下面的分隔符系统中在定义的时候是一个char*的,所以我们必须传入一个字符串,所以我们下面用的SEP必须要是双引号包裹起来的空格
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串,也就是将每一个打散之后的子串的起始地址保存在这个g_argv中
char *g_argv[SIZE];//写一个环境变量的buffer,用来测试
//cmd_line每一次都会被清空,我们的环境变量为了防止被清空我们这里定义一个buffer用来测试
char g_myval[64];
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status=NONE_REDIR;
char *CheckRedir(char *start)
{assert(start);char *end=start+strlen(start)-1;//让其找到最后一个有效字符(不包含\0)//从输入的完整的字符串中从后往前寻找有没有重定向标识符,如果有就进入下面的判断while(end>=start){if(*end=='>'){//如果是连着的两个>符号(>>)//ls -a -l>>myfile.txtif(*(end-1)=='>'){//追加重定向redir_status=APPEND_REDIR;*(end-1)='\0';end++;//找到文件名的其实字符串break;}//ls -a -l>myfile.txtredir_status=OUTPUT_REDIR;*end='\0';end++;break;}else if(*end=='<'){//cat<myfile.txt输入重定向redir_status=INPUT_REDIR;*end='\0';end++;break;}else{//不是大于也不是小于end--;}}//如果循环走完的时候,这里条件还是满足的,就说明是提前跳出的if(end>=start){return end;//要打开的文件的字符串地址}else{//没有遇到>或者<符号return NULL;}
}int main()
{//8.创建一个全局变量指针extern char**environ;//0.命令行解释器:通过让子进程执行命令,父进程等待&&解析命令while(1){//1.打印出提示信息//这里我们减少学习成本,直接粘贴和打印出来printf("[root@我的主机 myshell]#");//将提示符信息立马刷新出来,不然其信息一直都会呆在缓冲区内,不被打印出来fflush(stdout);//sizeof可以不带圆括号,直接求大小//初始化我们的缓冲区,将我们的缓冲区也就是cmd_line中的全部内容(也就是大小为sizeof(cmd_line)的空间)全部初始化为'\0'memset(cmd_line,'\0',sizeof cmd_line);//2.获取用户的键盘输入//第一个参数是将我们读取到的字符放入哦我们上面定义的缓冲区中,第二个参数是定义放入的字符的大小,第三个参数是指从输入流中读取//"ls -a -l >log.txt"//"ls -a -l >>log.txt"//"ls -a -l <log.txt"if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL){//如果读取到的是空,也就是出错了,就直接进行下一次循环,重新初始化continue;}//将我们输入的内容打印出来//但是我们输入的内容实际上最后带有一个\n//ls -a -l -i\n//我们需要将这个最后的\n给去除掉//strlen求字符串长度的时候不包括后面的\0//由于我们的下标是从0开始的,所以我们需要将cmd_line的长度-1的位置,也就是\n的位置置为\0cmd_line[strlen(cmd_line)-1]='\0';//2.1:分析是否有重定向,"ls-a -l>logtxt"->"ls -a -l\0log.txt"//"ls -a -l -i\n\0"char *sep=CheckRedir(cmd_line);//        printf("echo:%s\n",cmd_line);//3.打散字符串//我们将字符串中的空格部分置为\0并且将每个子字符串的起始地址用指针指向,就得到了我们的打散的子字符串//第一个参数你要解析的子串,第二个参数是分隔符//export myval=105 左侧的是命令,右侧的是环境变量g_argv[0]=strtok(cmd_line,SEP);//第一次调用,要传入原始字符串int index=1;//6.给我们的shell设置颜色if(strcmp(g_argv[0],"ls")==0){g_argv[index++]=(char*)"--color=auto";}//7.让我们的shell支持别名//这里我们仅仅是支持ll,也就是ls的别名if(strcmp(g_argv[0],"ll")==0){g_argv[0]=(char*)"ls";g_argv[index++]=(char*)"-l";g_argv[index++]=(char*)"--color=auto";}//进行循环读取
//        while(g_argv[index-1])
//        {
//            g_argv[index]= strtok(NULL,SEP);//第二次如果还要解析原始字符串,传入NULL,也就是第一个参数,第二个参数分隔符还是SEP
//            index++;
//        }//我们测试一下打散的操作成不成功
//        for(index=0;g_argv[index];index++)
//        {
//            printf("g_argv[%d]:%s\n",index,g_argv[index]);
//        }//先解析,再赋值,最后while解析g_argv,解析到最后,全部都解析完成的时候g_argv的值为null,while循环条件不满足,循环退出while(g_argv[index++]= strtok(NULL,SEP));//上面的代码将我们全部的命令都解析完,然后我们下面再进行分析,不然我们下面的argv[1]就可能并没有被解析,然后就并不会进入下面添加环境变量的操作//8.让我们的shell支持修改环境变量//并且我们的环境变量不空我们才能将我们的环境变量导入if(strcmp(g_argv[0],"export")==0&&g_argv[1]!=NULL){//防止下一次导入的时候,将环境变量覆盖了strcpy(g_myval,g_argv[1]);//将新的环境变量导入到我们的shell当中//然后它的环境变量在地址空间中是在它的栈的上面的命令行地址空间中,就被添加进去了int ret=putenv(g_myval);//看看环境变量有没有成功导入if(ret==0){printf("%s export success\n",g_argv[1]);}//当前我们的环境变量已经添加到系统中了//将获取到的环境变量打印出来
//            for(int i=0;environ[i];i++)
//            {
//                printf("%d:%s\n",i,environ[i]);
//            }continue;}//4.TODO内置命令//内置命令:让父进程shell自己执行的命令,我们叫做内置命令,内建命令//内建命令本质上就是shell内部中的一个函数if(strcmp(g_argv[0],"cd")==0)//不让我们的子进程去执行cd命令,而是交给我们的父进程去完成{if(g_argv[1]!=NULL){//将要切换的目标路径传进来chdir(g_argv[1]);//cd ..}//进入下一次循环continue;}//5.fork()pid_t id=fork();if(id==0)//child{if(sep!=NULL){//重定向工作//说明曾经有重定向//程序替换只会影响代码和数据,不会对进程所打开的文件和数据有任何影响,也就是对这里的文件描述符也没有任何影响//在switc-case中不能定义变量,所以我们就直接将这个fd定义在swtich-case的外面int fd=-1;switch(redir_status) {//sep指向文件名的起始地址case INPUT_REDIR:fd=open(sep,O_RDONLY);dup2(fd,0);break;case OUTPUT_REDIR://写入,清空,创建fd=open(sep,O_WRONLY|O_TRUNC|O_CREAT,0666);//重定向到标准输出dup2(fd,1);break;case APPEND_REDIR:fd=open(sep,O_WRONLY|O_APPEND|O_CREAT,0666);dup2(fd,1);break;default:printf("bug?\n");break;}}//环境变量的测试代码
//            printf("下面功能让子进程执行的\n");
//            printf("child,MYVAL:%s\n",getenv("MYVAL"));
//            printf("child,PATH:%s\n",getenv("PATH"));//ls -a -l -i,第一个参数g_argv[0]保存的就是我们的命令,后面的全部都是我们的参数execvp(g_argv[0],g_argv);
//            execvpe(g_argv[0],g_argv,environ);exit(1);}//fatherint status=0;//阻塞式等待pid_t ret= waitpid(id,& status,0);if(ret>0){printf("exit code:%d\n", WEXITSTATUS(status));}}
}

我们上面代码中的这块部分就是将我们的不同的重定向标识符定义一下,分别是输入重定向,输出重定向和追加重定向,然后more扥NONE_REDIR就是没有重定向,也就是0,

然后我们用redir_status来记录我们当前的重定向的状态是属于哪一类的。

上面代码中的函数是用来检测我们的输入的重定向中是

1.> 输出重定向

2.>> 追加重定向

3.< 输入重定向

然后对其分别分类,然后将我们的redir_status分别置为不同的状态。

当然,如果没有重定向的话,就会被最终划分到return  NULL的那里,返回NULL

我们这里的的CheckRedir从我们传入的字符串的最后有效字符的位置从后向前寻找,知道寻找到重定向字符串为止。

然后我们在这里定义了检测是否有重定向的代码,其中cmd_line是我们一开始定义的完整的输入的字符串。

然后我们后面的这段代码就是将重定向工作判断一下,然后执行对应不同的方法

也就是我们的redir_status如果是不同的数字,那么我们的打开就是不同的方式,然后我们下面的dup2也就分别替换掉不同的IO文件,0为标准输入,1位标准输出,2位标准错误

下面我们就重新将我们的minishell运行起来看看输入下面的测试代码的结果,来看看我们的重定向操作是不是已经可以运行了。

e测试一下输入重定向,注意,输入重定向是会先清空,然后再写入的

ls -a -l >log.txt

echo aaaa>log.txt

我们再来测试一下追加重定向

echo aaaaaaa>>log.txt

echo bbbb>>log.txt

下面就是我们的输入重定向

cat<log.txt

1.close关闭fd之后文件内部没有数据

解答之前不fflush刷新就没办法将打印出来的文件打印到文件中的问题

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{close(0);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}const char* msg="hello world\n";write(fd,msg,strlen(msg));close(fd);return 0;
}

上面的代码是可以正常将hello world重定向到我们的log.txt当中的

如果我们在进程退出前将fd关闭了呢?

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("helloworld:%d\n",fd);
//    const char* msg="hello world\n";
//    write(fd,msg,strlen(msg));close(fd);return 0;
}

这个格式化字符串我们之前说是因为这个打印出来的文本在缓冲区中,我们没有刷新就直接将文件关闭了,我们没有fflush就没有打印到文件中

数据会暂存在stdout的缓冲区中,虽然我们在hello world之后带了\n,其刷新策略已经变成了满刷新,所以我们的数据在缓冲区中的时候,但是对应的fd先关了,数据便无法刷新了!

下面是我们加上了fflush之后的代码

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//myfile helloworld将helloworld打印到文件里
int main()
{close(1);int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);if(fd<0){perror("open");return 1;}printf("helloworld:%d\n",fd);fflush(stdout);
//    const char* msg="hello world\n";
//    write(fd,msg,strlen(msg));close(fd);return 0;
}

加上了fflush就可以成功重定向到我们的文件当中了

2.1,2 stdout stderr的区别

下面打印内容后面的数字就是它们打印到的目标文件的文件描述符。

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <iostream>
//myfile helloworld将helloworld打印到文件里
int main()
{//向stdout中打印-->1printf("hello printf 1\n");fprintf(stdout,"hello fprintf 1\n");//向stdout里面打印-->2perror("hello perror 2");//stderr//向stdin里面打印const char *s1="hello write 1\n";write(1,s1,strlen(s1));const char *s2="hello write 2\n";write(2,s2,strlen(s2));//cout->1std::cout<<"hello cout 1"<<std::endl;//cerr->2std::cerr<<"hello cerr 2"<<std::endl;return 0;
}

./test >log.txt

我们观察到我们重定向到2号文件的打印依旧被打印到了显示器上,但是我们打印到1号文件的打印全部都被打印到了log.txt当中

重定向改的是几号文件描述符的内容呢?

为什么只有1号文件描述符的内容被写入到了log.txt当中?

1和2对应的都是显示器文件,但是他们两个是不同的显示器文件!

如同认为,同一个显示器文件,被打开了两次!

一般而言,如果程序运行有可能有问题的话,建议使用stderr,或者std::cerr来打印

如果是常规的文本内容的话,建议使用sdtout打印

将标准输出和错误分开打印

把本应该显示到2号文件描述符中的内容打印到err.txt文件中,然后如果是正常的标注输出文件中的内容打印到ok.txt文件中

./test >ok.txt 2 >err.txt

就相当于我们想要将我们的程序运行的代码运行的结果并不保存在显示器上,而是保存在文件日志中,然后查看日志就知道有没有错误

将1和2号文件中所有的内容都打印到log.txt中

这里的&1就是将1中的内容给2拷贝一份,然后全部都重定向到log.txt当中

./test >log.txt 2>&1

先将log.txt中的内容重定向到cat当中,然后再将cat中的内容拷贝到back.txt当中,也就是完成了一个拷贝操作。

cat <log.txt >back.txt

perror()

perror()向2号文件描述符指向的文件打印

我们观察到perror打印的内容多了一个success

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <iostream>
//myfile helloworld将helloworld打印到文件里
int main()
{//向stdout中打印-->1printf("hello printf 1\n");fprintf(stdout,"hello fprintf 1\n");//向stdout里面打印-->2perror("hello perror 2");//stderr//向stdin里面打印const char *s1="hello write 1\n";write(1,s1,strlen(s1));const char *s2="hello write 2\n";write(2,s2,strlen(s2));//cout->1std::cout<<"hello cout 1"<<std::endl;//cerr->2std::cerr<<"hello cerr 2"<<std::endl;return 0;
}

那么这个多打印的Success是什么意思呢?

这个与errno有关,这里我们将errno设置为3看看会有什么结果

#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <iostream>
#include <errno.h>
//myfile helloworld将helloworld打印到文件里
int main()
{//向stdout中打印-->1printf("hello printf 1\n");fprintf(stdout,"hello fprintf 1\n");errno=3;//向stdout里面打印-->2perror("hello perror 2");//stderr//向stdin里面打印const char *s1="hello write 1\n";write(1,s1,strlen(s1));const char *s2="hello write 2\n";write(2,s2,strlen(s2));//cout->1std::cout<<"hello cout 1"<<std::endl;//cerr->2std::cerr<<"hello cerr 2"<<std::endl;return 0;
}

我们发现将其设置为3之后,我们这里的Success就变成了No such process

打开文件失败了之后,其实会自动设置errno的值,来代表不同的错误原因。

strerror

自己写一个strerror

注意这里我们的log.txt文件不存在,并且我们是以只读的方式打开的,所以并不会自动创建文件。


#include<stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <iostream>
#include <errno.h>
//myfile helloworld将helloworld打印到文件里void myperror(const char*msg)
{//第一个%s所打印的是open,然后strerror(errno)是具体的错误信息fprintf(stderr,"%s:%s\n",msg,strerror(errno));
}
int main()
{//这个log.txt当前并不存在int fd=open("log.txt",O_RDONLY);if(fd<0){myperror("open");return 1;}return 0;
}

我们发现我们的错误原因就被打印出来了

八、理解文件系统

文件系统的背景知识

背景知识:

以前我们讲的文件都是打开的文件,就是我们的进程和被打开的文件之间的关系,那~

1.有没有没有被打开的文件呢?在哪里呢?

​​​​​​​当然存在,在哪里呢?磁盘(磁盘级文件 )

2.如果文件没有被打开,就在磁盘上,我们的侧重点在哪里呢?

单个文件角度:这个文件在哪里?这个文件多大?这个文件的其他属性是什么?

站在系统角度:一共有多少个文件?各自属性在哪里?如何快速找到?我还可以存储多少文件?如何快速地找到指定的文件……

如何进行对磁盘文件的分门别类的存储,用来支持更好的存取!

也就是我们是一个顺丰的快递员,我们应该如何放置这些快递,别人才能够更方便地找到他们的快递。

比方说一个手机工厂生产出一个个产品。我们需要将手机组装起来,然后放到包装盒里,然后放到指定的货架上,转到指定的仓库中。

我们之前研究的是如何将手机组装起来,然后放到包装盒中的。

我们接下来研究如何更好地放入货架上,转入指定的仓库中。

磁盘文件

磁盘文件-了解磁盘

内存-掉电易失存储介质

磁盘-永久性存储介质

但并不代表着永久的存储介质只有磁盘,还有SSD(固态硬盘),U盘,flash卡,光盘,磁带等

SSD(固态硬盘)中一般是没有什么机械式设备了,一般比磁盘更贵,并且SSD比磁盘快很多,但是比内存还是慢很多。但是因为磁盘容量大,价格低,性价比高,所以企业中一般都是用磁盘存储的。

磁盘是一个外设(在冯诺依曼体系中承担输入输出的设备 ),还是我们计算机中唯一的一个机械设备,这就直接决定了磁盘这个设备会非常的慢。

(操作系统一定会有一些提速的方式 (比方说预加载等等))

磁盘结构

磁盘结构

磁盘中是存在磁盘盘片,磁头,伺服系统,音圈马达

这个像光盘一样的就是磁盘盘片,磁头就是光盘上面那个指针,伺服系统就是那些硬件电路。

在磁盘运行起来的时候,我们的马达就会带动磁盘盘片转动,然后我们的磁头就会在磁盘的不同位置去读取数据

(图片取自于百度)

无论是磁盘还是光盘,其盘面上都会存储数据。

计算机只认识二进制,那么存储的数据本质上就是二进制。那么二进制在硬件层面上应该如何去理解呢?

内存可以从有电和没电来区分0和1,那么磁盘呢?

可以用磁铁!也就是磁铁有南极和北极,N和S,也就可以用正负表示为0或者1.由无数的磁铁小颗粒组成的盘面就是我们的磁盘。

想磁盘写入,本质就是改变磁盘上的磁铁的小颗粒正负性。

磁头在来回读写磁盘的时候,就会想磁盘的特定位置产生放电行为,从而改变我们磁盘上磁铁小颗粒的正负性,从而改变我们的磁盘中存储的数据。

磁头和盘面都是一摞的,然后磁头和盘片其实是不接触的,因为只要挨着,就一定会将盘片刮花,我们的二进制数据就可能会发生丢失。

所以我们的磁头就相当于是一架播音747以1米的距地距离飞行。

笔记本最好不要开机状态,来回地搬运,一旦磁头上下震动,就会磕着盘片,然后就会有数据发上丢失。

磁盘的存储结构

我写入了文件,但我们的磁盘怎么知道我的文件在这个位置呢?

磁盘是由一系列同心圆构成的,数据只能写在这些同心圆上面,这些同心圆就被称为磁道。

但是磁道一圈的容量太大了,所以我们以圆心为中点,向外等角度地将我们磁盘的盘片划分成多个扇区。

相同半径的多个磁道,我们将其称为柱面。

扇区是磁盘存储的基本单位。

(图片取自于网络)

在物理上,如何将输入写入到磁盘中呢?

(本质上就是如何找到一个扇区)

我们如何知道我们的数据在哪一个扇区呢?

1.我们首先知道我们的数据在哪一个盘面上。对应的问题就是哪一个磁头H

2.在哪一个磁道(柱面)上,在哪一个同心圆上C

3.在哪一个磁道上S

这种寻址方式就是(CHS)寻址方式,也就是只需要CHS三个要素,就能够找到任意一个扇区了,那么所有的扇区我们都能找到啦

一般扇区的大小是512字节,当然也有更好地4kb一个扇区的。

这里的512kb是硬件上的要求。

磁盘的抽象结构(虚拟,逻辑结构)(下面我们以磁带举例)

我们将磁带中的两个轴上的磁带带子从一个轴转到另外一个轴上的时候,我们就称为倒带。磁带的带面上就存在着小磁铁,也就是用于存储二进制数据的。

(图片取自于网络)

那如果我们将磁带全部拉出来,让磁带的带子从一个圆形的结构变成一个线性结构。

我们可以将这个卷在一起的磁带想象成我们的磁盘。在物理上是环形结构,但是我们可以在将其想象成是一个线性结构。(想象将每一条环形的磁道拉直)

然后我将你拉开来之后的线性结构,然后按照扇区的大小,按512字节划分成一个小块,

我们逻辑上可以将我们的磁盘抽象成一个大数组!!!!

sector disk[1024*1024*n];

所以我们将来要访问一个扇区,只要知道数组的下标是多少,就能够找到我们的扇区。我们将这种逻辑上线性抽象的数组,叫做(LBA)logical block address,逻辑块编址

只要我们将LBA地址转换成CHS地址(我们上面的磁头,扇区,磁道的地址),我们就能够访问对应的物理地址了。

(有点像我们的虚拟地址转换成物理地址的过程)

所以操作系统访问我们磁盘当中的某一个扇区的时候,只要使用LBA地址就能找到我们对应的文件了。

将数据存储到磁盘,在逻辑上就转化成了将数据存储到该数组!

找到磁盘特定扇区的位置, 转化成了找到数组的特定位置

对磁盘的管理就变成了对该数组的管理。

(我们这里就完成了一个抽象的过程)

大空间,我们是非常难以管理,直接对我们的整一个磁盘进行管理,成本非常高,所以我们可以将一整个磁盘进行拆分一下,就变成了一个个更小的区域。

这就是我们分区的过程!

对磁盘的管理转换成了对小分区的管理!

相当于我们将磁盘分好了,我们将每一个小区域都管好,就能将整一个磁盘都管好。

(国家中有一个个省,一个个省中有一个个市,以此类推)

比方说我们这里有100GB的小分区空间,这个空间依旧很大,我们继续对这100GB的空间进行拆分,我们下面图中也就是将一个个的block group也就是块组,又分成了一个个小小的块。现在也就是只要将一个个块组管理好了,全部的组都能管理好了。

我们先讨论清楚一个块组里面是什么东西?

共我们上面的图中,所有的块组之前有一个BOOT BLOCK,它可能会存在多份,以进行备份,以防其中一份损坏。(这个存储的是我们的启动装载的相关数据)

然后这每一个Block group 中有下面的六个内容

super block:是我们linux系统的核心数据块,代表的就是文件系统本身,含有文件系统的属性信息,比方说整个分区有多少个块组,有多少个块组被使用了,又有多少个没有被使用。(文件系统的属性信息)

(如果我们的磁盘的磁头将盘面刮花了,然后我们的super block坏掉了怎么办?其实在后面的块组中其实会有很多分拷贝的super block。也就是我们的电脑蓝屏之后,重启一下,然后就能够从这些拷贝中恢复我们原本的数据)

data block:虽然磁盘的基本单位是扇区(512字节),但是操作系统(文件系统)和磁盘进行IO的基本单数为:4kb(8*512字节byte)

(Linux下文件的书型盒内容时分开管理的)

(即使我只想读取1kb的内容,也必须先读取这个4kb的内容才能读取到这1kb的数据)

那么为什么不以512字节为单位呢?

①因为512字节太小了,我们进行数据拷贝的时候可能会超过这个大小,所以我们需要进行多次IO操作,进而导致我们效率的降低。

②如果操作系统使用和磁盘一样的大小,那万一磁盘基本大小变了的话,操作系统的源代码要不要改呢?(如果我今天操作系统和磁盘的基本大小强耦合,那明天要是不一样了怎么办?)要改!这就是将我们的硬件和软件(OS)进行解耦合。

(不管你硬盘的基本大小是多少,我操作系统都是4kb为基本单位,也就是一个块的大小,称作块大小,所以我们将磁盘称为块设备)

所以我们可以将data block理解成多个4kb(扇区*8)大小的集合

文件=内容+属性,而Linux在磁盘上存储文件的时候,将内容和属性是分开存储的!

data block中保存的都是文件的特定的文件内容。

inode table:每一个文件都有一个inode编号,不同的文件都有不同的编号。一般而言,inode是一个大小为128字节的空间,保存的是对应文件的属性,该块组内,所有文件的inode空间的集合。有多个文件的话,需要表示唯一性。所以每一个inode块,都需要有一个inode编号。

一般而言,一个文件,一个inode,一个inode编号!

所以文件的内容存储在data block中,然后其属性存储在inode块中

无论是data block还是inode block都是有多个的,我怎么知道哪些block已经被占用了,哪些没有被占用呢?

BlockBitmap:如果我们有10000个block,那么我们的BlockBitmap就有10000+个比特位,比特位和特定的block是一一对应的,其中比特位(标记位)为1表示这个block被占用,否则表示可用!

inodeBitmap:假设有10000+个inode节点,那我们的inodeBitmap中就有10000+个比特位,比特位和特定的inode是一一对应的。其中bitmap中比特位(标记位)为1,代表该inode被占用了,否则表示可用。

这两张map中存储的是数据的管理信息

GDT(group descriptor table):块组描述符,主要描述的是这个块组多大,已经使用多少了,有多少个inode,已经占用了多少个,还剩多少个,一共有多少个block,使用了多少。表述的是这个块组的属性信息。

所以将上面的信息搞清楚,我们就能够让一个文件的信息可追溯,可管理。

我们将块组分割称为上面的内容,并且写入相关的管理数据->每一个块组都这么干,那么整个分区就被写入了文件系统信息。

那么这一个过程就被我们称为格式化!

一个文件“只”对应一个inode属性节点,inode编号,

一个文件只能有一个block吗??

如果一个文件是4kb以内,那么一个block就够了,那么如果文件的大小大于4kb呢?那就需要多个block了。

1.那么哪些block属于同一个文件呢?

2.找文件的时候先找到inode就可以了,那么怎么找到inode呢,知道inode编号就可以了

找到文件只要找到该文件对应的inode1编号就能找到该文件的inode属性集合,可是文件的内容呢?

这个inode中的blocks属性就是保存的和当前文件同一个块组的块的编号

struct inode
{//文件的大小//文件的inode编号//其他属性//int blocks[15];
}

假设我们的一个文件中保存在6,7,8号块中,那么我们不妨这样设置

blocks[0]=6;
blocks[1]=7;
blocks[2]=8;

我们就能够找到相关的文件内容

这个文件特别大,怎么办?

需要使用更多的块,那么你怎么用这个数组进行映射呢?

data block中,不是所有的data block只能存文件数据,也可以存其他块的块号!!

比方说block[12]中可以存其它块的编号。

一个块大小为4kb,里面可以存储大量的块号

这样就可以形成一棵多叉树,形成二级、三级等等的文件,用内容块去索引其他的文件块,这样我们的二叉树的叶子结点就可以存储大量的文件。

inode vs 文件名

找到文件,必须先找到inode编号,才能找到分区特定的block group

找到了block group才能找到文件的inode

才能知道文件的属性和内容

查看磁盘文件的存储

df -h

但是我们怎么知道inode编号呢?我用的是不是文件名吗?

文件名和inode之间有什么关系呢?

linux中,inode属性里面,没有文件名这样的说法 !也就是inode中不保存文件名

那它是怎么做到的呢?

你想要找到inode编号,一定要依托于对应的目录文件的映射关系表的!

1. 一个目录下,是不是可以保存很多文件?但是这些文件没有重复的文件名。

(在同一个目录下,我们的文件名不可以重复)

2.那么根据我们的Linux下一切皆文件,那么目录是文件吗?

当然是!那么目录需不需要有自己的inode,和自己的data block?

需要!

在目录里面创文件,文件名不放在文件自身的inode里面,是放在所属的目录的data block里面!

目录中存储文件的文件名和文件的inode编号的映射关系。

所以他们两个互为key值的,文件系统既可以用文件名去找inode,也可以用inode去找文件名

进入目录(x权限,执行权限)

创建文件(w权限,写入权限)

因为你要创建文件,目录也是文件,有自己的数据块,里面存储文件的文件名和文件的inode的映射关系,我们需要将这个映射关系写入沃尔玛的呢目录文件中,所以是写入权限w

显示文件名与属性 (r权限,读取权限)

因为我们想要显示文件名,文件的inode里面又没有文件名,我们需要从目录文件中拿去文件的文件名。通过我们的映射表,要么从inode找到文件名,要么从文件名找到inode

所以,你想要找到inode编号,一定要依托于对应的目录文件的映射关系表的!

创建文件,系统做了什么?

当前创建的文件对应的目录的分区是知道的,然后我们找到我们目录所在的分区和块组,

根据文件系统,确定我们要保存我们文件的块,然后再我们的inode bitmap里找到第一个为0的比特位,将其置1,表示其已经被占用。

然后在inode表里面将我们的文件的属性写进去,创建的时间,文件的大小,等等

将其内容清空,建立inode和文件名的映射关系,写入目录当中(找到目录的编号,找到其数据块,写入)

在datablock中将文件的数据写入

(这里的文件名是从用户提供的,inode是文件系统提供的(内部创建好以后再给我们))

我们如何知道目录的inode编号呢?

Linux内核有一棵文件树, 标记了不同目录的inode编号

删除文件,系统做了什么?

找到目录对应的data block,在目录文件中以文件名为索引找到inode

将inode bitmap对应的比特位的1置为0

然后将数据块block bitmap对应的位图从1置为0

然后从目录中将我们的文件名和inode的映射关系删掉

为什么删除比插入快得多?

删的时候不需要把data block的对应的文件部分清空,只需要在inode bitmap表中标记文件无效即可。

所以我们其实可恢复删除的文件。

只要你还能找到曾经删掉的文件的inode,然后将对应的分区当中的inode编号的inode bitmap恢复,就知道了对应的属性,知道了哪些数据块属于这个文件,然后恢复block bitmap

inode table和data block是不会删掉的

Linux的删除日志里面会保存你删除的文件的inode编号的

当然,这里的能够恢复的前提是你曾经的inode编号,没有被使用,inode和datablock没有被重复占用。

因为你删除了这个文件,你把这个datablock和inode置为无效了,如果系统有用这块空间来写入文件,那么就没办法恢复了,会覆盖掉的!

所以你误删了文件,最好的办法就是什么都不要做,怕文件被覆盖掉。

查看文件,系统做了什么 ?

比方所我们在目录下ll,或者是ls,或者是cat一个文件

ls的时候只要找到一个文件的目录,找到这个目录对应的datablock,然后这个datablock中存储了文件的文件名和inode的映射关系,然后通过每一个文件的inode找到每一个文件的属性,然后将所有的信息都打印出来

这个位图一开始一定会被操作系统全部都清零,表示没有被使用,然后哪些代表inodebitmap,inode table, data block,是什么时候划分,属性什么时候填入supper block中,是谁进行管理的呢?

对磁盘进行分区,对磁盘的500个G分了4个区,然后我们对磁盘进行格式化,这里的格式化就叫做写入文件系统!

写入文件系统就写入了我们上面的这些内容。写入了文件系统之后,我们的磁盘才能够被使用。

inode的个数是固定的,datablock的个数是固定的 ,但是你一旦固定好了,但是我看到磁盘中还有空间,但是创建文件失败了?

inode还有,datablock没有了,或者inode没有,datablock没了

这样的话,我们的文件就没有办法创建了。

(这种情况特别少)

九、软硬链接

一个文件一个inode

为我们的testlink.txt文件在当前目录下创建一个名为soft.link的软链接

(加上-s选项创建的就是软链接)

ln -s testlink.txt soft.link

为我们的testlink1.txt文件在当前目录下创建一个名为hard.link的软链接

(不加上-s选项创建的就是硬链接)

 ln testlink1.test hard.link

软硬链接有什么本质区别?有没有独立的inode

软链接有自己独立的inode

硬链接没有自己独立的inode

软链接有自己独立的inode,说明软链接是一个独立的文件!

硬链接没有自己独立的inode,说明硬链接不是一个独立的文件!

软链接

特性

假设你有一个可执行文件bin.exe,在你自己的路径下。那如果你这个exe路径特别深

我们当前的test可执行文件就在我们的test_2022_10_31/bin/exe这一个很深的路径下

ln -s bin/exe/test ./

我们在home/zhuyuan路径下为我们的bin/exe/test创建了一个软链接,我们观察到,此时我们的这个路径中就有一个test文件了,然后./test执行这个文件,也是没有任何问题的!

这个软链接就相当于是我们的可执行程序的快捷方式,

我们可以理解称为:软链接的文件内容,是指向的文件对应的路径。

硬链接

硬链接没有独立的文件,创建硬链接不是真正的创建新文件。

硬链接有inode,但是是别人的inode,别人的文件的属性

那硬链接做了什么呢?创建硬链接究竟在做什么呢?

创建硬链接就是在指定的目录下建立了文件名和对应的指定inode的映射关系,仅此而已。

它就是起了一个别名

我们不妨删除我们的文件,看看这个硬链接会有什么变化

删除之前,这个数字是2,删除之后,这个数字是1

第一张图是删除之前,第二张图是删除之后

这个数字就是硬链接数。

假设有一个

file1:1234(inode)

hard_link:1234(inode)

那个站在文件系统的角度,我怎么知道有多少个文件名是关联的呢??

这个就创建一个引用计数,int count

引用这个文件,就将这个计数++

删除这个文件,就将这个计数++

当我们删除一个文件的时候,并不是把这个文件的inode删除,而是将这个文件的inode引用计数--。当引用计数为0的时候,这个文件才被真正删除!

引用计数为0的时候,也就是没有文件名和我关联了,没有用户关心这个文件了。

我们下面再将硬链接进行硬链接第二个链接第一个,第三个链接第二个,我们的三个硬链接的链接数全部都变成了3

unlink +文件名,取消链接,就相当于是删除了这个文件

unlink 文件名

为什么我们创建了一个文件,这里链接数就是1?

因为我们的自己的文件名和inode就是一组映射关系,所以这里的硬链接数默认就是1

为什么默认创建目录,引用计数(硬链接)为什么是2呢?

这个目录本身的名字和inode就有一层链接

然后这个目录下默认就有两个文件,一个是.,一个是..

然后从下面的图中我们的test2的inode和test2中的.的inode是一样的

这就是第二层硬链接

为什么我们在文件夹里面再创建一个目录文件,就会有三个硬链接?

路径中的.文件硬链接的是自身目录

路径中的..文件硬链接的是上一层目录

所以除了我们目录自身的inode和文件的硬链接,第一层

还有我们文件中的.文件链接自身目录,也就是第二层

还有我们目录中的子目录的..,也就是第三层

所以目录的硬链接的个数-2,就是目录中的合法目录数目

​​​​​​​

甚至我们将可执行程序链接给了[,都能运行

十、动静态库

下面的这篇博文中的的二部分有关于静态库和动态库的初步介绍

Linux【vim】【gcc/g++】【make/Makefile】_桜キャンドル淵的博客-CSDN博客

此处不再赘述

1.我如果想写一个库呢?(编写库的人的角度)

2.如果我把库给别人,别人是怎么用的呢?(使用者的角度)

静态库(.a)

动态库(.so)

创建静态库

创建下面几个文件

库中不能有main函数,因为你写的库是给别人用的,别人会写main函数的

在myprint.h文件中写入下面的代码

#pragma once#include <stdio.h>
#include <time.h>
extern void Print(const char *str);

在myprint.c文件中写入下面的代码

#include "myprint.h"
void Print(const char *str)
{printf("%s[%d]\n",str,(int)time(NULL));
}

在mymath.h中写入下面的代码

#pragma once#include <stdio.h>
extern int addToTarget(int from,int to);

在mymath.c中写入下面的代码

#include "mymath.h"
int addToTarget(int from,int to)
{int sum=0;for(int i=from;i<=to;i++){sum+=i;}return sum;
}

当然我们可以按照下面的方式调用

将我们的main函数所在的文件放在我们的另外一个文件夹userlib中

mkdir userlib

#include "myprint.h"
#include "mymath.h"
#include <stdio.h>
int main()
{Print("hello world");int sum= addToTarget(2,10);printf("%d\n",sum);return 0;
}

编译我们的程序,当然如果此时我们再将main文件给编译一下,然后将这三个文件链接到一起,我们的程序就可以运行了。

gcc -c mymath.c -o mymath.o
gcc -c myprint.c -o myprint.o

如果我只把.h和.o给别人,别人能用么?

使用cp命令将我们生成的.o和.h文件拷贝到userlib中

cp *.h ../userlib
cp *.o ../userlib

gcc -c main.c -o main.o

gcc main.o mymath.o myprint.o -o my.exe
./my.exe

我们发现我们的程序可以被执行!

所有的.o文件手动敲非常麻烦,我们将所有的.o文件打一个包,就变成了一个静态库

归档 archive files

-r replace

-c创建

库的前缀必须是lib

库的后缀名字必须是.a

ar -r libhello.a mymath.o myprint.o

这个生成的libhello.a就是我们生成的静态库!

但是这样写太麻烦了,我们使用makefile来帮助我们自动创建新的库

touch makefile
libhello.a:mymath.o myprint.oar-rc libhello.a mymath.o myprint.o
mymath.o:mymath.cgcc -c mymath.c -o mymath.o
myprint.o:myprint.cgcc -c myprint.c -o myprint.o
.PHONY:clean
clean:rm -f *.o libhello.a

库怎么给别人呢?

库有一个文件夹

libhello -include(库的所有的头文件)-lib(保存对应的库文件)

libhello.a: mymath.o myprint.oar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.cgcc -c mymath.c -o mymath.o -std=c11
myprint.o:myprint.cgcc -c myprint.c -o myprint.o -std=c11.PHONY:hello
hello:mkdir -p hello/libmkdir -p hello/includecp -rf *.h hello/includecp -rf *.a hello/lib.PHONY:clean
clean:rm -rf *.o libhello.a hello

​​​​​​​

将这个生成的静态库拷贝到我们的userlib文件夹中 ,我们模拟别人使用这个库

cp -rf hello ../userlib/

删到只剩下一个main.c和hello文件

怎么用这个库?

1.将库添加到系统路径中

将这个库拷贝到系统库文件路径下

头文件gcc的默认搜索路径是:/usr/include

库文件的默认搜索路径是 /usr/lib64 或者/lib64

sudo cp hello/include/* /usr/include/ -rf

看一下我们的系统库中有没有这两个头文件

ls /usr/include/myprint.h
ls /usr/include/mymath.h

将我们的库文件拷贝到我们的系统的库中

sudo cp hello/lib/* /usr/lib64/ -rf

但是直接运行为什么报错了?

ls /lib64/libc.a

你自己写的库属于第三方库,所以我们需要指定引用的库的名字。

(库的名字需要去掉前缀lib和后缀.a)

gcc main.c -lhello

我们刚刚的拷贝到系统的默认路径下,就叫做库的安装。

安装软件就是拷贝,就是将软件拷贝到指定目录。

不建议将自己的库添加到系统的目录下,因为自己写的没有经过测试,容易出bug,会污染别人的头文件和库文件的内容

sudo rm /usr/include/myprint.h
sudo rm /usr/include/mymath.h

把头文件和库删掉的过程就是卸载

2.硬使用这个库

-I就是除了在系统路径和当前路径下,在我给你指定的路径下搜索头文件(头文件搜索路径)

-L在对应的路径下搜索我们的库(库文件搜索路径)

-l需要说明是这里路径下的那一个库(你要在特定路径下,使用哪一个库呢?)

gcc main.c -I ./hello/include/ -L ./hello/lib/ -lhello

创建动态库

1.站在制作库工程师的角度--动态库

2.站在一个使用者(程序员)角度--使用动态库

形成一个与对应位置无关的二进制文件。

也就是这个库形成了在任意位置都可以加载。

(静态库的话,你的代码必须要在特定的位置,绝对编址,但是动态库,你的代码可以放在任意地方,也就是采用相对编址的方式)

静态库就是直接拷贝到我们的程序中,然后动态库是链接到我们的程序中

相比于形成静态库,我们需要加上-fPIC选项。

gcc -fPIC-c mymath.c -o mymath.o -std=c11 
gcc -fPIC-c myprint.c -o myprint.o -std=c11 

readelf -S mymath.o

address里面几乎全部都是0,

打包,前缀为lib,后缀为.so

-shared,形成动态库

gcc -shared myprint.o mymath.o -o libhello.so

使用Makefile来帮助我们打包,同时形成一个静态库和一个动态库

.PHONY:all
all:libhello.so libhello.alibhello.so: mymath_d.o myprint_d.ogcc -shared mymath_d.o myprint_d.o -o libhello.so
mymath_d.o:mymath.cgcc -c -fPIC mymath.c -o mymath_d.o -std=c11
myprint_d.o:myprint.cgcc -c -fPIC myprint.c -o myprint_d.o -std=c11libhello.a:mymath.o myprint.oar -rc libhello.a mymath.o myprint.o
mymath.o:mymath.cgcc -c mymath.c -o mymath.o -std=c11
myprint.o:myprint.cgcc -c myprint.c -o myprint.o -std=c11.PHONY:output
output:mkdir -p output/libmkdir -p output/includecp -rf *.h output/includecp -rf *.a output/libcp -rf *.so output/lib.PHONY:clean
clean:rm -rf *.o *.a *.so output

完成库的打包

make output
tree output

我们将我们生成的动静态库移动到userlib,也就是模拟将这个库给别人去使用

mv output ../userlib

压缩,发布到网上,别人下载下来并且解压之后,别人就可以用了。

tar czf mylib.tgz output

当然,先跟之前的硬链接一样,可以将库文件拷贝到系统路径中。

1.硬编译

gcc main.c -I output/include -L output/lib -lhello

这里我们有两个库

libhello.a静态库和libhello.so动态库

那么gcc默认是用动态库还是静态库呢?

ldd a.out

gcc默认使用的是动态库!

我们将这个动态库移除出这个文件夹

mv output/lib/libhello.so ./

gcc main.c -I output/include -L output/lib -lhello
ldd a.out

现在它调用的是静态库,它并没有调用我们的动态库。

a.如果只有静态库的话,gcc就只能针对该库进行静态链接(拷贝)

b.如果同时存在动态库的静态库的话,就优先调用动态库

c.如果动静态库同时存在,我非要使用静态库呢?

加上-static选项

-static的意义:摒弃默认优先使用动态库的原则,而是直接使用静态库的方案!

gcc main.c -I output/include -L output/lib -lhello -static

那为什么我们这里动态库一使用就报错了呢?

动态库的加载

 

动态库是一个独立的库文件

动态库可以和可执行程序,分批加载!!

进程地址空间有一块代码区,静态库的代码和我的代码都会被加载到代码区,他们全部都会被编址,所以是与地址有关

但是动态库呢?

内存中有一块堆区(向地址减小的方向增长),栈区(向地址增大的方向增长)

堆区和栈区相向而生,但其之间存在镂空,这片空间也就是我们的共享区。

我们的a.out要访问动态库的时候,我们就将我们的动态库加载到了我们的共享区中。

然后我们想使用库中某一个方法,我们就将其和页表建立映射,然后将去放入共享区中。

所以我们的a.out只需要根据页表中找到这个共享区中的代码,然后调用这个方法就可以了。找到这个方法并且执行完成之后,然后返回就可以了。

也就是可执行程序先加载,然后再见给我们的库文件加载进去。

那如果我的系统中有大量的进程需要使用这一个动态库的代码呢?

我们只需要将这个库都建立与这每一个进程的映射关系。只要这个库被加载到我们的共享区中,我们的其他进程都只要去这个共享区的地方调用这个库的代码就可以了。

也就是说,如果是静态库的话,我们的内存中就会出现大量重复的库的代码,但是我们的动态库的话,只需要加载一次,也就是只需要在内存中存在一份就可以了!

所以我们这里找不到动态库是因为我们的可执行程序被加载到内存中了,但是我们的库当前没有被夹在到内存中。

可是我不是已经告诉了你我们的动态库的路径了吗? 

我们刚刚的代码,告诉我们的gcc我们的动态库的位置的那些参数全部都是跟gcc说的。

但是我们的代码的加载与gcc没有关系!

我们需要跟我们的加载器,或者是我们的操作系统说!

which ld

我们需要跟我们的系统也说一下我们的动态库在哪里。

静态库为什么不用告诉系统?

因为我们编译好了,我们的库的代码已经在我们的可执行程序中了,不用跟系统说。

我们的动态库需要跟系统说

库加载的搜索路径

LD_LIBRARY_PATH

ls output/lib/

系统在搜索库文件的时候就在下面的路径中搜索

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:你的库的路径
ldd a.out

但是我们的这个只能作为一个临时方案,也就是重新登录我们的xshell,我们的这个路径就没有了

新增配置文件​​​​​​​ 

我们可以写一个配置文件放到我们的这个路径下面

ls /etc/ld.so.conf.d/ -ld

sudo touch /etc/ld.so.conf.d/test.conf

我们将我们刚刚自定义的库的路径直接粘贴到这个文件中就可以了

sudo vim /etc/ld.so.conf.d/test.conf

更新一下我们的配置路径,让其生效。

sudo ldconfig
ls /etc/ld.so.conf.d/ 
ldd a.out

sudo rm /etc/ld.so.conf.d/test.conf

我们发现删掉了我们的配置文件,我们的程序还能跑,这是因为我们的缓存还在

sudo ldconfig

跑不了啦

2.建立软链接

用软链接,链接到我们的动态库

sudo ln -s /home/zhuyuan/test_2022_11_05/output/lib/libhello.so /lib64/libhello.so
ls /lib64/libhello.so

运行我们的a.out,我们发现我们的程序又可以跑了。

ldd a.out

或者是修改配置文件 

vim .bashrc
vim .bash_prfile

为什么要有库?

我们一定要用别人写好的成熟的库。

站在使用库的角度,库的存在,可以大大减少我们开发的总周期,提高我们软件本身的质站在写库人的角度,他为啥不给我们源代码,只是给我们一个库呢?

1.使用库更加简单,我们的代码之间的关系完全是解耦的。

2.使代码更加安全。因为他并不想将代码公开。将代码变成二进制,很难还原。但是现在很多逆向工程,其实是能将这种.o文件还原成源文件的。

(将库放到网络上,提供对应的网络接口,我们就能够通过网络的接口来使用对应的服务)

【Linux】【基础IO】相关推荐

  1. Linux 基础—— IO 全面介绍

    Linux - 基础 IO Linux - 基础 IO 文件 IO 相关操作 stdin & stdout & stderr 系统文件 I/O 文件的宏观理解: 狭义理解: 1.文件在 ...

  2. Linux 基础IO

    文章目录 先来段代码回顾C文件接口 写文件 读文件 输出信息到显示器,你有哪些方法 默认打开的三个流:stdin & stdout & stderr 系统接口 open close w ...

  3. Linux——基础IO(总结)

  4. Linux中的基础IO(二)

    Linux中的基础IO(二) 文章目录 Linux中的基础IO(二) 一.基本接口 二.文件描述符 三.文件描述符的分配规则 四.重定向 五.dup2系统调用 六.minishell 一.基本接口 i ...

  5. Linux中的基础IO(一)

    Linux中的基础IO 文章目录 Linux中的基础IO 一.C语言中的文件接口 二.随机读写数据文件 三.文件读写的出错检测 一.C语言中的文件接口 写在前面 计算机文件是以计算机硬盘为载体存储在计 ...

  6. linux mysql io压力大_MySQL 调优基础(四) Linux 磁盘IO_MySQL

    1. IO处理过程 磁盘IO经常会成为系统的一个瓶颈,特别是对于运行数据库的系统而言.数据从磁盘读取到内存,在到CPU缓存和寄存器,然后进行处理,最后写回磁盘,中间要经过很多的过程,下图是一个以wri ...

  7. Linux系统编程25:基础IO之亲自实现一个动静态库

    本文接:Linux系统编程24:基础IO之在Linux下深刻理解C语言中的动静态库以及头文件和库的关系 文章目录 A:说明 B:实现静态库 C:实现动态库 A:说明 前面说过,库其实就是头文件和和.a ...

  8. Linux基础(6)--文件IO操作

    文件IO操作 1. open打开操作 2. close关闭操作 3. creat创建操作 4. write写操作 5. read读操作 Linux下一切皆文件,所以文件IO是很重要的也是很基础的操作. ...

  9. Linux系统:基础IO

    基础IO 1. 内存文件 1.1 理解内存文件 文件是内容和属性的集合,文件不仅有内容,还有各种属性.任何文件操作都可以分为对文件内容的操作和对文件属性的操作. 文件没被打开时是放在磁盘上的,想要打开 ...

  10. Linux系统进阶-基础IO

    Linux系统进阶-基础IO 文章目录 Linux系统进阶-基础IO C语言中的文件接口 对文件进行写入 对文件进行读取 什么是当前路径 默认打开的三个流 stdout & stderr 系统 ...

最新文章

  1. spring 依赖注入的3种方式
  2. 分布式内存数据库的CAP-BASE原理
  3. php替换不区分大小写_PHP大小写问题:函数名和类名不区分,变量名区分
  4. Mom and Dad
  5. 全向轮机器人运动模型及应用分析(图片版)
  6. 前端基础:call,apply,bind的的理解
  7. 对话 “智能+”平台大师,看IBM如何重塑企业数字化
  8. mac php环境一键安装包,XAMPP for Mac 8.0.0 PHP集成环境一键安装包
  9. STM32 W5500 MQTT Client 发布订阅及断线重连
  10. Robotframework-RED-red.xml引用library的介绍
  11. java绘制棋盘_java绘制五子棋棋盘
  12. c++和java学哪个好,c++和java区别 学哪个比较好
  13. python 动态仪表盘_利用EXCEL的power pivot+切片器制作动态仪表盘
  14. 洗衣机水位传感器原理:检测水位频率
  15. 基于stm32人脸识别和红外测温
  16. JavaScript运算符 详解
  17. [USACO16JAN]堡哞Fort Moo
  18. 【C++】读取 .csv / .xlsx 文件中的指定数据(非常实用)
  19. 明翰经验系列之恋爱篇V5.6(持续更新)
  20. 系统开发四 模拟器操作

热门文章

  1. 关于gitlab报500的问题解决方案
  2. python使用日常备忘录
  3. 获取默认音频输出设备 vc_Vector 3 for Mac(音频编辑和录音软件) v3.5和谐版
  4. 汽车美容店电话邀约误区与“技巧”
  5. 编译官方DV300的SDKv2.0
  6. 为什么做UTDD(单元测试驱动开发)
  7. VM下的ubuntu连不上网的解决办法
  8. raid数据恢复_EMC FC AX-4存储崩溃恢复
  9. 电池上php,电脑的电池在哪里
  10. 【npm下载任何依赖都报错的解决方法】