自学内容网 自学内容网

Linux 文件系统(上)

目录

 一.预备阶段

1.认识文件

2.OS对内存文件的管理

3.C库函数和系统调用接口

a.C库函数——fopen

b.系统调用接口——open

二.理解文件描述符

1.一张图,详解文件描述符的由来

2.fd的分配规则

3.从fd的角度理解FILE

三.重定向和缓冲区

1.前置知识——理解向文件写入数据的底层逻辑

2.重定向

a.什么是重定向?

b.输出、输入重定向

c.重定向的底层逻辑

d. dup2

3.缓冲区

a.什么是缓冲区?

b.缓冲区谁提供的??

c.为什么要有缓冲区?

d.缓冲区的刷新机制

e.重点样例


 一.预备阶段

1.认识文件

一个文件,通常包括文件的内容数据(文件内存储的东西)和文件的属性数据(描述该文件本身状态的字段),所以,我们对文件进行操作,本质就是对文件内容和文件属性进行操作。

同理,文件的内容是数据,属性也是数据,所以,存储文件必须既存储内容数据,又要存储属性数据 ( 默认指的是磁盘中的文件)。

当我们要访问一个文件的时候,都是要先把这个文件打开,文件打开前,它是普通的磁盘文件。打开文件,就是将文件加载到内存

所以,根据文件所处位置,我们可以将文件分成两类:①普通的磁盘文件;②加载到内存的文件。

众所周知,我们的磁盘或内存中,会存在大量的文件,由于OS是计算机软硬件资源的管理者,所以OS需要管理这些文件,即能够高效的对这些文件进行增删查改操作,这就是文件系统!!

而在该文章中,咱们主要了解的是“OS对内存文件的管理”,至于磁盘中的文件,博主会在下一篇博客中详解。

2.OS对内存文件的管理

那么,操作系统是如果管理内存文件的? --- 先描述,再组织!!

看过博主往期博客的都知道,在操作系统中,一旦涉及到“管理”这两个字,那一定就有这六个字,先描述,再组织!!

什么是先描述?文件在加载到内存前,OS就需要在内核中创建对应文件的结构体对象,该结构体内存有大量有关对应文件的属性信息,这样一来,当文件被加载到内存后,OS就可以把这个结构体当成对应的文件。

什么是再组织?当描述文件的结构体对象被创建后,OS会把这个结构体放到特定的数据结构中,以便后续OS对该结构体进行操作,这样一来,OS对内存文件的管理,就变成了对特定数据结构中某一结构体对象的增删查改!!

文件是如何加载到内存的?

我们知道,文件被加载到内存后,就变成了系统资源的一部分,而进程是“承担分配系统资源的基本实体”,所以,文件想要被加载到内存,一定是因为系统中的某个或某些进程需要它!!

进程通过操作系统打开文件,由于操作系统内核不允许任何人直接干涉,所以,我们想要使用操作系统的某一功能时,就只能使用操作系统对上层提供的系统调用接口,来间接使用操作系统的功能。

故,我们所学习的C语言打开文件的库函数,其底层一定封装了系统调用接口!!!

d.一个进程可以打开多个文件吗?多个进程可以打开多个文件吗?加载到内存中,被打开的文件可能会存在多个

3.C库函数和系统调用接口

a.C库函数——fopen

FILE*  fopen(const char* path, const char* mode);   

这个函数的功能是,以读或写的方式打开某一文件(绝对路径或相对路径)。

若不写路径,只写文件名,则path是写在cwd(当前工作目录)下的,而 ls /proc/pid 可以查看某一进程的属性数据,包括cwd,代码中,用chdir(“new_path”)可以改变cwd。

例1:FILE* fp = fopen("log.txt","w");

以写方式打开文件,若文件不存在,则先创建再打开;若文件存在,则先清空再打开。

可知:其与 输出重定向">"的功能相似。

例2:FILE* fp = fopen("log.txt","a");

以追加的方式打开文件,从文件的结尾处以追加的方式写入数据,不清空原有数据。

可知:与追加重定向 ">>" 的功能类似。

b.系统调用接口——open

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

第一个参数,filename:即想要打开文件的文件名。

第二个参数,flags:

O_RDONLY(以只读的方式打开)

O_WRONLY(以只写的方式打开)

O_RDWR(以读写的方式打开)

O_CREAT(文件不存在,则创建)

O_TRUNC(若文件存在,则清空文件内容)

O_APPEND(若文件存在,则以追加的方式写入数据)

第三个参数,mode:只有当该文件是新创建的,才会用到这个参数,mode是默认权限,可以给新创建的文件设置文件权限。

返回值:若创建失败,则返回-1;若创建成功,则返回对应文件的文件描述符

比特位级别的标记位使用方式 --- Linux中常用的传参方式

示例1:

int fd = open("log.txt",O_WRONLY | O_CREAT); 

新创建一个文件,将其命名为 log.txt,但是由于没给该文件设置权限,导致文件权限乱码:

修改后:

int fd = open("log.txt",O_WRONLY | O_CREAT, 0666);  

文件不存在的话,就创建该文件,并且将log.txt文件的默认权限设为0666.

示例2:

int fd = open("log.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);

以该方式打开文件,运行后,会将文件内原有的内容清零

示例3:

int fd = open("log.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);:

以该方式打开文件,运行后,会在文件内容后,追加数据

综上所述,我们能够发现,系统调用中的open接口,与C库中的 fopen,有着极其相似的功能,咱们可以得出一个结论:C库中文件相关操作的库函数,其底层都封装了系统调用接口。

二.理解文件描述符

1.一张图,详解文件描述符的由来

通过上图,我们可以发现,所谓的“文件描述符表”,其本质就是一个struct files_struct*类型的数组而已,数组内存放的是文件struct file结构体的地址,而所谓的“文件描述符fd”,它本质就是文件描述符表内元素的下标而已。

例如:

int fda = open("loga.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);  // fda = 3

int fdb = open("logb.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fdb = 4 

int fdc = open("logc.txt",O_WRONLY | O_CREAT | O_APPEND, 0666);  // fdc = 5

int fdd = open("logd.txt",O_WRONLY | O_CREAT | O_APPEND, 0666); // fdd = 6

fda、fdb、fdc、fdd它们分别是3、4、5、6,意味着:新打开文件的struct file地址,是从文件描述符表的3号下标开始填充的。

那0、1、2去哪了呢?

程序在运行的时候,默认是把下面这三个程序(文件)打开的:

①标准输入  键盘       stdin       0号下标

②标准输出  显示器    stdout    1号下标

③标准错误  显示器    stderr     2号下标

所以,一个进程最新打开的文件的fd,是从3开始的!!

OS/C语言为什么默认要把0、1、2、stdin、stdout、stderr打开呢??--- 就是为了让程序员默认进行输入、输出和代码编写!!

2.fd的分配规则

当进程新打开一个文件时,OS会遍历该进程对应的文件描述符表(下标由低到高依次遍历),当OS找到表中空余位置时,无论该位置后面是否存在元素,OS都会将新创建文件的struct file地址填充到该位置,并向进程返回该位置对应的数组下标。

3.从fd的角度理解FILE

那FILE究竟是什么东西??

它是一个C语言提供的结构体类型,其内部必定封装了文件描述符!!!

举例验证:

printf("stdin -> fd: %d\n", stdin->_fileno);//0 

printf("stdout -> fd: %d\n", stdout->_fileno);//1

printf("stderr -> fd: %d\n", stderr->_fileno);//2

说明,_fileno 字段,就是 FILE 内部封装的文件描述符!

三.重定向和缓冲区

1.前置知识——理解向文件写入数据的底层逻辑

在学习重定向和缓冲区之前,我们不妨先思考一下:当我们调用read、write、fread、fwrite时,OS底层究竟做了哪些是?一张图,咱们扒光它!!

无论读取还是写入数据,都要先把文件数据加载到文件缓冲区内!!

用户对文件中数据的读写的本质:用户通过fd变量,找到struct file* fd_array[]数组中的文件结构体地址,通过地址找到对应的结构体,再通过结构体中的数据属性,找到该文件对应的文件缓冲区(内存中存放文件内容数据的一块空间),对文件缓冲区中的数据进行读取或写入。

2.重定向

a.什么是重定向?

简单来说,就是将本该写入A文件中的数据,转而写入B文件中,这就是重定向(输出)。

b.输出、输入重定向

我们知道,在文件描述符表中,0、1、2三个文件是默认打开的,而根据fd的分配规则我们可以知道,如果我们将1号fd关闭,当我们接着打开一个新文件时,OS为该文件分配的fd就是1。

由于C库中的printf()函数本质是向1号文件(屏幕)写入数据,所以,如果我们此时调用printf的话,就会将本该打印到屏幕上的数据重定向写入到新打开的文件中。

而这,其实就是输出重定向的底层原理。代码如下:

c.重定向的底层逻辑

①上述代码中,为啥显示器上什么都没输出??

--- 因为1号文件描述符,也就是显示器被关了

②为啥本该输出的数据被写到了FILE_NAME文件里??

--- 因为printf()函数只认stdout——标准输出流,而stdout只认1号文件描述符,由于原先1号文件描述符对应的显示器被关了,而新打开文件的文件描述符根据fd的分配规则,被重新分配到了1号位置,所以本该在显示器上数出的数据跑到了FILE_NAME文件中。该过程便可称为输出重定向!

③上述代码中,为啥本该从键盘上读取数据的操作,变成了读取FILE_NAME文件中的数据??

--- 因为0号文件描述符,即键盘文件被关了,而以读的方式打开FILE_NAME文件时,根据fd的分配规则,0号位置被重新分配给了FILE_NAME文件,由于stdin——标准输入流,只认0号数组位置,所以本该从键盘上读取数据的操作变成了从FILE_NAME文件中读取数据。该过程便可称为输入重定向!

所以,重定向的本质,是文件描述符级别的数组内容的拷贝!

那么,我们的输入、输出重定向都需要先关闭0号或1号文件,然后再打开新的文件吗?--- 重定向的底层逻辑都是这样,但是这个操作有人已经为我们封装成了函数接口(dup2),咱们可以直接使用函数来实现重定向操作。

d. dup2

dup2(int  dest_fd, int  src_fd); 

让本该向src_fd文件输入、输出的数据,改向dest_fd文件进行输入、输出。

即在文件描述符表中,让src对应的struct file* 被dest对应的struct file*覆盖!

输出重定向:

输入重定向:

标准输出流和标准错误流

解释:将mytest文件中“原本向标准输出”打印的内容重定向写到normal文件中,将“原本向标准错误流”中打印的内容重定向写到err.log文件中,实现打印错误信息和打印正常信息的分流!

3.缓冲区

a.什么是缓冲区?

--- 缓冲区本质是一块内存空间,我们可以把它看成一个小水池,池水就是数据,当水池内的水不够多时,水池蓄水(数据存储);当水池内的水达到一定条件时,咱们就开闸放水(数据刷新)。

b.缓冲区谁提供的??

--- 若该内存是由用户开辟出来的,如变量、数组等,则称为用户缓冲区若该缓冲区是C语言提供的,则称为C标准库缓冲区若是由操作系统提供的,则称为操作系统缓冲区

c.为什么要有缓冲区?

--- 无论是用户与内核间的数据IO,还是内核与磁盘间的数据IO,它们都是需要花费一定成本的,IO次数越少,所消耗的成本也就越低,所以,缓冲区的存在就是为了减少IO次数,从而降低IO所带来的开销问题。

d.缓冲区的刷新机制

① 无缓冲 --- 缓冲区内一有数据就直接刷新.

② 行缓冲 --- 只有向缓冲区内写入换行字符,即'\n'时,才会刷新数据,否则不刷新.

③ 全缓冲 --- 只有把缓冲区写满了,才刷新;否则不刷新.

④ 强制刷新 --- 调用fflush(stdout),或当进程退出时,OS会自动刷新该进程内缓冲区的数据.

一般对于显示器文件 --- 行缓冲;

对于磁盘上的文件 ---全缓冲.

示例:

答:因为向显示器文件中写入数据的刷新准则是行刷新,而上述代码的 pirntf 中并没有 换行符'\n',所以 printf 仅仅是将 hello world 写入C库缓冲区或内核缓冲区,其内容并没有写入显示器文件中,而sleep 3秒后,代码执行完毕,进程退出,缓冲区内的数据会被强制刷新到显示器文件中(屏幕)。

e.重点样例

为什么多一个fork()时,使用C库函数写入文件的内容打印了两次,而使用系统调用接口写入文件的内容不变??fork()对C库函数的输出函数又起到了怎样的影响??

理解样例:

①当我们直接向显示器打印的时候,显示器文件的刷新方式是行刷新!由于我们的代码输出的所有字符串之前都有\n, 所以在fork()之前,缓冲区上的数据就已经全被刷新出来了,包括systemcall(系统调用)

②重定向到log.txt文件,本质是向磁盘文件写入,系统对于数据的刷新方式已经由行刷新,变成了全缓冲!

③全缓冲意味着缓冲区变大,实际写入的简单数据,不足以把缓冲区写满,fork执行的时候,数据依旧在缓冲区中!

④C语言库函数输出的内容写在C库缓冲区,而系统调用接口输出的数据是直接写到内核缓冲区的fork后会创建子进程。

⑤当进程退出时,即使我们的数据没有达到刷新的条件,OS也会自动刷新C标准库缓冲区对于多进程(父子进程)来说,刷新缓冲区属于对其一进程进行“清空”或“写入”操作,所以,fork退出时,刷新缓冲区,就要进行写时拷贝!!

而write是系统调用,没有使用C的缓冲区,直接写入到操作系统,不属于进程了,不发生写时拷贝!

用C语言库函数对文件写入的实质:调用C函数会先把要写的内容写入C缓冲区,当C缓冲区满足刷新条件时,会将缓冲区里的内容通过系统调用接口(write(1,BUFFER))拷贝到对应的文件缓冲区内。


原文地址:https://blog.csdn.net/2201_75663820/article/details/142330032

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!