初步认识Linux下的进程文件系统

文章目录

  • 前言
  • 1.前置知识思考
  • 2.对编程语言中文件操作的理解
    • 1.系统调用接口
    • 2.文件描述符
    • 3.对重定向的理解
    • 4.对C语言FLIE结构体的理解
  • 3.从操作系统的层面来看待文件
  • 1.如何理解Linux下一切皆文件
  • 2.关于对语言层面缓冲区的理解

前言

本文将对Linux下的文件系统进行初步介绍,主要围绕语言库中的文件操作和系统调用文件操作进行讲解,了解认识到Linux下操作体系对文件的管理。

1.前置知识思考

我们之前提到过,文件是由两部分组成的:属性+内容。那么从这个角度上看对文件的操作就是对文件属性和文件内容进行操作。对于一个文件来说如果不操作它,它是存储在磁盘上的,这是常识。如果我们想操作一个文件那必然它会加载到内存中等待cpu的处理,这是依据冯诺依曼体系决定的。我们先暂且不管加载到内存中的是文件属性还是文件内容,但是至少文件属性一定会被加载到内存中,不然无法定位文件更谈不上操作了.系统内存可能不止一个文件被打开进行操作,众多的文件被操作都是需要系统进行管理。结合之前的知识,操作系统如果想高效的管理这些文件,可以先描述再组织,也就是说我操作系统可以为每个加载到内存的文件创建一个struct _file进行管理,struct_file中有文件对应的属性字段。通过管理这些结构体来管理文件。

初步认识Linux下的进程文件系统_第1张图片

如果我们想操作一个文件,那么这个文件先得被打开。因为操作文件的发生再内存中,所以只有操作系统才能做打开文件。那么是谁让操作系统打卡文件的呢?那必然是上层的用户,站在系统的角度看这个用户的具体的代表就是进程。也就是进程向操作系统发送这个请求,然后操作系统处理这个进程请求。我们对文件的操作都是被打开文件与进程之间的关系,也就是struct_task和struct_file之间的关系。所以文件其实可以分为两大部分磁盘上的文件和内存文件(被打开的文件)。

2.对编程语言中文件操作的理解

各种编程语言都有自己的一套操作文件的接口函数。有没有一种统一视角来看待这些文件操作呢?其实是有的,在Linux下系统是有一批针对文件操作的系统调用接口的,语言库中的文件操作函数都是对这些系统调用接口的封装。

初步认识Linux下的进程文件系统_第2张图片

我们无法绕过操作系统直接和底层进行交互,我们通过编程语言来调用对应语言库中的函数进行文件操作,库函数本质还是调用对应的系统调用接口来操作。我们来认知一下系统调用接口。

1.系统调用接口

在这里插入图片描述

这个系统调用接口open是用来打开文件的,第一个参数是文件名,第二个参数是标志位,这个标志位其实是一个位图结构,标志位是用来表示将文件打开后所进行的操作。比如C语言中的 FILE * ps = fopen(“test.txt”, “w”);这个标志位就是对应的w。不过和C语言有所不同的是,这个系统的调用的接口的标志位每种选择都有一个选项。比如打开文件如果没有文件就创建文件这实际上的对应着两个选项的。因为一个整型有32个比特位,那么最多可以表示32种选择,足够用了。我们可以通过man指令查看文件操作对应的选项。

初步认识Linux下的进程文件系统_第3张图片

这些选项其实都是宏,本质是一些特定的数字,通过位运算莱执行不同的判断分流逻辑。最后一个参数是用来设置文权限的

代码示例
初步认识Linux下的进程文件系统_第4张图片

#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main()
 {
    umask(0);
    int fd = open(LOG, O_WRONLY | O_CREAT , 0666); 
    if(fd == -1)
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd, 
         errno, strerror(errno));
    }
    else
    {
        printf("fd: %d, errno: %d, errstring: %s\n", fd,
           errno, strerror(errno));
    }
    const char *msg = "bbb";
    int count = 5;
    while(count)
    {
        char line[128];
        snprintf(line, sizeof(line), "%s, %d\n", msg, count);
        write(fd, line, strlen(line)); 
        count--;
    }
    close(fd);
    return 0;
}

这里使用了3个系统调用接口分别是open write close.这里说一下这个open,这个open函数如果成功打开了文件会返回一个大于0的整数,如果打开失败就会返回-1,这个返回值被称为文件描述符。关于文件描述符下面将会对其讲解。这个文件描述符就相当于定位了对应的文件。我们通过文件描述符来操作这个文件。我们接着看看这个标志位对应的参数,O_WRONLY只写打开(打开文件后要进行写操作)O_CREAT若文件不存在,则创建文件,O_TRUNC每次打开文件对文件原有的内容先进行清空处理,最后0666对应的就是文件的权限,因为权限掩码的存在,这里使用了umask将默认权限掩码设置为0.

初步认识Linux下的进程文件系统_第5张图片

write就是对文件进行写入操作,将字符串的内容写入到文件中,close关闭文件。这些系统接口都是通过文件描述符进行文件操作的。

2.文件描述符

我们看到文件描述符是3开始分配的,这是为啥呢?Linux进程默认情况下会3个三个文件,3个文件描述符,分别是标准输入0, 标准输出1, 标准错误2; 0,1,2对应的物理设备一般是:键盘,显示器,显示器.在C语言中这3个文件分别对应着stdin(标准输入),stdout(标准输出),stderr(标准错误);在C++分别对应cin cout cerr。所以这个文件描述符是从3开始的。我们再来看看一段代码更深入了解一下文件描述符。

初步认识Linux下的进程文件系统_第6张图片

初步认识Linux下的进程文件系统_第7张图片

我们看到一个很有意思的现象这个文件描述符和数组下标很像,其实这个文件的描述就是数组下标。我们知道一个进程可以打开多个文件,也就是说进程和文件对应关系的是1对多的,也就是1:n。我们知道每个被加载到内存的文件都会被系统创建一个struct_file对象,将每个进程打开的文件对应的struct_file地址按顺序放入一个文件指针数组,这个数组包含在一个file_strutc表中,进程对应的pcb其中一个字段指向这个表,这样就把文件和对应的进程关联起来了,从而实现了进程对文件的操作。文件描述符本质就是这个数组对应的下标,通过文件描述符拿到对应的数组下标从而找到对应的文件。

初步认识Linux下的进程文件系统_第8张图片

文件描述符分配规则就是:在文件描述表中最小的没有被使用的数组下标分配给新打开的文件.还需要注意的一点就是:当文件被关闭以后该文件的对应的文件描述符就被收回了,需要使用该文件描述符的时候在分配给新打开的文件。关于这个文件对应的表这里再补充一下:在操作系统层面,每个进程都有一个 file descriptor table(文件描述符表),用于跟踪它打开的文件和设备。每个文件描述符对应着内核中的一个 file struct 结构体。file struct 并不是进程私有的,而是所有进程共享的,因为它们都指向同一个打开的文件或设备.内核维护的是所有进程打开文件的结构体,每个进程拿的只是自己进程打开的文件的结构体,这个结构体是从内核中来的。

3.对重定向的理解

我们知道这个文件描述符1默认对应的是stdout那么我们手动将这个文件描述符符更改会发生什么现象呢?我们看看如下代码。

初步认识Linux下的进程文件系统_第9张图片

初步认识Linux下的进程文件系统_第10张图片

这里打开文件的时候没有对原有内容进行清空选项,所以查看文件的时候会有之前的内容。上述代码首先将1号文件描述符对应的文件先关闭,实际上就是先关闭了stdout,这个时候我们用open打开文件的时候这个1就会分配给log.txt。printf默认是向stdout打印数据,这个stdout对应的文件描述符是1。也就是说printf是向文件标识符为1的文件打印,我们直接更改了1对应的文件标识符,这个时候打印结果都到了log.txt文件中。也就是说重定向的原理就是再上层无法感知的情况下,更改文件描述符表中特定下标的指向。

上述代码中我们手动关闭文件描述符的方式比较搓,在Linux下系统提供了更改文件描述的系统接口。

初步认识Linux下的进程文件系统_第11张图片

这里主要使用的接口是dup2,这个接口的参数名字和下面对返回值的介绍比较迷惑人。这个函数会将对应文件数组下标的内容进行拷贝。也就是或newfd是oldfd的一份拷贝,那么上述代码如果要传参就是dup2(fd,1)。

这里还要补充一点标准输出和标准错误虽然默认都是在显示器上输出,但是其实两者是有区别的。

初步认识Linux下的进程文件系统_第12张图片

我们在输出重定向的时候发现只有标准输出重定向到了目标文件中,但是标准错误还是输出到了屏幕中。stdout:标准输出流,通常用于输出正常的程序输出,如一些普通的提示信息,正常的程序运行结果等。stderr:标准错误流,通常用于程序运行错误信息的输出,如文件不存在、内存分配失败等错误信息。标准错误流 stderr 没有被重定向的原因是为了避免在发生错误时把错误信息丢失,同时也能让开发者及时地发现问题并加以处理。当程序发生错误时,错误信息通常会被输出到标准错误流 stderr 中,比如文件读取失败、资源申请失败、逻辑错误等。在这种情况下,如果将 stderr 也重定向到一个文件中,我们可能无法及时地看到错误信息并进行及时的调整,这将会增加程序的调试难度。另外,将 stderr 重定向到一个文件中可能会导致一些意外情况,比如文件写入失败、文件系统访问错误等,这些错误信息同样需要被输出到 stderr 中以便及时处理。因此,为了能够及时地发现并处理程序错误,保证程序的稳定性和可靠性,操作系统并没有将标准错误流 stderr 重定向到其他设备或文件中。

4.对C语言FLIE结构体的理解

我们知道这个C语言库中的文件操作库函数是对这个系统调用接口的封装,那么也就是说这些文件接口最后还是得拿到这个文件描述符才能操作文件。FILE是C语言库中自定义的类型,这个类型的字段中一定有对应文件描述符fd。其实,FILE中有个fileno的字段表示文件描述符。

初步认识Linux下的进程文件系统_第13张图片

C语言库中的FILE结构体是专门为C语言设计的,这和内存中的struct_file没有任何关系。

3.从操作系统的层面来看待文件

初步认识Linux下的进程文件系统_第14张图片

当我们调用write函数对文件写入数据的时候,首先会根据文件描述符fd找到对应文件的结构体,操作系统会为每个文件创建一个对应的数据缓冲区,要写入的数据会被放入这个数据缓冲区中。操作系统会根据数据刷新策略将数据刷新到磁盘对应的位置上。read函数不就是将文件的内容写入到缓冲区。所以这个read函数和write函数就是一个拷贝函数,在用户空间和内核空间来回进行拷贝。

1.如何理解Linux下一切皆文件

Linux下一切皆文件。当第一次听到这句话的时候我们相信很多人都会一脸懵逼。显示器,键盘这些硬件怎么可能是文件呢。这都不是一类东西啊。下面我将画图来帮助理解。

初步认识Linux下的进程文件系统_第15张图片

我们知道在操作系统之下是驱动程序和硬件设备,站在操作系统的角度我们可以为每个设备创建对应的文件结构体,当设备需要调用驱动程序进行IO读写时就可以像操作文件那样,通过调用对应的驱动程序往读写读写缓冲区进行数据的交换这样实现外设和用户层面的数据交互。我们使用OS系统本质就是通过进程方式进行Os的访问,上层用户并不关心底层是怎么实现IO交互的,它只关心自己的任务请求完没完成。它看不到底层硬件。它只能以进程的方式来看待任务的执行情况。因此虽然硬件设备并不是经典意义下的文件,但在Linux中,它们被视为文件,使得访问和管理设备和其他文件具有相似的方式和命令。这种设计的优点是设备和其他文件可以一起进行管理和操作,简化文件系统的管理和实现。那么其实上述默认打开的3个文件标准输入 标准输出,标准错误其实对应的就是键盘文件 显示屏文件。

2.关于对语言层面缓冲区的理解

我们知道系统会为内存文件创建缓冲区,但是语言层面也有缓冲区。这是语言自己设计好的。比如C库就有自己的缓冲区。看看如下代码。

初步认识Linux下的进程文件系统_第16张图片

这里我们看到在输出重定向的时候除了系统调用的向stdout打印的内容重定向了一次,其余调用C语言接口的文件操作函数都是重定向了两次。这是为啥呢?其实在语言层面也有一个文件缓冲区,这是语言自带封装的。C语言缓冲区有3种刷新策略,无缓冲 ,行缓冲 全缓冲。无缓冲:输出立即进行,字符一旦被打印,就立即出现在屏幕上。这种策略适用于需要实时将数据输出的场合。行缓冲:输出在一行结束时刷新,或者缓冲区被填满时刷新。例如,大多数标准输入输出操作默认使用的就是行缓冲策略。当写操作遇到换行符 ‘\n’ 时,或者缓冲区被填满时,缓冲区中的数据就会被刷新到屏幕上。全缓冲:输出在缓冲区被填满时刷新,或者调用 fflush() 函数时刷新。常见的输出设备如文件都是全缓冲的。这种策略可以提高效率,但可能会导致输出结果不及时。c语言库会结合一定的刷新策略将文件将数据写入给OS。当我们向显示屏打印的时候是采用的行缓冲,当我们采用向普通文件重定向的时候,行缓冲变为全缓冲。这个时候数据不会被历即刷新,fork以后子进程会拷贝一份父进程的数据,这个时候缓冲区的数据也被子进程拷贝。当程序结束的时候,这个时候要进行数据刷新,就会发生写时拷贝。父子进程的数据都是独立的且都会被刷新出来,这样的话我们就看到调用C语言的文件库函数的时候数据被重定向了两次。系统调用接口是直接将数据写给OS的。

那么为啥语言层面要有缓冲区呢?

当我们频繁调用系统接口向操作系统写入数据的时候,这个时候也会时间上的开销。当我们在语言层面设计一个缓冲区,尽可能一次多的向操作系统写入数据这样节省系统调用的时间,也提高写入效率。这里要注意一点语言层面的缓冲区是用户缓冲区,而系统缓冲区是内核缓冲区。两者是不一样的。

那这个语言层面的缓冲区在哪里呢?

其实这个我们用fopen打开文件的时候会得到这个FILE结构体,这个缓冲区就在FILE中,一般都是malloc出来的,当我们关闭文件的时候fcloseg其实也释放了这个用户缓冲区。

你可能感兴趣的:(Liunx操作系统,linux,学习)